feat: batch operation task (#860)

This commit is contained in:
Levi authored and GitHub committed 2024-12-27 18:52:23 +08:00
1 parent 727b7edb84
commit f54ed5df5a
24 files changed
+239 -73

No files matched your search

+21 -4
View File
@@ -129,12 +129,20 @@ Future<void> continueTask(String id) async {
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
}
Future<void> pauseAllTasks() async {
return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null);
Future<void> pauseAllTasks(List<String>? ids) async {
return _parse(
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
"id": ids,
}),
null);
}
Future<void> continueAllTasks() async {
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
Future<void> continueAllTasks(List<String>? ids) async {
return _parse(
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
"id": ids,
}),
null);
}
Future<void> deleteTask(String id, bool force) async {
@@ -142,6 +150,15 @@ Future<void> deleteTask(String id, bool force) async {
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
}
Future<void> deleteTasks(List<String>? ids, bool force) async {
return _parse(
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
"id": ids,
"force": force,
}),
null);
}
Future<DownloaderConfig> getConfig() async {
return _parse(() => _client.dio.get("/api/v1/config"),
(data) => DownloaderConfig.fromJson(data));
@@ -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,
@@ -6,5 +6,4 @@ class TaskController extends GetxController {
final tabIndex = 0.obs;
final scaffoldKey = GlobalKey<ScaffoldState>();
final selectTask = Rx<Task?>(null);
final copyUrlDone = false.obs;
}
@@ -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);
}
});
}
@@ -12,6 +12,7 @@ abstract class TaskListController extends GetxController {
TaskListController(this.statuses, this.compare);
final tasks = <Task>[].obs;
final selectedTaskIds = <String>[].obs;
final isRunning = false.obs;
late final Timer _timer;
@@ -9,6 +9,7 @@ class TaskDownloadedView extends GetView<TaskDownloadedController> {
@override
Widget build(BuildContext context) {
return BuildTaskListView(tasks: controller.tasks);
return BuildTaskListView(
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
}
}
@@ -9,6 +9,7 @@ class TaskDownloadingView extends GetView<TaskDownloadingController> {
@override
Widget build(BuildContext context) {
return BuildTaskListView(tasks: controller.tasks);
return BuildTaskListView(
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
}
}
+154 -49
View File
@@ -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<Task> tasks;
final List<String> 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<void> showDeleteDialog(String id) {
Future<void> showDeleteDialog(List<String> ids) {
final appController = Get.find<AppController>();
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<TaskController>();
final taskListController = taskController.tabIndex.value == 0
? Get.find<TaskDownloadingController>()
: Get.find<TaskDownloadedController>();
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<String> 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),
),
);
}
}
+4 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -51,7 +51,7 @@ const faIR = {
'effectAfterRestart': 'Effect after restart',
'startAll': 'شروع همه',
'pauseAll': 'توقف همه',
'deleteTask': 'پاک کردن کار',
'deleteTask': 'حذف @count کار',
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
'delete': 'پاک کردن',
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -53,7 +53,7 @@ const jaJP = {
'effectAfterRestart': '再起動後の効果',
'startAll': 'すべてを開始',
'pauseAll': 'すべてを一時停止',
'deleteTask': 'タスクを削除',
'deleteTask': '@count タスクを削除',
'deleteTaskTip': 'ダウンロードしたファイルを保持',
'delete': '削除',
'newVersionTitle': '新しいバージョン @version を発見する',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -57,7 +57,7 @@ const ruRU = {
'effectAfterRestart': 'Эффект после перезагрузки',
'startAll': 'Запустить все',
'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить задачу',
'deleteTask': 'Удалить @count задач',
'deleteTaskTip': 'Сохранить загруженные файлы',
'delete': 'Удалить',
'newVersionTitle': 'Обнаружена новая версия @version',
+1 -1
View File
@@ -63,7 +63,7 @@ const taTA = {
'show': 'காட்டு',
'startAll': 'அனைத்தையும் தொடங்கு',
'pauseAll': 'அனைத்தையும் நிறுத்திவை',
'deleteTask': 'பணியை நீக்கு',
'deleteTask': 'பணிகளை நீக்கு @count',
'deleteTaskTip': 'பதிவிறக்கம் செய்யப்பட்ட கோப்புகளை வைத்திரு',
'delete': 'நீக்கு',
'newVersionTitle': 'புதிய பதிப்பைக் கண்டறியவும் @version',
+1 -1
View File
@@ -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',
+1 -1
View File
@@ -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',
+4 -1
View File
@@ -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',
+4 -1
View File
@@ -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',
+24
View File
@@ -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:
+1
View File
@@ -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: