mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-14 20:33:41 +00:00
feat: support start and pause all tasks (#121)
This commit is contained in:
1 parent
0e3ca912a7
commit
eb9d966855
13 files changed
+141
-1
No files matched your search
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
@@ -332,6 +333,41 @@ func (d *Downloader) Continue(id string) (err error) {
|
|||||||
return d.doContinue(task)
|
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) {
|
func (d *Downloader) Delete(id string, force bool) (err error) {
|
||||||
d.lock.Lock()
|
d.lock.Lock()
|
||||||
defer d.lock.Unlock()
|
defer d.lock.Unlock()
|
||||||
|
|||||||
@@ -74,6 +74,22 @@ func ContinueTask(w http.ResponseWriter, r *http.Request) {
|
|||||||
WriteJson(w, model.NewNilResult())
|
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) {
|
func DeleteTask(w http.ResponseWriter, r *http.Request) {
|
||||||
vars := mux.Vars(r)
|
vars := mux.Vars(r)
|
||||||
taskId := vars["id"]
|
taskId := vars["id"]
|
||||||
|
|||||||
@@ -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.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}/pause").HandlerFunc(PauseTask)
|
||||||
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
|
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.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/{id}").HandlerFunc(GetTask)
|
||||||
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
|
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
|
||||||
|
|||||||
@@ -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) {
|
func TestDeleteTask(t *testing.T) {
|
||||||
doTest(func() {
|
doTest(func() {
|
||||||
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
|
||||||
@@ -275,6 +308,16 @@ func doTest(handler func()) {
|
|||||||
Stop()
|
Stop()
|
||||||
}()
|
}()
|
||||||
defer Downloader.Clear()
|
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
|
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
|
||||||
handler()
|
handler()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"notSet": "NS",
|
"notSet": "NS",
|
||||||
"set": "SET",
|
"set": "SET",
|
||||||
"effectAfterRestart": "Effect after restart",
|
"effectAfterRestart": "Effect after restart",
|
||||||
|
"startAll": "Start All",
|
||||||
|
"pauseAll": "Pause All",
|
||||||
"deleteTask": "Delete Task",
|
"deleteTask": "Delete Task",
|
||||||
"deleteTaskTip": "Keep downloaded files",
|
"deleteTaskTip": "Keep downloaded files",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
|
|||||||
@@ -47,6 +47,8 @@
|
|||||||
"notSet": "NS",
|
"notSet": "NS",
|
||||||
"set": "SET",
|
"set": "SET",
|
||||||
"effectAfterRestart": "Effect after restart",
|
"effectAfterRestart": "Effect after restart",
|
||||||
|
"startAll": "شروع همه",
|
||||||
|
"pauseAll": "توقف همه",
|
||||||
"deleteTask": "پاک کردن کار",
|
"deleteTask": "پاک کردن کار",
|
||||||
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
|
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
|
||||||
"delete": "پاک کردن",
|
"delete": "پاک کردن",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"notSet": "Не установлено",
|
"notSet": "Не установлено",
|
||||||
"set": "Установлено",
|
"set": "Установлено",
|
||||||
"effectAfterRestart": "Эффект после перезагрузки",
|
"effectAfterRestart": "Эффект после перезагрузки",
|
||||||
|
"startAll": "Запустить все",
|
||||||
|
"pauseAll": "Приостановить все",
|
||||||
"deleteTask": "Удалить задачу",
|
"deleteTask": "Удалить задачу",
|
||||||
"deleteTaskTip": "Сохранить загруженные файлы",
|
"deleteTaskTip": "Сохранить загруженные файлы",
|
||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
|
|||||||
@@ -50,6 +50,8 @@
|
|||||||
"notSet": "未设置",
|
"notSet": "未设置",
|
||||||
"set": "已设置",
|
"set": "已设置",
|
||||||
"effectAfterRestart": "此配置项将在重启应用后生效",
|
"effectAfterRestart": "此配置项将在重启应用后生效",
|
||||||
|
"startAll": "全部开始",
|
||||||
|
"pauseAll": "全部暂停",
|
||||||
"deleteTask": "删除任务",
|
"deleteTask": "删除任务",
|
||||||
"deleteTaskTip": "保留已下载的文件",
|
"deleteTaskTip": "保留已下载的文件",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
|
|||||||
@@ -111,6 +111,14 @@ Future<void> continueTask(String id) async {
|
|||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
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> continueAllTasks() async {
|
||||||
|
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> deleteTask(String id, bool force) async {
|
Future<void> deleteTask(String id, bool force) async {
|
||||||
return _parse(
|
return _parse(
|
||||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
||||||
|
|||||||
@@ -118,6 +118,11 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _initDeepLinks() async {
|
Future<void> _initDeepLinks() async {
|
||||||
|
// currently only support android
|
||||||
|
if (!Util.isAndroid()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
_appLinks = AppLinks();
|
_appLinks = AppLinks();
|
||||||
|
|
||||||
// Handle link when app is in warm state (front or background)
|
// 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),
|
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(
|
MenuItem(
|
||||||
label: 'setting'.tr,
|
label: 'setting'.tr,
|
||||||
onClick: (menuItem) async => {
|
onClick: (menuItem) async => {
|
||||||
|
|||||||
@@ -5,4 +5,5 @@ class CreateController extends GetxController {
|
|||||||
final RxList fileInfos = [].obs;
|
final RxList fileInfos = [].obs;
|
||||||
final RxList selectedIndexes = [].obs;
|
final RxList selectedIndexes = [].obs;
|
||||||
final RxList openedFolders = [].obs;
|
final RxList openedFolders = [].obs;
|
||||||
|
final isResolving = false.obs;
|
||||||
}
|
}
|
||||||
@@ -115,9 +115,10 @@ class CreateView extends GetView<CreateController> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doResolve() async {
|
Future<void> _doResolve() async {
|
||||||
if (_confirmController.currentState == ButtonState.loading) {
|
if (controller.isResolving.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
controller.isResolving.value = true;
|
||||||
try {
|
try {
|
||||||
_confirmController.start();
|
_confirmController.start();
|
||||||
if (_resolveFormKey.currentState!.validate()) {
|
if (_resolveFormKey.currentState!.validate()) {
|
||||||
@@ -130,6 +131,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
showErrorMessage(e);
|
showErrorMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
_confirmController.reset();
|
_confirmController.reset();
|
||||||
|
controller.isResolving.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ class LocaleKeys {
|
|||||||
static const notSet = 'notSet';
|
static const notSet = 'notSet';
|
||||||
static const set = 'set';
|
static const set = 'set';
|
||||||
static const effectAfterRestart = 'effectAfterRestart';
|
static const effectAfterRestart = 'effectAfterRestart';
|
||||||
|
static const startAll = 'startAll';
|
||||||
|
static const pauseAll = 'pauseAll';
|
||||||
static const deleteTask = 'deleteTask';
|
static const deleteTask = 'deleteTask';
|
||||||
static const deleteTaskTip = 'deleteTaskTip';
|
static const deleteTaskTip = 'deleteTaskTip';
|
||||||
static const delete = 'delete';
|
static const delete = 'delete';
|
||||||
@@ -127,6 +129,8 @@ class Locales {
|
|||||||
'notSet': 'NS',
|
'notSet': 'NS',
|
||||||
'set': 'SET',
|
'set': 'SET',
|
||||||
'effectAfterRestart': 'Effect after restart',
|
'effectAfterRestart': 'Effect after restart',
|
||||||
|
'startAll': 'Start All',
|
||||||
|
'pauseAll': 'Pause All',
|
||||||
'deleteTask': 'Delete Task',
|
'deleteTask': 'Delete Task',
|
||||||
'deleteTaskTip': 'Keep downloaded files',
|
'deleteTaskTip': 'Keep downloaded files',
|
||||||
'delete': 'Delete',
|
'delete': 'Delete',
|
||||||
@@ -184,6 +188,8 @@ class Locales {
|
|||||||
'notSet': 'NS',
|
'notSet': 'NS',
|
||||||
'set': 'SET',
|
'set': 'SET',
|
||||||
'effectAfterRestart': 'Effect after restart',
|
'effectAfterRestart': 'Effect after restart',
|
||||||
|
'startAll': 'شروع همه',
|
||||||
|
'pauseAll': 'توقف همه',
|
||||||
'deleteTask': 'پاک کردن کار',
|
'deleteTask': 'پاک کردن کار',
|
||||||
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
|
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
|
||||||
'delete': 'پاک کردن',
|
'delete': 'پاک کردن',
|
||||||
@@ -245,6 +251,8 @@ class Locales {
|
|||||||
'notSet': 'Не установлено',
|
'notSet': 'Не установлено',
|
||||||
'set': 'Установлено',
|
'set': 'Установлено',
|
||||||
'effectAfterRestart': 'Эффект после перезагрузки',
|
'effectAfterRestart': 'Эффект после перезагрузки',
|
||||||
|
'startAll': 'Запустить все',
|
||||||
|
'pauseAll': 'Приостановить все',
|
||||||
'deleteTask': 'Удалить задачу',
|
'deleteTask': 'Удалить задачу',
|
||||||
'deleteTaskTip': 'Сохранить загруженные файлы',
|
'deleteTaskTip': 'Сохранить загруженные файлы',
|
||||||
'delete': 'Удалить',
|
'delete': 'Удалить',
|
||||||
@@ -304,6 +312,8 @@ class Locales {
|
|||||||
'notSet': '未设置',
|
'notSet': '未设置',
|
||||||
'set': '已设置',
|
'set': '已设置',
|
||||||
'effectAfterRestart': '此配置项将在重启应用后生效',
|
'effectAfterRestart': '此配置项将在重启应用后生效',
|
||||||
|
'startAll': '全部开始',
|
||||||
|
'pauseAll': '全部暂停',
|
||||||
'deleteTask': '删除任务',
|
'deleteTask': '删除任务',
|
||||||
'deleteTaskTip': '保留已下载的文件',
|
'deleteTaskTip': '保留已下载的文件',
|
||||||
'delete': '删除',
|
'delete': '删除',
|
||||||
|
|||||||
Reference in new issue
Block a user