Skip to content

Commit f54ed5d

Browse files
authored
feat: batch operation task (#860)
1 parent 727b7ed commit f54ed5d

24 files changed

+239
-73
lines changed

ui/flutter/lib/api/api.dart

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,19 +129,36 @@ Future<void> continueTask(String id) async {
129129
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
130130
}
131131

132-
Future<void> pauseAllTasks() async {
133-
return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null);
132+
Future<void> pauseAllTasks(List<String>? ids) async {
133+
return _parse(
134+
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
135+
"id": ids,
136+
}),
137+
null);
134138
}
135139

136-
Future<void> continueAllTasks() async {
137-
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
140+
Future<void> continueAllTasks(List<String>? ids) async {
141+
return _parse(
142+
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
143+
"id": ids,
144+
}),
145+
null);
138146
}
139147

140148
Future<void> deleteTask(String id, bool force) async {
141149
return _parse(
142150
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
143151
}
144152

153+
Future<void> deleteTasks(List<String>? ids, bool force) async {
154+
return _parse(
155+
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
156+
"id": ids,
157+
"force": force,
158+
}),
159+
null);
160+
}
161+
145162
Future<DownloaderConfig> getConfig() async {
146163
return _parse(() => _client.dio.get("/api/v1/config"),
147164
(data) => DownloaderConfig.fromJson(data));

ui/flutter/lib/app/modules/app/controllers/app_controller.dart

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,11 @@ class AppController extends GetxController with WindowListener, TrayListener {
218218
),
219219
MenuItem(
220220
label: "startAll".tr,
221-
onClick: (menuItem) async => {continueAllTasks()},
221+
onClick: (menuItem) async => {continueAllTasks(null)},
222222
),
223223
MenuItem(
224224
label: "pauseAll".tr,
225-
onClick: (menuItem) async => {pauseAllTasks()},
225+
onClick: (menuItem) async => {pauseAllTasks(null)},
226226
),
227227
MenuItem(
228228
label: 'setting'.tr,

ui/flutter/lib/app/modules/task/controllers/task_controller.dart

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,4 @@ class TaskController extends GetxController {
66
final tabIndex = 0.obs;
77
final scaffoldKey = GlobalKey<ScaffoldState>();
88
final selectTask = Rx<Task?>(null);
9-
final copyUrlDone = false.obs;
109
}

ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,13 @@ class TaskDownloadingController extends TaskListController {
99
Status.pause,
1010
Status.wait,
1111
Status.error
12-
], (a, b) => b.createdAt.compareTo(a.createdAt));
12+
], (a, b) {
13+
if (a.status == Status.running && b.status != Status.running) {
14+
return -1;
15+
} else if (a.status != Status.running && b.status == Status.running) {
16+
return 1;
17+
} else {
18+
return b.updatedAt.compareTo(a.updatedAt);
19+
}
20+
});
1321
}

ui/flutter/lib/app/modules/task/controllers/task_list_controller.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ abstract class TaskListController extends GetxController {
1212
TaskListController(this.statuses, this.compare);
1313

1414
final tasks = <Task>[].obs;
15+
final selectedTaskIds = <String>[].obs;
1516
final isRunning = false.obs;
1617

1718
late final Timer _timer;

ui/flutter/lib/app/modules/task/views/task_downloaded_view.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class TaskDownloadedView extends GetView<TaskDownloadedController> {
99

1010
@override
1111
Widget build(BuildContext context) {
12-
return BuildTaskListView(tasks: controller.tasks);
12+
return BuildTaskListView(
13+
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
1314
}
1415
}

ui/flutter/lib/app/modules/task/views/task_downloading_view.dart

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ class TaskDownloadingView extends GetView<TaskDownloadingController> {
99

1010
@override
1111
Widget build(BuildContext context) {
12-
return BuildTaskListView(tasks: controller.tasks);
12+
return BuildTaskListView(
13+
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
1314
}
1415
}

ui/flutter/lib/app/views/buid_task_list_view.dart

Lines changed: 154 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import 'package:contextmenu/contextmenu.dart';
12
import 'package:flutter/material.dart';
23
import 'package:get/get.dart';
34
import 'package:styled_widget/styled_widget.dart';
@@ -8,16 +9,20 @@ import '../../util/message.dart';
89
import '../../util/util.dart';
910
import '../modules/app/controllers/app_controller.dart';
1011
import '../modules/task/controllers/task_controller.dart';
12+
import '../modules/task/controllers/task_downloaded_controller.dart';
13+
import '../modules/task/controllers/task_downloading_controller.dart';
1114
import '../modules/task/views/task_view.dart';
1215
import '../routes/app_pages.dart';
1316
import 'file_icon.dart';
1417

1518
class BuildTaskListView extends GetView {
1619
final List<Task> tasks;
20+
final List<String> selectedTaskIds;
1721

1822
const BuildTaskListView({
1923
Key? key,
2024
required this.tasks,
25+
required this.selectedTaskIds,
2126
}) : super(key: key);
2227

2328
@override
@@ -56,11 +61,15 @@ class BuildTaskListView extends GetView {
5661
return task.status == Status.running;
5762
}
5863

64+
bool isSelect() {
65+
return selectedTaskIds.contains(task.id);
66+
}
67+
5968
bool isFolderTask() {
6069
return task.isFolder;
6170
}
6271

63-
Future<void> showDeleteDialog(String id) {
72+
Future<void> showDeleteDialog(List<String> ids) {
6473
final appController = Get.find<AppController>();
6574

6675
final context = Get.context!;
@@ -69,7 +78,8 @@ class BuildTaskListView extends GetView {
6978
context: context,
7079
barrierDismissible: false,
7180
builder: (_) => AlertDialog(
72-
title: Text('deleteTask'.tr),
81+
title: Text(
82+
'deleteTask'.trParams({'count': ids.length.toString()})),
7383
content: Obx(() => CheckboxListTile(
7484
value: appController
7585
.downloaderConfig.value.extra.lastDeleteTaskKeep,
@@ -95,7 +105,7 @@ class BuildTaskListView extends GetView {
95105
final force = !appController
96106
.downloaderConfig.value.extra.lastDeleteTaskKeep;
97107
await appController.saveConfig();
98-
await deleteTask(id, force);
108+
await deleteTasks(ids, force);
99109
Get.back();
100110
} catch (e) {
101111
showErrorMessage(e);
@@ -143,12 +153,35 @@ class BuildTaskListView extends GetView {
143153
list.add(IconButton(
144154
icon: const Icon(Icons.delete),
145155
onPressed: () {
146-
showDeleteDialog(task.id);
156+
showDeleteDialog([task.id]);
147157
},
148158
));
149159
return list;
150160
}
151161

162+
Widget buildContextItem(IconData icon, String label, Function() onTap,
163+
{bool enabled = true}) {
164+
return ListTile(
165+
dense: true,
166+
visualDensity: const VisualDensity(vertical: -1),
167+
minLeadingWidth: 12,
168+
leading: Icon(icon, size: 18),
169+
title: Text(label,
170+
style: const TextStyle(
171+
fontWeight: FontWeight.bold, // Make the text bold
172+
)),
173+
onTap: () async {
174+
Get.back();
175+
try {
176+
await onTap();
177+
} catch (e) {
178+
showErrorMessage(e);
179+
}
180+
},
181+
enabled: enabled,
182+
);
183+
}
184+
152185
double getProgress() {
153186
final totalSize = task.meta.res?.size ?? 0;
154187
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
@@ -167,55 +200,127 @@ class BuildTaskListView extends GetView {
167200
}
168201

169202
final taskController = Get.find<TaskController>();
203+
final taskListController = taskController.tabIndex.value == 0
204+
? Get.find<TaskDownloadingController>()
205+
: Get.find<TaskDownloadedController>();
170206

171-
return Card(
172-
elevation: 4.0,
173-
child: InkWell(
174-
onTap: () {
175-
taskController.scaffoldKey.currentState?.openEndDrawer();
176-
taskController.selectTask.value = task;
177-
},
178-
onDoubleTap: () {
179-
task.open();
180-
},
181-
child: Column(
182-
mainAxisSize: MainAxisSize.min,
183-
children: [
184-
ListTile(
185-
title: Text(task.name),
186-
leading: Icon(
187-
fileIcon(task.name,
188-
isFolder: isFolderTask(),
189-
isBitTorrent: task.protocol == Protocol.bt),
190-
)),
191-
Row(
207+
// Filter selected task ids that are still in the task list
208+
filterSelectedTaskIds(Iterable<String> selectedTaskIds) => selectedTaskIds
209+
.where((id) => tasks.any((task) => task.id == id))
210+
.toList();
211+
212+
return ContextMenuArea(
213+
width: 140,
214+
builder: (context) => [
215+
buildContextItem(Icons.checklist, 'selectAll'.tr, () {
216+
if (tasks.isEmpty) return;
217+
218+
if (selectedTaskIds.isNotEmpty) {
219+
taskListController.selectedTaskIds([]);
220+
} else {
221+
taskListController.selectedTaskIds(tasks.map((e) => e.id).toList());
222+
}
223+
}),
224+
buildContextItem(Icons.check, 'select'.tr, () {
225+
if (isSelect()) {
226+
taskListController.selectedTaskIds(taskListController
227+
.selectedTaskIds
228+
.where((element) => element != task.id)
229+
.toList());
230+
} else {
231+
taskListController.selectedTaskIds(
232+
[...taskListController.selectedTaskIds, task.id]);
233+
}
234+
}),
235+
const Divider(
236+
indent: 8,
237+
endIndent: 8,
238+
),
239+
buildContextItem(Icons.play_arrow, 'continue'.tr, () async {
240+
try {
241+
await continueAllTasks(filterSelectedTaskIds(
242+
{...taskListController.selectedTaskIds, task.id}));
243+
} finally {
244+
taskListController.selectedTaskIds([]);
245+
}
246+
}, enabled: !isDone() && !isRunning()),
247+
buildContextItem(Icons.pause, 'pause'.tr, () async {
248+
try {
249+
await pauseAllTasks(filterSelectedTaskIds(
250+
{...taskListController.selectedTaskIds, task.id}));
251+
} finally {
252+
taskListController.selectedTaskIds([]);
253+
}
254+
}, enabled: !isDone() && isRunning()),
255+
buildContextItem(Icons.delete, 'delete'.tr, () async {
256+
try {
257+
await showDeleteDialog(filterSelectedTaskIds(
258+
{...taskListController.selectedTaskIds, task.id}));
259+
} finally {
260+
taskListController.selectedTaskIds([]);
261+
}
262+
}),
263+
],
264+
child: Obx(
265+
() => Card(
266+
elevation: 4.0,
267+
shape: isSelect()
268+
? RoundedRectangleBorder(
269+
borderRadius: BorderRadius.circular(8.0),
270+
side: BorderSide(
271+
color: Theme.of(context).colorScheme.primary,
272+
width: 2.0,
273+
),
274+
)
275+
: null,
276+
child: InkWell(
277+
onTap: () {
278+
taskController.scaffoldKey.currentState?.openEndDrawer();
279+
taskController.selectTask.value = task;
280+
},
281+
onDoubleTap: () {
282+
task.open();
283+
},
284+
child: Column(
285+
mainAxisSize: MainAxisSize.min,
192286
children: [
193-
Expanded(
194-
flex: 1,
195-
child: Text(
196-
getProgressText(),
197-
style: Get.textTheme.bodyLarge
198-
?.copyWith(color: Get.theme.disabledColor),
199-
).padding(left: 18)),
200-
Expanded(
201-
flex: 1,
202-
child: Row(
203-
mainAxisAlignment: MainAxisAlignment.end,
204-
children: [
205-
Text("${Util.fmtByte(task.progress.speed)} / s",
206-
style: Get.textTheme.titleSmall),
207-
...buildActions()
208-
],
287+
ListTile(
288+
title: Text(task.name),
289+
leading: Icon(
290+
fileIcon(task.name,
291+
isFolder: isFolderTask(),
292+
isBitTorrent: task.protocol == Protocol.bt),
209293
)),
294+
Row(
295+
children: [
296+
Expanded(
297+
flex: 1,
298+
child: Text(
299+
getProgressText(),
300+
style: Get.textTheme.bodyLarge
301+
?.copyWith(color: Get.theme.disabledColor),
302+
).padding(left: 18)),
303+
Expanded(
304+
flex: 1,
305+
child: Row(
306+
mainAxisAlignment: MainAxisAlignment.end,
307+
children: [
308+
Text("${Util.fmtByte(task.progress.speed)} / s",
309+
style: Get.textTheme.titleSmall),
310+
...buildActions()
311+
],
312+
)),
313+
],
314+
),
315+
isDone()
316+
? Container()
317+
: LinearProgressIndicator(
318+
value: getProgress(),
319+
),
210320
],
211321
),
212-
isDone()
213-
? Container()
214-
: LinearProgressIndicator(
215-
value: getProgress(),
216-
),
217-
],
218-
),
219-
)).padding(horizontal: 14, top: 8);
322+
)).padding(horizontal: 14, top: 8),
323+
),
324+
);
220325
}
221326
}

ui/flutter/lib/i18n/langs/en_us.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ const enUS = {
88
'on': 'On',
99
'off': 'Off',
1010
'selectAll': 'Select All',
11+
'select': 'Select',
1112
'task': 'Tasks',
1213
'downloading': 'downloading',
1314
'downloaded': 'downloaded',
@@ -64,9 +65,11 @@ const enUS = {
6465
'developer': 'Developer',
6566
'logDirectory': 'Log Directory',
6667
'show': 'Show',
68+
'continue': 'Continue',
69+
'pause': 'Pause',
6770
'startAll': 'Start All',
6871
'pauseAll': 'Pause All',
69-
'deleteTask': 'Delete Task',
72+
'deleteTask': 'Delete @count tasks',
7073
'deleteTaskTip': 'Keep downloaded files',
7174
'delete': 'Delete',
7275
'newVersionTitle': 'Discover new version @version',

0 commit comments

Comments
 (0)