From f5891ee03da0aabea58c33da2ee8c513c44b3575 Mon Sep 17 00:00:00 2001 From: Levi Date: Fri, 27 Dec 2024 11:17:17 +0800 Subject: [PATCH 1/3] feat: add batch create tips (#858) --- README.md | 2 +- README_ja-JP.md | 2 +- README_vi-VN.md | 2 +- README_zh-CN.md | 2 +- README_zh-TW.md | 2 +- ui/flutter/lib/i18n/langs/en_us.dart | 3 +-- ui/flutter/lib/i18n/langs/zh_cn.dart | 2 +- ui/flutter/lib/i18n/langs/zh_tw.dart | 2 +- 8 files changed, 8 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index bd56ee795..e82463893 100644 --- a/README.md +++ b/README.md @@ -174,7 +174,7 @@ command: - android ```bash -gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile +gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile cd ui/flutter flutter build apk ``` diff --git a/README_ja-JP.md b/README_ja-JP.md index bada88c1e..be61a498e 100644 --- a/README_ja-JP.md +++ b/README_ja-JP.md @@ -162,7 +162,7 @@ gomobile init - android ```bash -gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile +gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile cd ui/flutter flutter build apk ``` diff --git a/README_vi-VN.md b/README_vi-VN.md index df1499a66..abb3597bf 100644 --- a/README_vi-VN.md +++ b/README_vi-VN.md @@ -170,7 +170,7 @@ command: - android ```bash -gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile +gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile cd ui/flutter flutter build apk ``` diff --git a/README_zh-CN.md b/README_zh-CN.md index 78f6a235e..fd21c137c 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -174,7 +174,7 @@ gomobile init - android ```bash -gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile +gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile cd ui/flutter flutter build apk ``` diff --git a/README_zh-TW.md b/README_zh-TW.md index aed8e07ac..d13172483 100644 --- a/README_zh-TW.md +++ b/README_zh-TW.md @@ -170,7 +170,7 @@ gomobile init - android ```bash -gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile +gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg="com.gopeed" github.com/GopeedLab/gopeed/bind/mobile cd ui/flutter flutter build apk ``` diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index 91b9a4757..7a2656da3 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -20,8 +20,7 @@ const enUS = { 'followSettings': 'Follow Settings', 'downloadLink': 'Download Link', 'downloadLinkValid': 'Please enter the download link', - 'downloadLinkHit': - 'Please enter the download link, HTTP/HTTPS/MAGNET supported@append', + 'downloadLinkHit': 'Please enter the download link, one per line@append', 'downloadLinkHitDesktop': ', or drag the torrent file here directly', 'download': 'Download', 'noFileSelected': 'Please select at least one file to continue.', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index 1d72e82c7..b408ff145 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -20,7 +20,7 @@ const zhCN = { 'followSettings': '跟随设置', 'downloadLink': '下载链接', 'downloadLinkValid': '请输入下载链接', - 'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append', + 'downloadLinkHit': '请输入下载链接,支持批量下载,每行一个链接@append', 'downloadLinkHitDesktop': ',也可以直接拖拽种子文件到此处', 'download': '下载', 'noFileSelected': '请至少选择一个文件下载', diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 039e76a23..40f47750a 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -20,7 +20,7 @@ const zhTW = { 'followSettings': '跟隨設定', 'downloadLink': '下載連結', 'downloadLinkValid': '請輸入下載連結', - 'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append', + 'downloadLinkHit': '請輸入下載連結,支援批量下載,每行一個連結@append', 'downloadLinkHitDesktop': ',或直接拖曳種子檔到此處', 'download': '下載', 'noFileSelected': '請至少選擇一個檔案以繼續。', From 727b7edb84c5bf4e5a41e5cc3d3c061c0cffc7da Mon Sep 17 00:00:00 2001 From: Levi Date: Fri, 27 Dec 2024 11:34:56 +0800 Subject: [PATCH 2/3] feat: file tree view responsive support (#856) --- .../modules/task/views/task_files_view.dart | 5 +- .../lib/app/views/buid_task_list_view.dart | 8 +- ui/flutter/lib/app/views/file_icon.dart | 5 +- ui/flutter/lib/app/views/file_tree_view.dart | 100 ++++++++++-------- ui/flutter/pubspec.lock | 4 +- ui/flutter/pubspec.yaml | 2 +- 6 files changed, 65 insertions(+), 59 deletions(-) diff --git a/ui/flutter/lib/app/modules/task/views/task_files_view.dart b/ui/flutter/lib/app/modules/task/views/task_files_view.dart index 5dcde3336..8bccc6df2 100644 --- a/ui/flutter/lib/app/modules/task/views/task_files_view.dart +++ b/ui/flutter/lib/app/modules/task/views/task_files_view.dart @@ -64,9 +64,8 @@ class TaskFilesView extends GetView { [meta.opts.path, meta.res!.name, fileRelativePath]); final fileName = basename(filePath); return ListTile( - leading: file.isDirectory - ? const Icon(folderIcon) - : Icon(fileIcon(fileName)), + leading: + Icon(fileIcon(fileName, isFolder: file.isDirectory)), title: Text(fileName), subtitle: file.isDirectory ? Text('items'.trParams({ diff --git a/ui/flutter/lib/app/views/buid_task_list_view.dart b/ui/flutter/lib/app/views/buid_task_list_view.dart index 69b37d686..41b3ab927 100644 --- a/ui/flutter/lib/app/views/buid_task_list_view.dart +++ b/ui/flutter/lib/app/views/buid_task_list_view.dart @@ -183,9 +183,11 @@ class BuildTaskListView extends GetView { children: [ ListTile( title: Text(task.name), - leading: isFolderTask() - ? const Icon(folderIcon) - : Icon(fileIcon(task.name))), + leading: Icon( + fileIcon(task.name, + isFolder: isFolderTask(), + isBitTorrent: task.protocol == Protocol.bt), + )), Row( children: [ Expanded( diff --git a/ui/flutter/lib/app/views/file_icon.dart b/ui/flutter/lib/app/views/file_icon.dart index ea118c6e0..4a62edd86 100644 --- a/ui/flutter/lib/app/views/file_icon.dart +++ b/ui/flutter/lib/app/views/file_icon.dart @@ -52,9 +52,6 @@ final Map _iconCache = Map.fromEntries( ), ); -const folderIcon = Gopeed.folder; -const folderBtIcon = Gopeed.folder_bt; - String fileExt(String? name) { if (name == null) { return ''; @@ -71,7 +68,7 @@ String fileExt(String? name) { IconData fileIcon(String? name, {bool isFolder = false, bool isBitTorrent = false}) { if (isFolder) { - return isBitTorrent ? folderBtIcon : folderIcon; + return isBitTorrent ? Gopeed.folder_bt : Gopeed.folder; } final ext = fileExt(name); diff --git a/ui/flutter/lib/app/views/file_tree_view.dart b/ui/flutter/lib/app/views/file_tree_view.dart index 7d1bd4b07..354bc1a97 100644 --- a/ui/flutter/lib/app/views/file_tree_view.dart +++ b/ui/flutter/lib/app/views/file_tree_view.dart @@ -14,6 +14,7 @@ const _toggleSwitchIcons = [ Gopeed.file_audio, Gopeed.file_image, ]; +const _sizeGapWidth = 72.0; class FileTreeView extends StatefulWidget { final List files; @@ -50,7 +51,7 @@ class _FileTreeViewState extends State { final selectedFileCount = key.currentState?.getSelectedValues().where((e) => e != null).length ?? widget.files.length; - final selectdFileSize = calcSelectedSize(null); + final selectedFileSize = calcSelectedSize(null); return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -144,52 +145,59 @@ class _FileTreeViewState extends State { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - InkWell( - onTap: () {}, - child: ToggleSwitch( - minHeight: 32, - cornerRadius: 8, - doubleTapDisable: true, - inactiveBgColor: Theme.of(context).dividerColor, - activeBgColor: [Theme.of(context).colorScheme.primary], - initialLabelIndex: toggleSwitchIndex, - icons: _toggleSwitchIcons, - onToggle: (index) { - toggleSwitchIndex = index; - if (index == null) { - key.currentState?.setSelectedValues(List.empty()); - return; - } + Flexible( + flex: 4, + child: InkWell( + onTap: () {}, + child: ToggleSwitch( + minHeight: 32, + cornerRadius: 8, + doubleTapDisable: true, + inactiveBgColor: Theme.of(context).dividerColor, + activeBgColor: [Theme.of(context).colorScheme.primary], + initialLabelIndex: toggleSwitchIndex, + icons: _toggleSwitchIcons, + onToggle: (index) { + toggleSwitchIndex = index; + if (index == null) { + key.currentState?.setSelectedValues(List.empty()); + return; + } - final iconFileExtArr = - iconConfigMap[_toggleSwitchIcons[index]] ?? []; - final selectedFileIndexes = widget.files - .asMap() - .entries - .where( - (e) => iconFileExtArr.contains(fileExt(e.value.name))) - .map((e) => e.key) - .toList(); - key.currentState?.setSelectedValues(selectedFileIndexes); - }, + final iconFileExtArr = + iconConfigMap[_toggleSwitchIcons[index]] ?? []; + final selectedFileIndexes = widget.files + .asMap() + .entries + .where((e) => + iconFileExtArr.contains(fileExt(e.value.name))) + .map((e) => e.key) + .toList(); + key.currentState?.setSelectedValues(selectedFileIndexes); + }, + ), ), ), - Row( - children: [ - Text('fileSelectedCount'.tr), - Text( - selectedFileCount.toString(), - style: Theme.of(context).textTheme.bodySmall, - ), - const SizedBox(width: 16), - Text('fileSelectedSize'.tr), - Text( - selectedFileCount > 0 && selectdFileSize == 0 - ? 'unknown'.tr - : Util.fmtByte(selectdFileSize), - style: Theme.of(context).textTheme.bodySmall, - ), - ], + Flexible( + flex: 6, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text('fileSelectedCount'.tr), + Text( + selectedFileCount.toString(), + style: Theme.of(context).textTheme.bodySmall, + ), + const SizedBox(width: 12), + Text('fileSelectedSize'.tr), + Text( + selectedFileCount > 0 && selectedFileSize == 0 + ? 'unknown'.tr + : Util.fmtByte(selectedFileSize), + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), ), ], ), @@ -241,7 +249,7 @@ class _FileTreeViewState extends State { return size > 0 ? Text(Util.fmtByte(calcSelectedSize(node)), style: Theme.of(context).textTheme.bodySmall) - : const SizedBox(); + : const SizedBox(width: _sizeGapWidth); }, children: [], ); @@ -265,7 +273,7 @@ class _FileTreeViewState extends State { return file.size > 0 ? Text(Util.fmtByte(file.size), style: Theme.of(context).textTheme.bodySmall) - : const SizedBox(); + : const SizedBox(width: _sizeGapWidth); }, isSelected: widget.initialValues.contains(i), children: [], diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index a0a719b9c..65efd339d 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -149,10 +149,10 @@ packages: dependency: "direct main" description: name: checkable_treeview - sha256: e38cf0a1088b803707a1f0b271ac46210108aac81c2dad506d0d56e7932ab690 + sha256: "3d7543e2c93a968864a57ca0666fc3b5827d89d13b22cb637a427596032f9272" url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.3.1" checked_yaml: dependency: transitive description: diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index 9ba4cb490..7d7891c59 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -68,7 +68,7 @@ dependencies: toggle_switch: ^2.3.0 permission_handler: ^11.3.1 device_info_plus: ^9.1.2 - checkable_treeview: ^1.3.0 + checkable_treeview: ^1.3.1 dependency_overrides: permission_handler_windows: git: From f54ed5df5a453e335de0997918535c979dd182a6 Mon Sep 17 00:00:00 2001 From: Levi Date: Fri, 27 Dec 2024 18:52:23 +0800 Subject: [PATCH 3/3] feat: batch operation task (#860) --- ui/flutter/lib/api/api.dart | 25 ++- .../app/controllers/app_controller.dart | 4 +- .../task/controllers/task_controller.dart | 1 - .../task_downloading_controller.dart | 10 +- .../controllers/task_list_controller.dart | 1 + .../task/views/task_downloaded_view.dart | 3 +- .../task/views/task_downloading_view.dart | 3 +- .../lib/app/views/buid_task_list_view.dart | 203 +++++++++++++----- ui/flutter/lib/i18n/langs/en_us.dart | 5 +- ui/flutter/lib/i18n/langs/es_es.dart | 2 +- ui/flutter/lib/i18n/langs/fa_ir.dart | 2 +- ui/flutter/lib/i18n/langs/fr_fr.dart | 2 +- ui/flutter/lib/i18n/langs/id_id.dart | 2 +- ui/flutter/lib/i18n/langs/it_it.dart | 2 +- ui/flutter/lib/i18n/langs/ja_jp.dart | 2 +- ui/flutter/lib/i18n/langs/pl_pl.dart | 2 +- ui/flutter/lib/i18n/langs/ru_ru.dart | 2 +- ui/flutter/lib/i18n/langs/ta_ta.dart | 2 +- ui/flutter/lib/i18n/langs/tr_tr.dart | 2 +- ui/flutter/lib/i18n/langs/vi_vn.dart | 2 +- ui/flutter/lib/i18n/langs/zh_cn.dart | 5 +- ui/flutter/lib/i18n/langs/zh_tw.dart | 5 +- ui/flutter/pubspec.lock | 24 +++ ui/flutter/pubspec.yaml | 1 + 24 files changed, 239 insertions(+), 73 deletions(-) diff --git a/ui/flutter/lib/api/api.dart b/ui/flutter/lib/api/api.dart index a30408204..3d4d3d3ad 100644 --- a/ui/flutter/lib/api/api.dart +++ b/ui/flutter/lib/api/api.dart @@ -129,12 +129,20 @@ Future continueTask(String id) async { return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null); } -Future pauseAllTasks() async { - return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null); +Future pauseAllTasks(List? ids) async { + return _parse( + () => _client.dio.put("/api/v1/tasks/pause", queryParameters: { + "id": ids, + }), + null); } -Future continueAllTasks() async { - return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null); +Future continueAllTasks(List? ids) async { + return _parse( + () => _client.dio.put("/api/v1/tasks/continue", queryParameters: { + "id": ids, + }), + null); } Future deleteTask(String id, bool force) async { @@ -142,6 +150,15 @@ Future deleteTask(String id, bool force) async { () => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null); } +Future deleteTasks(List? ids, bool force) async { + return _parse( + () => _client.dio.delete("/api/v1/tasks", queryParameters: { + "id": ids, + "force": force, + }), + null); +} + Future getConfig() async { return _parse(() => _client.dio.get("/api/v1/config"), (data) => DownloaderConfig.fromJson(data)); diff --git a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart index f6afe3613..efec397d6 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -218,11 +218,11 @@ class AppController extends GetxController with WindowListener, TrayListener { ), MenuItem( label: "startAll".tr, - onClick: (menuItem) async => {continueAllTasks()}, + onClick: (menuItem) async => {continueAllTasks(null)}, ), MenuItem( label: "pauseAll".tr, - onClick: (menuItem) async => {pauseAllTasks()}, + onClick: (menuItem) async => {pauseAllTasks(null)}, ), MenuItem( label: 'setting'.tr, diff --git a/ui/flutter/lib/app/modules/task/controllers/task_controller.dart b/ui/flutter/lib/app/modules/task/controllers/task_controller.dart index f5e37c9e2..9258090f9 100644 --- a/ui/flutter/lib/app/modules/task/controllers/task_controller.dart +++ b/ui/flutter/lib/app/modules/task/controllers/task_controller.dart @@ -6,5 +6,4 @@ class TaskController extends GetxController { final tabIndex = 0.obs; final scaffoldKey = GlobalKey(); final selectTask = Rx(null); - final copyUrlDone = false.obs; } diff --git a/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart b/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart index 1f69ee0df..414b396b8 100644 --- a/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart +++ b/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart @@ -9,5 +9,13 @@ class TaskDownloadingController extends TaskListController { Status.pause, Status.wait, Status.error - ], (a, b) => b.createdAt.compareTo(a.createdAt)); + ], (a, b) { + if (a.status == Status.running && b.status != Status.running) { + return -1; + } else if (a.status != Status.running && b.status == Status.running) { + return 1; + } else { + return b.updatedAt.compareTo(a.updatedAt); + } + }); } diff --git a/ui/flutter/lib/app/modules/task/controllers/task_list_controller.dart b/ui/flutter/lib/app/modules/task/controllers/task_list_controller.dart index bca359678..19c240ee7 100644 --- a/ui/flutter/lib/app/modules/task/controllers/task_list_controller.dart +++ b/ui/flutter/lib/app/modules/task/controllers/task_list_controller.dart @@ -12,6 +12,7 @@ abstract class TaskListController extends GetxController { TaskListController(this.statuses, this.compare); final tasks = [].obs; + final selectedTaskIds = [].obs; final isRunning = false.obs; late final Timer _timer; diff --git a/ui/flutter/lib/app/modules/task/views/task_downloaded_view.dart b/ui/flutter/lib/app/modules/task/views/task_downloaded_view.dart index 331f49df5..22dcb5715 100644 --- a/ui/flutter/lib/app/modules/task/views/task_downloaded_view.dart +++ b/ui/flutter/lib/app/modules/task/views/task_downloaded_view.dart @@ -9,6 +9,7 @@ class TaskDownloadedView extends GetView { @override Widget build(BuildContext context) { - return BuildTaskListView(tasks: controller.tasks); + return BuildTaskListView( + tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds); } } diff --git a/ui/flutter/lib/app/modules/task/views/task_downloading_view.dart b/ui/flutter/lib/app/modules/task/views/task_downloading_view.dart index ae6a36d0d..dbb74a88e 100644 --- a/ui/flutter/lib/app/modules/task/views/task_downloading_view.dart +++ b/ui/flutter/lib/app/modules/task/views/task_downloading_view.dart @@ -9,6 +9,7 @@ class TaskDownloadingView extends GetView { @override Widget build(BuildContext context) { - return BuildTaskListView(tasks: controller.tasks); + return BuildTaskListView( + tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds); } } diff --git a/ui/flutter/lib/app/views/buid_task_list_view.dart b/ui/flutter/lib/app/views/buid_task_list_view.dart index 41b3ab927..6d6d1007b 100644 --- a/ui/flutter/lib/app/views/buid_task_list_view.dart +++ b/ui/flutter/lib/app/views/buid_task_list_view.dart @@ -1,3 +1,4 @@ +import 'package:contextmenu/contextmenu.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:styled_widget/styled_widget.dart'; @@ -8,16 +9,20 @@ import '../../util/message.dart'; import '../../util/util.dart'; import '../modules/app/controllers/app_controller.dart'; import '../modules/task/controllers/task_controller.dart'; +import '../modules/task/controllers/task_downloaded_controller.dart'; +import '../modules/task/controllers/task_downloading_controller.dart'; import '../modules/task/views/task_view.dart'; import '../routes/app_pages.dart'; import 'file_icon.dart'; class BuildTaskListView extends GetView { final List tasks; + final List selectedTaskIds; const BuildTaskListView({ Key? key, required this.tasks, + required this.selectedTaskIds, }) : super(key: key); @override @@ -56,11 +61,15 @@ class BuildTaskListView extends GetView { return task.status == Status.running; } + bool isSelect() { + return selectedTaskIds.contains(task.id); + } + bool isFolderTask() { return task.isFolder; } - Future showDeleteDialog(String id) { + Future showDeleteDialog(List ids) { final appController = Get.find(); final context = Get.context!; @@ -69,7 +78,8 @@ class BuildTaskListView extends GetView { context: context, barrierDismissible: false, builder: (_) => AlertDialog( - title: Text('deleteTask'.tr), + title: Text( + 'deleteTask'.trParams({'count': ids.length.toString()})), content: Obx(() => CheckboxListTile( value: appController .downloaderConfig.value.extra.lastDeleteTaskKeep, @@ -95,7 +105,7 @@ class BuildTaskListView extends GetView { final force = !appController .downloaderConfig.value.extra.lastDeleteTaskKeep; await appController.saveConfig(); - await deleteTask(id, force); + await deleteTasks(ids, force); Get.back(); } catch (e) { showErrorMessage(e); @@ -143,12 +153,35 @@ class BuildTaskListView extends GetView { list.add(IconButton( icon: const Icon(Icons.delete), onPressed: () { - showDeleteDialog(task.id); + showDeleteDialog([task.id]); }, )); return list; } + Widget buildContextItem(IconData icon, String label, Function() onTap, + {bool enabled = true}) { + return ListTile( + dense: true, + visualDensity: const VisualDensity(vertical: -1), + minLeadingWidth: 12, + leading: Icon(icon, size: 18), + title: Text(label, + style: const TextStyle( + fontWeight: FontWeight.bold, // Make the text bold + )), + onTap: () async { + Get.back(); + try { + await onTap(); + } catch (e) { + showErrorMessage(e); + } + }, + enabled: enabled, + ); + } + double getProgress() { final totalSize = task.meta.res?.size ?? 0; return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize; @@ -167,55 +200,127 @@ class BuildTaskListView extends GetView { } final taskController = Get.find(); + final taskListController = taskController.tabIndex.value == 0 + ? Get.find() + : Get.find(); - return Card( - elevation: 4.0, - child: InkWell( - onTap: () { - taskController.scaffoldKey.currentState?.openEndDrawer(); - taskController.selectTask.value = task; - }, - onDoubleTap: () { - task.open(); - }, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - title: Text(task.name), - leading: Icon( - fileIcon(task.name, - isFolder: isFolderTask(), - isBitTorrent: task.protocol == Protocol.bt), - )), - Row( + // Filter selected task ids that are still in the task list + filterSelectedTaskIds(Iterable selectedTaskIds) => selectedTaskIds + .where((id) => tasks.any((task) => task.id == id)) + .toList(); + + return ContextMenuArea( + width: 140, + builder: (context) => [ + buildContextItem(Icons.checklist, 'selectAll'.tr, () { + if (tasks.isEmpty) return; + + if (selectedTaskIds.isNotEmpty) { + taskListController.selectedTaskIds([]); + } else { + taskListController.selectedTaskIds(tasks.map((e) => e.id).toList()); + } + }), + buildContextItem(Icons.check, 'select'.tr, () { + if (isSelect()) { + taskListController.selectedTaskIds(taskListController + .selectedTaskIds + .where((element) => element != task.id) + .toList()); + } else { + taskListController.selectedTaskIds( + [...taskListController.selectedTaskIds, task.id]); + } + }), + const Divider( + indent: 8, + endIndent: 8, + ), + buildContextItem(Icons.play_arrow, 'continue'.tr, () async { + try { + await continueAllTasks(filterSelectedTaskIds( + {...taskListController.selectedTaskIds, task.id})); + } finally { + taskListController.selectedTaskIds([]); + } + }, enabled: !isDone() && !isRunning()), + buildContextItem(Icons.pause, 'pause'.tr, () async { + try { + await pauseAllTasks(filterSelectedTaskIds( + {...taskListController.selectedTaskIds, task.id})); + } finally { + taskListController.selectedTaskIds([]); + } + }, enabled: !isDone() && isRunning()), + buildContextItem(Icons.delete, 'delete'.tr, () async { + try { + await showDeleteDialog(filterSelectedTaskIds( + {...taskListController.selectedTaskIds, task.id})); + } finally { + taskListController.selectedTaskIds([]); + } + }), + ], + child: Obx( + () => Card( + elevation: 4.0, + shape: isSelect() + ? RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8.0), + side: BorderSide( + color: Theme.of(context).colorScheme.primary, + width: 2.0, + ), + ) + : null, + child: InkWell( + onTap: () { + taskController.scaffoldKey.currentState?.openEndDrawer(); + taskController.selectTask.value = task; + }, + onDoubleTap: () { + task.open(); + }, + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Expanded( - flex: 1, - child: Text( - getProgressText(), - style: Get.textTheme.bodyLarge - ?.copyWith(color: Get.theme.disabledColor), - ).padding(left: 18)), - Expanded( - flex: 1, - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text("${Util.fmtByte(task.progress.speed)} / s", - style: Get.textTheme.titleSmall), - ...buildActions() - ], + ListTile( + title: Text(task.name), + leading: Icon( + fileIcon(task.name, + isFolder: isFolderTask(), + isBitTorrent: task.protocol == Protocol.bt), )), + Row( + children: [ + Expanded( + flex: 1, + child: Text( + getProgressText(), + style: Get.textTheme.bodyLarge + ?.copyWith(color: Get.theme.disabledColor), + ).padding(left: 18)), + Expanded( + flex: 1, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text("${Util.fmtByte(task.progress.speed)} / s", + style: Get.textTheme.titleSmall), + ...buildActions() + ], + )), + ], + ), + isDone() + ? Container() + : LinearProgressIndicator( + value: getProgress(), + ), ], ), - isDone() - ? Container() - : LinearProgressIndicator( - value: getProgress(), - ), - ], - ), - )).padding(horizontal: 14, top: 8); + )).padding(horizontal: 14, top: 8), + ), + ); } } diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index 7a2656da3..4c968cd7b 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -8,6 +8,7 @@ const enUS = { 'on': 'On', 'off': 'Off', 'selectAll': 'Select All', + 'select': 'Select', 'task': 'Tasks', 'downloading': 'downloading', 'downloaded': 'downloaded', @@ -64,9 +65,11 @@ const enUS = { 'developer': 'Developer', 'logDirectory': 'Log Directory', 'show': 'Show', + 'continue': 'Continue', + 'pause': 'Pause', 'startAll': 'Start All', 'pauseAll': 'Pause All', - 'deleteTask': 'Delete Task', + 'deleteTask': 'Delete @count tasks', 'deleteTaskTip': 'Keep downloaded files', 'delete': 'Delete', 'newVersionTitle': 'Discover new version @version', diff --git a/ui/flutter/lib/i18n/langs/es_es.dart b/ui/flutter/lib/i18n/langs/es_es.dart index 22219bc77..8fa896155 100644 --- a/ui/flutter/lib/i18n/langs/es_es.dart +++ b/ui/flutter/lib/i18n/langs/es_es.dart @@ -63,7 +63,7 @@ const esES = { 'show': 'Mostrar', 'startAll': 'Iniciar Todo', 'pauseAll': 'Pausar Todo', - 'deleteTask': 'Eliminar Tarea', + 'deleteTask': 'Eliminar @count tareas', 'deleteTaskTip': 'Mantener archivos descargados', 'delete': 'Eliminar', 'newVersionTitle': 'Nueva versión @version disponible', diff --git a/ui/flutter/lib/i18n/langs/fa_ir.dart b/ui/flutter/lib/i18n/langs/fa_ir.dart index b9825baaa..96815ef2f 100644 --- a/ui/flutter/lib/i18n/langs/fa_ir.dart +++ b/ui/flutter/lib/i18n/langs/fa_ir.dart @@ -51,7 +51,7 @@ const faIR = { 'effectAfterRestart': 'Effect after restart', 'startAll': 'شروع همه', 'pauseAll': 'توقف همه', - 'deleteTask': 'پاک کردن کار', + 'deleteTask': 'حذف @count کار', 'deleteTaskTip': 'فایل های دانلود شده را نگه دارد', 'delete': 'پاک کردن', 'newVersionTitle': '@version عنوان: کشف نسخه جدید', diff --git a/ui/flutter/lib/i18n/langs/fr_fr.dart b/ui/flutter/lib/i18n/langs/fr_fr.dart index e5e94c3da..75f630ff3 100644 --- a/ui/flutter/lib/i18n/langs/fr_fr.dart +++ b/ui/flutter/lib/i18n/langs/fr_fr.dart @@ -61,7 +61,7 @@ const frFR = { 'show': 'Afficher', 'startAll': 'Tout démarrer', 'pauseAll': 'Tout suspendre', - 'deleteTask': 'Supprimer la tâche', + 'deleteTask': 'Supprimer @count tâches', 'deleteTaskTip': 'Conserver les fichiers téléchargés', 'delete': 'Supprimer', 'newVersionTitle': 'Nouvelle version @version disponible', diff --git a/ui/flutter/lib/i18n/langs/id_id.dart b/ui/flutter/lib/i18n/langs/id_id.dart index c58a4ed19..3b11b3a06 100644 --- a/ui/flutter/lib/i18n/langs/id_id.dart +++ b/ui/flutter/lib/i18n/langs/id_id.dart @@ -67,7 +67,7 @@ const idID = { 'show': 'Tampilkan', 'startAll': 'Mulai Semua', 'pauseAll': 'Jeda Semua', - 'deleteTask': 'Hapus Tugas', + 'deleteTask': 'Hapus @count Tugas', 'deleteTaskTip': 'Simpan file yang terunduh', 'delete': 'Hapus', 'newVersionTitle': 'Temukan versi batu @version', diff --git a/ui/flutter/lib/i18n/langs/it_it.dart b/ui/flutter/lib/i18n/langs/it_it.dart index 3c53c31d6..a6e9ffb58 100644 --- a/ui/flutter/lib/i18n/langs/it_it.dart +++ b/ui/flutter/lib/i18n/langs/it_it.dart @@ -64,7 +64,7 @@ const itIT = { 'show': 'Mostra', 'startAll': 'Avvia tutti', 'pauseAll': 'Mtti in pausa tutti', - 'deleteTask': 'Elimina attività', + 'deleteTask': 'Elimina @count attività', 'deleteTaskTip': 'Conserva i file scaricati', 'delete': 'Elimina', 'newVersionTitle': 'Scopri la nuova versione @version', diff --git a/ui/flutter/lib/i18n/langs/ja_jp.dart b/ui/flutter/lib/i18n/langs/ja_jp.dart index 26bc3111a..26d999dae 100644 --- a/ui/flutter/lib/i18n/langs/ja_jp.dart +++ b/ui/flutter/lib/i18n/langs/ja_jp.dart @@ -53,7 +53,7 @@ const jaJP = { 'effectAfterRestart': '再起動後の効果', 'startAll': 'すべてを開始', 'pauseAll': 'すべてを一時停止', - 'deleteTask': 'タスクを削除', + 'deleteTask': '@count タスクを削除', 'deleteTaskTip': 'ダウンロードしたファイルを保持', 'delete': '削除', 'newVersionTitle': '新しいバージョン @version を発見する', diff --git a/ui/flutter/lib/i18n/langs/pl_pl.dart b/ui/flutter/lib/i18n/langs/pl_pl.dart index 6327da1c4..c346dde8a 100644 --- a/ui/flutter/lib/i18n/langs/pl_pl.dart +++ b/ui/flutter/lib/i18n/langs/pl_pl.dart @@ -62,7 +62,7 @@ const plPL = { 'show': 'Pokaż', 'startAll': 'Zacznij wszystkie', 'pauseAll': 'Zatrzymaj wszystkie', - 'deleteTask': 'Usuń zadanie', + 'deleteTask': 'Usuń @count zadania', 'deleteTaskTip': 'Zachowaj pobrane pliki', 'delete': 'Usuń', 'newVersionTitle': 'Sprawdź aktualizację @version', diff --git a/ui/flutter/lib/i18n/langs/ru_ru.dart b/ui/flutter/lib/i18n/langs/ru_ru.dart index e5a6f3650..eb6cd998d 100644 --- a/ui/flutter/lib/i18n/langs/ru_ru.dart +++ b/ui/flutter/lib/i18n/langs/ru_ru.dart @@ -57,7 +57,7 @@ const ruRU = { 'effectAfterRestart': 'Эффект после перезагрузки', 'startAll': 'Запустить все', 'pauseAll': 'Приостановить все', - 'deleteTask': 'Удалить задачу', + 'deleteTask': 'Удалить @count задач', 'deleteTaskTip': 'Сохранить загруженные файлы', 'delete': 'Удалить', 'newVersionTitle': 'Обнаружена новая версия @version', diff --git a/ui/flutter/lib/i18n/langs/ta_ta.dart b/ui/flutter/lib/i18n/langs/ta_ta.dart index e9e78338a..b720f9101 100644 --- a/ui/flutter/lib/i18n/langs/ta_ta.dart +++ b/ui/flutter/lib/i18n/langs/ta_ta.dart @@ -63,7 +63,7 @@ const taTA = { 'show': 'காட்டு', 'startAll': 'அனைத்தையும் தொடங்கு', 'pauseAll': 'அனைத்தையும் நிறுத்திவை', - 'deleteTask': 'பணியை நீக்கு', + 'deleteTask': 'பணிகளை நீக்கு @count', 'deleteTaskTip': 'பதிவிறக்கம் செய்யப்பட்ட கோப்புகளை வைத்திரு', 'delete': 'நீக்கு', 'newVersionTitle': 'புதிய பதிப்பைக் கண்டறியவும் @version', diff --git a/ui/flutter/lib/i18n/langs/tr_tr.dart b/ui/flutter/lib/i18n/langs/tr_tr.dart index 5c7912164..15138bbdd 100644 --- a/ui/flutter/lib/i18n/langs/tr_tr.dart +++ b/ui/flutter/lib/i18n/langs/tr_tr.dart @@ -65,7 +65,7 @@ const trTR = { 'show': 'Göster', 'startAll': 'Hepsini başlat', 'pauseAll': 'Hepsini durdur', - 'deleteTask': 'Görevleri sil', + 'deleteTask': '@count görevi sil', 'deleteTaskTip': 'İndirilen dosyaları sakla', 'delete': 'Sil', 'newVersionTitle': 'Yeni sürümü keşfet @version', diff --git a/ui/flutter/lib/i18n/langs/vi_vn.dart b/ui/flutter/lib/i18n/langs/vi_vn.dart index de182fa85..309fdf69b 100644 --- a/ui/flutter/lib/i18n/langs/vi_vn.dart +++ b/ui/flutter/lib/i18n/langs/vi_vn.dart @@ -61,7 +61,7 @@ const viVN = { 'effectAfterRestart': 'Hiệu lực sau khi khởi động lại', 'startAll': 'Bắt đầu tất cả', 'pauseAll': 'Tạm dừng tất cả', - 'deleteTask': 'Xóa nhiệm vụ', + 'deleteTask': 'Xóa @count nhiệm vụ', 'deleteTaskTip': 'Giữ các tệp đã tải về', 'delete': 'Xóa', 'newVersionTitle': 'Khám phá phiên bản mới @version', diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index b408ff145..0a8ffa661 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -8,6 +8,7 @@ const zhCN = { 'on': '开启', 'off': '关闭', 'selectAll': '全选', + 'select': '选择', 'task': '任务', 'downloading': '下载中', 'downloaded': '已完成', @@ -63,9 +64,11 @@ const zhCN = { 'developer': '开发者', 'logDirectory': '日志目录', 'show': '显示', + 'continue': '继续', + 'pause': '暂停', 'startAll': '全部开始', 'pauseAll': '全部暂停', - 'deleteTask': '删除任务', + 'deleteTask': '删除 @count 个任务', 'deleteTaskTip': '保留已下载的文件', 'delete': '删除', 'newVersionTitle': '发现新版本 @version', diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 40f47750a..cf8bf0748 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -8,6 +8,7 @@ const zhTW = { 'on': '開啟', 'off': '關閉', 'selectAll': '全選', + 'select': '選擇', 'task': '任務', 'downloading': '下載中', 'downloaded': '已下載', @@ -63,9 +64,11 @@ const zhTW = { 'developer': '開發者', 'logDirectory': '日誌目錄', 'show': '顯示', + 'continue': '繼續', + 'pause': '暫停', 'startAll': '全部開始', 'pauseAll': '全部暫停', - 'deleteTask': '刪除任務', + 'deleteTask': '刪除 @count 個任務', 'deleteTaskTip': '保留已下載的檔案', 'delete': '刪除', 'newVersionTitle': '發現新版本 @version', diff --git a/ui/flutter/pubspec.lock b/ui/flutter/pubspec.lock index 65efd339d..7a09d9fdd 100644 --- a/ui/flutter/pubspec.lock +++ b/ui/flutter/pubspec.lock @@ -9,6 +9,14 @@ packages: url: "https://pub.dev" source: hosted version: "64.0.0" + after_layout: + dependency: transitive + description: + name: after_layout + sha256: "95a1cb2ca1464f44f14769329fbf15987d20ab6c88f8fc5d359bd362be625f29" + url: "https://pub.dev" + source: hosted + version: "1.2.0" analyzer: dependency: transitive description: @@ -17,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.2.0" + animations: + dependency: transitive + description: + name: animations + sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb + url: "https://pub.dev" + source: hosted + version: "2.0.11" app_links: dependency: "direct main" description: @@ -201,6 +217,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + contextmenu: + dependency: "direct main" + description: + name: contextmenu + sha256: e0c7d60e2fc9f316f5b03f5fe2c0f977d65125345d1a1f77eea02be612e32d0c + url: "https://pub.dev" + source: hosted + version: "3.0.0" convert: dependency: transitive description: diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index 7d7891c59..0e2029c4f 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: permission_handler: ^11.3.1 device_info_plus: ^9.1.2 checkable_treeview: ^1.3.1 + contextmenu: ^3.0.0 dependency_overrides: permission_handler_windows: git: