Skip to content

Commit

Permalink
feat: batch operation task (#860)
Browse files Browse the repository at this point in the history
  • Loading branch information
monkeyWie authored Dec 27, 2024
1 parent 727b7ed commit f54ed5d
Show file tree
Hide file tree
Showing 24 changed files with 239 additions and 73 deletions.
25 changes: 21 additions & 4 deletions ui/flutter/lib/api/api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -129,19 +129,36 @@ 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 {
return _parse(
() => _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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
203 changes: 154 additions & 49 deletions ui/flutter/lib/app/views/buid_task_list_view.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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
Expand Down Expand Up @@ -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!;
Expand All @@ -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,
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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),
),
);
}
}
5 changes: 4 additions & 1 deletion ui/flutter/lib/i18n/langs/en_us.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const enUS = {
'on': 'On',
'off': 'Off',
'selectAll': 'Select All',
'select': 'Select',
'task': 'Tasks',
'downloading': 'downloading',
'downloaded': 'downloaded',
Expand Down Expand Up @@ -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',
Expand Down
Loading

0 comments on commit f54ed5d

Please sign in to comment.