From eb9d966855a135b4f1884f4fa6bc6fe55a00121f Mon Sep 17 00:00:00 2001 From: Li Wei Date: Fri, 9 Jun 2023 22:15:59 +0800 Subject: [PATCH] feat: support start and pause all tasks (#121) --- pkg/download/downloader.go | 36 ++++++++++++++++ pkg/rest/api.go | 16 +++++++ pkg/rest/server.go | 2 + pkg/rest/server_test.go | 43 +++++++++++++++++++ ui/flutter/assets/locales/en_US.json | 2 + ui/flutter/assets/locales/fa_IR.json | 2 + ui/flutter/assets/locales/ru_RU.json | 2 + ui/flutter/assets/locales/zh_CN.json | 2 + ui/flutter/lib/api/api.dart | 8 ++++ .../app/controllers/app_controller.dart | 14 ++++++ .../create/controllers/create_controller.dart | 1 + .../app/modules/create/views/create_view.dart | 4 +- ui/flutter/lib/generated/locales.g.dart | 10 +++++ 13 files changed, 141 insertions(+), 1 deletion(-) diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 8a5d576a..e0a01791 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -7,6 +7,7 @@ import ( "github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/util" gonanoid "github.com/matoous/go-nanoid/v2" + "math" "os" "path" "sort" @@ -332,6 +333,41 @@ func (d *Downloader) Continue(id string) (err error) { return d.doContinue(task) } +func (d *Downloader) PauseAll() (err error) { + d.lock.Lock() + defer d.lock.Unlock() + for _, task := range d.tasks { + if task.Status == base.DownloadStatusRunning { + if err = d.doPause(task, base.DownloadStatusPause); err != nil { + return + } + } + } + return +} + +func (d *Downloader) ContinueAll() (err error) { + d.lock.Lock() + defer d.lock.Unlock() + runningCount := 0 + continueTasks := make([]*Task, 0) + for _, task := range d.tasks { + if task.Status == base.DownloadStatusRunning { + runningCount++ + } else if task.Status != base.DownloadStatusDone { + continueTasks = append(continueTasks, task) + } + } + // calculate how many tasks can be continued, can't exceed maxRunning + continueCount := int(math.Min(float64(d.cfg.MaxRunning-runningCount), float64(len(continueTasks)))) + for i := 0; i < continueCount; i++ { + if err = d.doContinue(continueTasks[i]); err != nil { + return + } + } + return +} + func (d *Downloader) Delete(id string, force bool) (err error) { d.lock.Lock() defer d.lock.Unlock() diff --git a/pkg/rest/api.go b/pkg/rest/api.go index a3d7556b..4443e7b3 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -74,6 +74,22 @@ func ContinueTask(w http.ResponseWriter, r *http.Request) { WriteJson(w, model.NewNilResult()) } +func PauseAllTask(w http.ResponseWriter, r *http.Request) { + if err := Downloader.PauseAll(); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) +} + +func ContinueAllTask(w http.ResponseWriter, r *http.Request) { + if err := Downloader.ContinueAll(); err != nil { + WriteJson(w, model.NewErrorResult(err.Error())) + return + } + WriteJson(w, model.NewNilResult()) +} + func DeleteTask(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r) taskId := vars["id"] diff --git a/pkg/rest/server.go b/pkg/rest/server.go index 86e50fd3..2e83fa74 100644 --- a/pkg/rest/server.go +++ b/pkg/rest/server.go @@ -85,6 +85,8 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask) r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask) r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask) + r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask) + r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask) r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask) r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask) r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks) diff --git a/pkg/rest/server_test.go b/pkg/rest/server_test.go index cb3d4da2..afda4b2f 100644 --- a/pkg/rest/server_test.go +++ b/pkg/rest/server_test.go @@ -144,6 +144,39 @@ func TestPauseAndContinueTask(t *testing.T) { }) } +func TestPauseAllAndContinueALLTasks(t *testing.T) { + doTest(func() { + cfg, err := Downloader.GetConfig() + if err != nil { + t.Fatal(err) + } + + createAndPause := func() { + taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq) + httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil) + } + + total := cfg.MaxRunning + 2 + for i := 0; i < total; i++ { + createAndPause() + } + + // continue all + httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil) + tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil) + if len(tasks) != cfg.MaxRunning { + t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning) + } + // pause all + httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil) + tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s,%s", base.DownloadStatusReady, base.DownloadStatusPause), nil) + if len(tasks) != total { + t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total) + } + + }) +} + func TestDeleteTask(t *testing.T) { doTest(func() { taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq) @@ -275,6 +308,16 @@ func doTest(handler func()) { Stop() }() defer Downloader.Clear() + defer func() { + tasks := Downloader.GetTasks() + taskIds := make([]string, len(tasks)) + for i, task := range tasks { + taskIds[i] = task.ID + } + for _, id := range taskIds { + Downloader.Delete(id, true) + } + }() taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName handler() } diff --git a/ui/flutter/assets/locales/en_US.json b/ui/flutter/assets/locales/en_US.json index c08c18e2..47f64279 100644 --- a/ui/flutter/assets/locales/en_US.json +++ b/ui/flutter/assets/locales/en_US.json @@ -50,6 +50,8 @@ "notSet": "NS", "set": "SET", "effectAfterRestart": "Effect after restart", + "startAll": "Start All", + "pauseAll": "Pause All", "deleteTask": "Delete Task", "deleteTaskTip": "Keep downloaded files", "delete": "Delete", diff --git a/ui/flutter/assets/locales/fa_IR.json b/ui/flutter/assets/locales/fa_IR.json index a5038654..f3e08920 100644 --- a/ui/flutter/assets/locales/fa_IR.json +++ b/ui/flutter/assets/locales/fa_IR.json @@ -47,6 +47,8 @@ "notSet": "NS", "set": "SET", "effectAfterRestart": "Effect after restart", + "startAll": "شروع همه", + "pauseAll": "توقف همه", "deleteTask": "پاک کردن کار", "deleteTaskTip": "فایل های دانلود شده را نگه دارد", "delete": "پاک کردن", diff --git a/ui/flutter/assets/locales/ru_RU.json b/ui/flutter/assets/locales/ru_RU.json index 34087cc9..72db02d1 100644 --- a/ui/flutter/assets/locales/ru_RU.json +++ b/ui/flutter/assets/locales/ru_RU.json @@ -50,6 +50,8 @@ "notSet": "Не установлено", "set": "Установлено", "effectAfterRestart": "Эффект после перезагрузки", + "startAll": "Запустить все", + "pauseAll": "Приостановить все", "deleteTask": "Удалить задачу", "deleteTaskTip": "Сохранить загруженные файлы", "delete": "Удалить", diff --git a/ui/flutter/assets/locales/zh_CN.json b/ui/flutter/assets/locales/zh_CN.json index 218dd168..16f5e78d 100644 --- a/ui/flutter/assets/locales/zh_CN.json +++ b/ui/flutter/assets/locales/zh_CN.json @@ -50,6 +50,8 @@ "notSet": "未设置", "set": "已设置", "effectAfterRestart": "此配置项将在重启应用后生效", + "startAll": "全部开始", + "pauseAll": "全部暂停", "deleteTask": "删除任务", "deleteTaskTip": "保留已下载的文件", "delete": "删除", diff --git a/ui/flutter/lib/api/api.dart b/ui/flutter/lib/api/api.dart index 3e9c3a06..421d7986 100644 --- a/ui/flutter/lib/api/api.dart +++ b/ui/flutter/lib/api/api.dart @@ -111,6 +111,14 @@ 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 continueAllTasks() async { + return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null); +} + Future deleteTask(String id, bool force) async { return _parse( () => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null); 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 fded8bfd..3fb1b09f 100644 --- a/ui/flutter/lib/app/modules/app/controllers/app_controller.dart +++ b/ui/flutter/lib/app/modules/app/controllers/app_controller.dart @@ -118,6 +118,11 @@ class AppController extends GetxController with WindowListener, TrayListener { } Future _initDeepLinks() async { + // currently only support android + if (!Util.isAndroid()) { + return; + } + _appLinks = AppLinks(); // Handle link when app is in warm state (front or background) @@ -159,6 +164,15 @@ class AppController extends GetxController with WindowListener, TrayListener { await Get.rootDelegate.offAndToNamed(Routes.CREATE), }, ), + MenuItem.separator(), + MenuItem( + label: "startAll".tr, + onClick: (menuItem) async => {continueAllTasks()}, + ), + MenuItem( + label: "pauseAll".tr, + onClick: (menuItem) async => {pauseAllTasks()}, + ), MenuItem( label: 'setting'.tr, onClick: (menuItem) async => { diff --git a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart index 17ff97aa..7cee16f1 100644 --- a/ui/flutter/lib/app/modules/create/controllers/create_controller.dart +++ b/ui/flutter/lib/app/modules/create/controllers/create_controller.dart @@ -5,4 +5,5 @@ class CreateController extends GetxController { final RxList fileInfos = [].obs; final RxList selectedIndexes = [].obs; final RxList openedFolders = [].obs; + final isResolving = false.obs; } diff --git a/ui/flutter/lib/app/modules/create/views/create_view.dart b/ui/flutter/lib/app/modules/create/views/create_view.dart index 07df545d..0c28a18b 100644 --- a/ui/flutter/lib/app/modules/create/views/create_view.dart +++ b/ui/flutter/lib/app/modules/create/views/create_view.dart @@ -115,9 +115,10 @@ class CreateView extends GetView { } Future _doResolve() async { - if (_confirmController.currentState == ButtonState.loading) { + if (controller.isResolving.value) { return; } + controller.isResolving.value = true; try { _confirmController.start(); if (_resolveFormKey.currentState!.validate()) { @@ -130,6 +131,7 @@ class CreateView extends GetView { showErrorMessage(e); } finally { _confirmController.reset(); + controller.isResolving.value = false; } } diff --git a/ui/flutter/lib/generated/locales.g.dart b/ui/flutter/lib/generated/locales.g.dart index 1fc112b8..810fb05c 100644 --- a/ui/flutter/lib/generated/locales.g.dart +++ b/ui/flutter/lib/generated/locales.g.dart @@ -64,6 +64,8 @@ class LocaleKeys { static const notSet = 'notSet'; static const set = 'set'; static const effectAfterRestart = 'effectAfterRestart'; + static const startAll = 'startAll'; + static const pauseAll = 'pauseAll'; static const deleteTask = 'deleteTask'; static const deleteTaskTip = 'deleteTaskTip'; static const delete = 'delete'; @@ -127,6 +129,8 @@ class Locales { 'notSet': 'NS', 'set': 'SET', 'effectAfterRestart': 'Effect after restart', + 'startAll': 'Start All', + 'pauseAll': 'Pause All', 'deleteTask': 'Delete Task', 'deleteTaskTip': 'Keep downloaded files', 'delete': 'Delete', @@ -184,6 +188,8 @@ class Locales { 'notSet': 'NS', 'set': 'SET', 'effectAfterRestart': 'Effect after restart', + 'startAll': 'شروع همه', + 'pauseAll': 'توقف همه', 'deleteTask': 'پاک کردن کار', 'deleteTaskTip': 'فایل های دانلود شده را نگه دارد', 'delete': 'پاک کردن', @@ -245,6 +251,8 @@ class Locales { 'notSet': 'Не установлено', 'set': 'Установлено', 'effectAfterRestart': 'Эффект после перезагрузки', + 'startAll': 'Запустить все', + 'pauseAll': 'Приостановить все', 'deleteTask': 'Удалить задачу', 'deleteTaskTip': 'Сохранить загруженные файлы', 'delete': 'Удалить', @@ -304,6 +312,8 @@ class Locales { 'notSet': '未设置', 'set': '已设置', 'effectAfterRestart': '此配置项将在重启应用后生效', + 'startAll': '全部开始', + 'pauseAll': '全部暂停', 'deleteTask': '删除任务', 'deleteTaskTip': '保留已下载的文件', 'delete': '删除',