From d9c5ca339fb86fbbbb3902d92159ff131094bc33 Mon Sep 17 00:00:00 2001 From: Li Wei Date: Wed, 10 May 2023 18:00:58 +0800 Subject: [PATCH] feat: support max running tasks config (#112) --- cmd/server.go | 10 +- pkg/base/constants.go | 3 +- pkg/download/downloader.go | 222 ++++++++++++------ pkg/download/downloader_test.go | 10 +- pkg/download/model.go | 32 ++- pkg/rest/api.go | 4 +- pkg/rest/model/server.go | 5 +- pkg/rest/server.go | 5 +- pkg/util/timer.go | 7 +- ui/flutter/assets/locales/en_US.json | 1 + ui/flutter/assets/locales/ru_RU.json | 1 + ui/flutter/assets/locales/zh_CN.json | 1 + ui/flutter/lib/api/api.dart | 13 +- .../lib/api/model/downloader_config.dart | 1 + .../lib/api/model/downloader_config.g.dart | 2 + ui/flutter/lib/api/model/task.dart | 4 +- ui/flutter/lib/api/model/task.g.dart | 1 + .../modules/setting/views/setting_view.dart | 43 +++- .../task_downloading_controller.dart | 8 +- .../app/views/views/buid_task_list_view.dart | 20 +- ui/flutter/lib/generated/locales.g.dart | 4 + ui/flutter/pubspec.yaml | 1 + 22 files changed, 269 insertions(+), 129 deletions(-) diff --git a/cmd/server.go b/cmd/server.go index a4cb70f8..e0464cc6 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -3,7 +3,6 @@ package cmd import ( _ "embed" "fmt" - "github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest/model" "net/http" @@ -20,18 +19,17 @@ func Start(cfg *model.StartConfig) { if err != nil { panic(err) } - exist, _, err := rest.Downloader.GetConfig() + downloadCfg, err := rest.Downloader.GetConfig() if err != nil { panic(err) } - if !exist { + if downloadCfg.FirstLoad { // Set default download dir to user download dir userDir, err := os.UserHomeDir() if err == nil { downloadDir := filepath.Join(userDir, "Downloads") - rest.Downloader.PutConfig(&download.DownloaderStoreConfig{ - DownloadDir: downloadDir, - }) + downloadCfg.DownloadDir = downloadDir + rest.Downloader.PutConfig(downloadCfg) } } diff --git a/pkg/base/constants.go b/pkg/base/constants.go index 3ec1db32..0fc711e4 100644 --- a/pkg/base/constants.go +++ b/pkg/base/constants.go @@ -3,9 +3,10 @@ package base type Status string const ( - DownloadStatusReady Status = "ready" + DownloadStatusReady Status = "ready" // task create but not start DownloadStatusRunning Status = "running" DownloadStatusPause Status = "pause" + DownloadStatusWait Status = "wait" // task is wait for running DownloadStatusError Status = "error" DownloadStatusDone Status = "done" ) diff --git a/pkg/download/downloader.go b/pkg/download/downloader.go index 87cb2984..8a5d576a 100644 --- a/pkg/download/downloader.go +++ b/pkg/download/downloader.go @@ -9,6 +9,7 @@ import ( gonanoid "github.com/matoous/go-nanoid/v2" "os" "path" + "sort" "strings" "sync" "sync/atomic" @@ -36,15 +37,15 @@ type Progress struct { } type Downloader struct { + cfg *DownloaderConfig fetchBuilders map[string]fetcher.FetcherBuilder fetcherMap map[string]fetcher.Fetcher storage Storage tasks []*Task listener Listener - refreshInterval int - lock *sync.Mutex - closed atomic.Bool + lock *sync.Mutex + closed atomic.Bool } func NewDownloader(cfg *DownloaderConfig) *Downloader { @@ -54,12 +55,13 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader { cfg.Init() d := &Downloader{ + cfg: cfg, + fetchBuilders: make(map[string]fetcher.FetcherBuilder), fetcherMap: make(map[string]fetcher.Fetcher), - refreshInterval: cfg.RefreshInterval, - storage: cfg.Storage, - lock: &sync.Mutex{}, + storage: cfg.Storage, + lock: &sync.Mutex{}, } for _, f := range cfg.FetchBuilders { for _, p := range f.Schemes() { @@ -74,6 +76,21 @@ func (d *Downloader) Setup() error { if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil { return err } + // load config from storage + var cfg DownloaderStoreConfig + exist, err := d.storage.Get(bucketConfig, "config", &cfg) + if err != nil { + return err + } + if exist { + d.cfg.DownloaderStoreConfig = &cfg + } else { + d.cfg.DownloaderStoreConfig = &DownloaderStoreConfig{ + FirstLoad: true, + } + } + // init default config + d.cfg.DownloaderStoreConfig.Init() // load tasks from storage var tasks []*Task if err := d.storage.List(bucketTask, &tasks); err != nil { @@ -94,6 +111,10 @@ func (d *Downloader) Setup() error { task.Status = base.DownloadStatusPause } } + // sort by create time + sort.Slice(tasks, func(i, j int) bool { + return tasks[i].CreatedAt.Before(tasks[j].CreatedAt) + }) } d.tasks = tasks @@ -106,9 +127,7 @@ func (d *Downloader) Setup() error { func() { task.lock.Lock() defer task.lock.Unlock() - if task.Status == base.DownloadStatusDone || - task.Status == base.DownloadStatusError || - task.Status == base.DownloadStatusPause { + if task.Status != base.DownloadStatusRunning { return } // check if task is deleted @@ -118,7 +137,7 @@ func (d *Downloader) Setup() error { current := task.fetcher.Progress().TotalDownloaded() task.Progress.Used = task.timer.Used() - task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.refreshInterval)/1000) + task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.cfg.RefreshInterval)/1000) task.Progress.Downloaded = current d.emit(EventKeyProgress, task) @@ -136,7 +155,7 @@ func (d *Downloader) Setup() error { }() } } - time.Sleep(time.Millisecond * time.Duration(d.refreshInterval)) + time.Sleep(time.Millisecond * time.Duration(d.cfg.RefreshInterval)) } }() return nil @@ -185,7 +204,7 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) { return } -func (d *Downloader) DirectCreate(req *base.Request, opts *base.Options) (taskId string, err error) { +func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) { rr, err := d.Resolve(req) if err != nil { return @@ -201,6 +220,8 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err if !ok { return "", errors.New("invalid resource id") } + defer delete(d.fetcherMap, rrId) + meta := fetcher.Meta() meta.Opts = opts res := meta.Res @@ -211,13 +232,11 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err } } if opts.Path == "" { - exist, storeConfig, err := d.GetConfig() + storeConfig, err := d.GetConfig() if err != nil { return "", err } - if exist { - opts.Path = storeConfig.DownloadDir - } + opts.Path = storeConfig.DownloadDir } fb, err := d.parseFb(fetcher.Meta().Req.URL) @@ -253,36 +272,27 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err task.fetcher = fetcher task.Meta = fetcher.Meta() task.Progress = &Progress{} - task.Status = base.DownloadStatusRunning + task.Status = base.DownloadStatusReady // calculate select files size for _, selectIndex := range opts.SelectFiles { task.Size += res.Files[selectIndex].Size } initTask(task) - task.timer.Start() err = func() error { d.lock.Lock() defer d.lock.Unlock() - if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { - return err - } d.tasks = append(d.tasks, task) - return nil + return d.storage.Put(bucketTask, task.ID, task.clone()) }() if err != nil { return } - d.emit(EventKeyStart, task) taskId = task.ID - go func() { - err := fetcher.Start() - if err != nil { - d.emit(EventKeyError, task, err) - return + d.tryRunning(func(remain int) { + if remain > 0 { + err = d.start(task) } - d.watch(task) - }() - delete(d.fetcherMap, rrId) + }) return } @@ -291,16 +301,10 @@ func (d *Downloader) Pause(id string) (err error) { if task == nil { return } - task.lock.Lock() - defer task.lock.Unlock() - if task.Status == base.DownloadStatusRunning { - task.Status = base.DownloadStatusPause - task.timer.Pause() - task.fetcher.Pause() - d.storage.Put(bucketTask, task.ID, task.clone()) - d.emit(EventKeyPause, task) + if err = d.doPause(task, base.DownloadStatusPause); err != nil { + return err } - return + return d.notifyRunning() } func (d *Downloader) Continue(id string) (err error) { @@ -308,24 +312,24 @@ func (d *Downloader) Continue(id string) (err error) { if task == nil { return } - task.lock.Lock() - defer task.lock.Unlock() - if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError { - task.Status = base.DownloadStatusRunning - task.Progress.Speed = 0 - err = d.restoreFetcher(task) + d.tryRunning(func(remain int) { + if remain == 0 { + // no more task can be running, need to doPause a running task + for _, t := range d.tasks { + if t.Status == base.DownloadStatusRunning { + err = d.doPause(t, base.DownloadStatusWait) + break + } + } + } if err != nil { return } - task.timer.Start() - err = task.fetcher.Continue() - if err != nil { - return - } - d.storage.Put(bucketTask, task.ID, task.clone()) - d.emit(EventKeyContinue, task) + }) + if err != nil { + return } - return + return d.doContinue(task) } func (d *Downloader) Delete(id string, force bool) (err error) { @@ -371,6 +375,11 @@ func (d *Downloader) Delete(id string, force bool) (err error) { } } d.emit(EventKeyDelete, task) + if task.Status == base.DownloadStatusRunning { + if err = d.doNotifyRunning(); err != nil { + return err + } + } task = nil return } @@ -380,7 +389,7 @@ func (d *Downloader) Close() error { defer d.lock.Unlock() d.closed.Store(true) for _, task := range d.tasks { - d.Pause(task.ID) + d.doPause(task, base.DownloadStatusPause) } return d.storage.Close() } @@ -426,27 +435,20 @@ func (d *Downloader) GetTasks() []*Task { return d.tasks } -func (d *Downloader) GetConfig() (bool, *DownloaderStoreConfig, error) { - var cfg DownloaderStoreConfig - exist, err := d.storage.Get(bucketConfig, "config", &cfg) - if err != nil { - return false, nil, err - } - return exist, &cfg, nil +func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) { + return d.cfg.DownloaderStoreConfig, nil } func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error { + d.cfg.DownloaderStoreConfig = v return d.storage.Put(bucketConfig, "config", v) } func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) { - exist, cfg, err := d.GetConfig() + cfg, err := d.GetConfig() if err != nil { return false, err } - if !exist { - return false, nil - } if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil { return false, nil } @@ -479,6 +481,7 @@ func (d *Downloader) watch(task *Task) { d.emit(EventKeyDone, task) } d.emit(EventKeyFinally, task, err) + d.notifyRunning() } func (d *Downloader) restoreFetcher(task *Task) error { @@ -507,13 +510,96 @@ func (d *Downloader) restoreFetcher(task *Task) error { } d.setupFetcher(task.fetcher) task.fetcher.Create(task.Meta.Opts) - go d.watch(task) } return nil } +func (d *Downloader) tryRunning(cb func(remain int)) { + d.lock.Lock() + defer d.lock.Unlock() + + runningCount := 0 + for _, task := range d.tasks { + if task.Status == base.DownloadStatusRunning { + runningCount++ + } + } + cb(d.cfg.MaxRunning - runningCount) +} + +func (d *Downloader) notifyRunning() error { + d.lock.Lock() + defer d.lock.Unlock() + + return d.doNotifyRunning() +} + +func (d *Downloader) doNotifyRunning() error { + for _, task := range d.tasks { + if task.Status == base.DownloadStatusReady || task.Status == base.DownloadStatusWait { + return d.doContinue(task) + } + } + return nil +} + +func (d *Downloader) start(task *Task) error { + var event EventKey + if task.Status == base.DownloadStatusReady { + event = EventKeyStart + } else { + event = EventKeyContinue + } + + task.Status = base.DownloadStatusRunning + task.Progress.Speed = 0 + task.timer.Start() + if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { + return err + } + var err error + if event == EventKeyStart { + err = task.fetcher.Start() + } + if event == EventKeyContinue { + err = task.fetcher.Continue() + } + if err != nil { + return err + } + go d.watch(task) + d.emit(event, task) + return nil +} + +func (d *Downloader) doPause(task *Task, status base.Status) (err error) { + task.lock.Lock() + defer task.lock.Unlock() + if task.Status == base.DownloadStatusRunning { + task.Status = status + task.timer.Pause() + task.fetcher.Pause() + d.storage.Put(bucketTask, task.ID, task.clone()) + d.emit(EventKeyPause, task) + } + return +} + +func (d *Downloader) doContinue(task *Task) (err error) { + task.lock.Lock() + defer task.lock.Unlock() + if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone { + err = d.restoreFetcher(task) + if err != nil { + return + } + err = d.start(task) + } + return +} + func initTask(task *Task) { - task.timer = &util.Timer{} + task.timer = util.NewTimer(task.Progress.Used) task.lock = &sync.Mutex{} task.speedArr = make([]int64, 0) } diff --git a/pkg/download/downloader_test.go b/pkg/download/downloader_test.go index 6cf52267..8761b3a1 100644 --- a/pkg/download/downloader_test.go +++ b/pkg/download/downloader_test.go @@ -170,8 +170,7 @@ func TestDownloader_Protocol_Config(t *testing.T) { } storeCfg := &DownloaderStoreConfig{ - RefreshInterval: 500, - DownloadDir: "./downloads", + DownloadDir: "./downloads", ProtocolConfig: map[string]any{ "http": map[string]any{ "connections": 4, @@ -191,17 +190,14 @@ func TestDownloader_Protocol_Config(t *testing.T) { }, } - if err := downloader.PutConfig(storeCfg); err != nil { + if err = downloader.PutConfig(storeCfg); err != nil { t.Fatal(err) } - exits, newStoreCfg, err := downloader.GetConfig() + newStoreCfg, err := downloader.GetConfig() if err != nil { t.Fatal(err) } - if !exits { - t.Errorf("GetConfig() got = %v, want %v", exits, true) - } if !test.JsonEqual(storeCfg, newStoreCfg) { t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg)) diff --git a/pkg/download/model.go b/pkg/download/model.go index 0f062869..ca44d7cf 100644 --- a/pkg/download/model.go +++ b/pkg/download/model.go @@ -70,7 +70,9 @@ func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 { type DownloaderConfig struct { Controller *controller.Controller FetchBuilders []fetcher.FetcherBuilder - Storage Storage + + RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms) + Storage Storage *DownloaderStoreConfig } @@ -85,24 +87,28 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig { new(bt.FetcherBuilder), } } - if cfg.Storage == nil { - cfg.Storage = NewMemStorage() - } - - if cfg.DownloaderStoreConfig == nil { - cfg.DownloaderStoreConfig = &DownloaderStoreConfig{} - } - if cfg.RefreshInterval == 0 { cfg.RefreshInterval = 350 } + if cfg.Storage == nil { + cfg.Storage = NewMemStorage() + } return cfg } // DownloaderStoreConfig is the config that can restore the downloader. type DownloaderStoreConfig struct { - RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms) - DownloadDir string `json:"downloadDir"` // DownloadDir is the directory to save the downloaded files - ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol - Extra map[string]any `json:"extra"` // Extra is the extra config + FirstLoad bool `json:"-"` // fromNoStore is the flag that the config is first time init and not from store + + DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files + MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count + ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol + Extra map[string]any `json:"extra"` // Extra is the extra config +} + +func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig { + if cfg.MaxRunning == 0 { + cfg.MaxRunning = 3 + } + return cfg } diff --git a/pkg/rest/api.go b/pkg/rest/api.go index 059aba35..a3d7556b 100644 --- a/pkg/rest/api.go +++ b/pkg/rest/api.go @@ -33,7 +33,7 @@ func CreateTask(w http.ResponseWriter, r *http.Request) { if req.Rid != "" { taskId, err = Downloader.Create(req.Rid, req.Opts) } else if req.Req != nil { - taskId, err = Downloader.DirectCreate(req.Req, req.Opts) + taskId, err = Downloader.CreateDirect(req.Req, req.Opts) } else { WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam)) return @@ -195,6 +195,6 @@ func writeError(w http.ResponseWriter, msg string) { } func getServerConfig() *download.DownloaderStoreConfig { - _, cfg, _ := Downloader.GetConfig() + cfg, _ := Downloader.GetConfig() return cfg } diff --git a/pkg/rest/model/server.go b/pkg/rest/model/server.go index bc685af2..cad94fe1 100644 --- a/pkg/rest/model/server.go +++ b/pkg/rest/model/server.go @@ -12,9 +12,9 @@ const ( type StartConfig struct { Network string `json:"network"` Address string `json:"address"` + RefreshInterval int `json:"refreshInterval"` Storage Storage `json:"storage"` StorageDir string `json:"storageDir"` - RefreshInterval int `json:"refreshInterval"` ApiToken string `json:"apiToken"` WebEnable bool @@ -28,6 +28,9 @@ func (cfg *StartConfig) Init() *StartConfig { if cfg.Address == "" { cfg.Address = "127.0.0.1:0" } + if cfg.RefreshInterval == 0 { + cfg.RefreshInterval = 350 + } if cfg.Storage == "" { cfg.Storage = StorageBolt } diff --git a/pkg/rest/server.go b/pkg/rest/server.go index 8adf32ad..86e50fd3 100644 --- a/pkg/rest/server.go +++ b/pkg/rest/server.go @@ -57,14 +57,15 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error } startCfg.Init() - downloadCfg := &download.DownloaderConfig{} + downloadCfg := &download.DownloaderConfig{ + RefreshInterval: startCfg.RefreshInterval, + } if startCfg.Storage == model.StorageBolt { downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir) } else { downloadCfg.Storage = download.NewMemStorage() } downloadCfg.Init() - downloadCfg.RefreshInterval = startCfg.RefreshInterval Downloader = download.NewDownloader(downloadCfg) if err := Downloader.Setup(); err != nil { return nil, nil, err diff --git a/pkg/util/timer.go b/pkg/util/timer.go index 0a12f6d4..ff0422ed 100644 --- a/pkg/util/timer.go +++ b/pkg/util/timer.go @@ -2,12 +2,17 @@ package util import "time" -// 计时器 type Timer struct { t int64 used int64 } +func NewTimer(used int64) *Timer { + return &Timer{ + used: used, + } +} + func (t *Timer) Start() { t.t = time.Now().UnixNano() } diff --git a/ui/flutter/assets/locales/en_US.json b/ui/flutter/assets/locales/en_US.json index 7f50a151..71c415d9 100644 --- a/ui/flutter/assets/locales/en_US.json +++ b/ui/flutter/assets/locales/en_US.json @@ -24,6 +24,7 @@ "downloadDir": "Download Directory", "downloadDirValid": "Please select the download directory", "connections": "Connections", + "maxRunning": "Max Running Tasks", "items": "@count items", "subscribeTracker": "Subscribe Tracker", "subscribeFail": "Subscribe failed, please check network or try again later", diff --git a/ui/flutter/assets/locales/ru_RU.json b/ui/flutter/assets/locales/ru_RU.json index 37a69e5f..e14a6a93 100644 --- a/ui/flutter/assets/locales/ru_RU.json +++ b/ui/flutter/assets/locales/ru_RU.json @@ -24,6 +24,7 @@ "downloadDir": "Папка загрузки", "downloadDirValid": "Пожалуйста, выберите папку для загрузки", "connections": "Соединений", + "maxRunning": "Максимальное количество активных задач", "items": "@подсчет элементов", "subscribeTracker": "Подпись трекера", "subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже", diff --git a/ui/flutter/assets/locales/zh_CN.json b/ui/flutter/assets/locales/zh_CN.json index c809491a..9dc2d2a3 100644 --- a/ui/flutter/assets/locales/zh_CN.json +++ b/ui/flutter/assets/locales/zh_CN.json @@ -24,6 +24,7 @@ "downloadDir": "下载目录", "downloadDirValid": "请选择下载目录", "connections": "连接数", + "maxRunning": "最大下载数", "items": "@count 条", "subscribeTracker": "订阅 Tracker", "subscribeFail": "订阅失败,请检查网络或稍后重试", diff --git a/ui/flutter/lib/api/api.dart b/ui/flutter/lib/api/api.dart index 7668dc5d..70c094c3 100644 --- a/ui/flutter/lib/api/api.dart +++ b/ui/flutter/lib/api/api.dart @@ -73,15 +73,12 @@ Future _parse( ) async { try { var resp = await fetch(); - if (fromJsonT != null) { - final result = Result.fromJson(resp.data, fromJsonT); - if (result.code == 0) { - return result.data as T; - } else { - throw Exception(result); - } + fromJsonT ??= (json) => null as T; + final result = Result.fromJson(resp.data, fromJsonT); + if (result.code == 0) { + return result.data as T; } else { - return null as T; + throw Exception(result); } } on DioError catch (e) { throw Exception(Result(code: 1000, msg: e.message)); diff --git a/ui/flutter/lib/api/model/downloader_config.dart b/ui/flutter/lib/api/model/downloader_config.dart index 46c0992d..2f45eedb 100644 --- a/ui/flutter/lib/api/model/downloader_config.dart +++ b/ui/flutter/lib/api/model/downloader_config.dart @@ -5,6 +5,7 @@ part 'downloader_config.g.dart'; @JsonSerializable(explicitToJson: true) class DownloaderConfig { String downloadDir = ''; + int maxRunning = 0; ProtocolConfig protocolConfig = ProtocolConfig(); ExtraConfig extra = ExtraConfig(); diff --git a/ui/flutter/lib/api/model/downloader_config.g.dart b/ui/flutter/lib/api/model/downloader_config.g.dart index b032f419..015f792c 100644 --- a/ui/flutter/lib/api/model/downloader_config.g.dart +++ b/ui/flutter/lib/api/model/downloader_config.g.dart @@ -9,6 +9,7 @@ part of 'downloader_config.dart'; DownloaderConfig _$DownloaderConfigFromJson(Map json) => DownloaderConfig() ..downloadDir = json['downloadDir'] as String + ..maxRunning = json['maxRunning'] as int ..protocolConfig = ProtocolConfig.fromJson( json['protocolConfig'] as Map?) ..extra = ExtraConfig.fromJson(json['extra'] as Map?); @@ -16,6 +17,7 @@ DownloaderConfig _$DownloaderConfigFromJson(Map json) => Map _$DownloaderConfigToJson(DownloaderConfig instance) => { 'downloadDir': instance.downloadDir, + 'maxRunning': instance.maxRunning, 'protocolConfig': instance.protocolConfig.toJson(), 'extra': instance.extra.toJson(), }; diff --git a/ui/flutter/lib/api/model/task.dart b/ui/flutter/lib/api/model/task.dart index 8e8dcffe..1b420ed1 100644 --- a/ui/flutter/lib/api/model/task.dart +++ b/ui/flutter/lib/api/model/task.dart @@ -1,12 +1,10 @@ import 'package:json_annotation/json_annotation.dart'; import 'meta.dart'; -import 'options.dart'; -import 'resource.dart'; part 'task.g.dart'; -enum Status { ready, running, pause, error, done } +enum Status { ready, running, pause, wait, error, done } @JsonSerializable(explicitToJson: true) class Task { diff --git a/ui/flutter/lib/api/model/task.g.dart b/ui/flutter/lib/api/model/task.g.dart index 03db8a09..000955e4 100644 --- a/ui/flutter/lib/api/model/task.g.dart +++ b/ui/flutter/lib/api/model/task.g.dart @@ -28,6 +28,7 @@ const _$StatusEnumMap = { Status.ready: 'ready', Status.running: 'running', Status.pause: 'pause', + Status.wait: 'wait', Status.error: 'error', Status.done: 'done', }; diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index 818b008f..ecdae509 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -67,11 +67,37 @@ class SettingView extends GetView { showLabel: false, ); }); + final buildMaxRunning = _buildConfigItem( + 'maxRunning', () => downloaderCfg.value.maxRunning.toString(), + (Key key) { + final maxRunningController = TextEditingController( + text: downloaderCfg.value.maxRunning.toString()); + maxRunningController.addListener(() async { + if (maxRunningController.text.isNotEmpty && + maxRunningController.text != + downloaderCfg.value.maxRunning.toString()) { + downloaderCfg.value.maxRunning = int.parse(maxRunningController.text); + + await debounceSave(); + } + }); + + return TextField( + key: key, + focusNode: FocusNode(), + controller: maxRunningController, + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + _NumericalRangeFormatter(min: 1, max: 256), + ], + ); + }); // http config items start final httpConfig = downloaderCfg.value.protocolConfig.http; final buildHttpConnections = _buildConfigItem( - 'connections'.tr, () => httpConfig.connections.toString(), (Key key) { + 'connections', () => httpConfig.connections.toString(), (Key key) { final connectionsController = TextEditingController(text: httpConfig.connections.toString()); connectionsController.addListener(() async { @@ -99,7 +125,7 @@ class SettingView extends GetView { final btExtConfig = downloaderCfg.value.extra.bt; final buildBtTrackerSubscribeUrls = _buildConfigItem( - 'subscribeTracker'.tr, + 'subscribeTracker', () => 'items'.trParams( {'count': btExtConfig.trackerSubscribeUrls.length.toString()}), (Key key) { @@ -155,7 +181,7 @@ class SettingView extends GetView { ); }); final buildBtTrackers = _buildConfigItem( - 'addTracker'.tr, + 'addTracker', () => 'items' .trParams({'count': btExtConfig.customTrackers.length.toString()}), (Key key) { @@ -298,7 +324,7 @@ class SettingView extends GetView { // advanced config items start final buildApiProtocol = _buildConfigItem( - 'protocol'.tr, + 'protocol', () => startCfg.value.network == 'tcp' ? 'TCP ${startCfg.value.address}' : 'Unix', @@ -396,7 +422,7 @@ class SettingView extends GetView { ); }, ); - final buildApiToken = _buildConfigItem('apiToken'.tr, + final buildApiToken = _buildConfigItem('apiToken', () => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'seted'.tr, (Key key) { final apiTokenController = @@ -426,7 +452,7 @@ class SettingView extends GetView { length: 2, child: Scaffold( appBar: PreferredSize( - preferredSize: Size.fromHeight(56), + preferredSize: const Size.fromHeight(56), child: AppBar( bottom: TabBar( tabs: [ @@ -448,9 +474,8 @@ class SettingView extends GetView { Text('general'.tr), Card( child: Column( - children: _addDivider([ - buildDownloadDir(), - ]), + children: _addDivider( + [buildDownloadDir(), buildMaxRunning()]), )), const Text('HTTP'), Card( diff --git a/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart b/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart index ce48e58e..f430c2e9 100644 --- a/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart +++ b/ui/flutter/lib/app/modules/task/controllers/task_downloading_controller.dart @@ -3,5 +3,11 @@ import 'task_list_controller.dart'; class TaskDownloadingController extends TaskListController { TaskDownloadingController() - : super([Status.ready, Status.running, Status.pause, Status.error]); + : super([ + Status.ready, + Status.running, + Status.pause, + Status.wait, + Status.error + ]); } diff --git a/ui/flutter/lib/app/views/views/buid_task_list_view.dart b/ui/flutter/lib/app/views/views/buid_task_list_view.dart index a1fbb526..c02d0d32 100644 --- a/ui/flutter/lib/app/views/views/buid_task_list_view.dart +++ b/ui/flutter/lib/app/views/views/buid_task_list_view.dart @@ -123,14 +123,22 @@ class BuildTaskListView extends GetView { list.add(IconButton( icon: const Icon(Icons.pause), onPressed: () async { - await pauseTask(task.id); + try { + await pauseTask(task.id); + } catch (e) { + showErrorMessage(e); + } }, )); } else { list.add(IconButton( icon: const Icon(Icons.play_arrow), onPressed: () async { - await continueTask(task.id); + try { + await continueTask(task.id); + } catch (e) { + showErrorMessage(e); + } }, )); } @@ -198,16 +206,14 @@ class BuildTaskListView extends GetView { : Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]), trailing: SizedBox( - width: task.status == Status.done ? 80 : 180, + width: 180, child: Row( // crossAxisAlignment: CrossAxisAlignment.baseline, // textBaseline: DefaultTextStyle.of(context).style.textBaseline, mainAxisAlignment: MainAxisAlignment.end, children: [ - task.status == Status.done - ? const SizedBox.shrink() - : Text("${Util.fmtByte(task.progress.speed)} / s", - style: context.textTheme.titleSmall), + Text("${Util.fmtByte(task.progress.speed)} / s", + style: context.textTheme.titleSmall), ...buildActions() ], ), diff --git a/ui/flutter/lib/generated/locales.g.dart b/ui/flutter/lib/generated/locales.g.dart index 1aae9d06..a2793ddd 100644 --- a/ui/flutter/lib/generated/locales.g.dart +++ b/ui/flutter/lib/generated/locales.g.dart @@ -38,6 +38,7 @@ class LocaleKeys { static const downloadDir = 'downloadDir'; static const downloadDirValid = 'downloadDirValid'; static const connections = 'connections'; + static const maxRunning = 'maxRunning'; static const items = 'items'; static const subscribeTracker = 'subscribeTracker'; static const subscribeFail = 'subscribeFail'; @@ -97,6 +98,7 @@ class Locales { 'downloadDir': 'Download Directory', 'downloadDirValid': 'Please select the download directory', 'connections': 'Connections', + 'maxRunning': 'Max Running Tasks', 'items': '@count items', 'subscribeTracker': 'Subscribe Tracker', 'subscribeFail': @@ -210,6 +212,7 @@ class Locales { 'downloadDir': 'Папка загрузки', 'downloadDirValid': 'Пожалуйста, выберите папку для загрузки', 'connections': 'Соединений', + 'maxRunning': 'Максимальное количество активных задач', 'items': '@подсчет элементов', 'subscribeTracker': 'Подпись трекера', 'subscribeFail': @@ -267,6 +270,7 @@ class Locales { 'downloadDir': '下载目录', 'downloadDirValid': '请选择下载目录', 'connections': '连接数', + 'maxRunning': '最大下载数', 'items': '@count 条', 'subscribeTracker': '订阅 Tracker', 'subscribeFail': '订阅失败,请检查网络或稍后重试', diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index efa4cffc..03362088 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -65,6 +65,7 @@ dev_dependencies: # rules and activating additional ones. flutter_lints: ^2.0.0 ffigen: ^6.1.2 + # build json model: flutter pub run build_runner build --delete-conflicting-outputs build_runner: ^2.2.1 json_serializable: ^6.3.2 flutter_launcher_icons: