Compare commits

...
12 Commits
55 changed files with 947 additions and 223 deletions

No files matched your search

+27 -19
View File
@@ -3,6 +3,10 @@ name: build
on: on:
workflow_dispatch: workflow_dispatch:
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
permissions: permissions:
contents: write contents: write
@@ -24,11 +28,12 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -123,10 +128,9 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- name: Build - name: Build
run: | run: |
docker pull techknowlogick/xgo:go-1.19.x docker pull techknowlogick/xgo:go-1.19.x
@@ -142,14 +146,15 @@ jobs:
needs: [get-release, build-macos-arm64-lib] needs: [get-release, build-macos-arm64-lib]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 16 node-version: 16
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg - name: Install appdmg
run: npm install -g appdmg run: npm install -g appdmg
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
@@ -197,14 +202,15 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: | - run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -256,15 +262,16 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
with: with:
distribution: "zulu" distribution: "zulu"
java-version: "11" java-version: "11"
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -292,11 +299,12 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
+5 -2
View File
@@ -16,6 +16,9 @@ on:
- "go.sum" - "go.sum"
- "Dockerfile" - "Dockerfile"
env:
GO_VERSION: 1.19
permissions: permissions:
contents: write contents: write
@@ -28,9 +31,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
- uses: release-drafter/release-drafter@v5 - uses: release-drafter/release-drafter@v5
id: create_release id: create_release
env: env:
+11 -7
View File
@@ -26,6 +26,10 @@ on:
- "go.mod" - "go.mod"
- "go.sum" - "go.sum"
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
jobs: jobs:
# lint: # lint:
# name: Lint # name: Lint
@@ -45,10 +49,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- name: Run Unit tests. - name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3
@@ -61,14 +64,15 @@ jobs:
needs: [test] needs: [test]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v3 - uses: actions/setup-go@v4
with: with:
go-version: "^1.19" go-version: ${{ env.GO_VERSION }}
cache: true
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: | - run: |
sudo apt-get update -y sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Build - name: Build
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
+12
View File
@@ -153,6 +153,18 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
go run cmd/web/main.go go run cmd/web/main.go
``` ```
## Thanks
### Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## License ## License
[GPLv3](LICENSE) [GPLv3](LICENSE)
+12
View File
@@ -152,6 +152,18 @@ go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed
go run cmd/web/main.go go run cmd/web/main.go
``` ```
## 感谢
### 贡献者
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 开源许可 ## 开源许可
基于 [GPLv3](LICENSE) 协议开源。 基于 [GPLv3](LICENSE) 协议开源。
+1
View File
@@ -0,0 +1 @@
<svg height="105" viewBox="0 0 105 105" width="105" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><linearGradient id="a" gradientUnits="userSpaceOnUse" x1="99.03" x2="61.54" y1="59.18" y2="90.91"><stop offset="0" stop-color="#087cfa"/><stop offset=".02" stop-color="#0d7bfa"/><stop offset=".37" stop-color="#5566f9"/><stop offset=".66" stop-color="#8a57f8"/><stop offset=".88" stop-color="#ab4ef7"/><stop offset="1" stop-color="#b74af7"/></linearGradient><linearGradient id="b" x1="37.54" x2="60.33" xlink:href="#a" y1="34.26" y2="8.33"/><linearGradient id="c" gradientUnits="userSpaceOnUse" x1="17.84" x2="87.09" y1="88.36" y2="43.86"><stop offset="0" stop-color="#087cfa"/><stop offset=".1" stop-color="#1598d3"/><stop offset=".22" stop-color="#23b6aa"/><stop offset=".35" stop-color="#2dcc8b"/><stop offset=".46" stop-color="#35dd74"/><stop offset=".57" stop-color="#39e767"/><stop offset=".67" stop-color="#3bea62"/></linearGradient><path d="m89.06 41.63 11.44 25.37-19.58 33.5-12.06-29.28z" fill="url(#a)"/><path d="m68.86 71.22 12.06 29.28-30.34-10.43z" fill="#b74af7"/><path d="m72.13 31.61-6.22-27.11h-34.49l-26.92 41.18 7.79 18.92-7.79 17.27 55.75-47.43z" fill="url(#b)"/><path d="m100.5 24.88-40.25 9.56-55.75 47.43 35.96 18.63 28.4-29.28z" fill="url(#c)"/><path d="m22.5 22.5h60v60h-60z"/><g fill="#fff"><path d="m28.13 71.25h22.5v3.75h-22.5z"/><path d="m26.44 42.81v-.07a12.7 12.7 0 0 1 12.97-12.74 13.62 13.62 0 0 1 9.73 3.39l-3.43 4.14a9.13 9.13 0 0 0 -6.47-2.53c-4 0-7.11 3.5-7.11 7.7v.07c0 4.53 3.11 7.85 7.5 7.85a8.67 8.67 0 0 0 5.12-1.49v-3.56h-5.48v-4.7h10.73v10.74a16.07 16.07 0 0 1 -10.57 3.93c-7.69 0-12.99-5.41-12.99-12.73z"/><path d="m52.3 42.81v-.07a12.79 12.79 0 0 1 13.19-12.74c7.63 0 13.11 5.69 13.11 12.72v.07a12.79 12.79 0 0 1 -13.18 12.8c-7.64-.05-13.12-5.74-13.12-12.78zm20.61 0v-.07a7.51 7.51 0 0 0 -7.49-7.74 7.4 7.4 0 0 0 -7.42 7.67v.07a7.52 7.52 0 0 0 7.5 7.78 7.4 7.4 0 0 0 7.41-7.71z"/></g></svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

+3
View File
@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
) )
func main() { func main() {
@@ -16,7 +17,9 @@ func main() {
} }
}). }).
Create(&base.Options{ Create(&base.Options{
Extra: http.OptsExtra{
Connections: 8, Connections: 8,
},
}) })
if err != nil { if err != nil {
panic(err) panic(err)
+9 -2
View File
@@ -13,9 +13,17 @@ import (
const progressWidth = 20 const progressWidth = 20
func main() { func main() {
args := parse() //args := parse()
var dir = "./"
var connections = 8
args := &args{
url: "https://sel-kor-ping.vultr.com/vultr.com.100MB.bin",
dir: &dir,
connections: &connections,
}
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(1)
_, err := download.Boot(). _, err := download.Boot().
URL(args.url). URL(args.url).
Listener(func(event *download.Event) { Listener(func(event *download.Event) {
@@ -46,7 +54,6 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
wg.Add(1)
wg.Wait() wg.Wait()
} }
+4 -6
View File
@@ -3,7 +3,6 @@ package cmd
import ( import (
_ "embed" _ "embed"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"net/http" "net/http"
@@ -20,18 +19,17 @@ func Start(cfg *model.StartConfig) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
exist, _, err := rest.Downloader.GetConfig() downloadCfg, err := rest.Downloader.GetConfig()
if err != nil { if err != nil {
panic(err) panic(err)
} }
if !exist { if downloadCfg.FirstLoad {
// Set default download dir to user download dir // Set default download dir to user download dir
userDir, err := os.UserHomeDir() userDir, err := os.UserHomeDir()
if err == nil { if err == nil {
downloadDir := filepath.Join(userDir, "Downloads") downloadDir := filepath.Join(userDir, "Downloads")
rest.Downloader.PutConfig(&download.DownloaderStoreConfig{ downloadCfg.DownloadDir = downloadDir
DownloadDir: downloadDir, rest.Downloader.PutConfig(downloadCfg)
})
} }
} }
+2 -1
View File
@@ -3,9 +3,10 @@ package base
type Status string type Status string
const ( const (
DownloadStatusReady Status = "ready" DownloadStatusReady Status = "ready" // task create but not start
DownloadStatusRunning Status = "running" DownloadStatusRunning Status = "running"
DownloadStatusPause Status = "pause" DownloadStatusPause Status = "pause"
DownloadStatusWait Status = "wait" // task is wait for running
DownloadStatusError Status = "error" DownloadStatusError Status = "error"
DownloadStatusDone Status = "done" DownloadStatusDone Status = "done"
) )
+181 -59
View File
@@ -7,8 +7,10 @@ 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"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -36,13 +38,13 @@ type Progress struct {
} }
type Downloader struct { type Downloader struct {
cfg *DownloaderConfig
fetchBuilders map[string]fetcher.FetcherBuilder fetchBuilders map[string]fetcher.FetcherBuilder
fetcherMap map[string]fetcher.Fetcher fetcherMap map[string]fetcher.Fetcher
storage Storage storage Storage
tasks []*Task tasks []*Task
listener Listener listener Listener
refreshInterval int
lock *sync.Mutex lock *sync.Mutex
closed atomic.Bool closed atomic.Bool
} }
@@ -54,10 +56,11 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
cfg.Init() cfg.Init()
d := &Downloader{ d := &Downloader{
cfg: cfg,
fetchBuilders: make(map[string]fetcher.FetcherBuilder), fetchBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherMap: make(map[string]fetcher.Fetcher), fetcherMap: make(map[string]fetcher.Fetcher),
refreshInterval: cfg.RefreshInterval,
storage: cfg.Storage, storage: cfg.Storage,
lock: &sync.Mutex{}, lock: &sync.Mutex{},
} }
@@ -74,6 +77,21 @@ func (d *Downloader) Setup() error {
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil { if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil {
return err 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 // load tasks from storage
var tasks []*Task var tasks []*Task
if err := d.storage.List(bucketTask, &tasks); err != nil { if err := d.storage.List(bucketTask, &tasks); err != nil {
@@ -94,6 +112,10 @@ func (d *Downloader) Setup() error {
task.Status = base.DownloadStatusPause 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 d.tasks = tasks
@@ -106,9 +128,7 @@ func (d *Downloader) Setup() error {
func() { func() {
task.lock.Lock() task.lock.Lock()
defer task.lock.Unlock() defer task.lock.Unlock()
if task.Status == base.DownloadStatusDone || if task.Status != base.DownloadStatusRunning {
task.Status == base.DownloadStatusError ||
task.Status == base.DownloadStatusPause {
return return
} }
// check if task is deleted // check if task is deleted
@@ -118,7 +138,7 @@ func (d *Downloader) Setup() error {
current := task.fetcher.Progress().TotalDownloaded() current := task.fetcher.Progress().TotalDownloaded()
task.Progress.Used = task.timer.Used() 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 task.Progress.Downloaded = current
d.emit(EventKeyProgress, task) d.emit(EventKeyProgress, task)
@@ -136,7 +156,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 return nil
@@ -185,7 +205,7 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return 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) rr, err := d.Resolve(req)
if err != nil { if err != nil {
return return
@@ -201,6 +221,8 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
if !ok { if !ok {
return "", errors.New("invalid resource id") return "", errors.New("invalid resource id")
} }
defer delete(d.fetcherMap, rrId)
meta := fetcher.Meta() meta := fetcher.Meta()
meta.Opts = opts meta.Opts = opts
res := meta.Res res := meta.Res
@@ -211,14 +233,12 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
} }
} }
if opts.Path == "" { if opts.Path == "" {
exist, storeConfig, err := d.GetConfig() storeConfig, err := d.GetConfig()
if err != nil { if err != nil {
return "", err return "", err
} }
if exist {
opts.Path = storeConfig.DownloadDir opts.Path = storeConfig.DownloadDir
} }
}
fb, err := d.parseFb(fetcher.Meta().Req.URL) fb, err := d.parseFb(fetcher.Meta().Req.URL)
if err != nil { if err != nil {
@@ -253,36 +273,27 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
task.fetcher = fetcher task.fetcher = fetcher
task.Meta = fetcher.Meta() task.Meta = fetcher.Meta()
task.Progress = &Progress{} task.Progress = &Progress{}
task.Status = base.DownloadStatusRunning task.Status = base.DownloadStatusReady
// calculate select files size // calculate select files size
for _, selectIndex := range opts.SelectFiles { for _, selectIndex := range opts.SelectFiles {
task.Size += res.Files[selectIndex].Size task.Size += res.Files[selectIndex].Size
} }
initTask(task) initTask(task)
task.timer.Start()
err = func() error { err = func() error {
d.lock.Lock() d.lock.Lock()
defer d.lock.Unlock() defer d.lock.Unlock()
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
d.tasks = append(d.tasks, task) d.tasks = append(d.tasks, task)
return nil return d.storage.Put(bucketTask, task.ID, task.clone())
}() }()
if err != nil { if err != nil {
return return
} }
d.emit(EventKeyStart, task)
taskId = task.ID taskId = task.ID
go func() { d.tryRunning(func(remain int) {
err := fetcher.Start() if remain > 0 {
if err != nil { err = d.start(task)
d.emit(EventKeyError, task, err)
return
} }
d.watch(task) })
}()
delete(d.fetcherMap, rrId)
return return
} }
@@ -291,16 +302,10 @@ func (d *Downloader) Pause(id string) (err error) {
if task == nil { if task == nil {
return return
} }
task.lock.Lock() if err = d.doPause(task, base.DownloadStatusPause); err != nil {
defer task.lock.Unlock() return err
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)
} }
return return d.notifyRunning()
} }
func (d *Downloader) Continue(id string) (err error) { func (d *Downloader) Continue(id string) (err error) {
@@ -308,22 +313,57 @@ func (d *Downloader) Continue(id string) (err error) {
if task == nil { if task == nil {
return return
} }
task.lock.Lock() d.tryRunning(func(remain int) {
defer task.lock.Unlock() if remain == 0 {
if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError { // no more task can be running, need to doPause a running task
task.Status = base.DownloadStatusRunning for _, t := range d.tasks {
task.Progress.Speed = 0 if t.Status == base.DownloadStatusRunning {
err = d.restoreFetcher(task) err = d.doPause(t, base.DownloadStatusWait)
break
}
}
}
if err != nil { if err != nil {
return return
} }
task.timer.Start() })
err = task.fetcher.Continue()
if err != nil { if err != nil {
return return
} }
d.storage.Put(bucketTask, task.ID, task.clone()) return d.doContinue(task)
d.emit(EventKeyContinue, 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 return
} }
@@ -371,6 +411,11 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
} }
} }
d.emit(EventKeyDelete, task) d.emit(EventKeyDelete, task)
if task.Status == base.DownloadStatusRunning {
if err = d.doNotifyRunning(); err != nil {
return err
}
}
task = nil task = nil
return return
} }
@@ -380,7 +425,7 @@ func (d *Downloader) Close() error {
defer d.lock.Unlock() defer d.lock.Unlock()
d.closed.Store(true) d.closed.Store(true)
for _, task := range d.tasks { for _, task := range d.tasks {
d.Pause(task.ID) d.doPause(task, base.DownloadStatusPause)
} }
return d.storage.Close() return d.storage.Close()
} }
@@ -426,27 +471,20 @@ func (d *Downloader) GetTasks() []*Task {
return d.tasks return d.tasks
} }
func (d *Downloader) GetConfig() (bool, *DownloaderStoreConfig, error) { func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
var cfg DownloaderStoreConfig return d.cfg.DownloaderStoreConfig, nil
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
if err != nil {
return false, nil, err
}
return exist, &cfg, nil
} }
func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error { func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
d.cfg.DownloaderStoreConfig = v
return d.storage.Put(bucketConfig, "config", v) return d.storage.Put(bucketConfig, "config", v)
} }
func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) { func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
exist, cfg, err := d.GetConfig() cfg, err := d.GetConfig()
if err != nil { if err != nil {
return false, err return false, err
} }
if !exist {
return false, nil
}
if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil { if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil {
return false, nil return false, nil
} }
@@ -479,6 +517,7 @@ func (d *Downloader) watch(task *Task) {
d.emit(EventKeyDone, task) d.emit(EventKeyDone, task)
} }
d.emit(EventKeyFinally, task, err) d.emit(EventKeyFinally, task, err)
d.notifyRunning()
} }
func (d *Downloader) restoreFetcher(task *Task) error { func (d *Downloader) restoreFetcher(task *Task) error {
@@ -507,13 +546,96 @@ func (d *Downloader) restoreFetcher(task *Task) error {
} }
d.setupFetcher(task.fetcher) d.setupFetcher(task.fetcher)
task.fetcher.Create(task.Meta.Opts) task.fetcher.Create(task.Meta.Opts)
go d.watch(task)
} }
return nil 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) { func initTask(task *Task) {
task.timer = &util.Timer{} task.timer = util.NewTimer(task.Progress.Used)
task.lock = &sync.Mutex{} task.lock = &sync.Mutex{}
task.speedArr = make([]int64, 0) task.speedArr = make([]int64, 0)
} }
+2 -6
View File
@@ -170,7 +170,6 @@ func TestDownloader_Protocol_Config(t *testing.T) {
} }
storeCfg := &DownloaderStoreConfig{ storeCfg := &DownloaderStoreConfig{
RefreshInterval: 500,
DownloadDir: "./downloads", DownloadDir: "./downloads",
ProtocolConfig: map[string]any{ ProtocolConfig: map[string]any{
"http": map[string]any{ "http": map[string]any{
@@ -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) t.Fatal(err)
} }
exits, newStoreCfg, err := downloader.GetConfig() newStoreCfg, err := downloader.GetConfig()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if !exits {
t.Errorf("GetConfig() got = %v, want %v", exits, true)
}
if !test.JsonEqual(storeCfg, newStoreCfg) { if !test.JsonEqual(storeCfg, newStoreCfg) {
t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg)) t.Errorf("GetConfig() got = %v, want %v", test.ToJson(storeCfg), test.ToJson(newStoreCfg))
+16 -10
View File
@@ -70,6 +70,8 @@ func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
type DownloaderConfig struct { type DownloaderConfig struct {
Controller *controller.Controller Controller *controller.Controller
FetchBuilders []fetcher.FetcherBuilder FetchBuilders []fetcher.FetcherBuilder
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage Storage Storage
*DownloaderStoreConfig *DownloaderStoreConfig
@@ -85,24 +87,28 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
new(bt.FetcherBuilder), new(bt.FetcherBuilder),
} }
} }
if cfg.Storage == nil {
cfg.Storage = NewMemStorage()
}
if cfg.DownloaderStoreConfig == nil {
cfg.DownloaderStoreConfig = &DownloaderStoreConfig{}
}
if cfg.RefreshInterval == 0 { if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350 cfg.RefreshInterval = 350
} }
if cfg.Storage == nil {
cfg.Storage = NewMemStorage()
}
return cfg return cfg
} }
// DownloaderStoreConfig is the config that can restore the downloader. // DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct { type DownloaderStoreConfig struct {
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms) 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 directory to save the downloaded files
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 ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` // Extra is the extra config 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
}
+18 -2
View File
@@ -33,7 +33,7 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
if req.Rid != "" { if req.Rid != "" {
taskId, err = Downloader.Create(req.Rid, req.Opts) taskId, err = Downloader.Create(req.Rid, req.Opts)
} else if req.Req != nil { } else if req.Req != nil {
taskId, err = Downloader.DirectCreate(req.Req, req.Opts) taskId, err = Downloader.CreateDirect(req.Req, req.Opts)
} else { } else {
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam)) WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
return return
@@ -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"]
@@ -195,6 +211,6 @@ func writeError(w http.ResponseWriter, msg string) {
} }
func getServerConfig() *download.DownloaderStoreConfig { func getServerConfig() *download.DownloaderStoreConfig {
_, cfg, _ := Downloader.GetConfig() cfg, _ := Downloader.GetConfig()
return cfg return cfg
} }
+4 -1
View File
@@ -12,9 +12,9 @@ const (
type StartConfig struct { type StartConfig struct {
Network string `json:"network"` Network string `json:"network"`
Address string `json:"address"` Address string `json:"address"`
RefreshInterval int `json:"refreshInterval"`
Storage Storage `json:"storage"` Storage Storage `json:"storage"`
StorageDir string `json:"storageDir"` StorageDir string `json:"storageDir"`
RefreshInterval int `json:"refreshInterval"`
ApiToken string `json:"apiToken"` ApiToken string `json:"apiToken"`
WebEnable bool WebEnable bool
@@ -28,6 +28,9 @@ func (cfg *StartConfig) Init() *StartConfig {
if cfg.Address == "" { if cfg.Address == "" {
cfg.Address = "127.0.0.1:0" cfg.Address = "127.0.0.1:0"
} }
if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 350
}
if cfg.Storage == "" { if cfg.Storage == "" {
cfg.Storage = StorageBolt cfg.Storage = StorageBolt
} }
+5 -2
View File
@@ -57,14 +57,15 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
} }
startCfg.Init() startCfg.Init()
downloadCfg := &download.DownloaderConfig{} downloadCfg := &download.DownloaderConfig{
RefreshInterval: startCfg.RefreshInterval,
}
if startCfg.Storage == model.StorageBolt { if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir) downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
} else { } else {
downloadCfg.Storage = download.NewMemStorage() downloadCfg.Storage = download.NewMemStorage()
} }
downloadCfg.Init() downloadCfg.Init()
downloadCfg.RefreshInterval = startCfg.RefreshInterval
Downloader = download.NewDownloader(downloadCfg) Downloader = download.NewDownloader(downloadCfg)
if err := Downloader.Setup(); err != nil { if err := Downloader.Setup(); err != nil {
return nil, nil, err return nil, nil, err
@@ -84,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)
+43
View File
@@ -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()
} }
+6 -1
View File
@@ -2,12 +2,17 @@ package util
import "time" import "time"
// 计时器
type Timer struct { type Timer struct {
t int64 t int64
used int64 used int64
} }
func NewTimer(used int64) *Timer {
return &Timer{
used: used,
}
}
func (t *Timer) Start() { func (t *Timer) Start() {
t.t = time.Now().UnixNano() t.t = time.Now().UnixNano()
} }
@@ -12,9 +12,54 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true" android:exported="true"
android:hardwareAccelerated="true" android:hardwareAccelerated="true"
android:launchMode="singleTop" android:launchMode="singleInstance"
android:theme="@style/LaunchTheme" android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:host="*"
android:pathPattern=".*torrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="file"
android:host="*"
android:mimeType="application/x-bittorrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="content"
android:host="*"
android:pathPattern=".*torrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="content"
android:host="*"
android:mimeType="application/x-bittorrent" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="magnet" />
</intent-filter>
<!-- Specifies an Android theme to apply to this Activity as soon as <!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues while the Flutter UI initializes. After that, this theme continues
+5
View File
@@ -9,6 +9,8 @@
"downloading": "downloading", "downloading": "downloading",
"downloaded": "downloaded", "downloaded": "downloaded",
"setting": "Setting", "setting": "Setting",
"donate": "Donate",
"exit": "Exit",
"create": "Create Task", "create": "Create Task",
"downloadLink": "Download Link", "downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link", "downloadLinkValid": "Please enter the download link",
@@ -24,6 +26,7 @@
"downloadDir": "Download Directory", "downloadDir": "Download Directory",
"downloadDirValid": "Please select the download directory", "downloadDirValid": "Please select the download directory",
"connections": "Connections", "connections": "Connections",
"maxRunning": "Max Running Tasks",
"items": "@count items", "items": "@count items",
"subscribeTracker": "Subscribe Tracker", "subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later", "subscribeFail": "Subscribe failed, please check network or try again later",
@@ -47,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",
+4
View File
@@ -7,6 +7,8 @@
"selectAll": "انتخاب همه", "selectAll": "انتخاب همه",
"task": "کار", "task": "کار",
"setting": "تنظیمات", "setting": "تنظیمات",
"donate": "کمک مالی",
"exit": "خروج",
"create": "ایجاد کار", "create": "ایجاد کار",
"downloading": "دانلود کردن", "downloading": "دانلود کردن",
"downloaded": "دانلود شدن", "downloaded": "دانلود شدن",
@@ -45,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": "پاک کردن",
+5
View File
@@ -9,6 +9,8 @@
"downloading": "Загрузка", "downloading": "Загрузка",
"downloaded": "Загружено", "downloaded": "Загружено",
"setting": "Настройки", "setting": "Настройки",
"donate": "Пожертвовать",
"exit": "Выход",
"create": "Создать задачу", "create": "Создать задачу",
"downloadLink": "Ссылка для скачивание", "downloadLink": "Ссылка для скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания", "downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
@@ -24,6 +26,7 @@
"downloadDir": "Папка загрузки", "downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки", "downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений", "connections": "Соединений",
"maxRunning": "Максимальное количество активных задач",
"items": "@подсчет элементов", "items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера", "subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже", "subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
@@ -47,6 +50,8 @@
"notSet": "Не установлено", "notSet": "Не установлено",
"set": "Установлено", "set": "Установлено",
"effectAfterRestart": "Эффект после перезагрузки", "effectAfterRestart": "Эффект после перезагрузки",
"startAll": "Запустить все",
"pauseAll": "Приостановить все",
"deleteTask": "Удалить задачу", "deleteTask": "Удалить задачу",
"deleteTaskTip": "Сохранить загруженные файлы", "deleteTaskTip": "Сохранить загруженные файлы",
"delete": "Удалить", "delete": "Удалить",
+5
View File
@@ -9,6 +9,8 @@
"downloading": "下载中", "downloading": "下载中",
"downloaded": "已完成", "downloaded": "已完成",
"setting": "设置", "setting": "设置",
"donate": "打赏",
"exit": "退出",
"create": "创建任务", "create": "创建任务",
"downloadLink": "下载链接", "downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接", "downloadLinkValid": "请输入下载链接",
@@ -24,6 +26,7 @@
"downloadDir": "下载目录", "downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录", "downloadDirValid": "请选择下载目录",
"connections": "连接数", "connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 条", "items": "@count 条",
"subscribeTracker": "订阅 Tracker", "subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试", "subscribeFail": "订阅失败,请检查网络或稍后重试",
@@ -47,6 +50,8 @@
"notSet": "未设置", "notSet": "未设置",
"set": "已设置", "set": "已设置",
"effectAfterRestart": "此配置项将在重启应用后生效", "effectAfterRestart": "此配置项将在重启应用后生效",
"startAll": "全部开始",
"pauseAll": "全部暂停",
"deleteTask": "删除任务", "deleteTask": "删除任务",
"deleteTaskTip": "保留已下载的文件", "deleteTaskTip": "保留已下载的文件",
"delete": "删除", "delete": "删除",
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

+9 -5
View File
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
@@ -73,16 +72,13 @@ Future<T> _parse<T>(
) async { ) async {
try { try {
var resp = await fetch(); var resp = await fetch();
if (fromJsonT != null) { fromJsonT ??= (json) => null as T;
final result = Result<T>.fromJson(resp.data, fromJsonT); final result = Result<T>.fromJson(resp.data, fromJsonT);
if (result.code == 0) { if (result.code == 0) {
return result.data as T; return result.data as T;
} else { } else {
throw Exception(result); throw Exception(result);
} }
} else {
return null as T;
}
} on DioError catch (e) { } on DioError catch (e) {
throw Exception(Result(code: 1000, msg: e.message)); throw Exception(Result(code: 1000, msg: e.message));
} }
@@ -115,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);
@@ -5,6 +5,7 @@ part 'downloader_config.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class DownloaderConfig { class DownloaderConfig {
String downloadDir = ''; String downloadDir = '';
int maxRunning = 0;
ProtocolConfig protocolConfig = ProtocolConfig(); ProtocolConfig protocolConfig = ProtocolConfig();
ExtraConfig extra = ExtraConfig(); ExtraConfig extra = ExtraConfig();
@@ -9,6 +9,7 @@ part of 'downloader_config.dart';
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) => DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
DownloaderConfig() DownloaderConfig()
..downloadDir = json['downloadDir'] as String ..downloadDir = json['downloadDir'] as String
..maxRunning = json['maxRunning'] as int
..protocolConfig = ProtocolConfig.fromJson( ..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>?) json['protocolConfig'] as Map<String, dynamic>?)
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?); ..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?);
@@ -16,6 +17,7 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) => Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
<String, dynamic>{ <String, dynamic>{
'downloadDir': instance.downloadDir, 'downloadDir': instance.downloadDir,
'maxRunning': instance.maxRunning,
'protocolConfig': instance.protocolConfig.toJson(), 'protocolConfig': instance.protocolConfig.toJson(),
'extra': instance.extra.toJson(), 'extra': instance.extra.toJson(),
}; };
+1 -3
View File
@@ -1,12 +1,10 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'meta.dart'; import 'meta.dart';
import 'options.dart';
import 'resource.dart';
part 'task.g.dart'; part 'task.g.dart';
enum Status { ready, running, pause, error, done } enum Status { ready, running, pause, wait, error, done }
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class Task { class Task {
+1
View File
@@ -28,6 +28,7 @@ const _$StatusEnumMap = {
Status.ready: 'ready', Status.ready: 'ready',
Status.running: 'running', Status.running: 'running',
Status.pause: 'pause', Status.pause: 'pause',
Status.wait: 'wait',
Status.error: 'error', Status.error: 'error',
Status.done: 'done', Status.done: 'done',
}; };
@@ -1,10 +1,16 @@
import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:uri_to_file/uri_to_file.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart'; import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart'; import '../../../../api/model/downloader_config.dart';
@@ -12,7 +18,9 @@ import '../../../../core/common/start_config.dart';
import '../../../../generated/locales.g.dart'; import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart'; import '../../../../util/localeManager.dart';
import '../../../../util/log_util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network"; const _startConfigNetwork = "start.network";
const _startConfigAddress = "start.address"; const _startConfigAddress = "start.address";
@@ -55,13 +63,153 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
return ret; return ret;
}); });
class AppController extends GetxController { class AppController extends GetxController with WindowListener, TrayListener {
static StartConfig? _defaultStartConfig; static StartConfig? _defaultStartConfig;
final startConfig = StartConfig().obs; final startConfig = StartConfig().obs;
final runningPort = 0.obs; final runningPort = 0.obs;
final downloaderConfig = DownloaderConfig().obs; final downloaderConfig = DownloaderConfig().obs;
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
@override
void onReady() {
super.onReady();
try {
_initDeepLinks();
} catch (e) {
logger.w("initDeepLinks error", e);
}
try {
_initWindows();
} catch (e) {
logger.w("initWindows error", e);
}
try {
_initTray();
} catch (e) {
logger.w("initTray error", e);
}
}
@override
void onClose() {
_linkSubscription?.cancel();
trayManager.removeListener(this);
}
@override
void onWindowClose() async {
final isPreventClose = await windowManager.isPreventClose();
if (isPreventClose) {
windowManager.hide();
}
}
@override
void onTrayIconMouseDown() {
windowManager.show();
}
@override
void onTrayIconRightMouseDown() {
trayManager.popUpContextMenu();
}
Future<void> _initDeepLinks() async {
// currently only support android
if (!Util.isAndroid()) {
return;
}
_appLinks = AppLinks();
// Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await _toCreate(uri);
});
// Check initial link if app was in cold state (terminated)
final uri = await _appLinks.getInitialAppLink();
if (uri != null) {
await _toCreate(uri);
}
}
Future<void> _initWindows() async {
if (!Util.isDesktop()) {
return;
}
windowManager.addListener(this);
}
Future<void> _initTray() async {
if (!Util.isDesktop()) {
return;
}
if (Util.isWindows()) {
await trayManager.setIcon('assets/tray_icon/icon.ico');
} else if (Util.isMacos()) {
await trayManager.setIcon('assets/tray_icon/icon_mac.png',
isTemplate: true);
} else {
await trayManager.setIcon('assets/tray_icon/icon.png');
}
final menu = Menu(items: [
MenuItem(
label: "create".tr,
onClick: (menuItem) async => {
await windowManager.show(),
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 => {
await windowManager.show(),
await Get.rootDelegate.offAndToNamed(Routes.SETTING),
},
),
MenuItem.separator(),
MenuItem(
label: 'donate'.tr,
onClick: (menuItem) => {
launchUrl(
Uri.parse(
"https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate"),
mode: LaunchMode.externalApplication)
},
),
MenuItem(
label: '${"version".tr}(${packageInfo.version})',
),
MenuItem.separator(),
MenuItem(
label: 'exit'.tr,
onClick: (menuItem) => {windowManager.destroy()},
),
]);
await trayManager.setContextMenu(menu);
trayManager.addListener(this);
}
Future<void> _toCreate(Uri uri) async {
final path = uri.scheme == "magnet"
? uri.toString()
: (await toFile(uri.toString())).path;
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
}
String runningAddress() { String runningAddress() {
if (startConfig.value.network == 'unix') { if (startConfig.value.network == 'unix') {
return startConfig.value.address; return startConfig.value.address;
@@ -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;
} }
@@ -1,8 +1,8 @@
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../../api/model/resolve_result.dart'; import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/result.dart';
import 'package:rounded_loading_button/rounded_loading_button.dart'; import 'package:rounded_loading_button/rounded_loading_button.dart';
import '../../../../api/api.dart'; import '../../../../api/api.dart';
@@ -12,8 +12,8 @@ import '../../../../api/model/request.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../../../views/views/directory_selector.dart'; import '../../../views/directory_selector.dart';
import '../../../views/views/file_list_view.dart'; import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../controllers/create_controller.dart'; import '../controllers/create_controller.dart';
@@ -27,6 +27,14 @@ class CreateView extends GetView<CreateController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final String? filePath = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty && filePath != null) {
_urlController.text = filePath;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _doResolve();
});
}
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
@@ -46,7 +54,10 @@ class CreateView extends GetView<CreateController> {
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
child: Column( child: Column(
children: [ children: [
TextFormField( Row(
children: [
Expanded(
child: TextFormField(
autofocus: true, autofocus: true,
controller: _urlController, controller: _urlController,
minLines: 1, minLines: 1,
@@ -55,12 +66,30 @@ class CreateView extends GetView<CreateController> {
hintText: _hitText(), hintText: _hitText(),
hintStyle: const TextStyle(fontSize: 12), hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr, labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link)), icon: const Icon(Icons.link),
suffixIcon: IconButton(
onPressed: _urlController.clear,
icon: const Icon(Icons.clear),
),
),
validator: (v) { validator: (v) {
return v!.trim().isNotEmpty return v!.trim().isNotEmpty
? null ? null
: 'downloadLinkValid'.tr; : 'downloadLinkValid'.tr;
}), }),
),
IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
var pr = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ["torrent"]);
if (pr != null) {
_urlController.text = pr.files[0].path ?? "";
}
}),
],
),
Center( Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(15.0), padding: const EdgeInsets.all(15.0),
@@ -71,7 +100,25 @@ class CreateView extends GetView<CreateController> {
), ),
child: RoundedLoadingButton( child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary, color: Get.theme.colorScheme.secondary,
onPressed: () async { onPressed: _doResolve,
controller: _confirmController,
child: Text('confirm'.tr),
),
)),
),
],
),
),
),
),
);
}
Future<void> _doResolve() async {
if (controller.isResolving.value) {
return;
}
controller.isResolving.value = true;
try { try {
_confirmController.start(); _confirmController.start();
if (_resolveFormKey.currentState!.validate()) { if (_resolveFormKey.currentState!.validate()) {
@@ -84,19 +131,8 @@ class CreateView extends GetView<CreateController> {
showErrorMessage(e); showErrorMessage(e);
} finally { } finally {
_confirmController.reset(); _confirmController.reset();
controller.isResolving.value = false;
} }
},
controller: _confirmController,
child: Text('confirm'.tr),
),
)),
),
],
),
),
),
),
);
} }
String _hitText() { String _hitText() {
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../../../views/views/responsive_builder.dart'; import '../../../views/responsive_builder.dart';
import '../controllers/home_controller.dart'; import '../controllers/home_controller.dart';
class HomeView extends GetView<HomeController> { class HomeView extends GetView<HomeController> {
@@ -11,6 +11,15 @@ class HomeView extends GetView<HomeController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) { return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
switch (currentRoute?.location) {
case Routes.SETTING:
controller.currentIndex.value = 1;
break;
default:
controller.currentIndex.value = 0;
break;
}
return Scaffold( return Scaffold(
// extendBody: true, // extendBody: true,
body: Row( body: Row(
@@ -1,21 +1,21 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../../util/message.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:badges/badges.dart' as badges;
import '../../../../generated/locales.g.dart'; import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart'; import '../../../../util/localeManager.dart';
import '../../../../util/message.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../views/views/check_list_view.dart'; import '../../../views/check_list_view.dart';
import '../../../views/views/directory_selector.dart'; import '../../../views/directory_selector.dart';
import '../../../views/views/outlined_button_loading.dart'; import '../../../views/outlined_button_loading.dart';
import '../../app/controllers/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../controllers/setting_controller.dart'; import '../controllers/setting_controller.dart';
@@ -67,11 +67,37 @@ class SettingView extends GetView<SettingController> {
showLabel: false, 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 // http config items start
final httpConfig = downloaderCfg.value.protocolConfig.http; final httpConfig = downloaderCfg.value.protocolConfig.http;
final buildHttpConnections = _buildConfigItem( final buildHttpConnections = _buildConfigItem(
'connections'.tr, () => httpConfig.connections.toString(), (Key key) { 'connections', () => httpConfig.connections.toString(), (Key key) {
final connectionsController = final connectionsController =
TextEditingController(text: httpConfig.connections.toString()); TextEditingController(text: httpConfig.connections.toString());
connectionsController.addListener(() async { connectionsController.addListener(() async {
@@ -99,7 +125,7 @@ class SettingView extends GetView<SettingController> {
final btExtConfig = downloaderCfg.value.extra.bt; final btExtConfig = downloaderCfg.value.extra.bt;
final buildBtTrackerSubscribeUrls = _buildConfigItem( final buildBtTrackerSubscribeUrls = _buildConfigItem(
'subscribeTracker'.tr, 'subscribeTracker',
() => 'items'.trParams( () => 'items'.trParams(
{'count': btExtConfig.trackerSubscribeUrls.length.toString()}), {'count': btExtConfig.trackerSubscribeUrls.length.toString()}),
(Key key) { (Key key) {
@@ -155,7 +181,7 @@ class SettingView extends GetView<SettingController> {
); );
}); });
final buildBtTrackers = _buildConfigItem( final buildBtTrackers = _buildConfigItem(
'addTracker'.tr, 'addTracker',
() => 'items' () => 'items'
.trParams({'count': btExtConfig.customTrackers.length.toString()}), .trParams({'count': btExtConfig.customTrackers.length.toString()}),
(Key key) { (Key key) {
@@ -298,7 +324,7 @@ class SettingView extends GetView<SettingController> {
// advanced config items start // advanced config items start
final buildApiProtocol = _buildConfigItem( final buildApiProtocol = _buildConfigItem(
'protocol'.tr, 'protocol',
() => startCfg.value.network == 'tcp' () => startCfg.value.network == 'tcp'
? 'TCP ${startCfg.value.address}' ? 'TCP ${startCfg.value.address}'
: 'Unix', : 'Unix',
@@ -396,7 +422,7 @@ class SettingView extends GetView<SettingController> {
); );
}, },
); );
final buildApiToken = _buildConfigItem('apiToken'.tr, final buildApiToken = _buildConfigItem('apiToken',
() => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'seted'.tr, () => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'seted'.tr,
(Key key) { (Key key) {
final apiTokenController = final apiTokenController =
@@ -426,7 +452,7 @@ class SettingView extends GetView<SettingController> {
length: 2, length: 2,
child: Scaffold( child: Scaffold(
appBar: PreferredSize( appBar: PreferredSize(
preferredSize: Size.fromHeight(56), preferredSize: const Size.fromHeight(56),
child: AppBar( child: AppBar(
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
@@ -448,9 +474,8 @@ class SettingView extends GetView<SettingController> {
Text('general'.tr), Text('general'.tr),
Card( Card(
child: Column( child: Column(
children: _addDivider([ children: _addDivider(
buildDownloadDir(), [buildDownloadDir(), buildMaxRunning()]),
]),
)), )),
const Text('HTTP'), const Text('HTTP'),
Card( Card(
@@ -502,7 +527,7 @@ class SettingView extends GetView<SettingController> {
], ],
), ),
], ],
).paddingOnly(left: 16, right: 16, top: 16, bottom: 64)), ).paddingOnly(left: 16, right: 16, top: 16, bottom: 16)),
), ),
); );
}); });
@@ -1,4 +1,3 @@
import 'dart:async';
import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart'; import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
@@ -3,5 +3,11 @@ import 'task_list_controller.dart';
class TaskDownloadingController extends TaskListController { class TaskDownloadingController extends TaskListController {
TaskDownloadingController() TaskDownloadingController()
: super([Status.ready, Status.running, Status.pause, Status.error]); : super([
Status.ready,
Status.running,
Status.pause,
Status.wait,
Status.error
]);
} }
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../views/views/buid_task_list_view.dart'; import '../../../views/buid_task_list_view.dart';
import '../controllers/task_downloaded_controller.dart'; import '../controllers/task_downloaded_controller.dart';
class TaskDownloadedView extends GetView<TaskDownloadedController> { class TaskDownloadedView extends GetView<TaskDownloadedController> {
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../views/views/buid_task_list_view.dart'; import '../../../views/buid_task_list_view.dart';
import '../controllers/task_downloading_controller.dart'; import '../controllers/task_downloading_controller.dart';
class TaskDownloadingView extends GetView<TaskDownloadingController> { class TaskDownloadingView extends GetView<TaskDownloadingController> {
@@ -1,16 +1,16 @@
import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:path/path.dart' as path;
import '../../../api/api.dart'; import '../../api/api.dart';
import '../../../api/model/task.dart'; import '../../api/model/task.dart';
import '../../../util/file_icon.dart'; import '../../util/file_explorer.dart';
import '../../../util/icons.dart'; import '../../util/file_icon.dart';
import '../../../util/message.dart'; import '../../util/icons.dart';
import '../../../util/util.dart'; import '../../util/message.dart';
import '../../routes/app_pages.dart'; import '../../util/util.dart';
import '../routes/app_pages.dart';
class BuildTaskListView extends GetView { class BuildTaskListView extends GetView {
final List<Task> tasks; final List<Task> tasks;
@@ -53,6 +53,18 @@ class BuildTaskListView extends GetView {
return task.status == Status.running; return task.status == Status.running;
} }
String buildExplorerUrl(Task task) {
if (task.meta.res.rootDir.trim().isEmpty) {
return path.join(
Util.safeDir(task.meta.opts.path),
Util.safeDir(task.meta.res.files[0].path),
task.meta.res.files[0].name);
} else {
return path.join(Util.safeDir(task.meta.opts.path),
Util.safeDir(task.meta.res.rootDir));
}
}
Future<void> showDeleteDialog(String id) { Future<void> showDeleteDialog(String id) {
final keep = true.obs; final keep = true.obs;
@@ -100,9 +112,7 @@ class BuildTaskListView extends GetView {
list.add(IconButton( list.add(IconButton(
icon: const Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: () async { onPressed: () async {
final file = File(Util.buildAbsPath(task.meta.opts.path, await FileExplorer.openAndSelectFile(buildExplorerUrl(task));
task.meta.res.files[0].path, task.meta.res.files[0].name));
await launchUrl(file.parent.uri);
}, },
)); ));
} }
@@ -111,14 +121,22 @@ class BuildTaskListView extends GetView {
list.add(IconButton( list.add(IconButton(
icon: const Icon(Icons.pause), icon: const Icon(Icons.pause),
onPressed: () async { onPressed: () async {
try {
await pauseTask(task.id); await pauseTask(task.id);
} catch (e) {
showErrorMessage(e);
}
}, },
)); ));
} else { } else {
list.add(IconButton( list.add(IconButton(
icon: const Icon(Icons.play_arrow), icon: const Icon(Icons.play_arrow),
onPressed: () async { onPressed: () async {
try {
await continueTask(task.id); await continueTask(task.id);
} catch (e) {
showErrorMessage(e);
}
}, },
)); ));
} }
@@ -186,15 +204,13 @@ class BuildTaskListView extends GetView {
: Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]), : Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]),
trailing: SizedBox( trailing: SizedBox(
width: task.status == Status.done ? 80 : 180, width: 180,
child: Row( child: Row(
// crossAxisAlignment: CrossAxisAlignment.baseline, // crossAxisAlignment: CrossAxisAlignment.baseline,
// textBaseline: DefaultTextStyle.of(context).style.textBaseline, // textBaseline: DefaultTextStyle.of(context).style.textBaseline,
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
task.status == Status.done Text("${Util.fmtByte(task.progress.speed)} / s",
? const SizedBox.shrink()
: Text("${Util.fmtByte(task.progress.speed)} / s",
style: context.textTheme.titleSmall), style: context.textTheme.titleSmall),
...buildActions() ...buildActions()
], ],
@@ -2,8 +2,8 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../util/mac_secure_util.dart'; import '../../util/mac_secure_util.dart';
import '../../../util/util.dart'; import '../../util/util.dart';
class DirectorySelector extends StatefulWidget { class DirectorySelector extends StatefulWidget {
final TextEditingController controller; final TextEditingController controller;
@@ -35,19 +35,17 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr; return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
}, },
)), )),
!Util.isDesktop() Util.isDesktop() || Util.isAndroid()
? null ? IconButton(
: IconButton(
icon: const Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: () async { onPressed: () async {
if (GetPlatform.isDesktop) {
var dir = await FilePicker.platform.getDirectoryPath(); var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) { if (dir != null) {
widget.controller.text = dir; widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir); MacSecureUtil.saveBookmark(dir);
} }
}
}) })
: null
].where((e) => e != null).map((e) => e!).toList(), ].where((e) => e != null).map((e) => e!).toList(),
); );
} }
@@ -3,14 +3,14 @@ import 'dart:ui' as ui;
import 'package:fluent_ui/fluent_ui.dart' as fluent; import 'package:fluent_ui/fluent_ui.dart' as fluent;
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../api/model/resource.dart'; import '../../api/model/resource.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import '../../../util/file_icon.dart'; import '../../util/file_icon.dart';
import '../../../util/icons.dart'; import '../../util/icons.dart';
import '../../../util/util.dart'; import '../../util/util.dart';
import '../../modules/app/controllers/app_controller.dart'; import '../modules/app/controllers/app_controller.dart';
import '../../modules/create/controllers/create_controller.dart'; import '../modules/create/controllers/create_controller.dart';
class FileListView extends GetView { class FileListView extends GetView {
final List<FileInfo> files; final List<FileInfo> files;
+24
View File
@@ -23,6 +23,8 @@ class LocaleKeys {
static const downloading = 'downloading'; static const downloading = 'downloading';
static const downloaded = 'downloaded'; static const downloaded = 'downloaded';
static const setting = 'setting'; static const setting = 'setting';
static const donate = 'donate';
static const exit = 'exit';
static const create = 'create'; static const create = 'create';
static const downloadLink = 'downloadLink'; static const downloadLink = 'downloadLink';
static const downloadLinkValid = 'downloadLinkValid'; static const downloadLinkValid = 'downloadLinkValid';
@@ -38,6 +40,7 @@ class LocaleKeys {
static const downloadDir = 'downloadDir'; static const downloadDir = 'downloadDir';
static const downloadDirValid = 'downloadDirValid'; static const downloadDirValid = 'downloadDirValid';
static const connections = 'connections'; static const connections = 'connections';
static const maxRunning = 'maxRunning';
static const items = 'items'; static const items = 'items';
static const subscribeTracker = 'subscribeTracker'; static const subscribeTracker = 'subscribeTracker';
static const subscribeFail = 'subscribeFail'; static const subscribeFail = 'subscribeFail';
@@ -61,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';
@@ -81,6 +86,8 @@ class Locales {
'downloading': 'downloading', 'downloading': 'downloading',
'downloaded': 'downloaded', 'downloaded': 'downloaded',
'setting': 'Setting', 'setting': 'Setting',
'donate': 'Donate',
'exit': 'Exit',
'create': 'Create Task', 'create': 'Create Task',
'downloadLink': 'Download Link', 'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link', 'downloadLinkValid': 'Please enter the download link',
@@ -97,6 +104,7 @@ class Locales {
'downloadDir': 'Download Directory', 'downloadDir': 'Download Directory',
'downloadDirValid': 'Please select the download directory', 'downloadDirValid': 'Please select the download directory',
'connections': 'Connections', 'connections': 'Connections',
'maxRunning': 'Max Running Tasks',
'items': '@count items', 'items': '@count items',
'subscribeTracker': 'Subscribe Tracker', 'subscribeTracker': 'Subscribe Tracker',
'subscribeFail': 'subscribeFail':
@@ -121,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',
@@ -137,6 +147,8 @@ class Locales {
'selectAll': 'انتخاب همه', 'selectAll': 'انتخاب همه',
'task': 'کار', 'task': 'کار',
'setting': 'تنظیمات', 'setting': 'تنظیمات',
'donate': 'کمک مالی',
'exit': 'خروج',
'create': 'ایجاد کار', 'create': 'ایجاد کار',
'downloading': 'دانلود کردن', 'downloading': 'دانلود کردن',
'downloaded': 'دانلود شدن', 'downloaded': 'دانلود شدن',
@@ -176,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': 'پاک کردن',
@@ -194,6 +208,8 @@ class Locales {
'downloading': 'Загрузка', 'downloading': 'Загрузка',
'downloaded': 'Загружено', 'downloaded': 'Загружено',
'setting': 'Настройки', 'setting': 'Настройки',
'donate': 'Пожертвовать',
'exit': 'Выход',
'create': 'Создать задачу', 'create': 'Создать задачу',
'downloadLink': 'Ссылка для скачивание', 'downloadLink': 'Ссылка для скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания', 'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
@@ -210,6 +226,7 @@ class Locales {
'downloadDir': 'Папка загрузки', 'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки', 'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений', 'connections': 'Соединений',
'maxRunning': 'Максимальное количество активных задач',
'items': '@подсчет элементов', 'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера', 'subscribeTracker': 'Подпись трекера',
'subscribeFail': 'subscribeFail':
@@ -234,6 +251,8 @@ class Locales {
'notSet': 'Не установлено', 'notSet': 'Не установлено',
'set': 'Установлено', 'set': 'Установлено',
'effectAfterRestart': 'Эффект после перезагрузки', 'effectAfterRestart': 'Эффект после перезагрузки',
'startAll': 'Запустить все',
'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить задачу', 'deleteTask': 'Удалить задачу',
'deleteTaskTip': 'Сохранить загруженные файлы', 'deleteTaskTip': 'Сохранить загруженные файлы',
'delete': 'Удалить', 'delete': 'Удалить',
@@ -252,6 +271,8 @@ class Locales {
'downloading': '下载中', 'downloading': '下载中',
'downloaded': '已完成', 'downloaded': '已完成',
'setting': '设置', 'setting': '设置',
'donate': '打赏',
'exit': '退出',
'create': '创建任务', 'create': '创建任务',
'downloadLink': '下载链接', 'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接', 'downloadLinkValid': '请输入下载链接',
@@ -267,6 +288,7 @@ class Locales {
'downloadDir': '下载目录', 'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录', 'downloadDirValid': '请选择下载目录',
'connections': '连接数', 'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 条', 'items': '@count 条',
'subscribeTracker': '订阅 Tracker', 'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试', 'subscribeFail': '订阅失败,请检查网络或稍后重试',
@@ -290,6 +312,8 @@ class Locales {
'notSet': '未设置', 'notSet': '未设置',
'set': '已设置', 'set': '已设置',
'effectAfterRestart': '此配置项将在重启应用后生效', 'effectAfterRestart': '此配置项将在重启应用后生效',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务', 'deleteTask': '删除任务',
'deleteTaskTip': '保留已下载的文件', 'deleteTaskTip': '保留已下载的文件',
'delete': '删除', 'delete': '删除',
+15
View File
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'api/api.dart' as api; import 'api/api.dart' as api;
import 'app/modules/app/controllers/app_controller.dart'; import 'app/modules/app/controllers/app_controller.dart';
@@ -11,6 +12,7 @@ import 'util/localeManager.dart';
import 'util/log_util.dart'; import 'util/log_util.dart';
import 'util/mac_secure_util.dart'; import 'util/mac_secure_util.dart';
import 'util/package_info.dart'; import 'util/package_info.dart';
import 'util/util.dart';
void main() async { void main() async {
await init(); await init();
@@ -21,6 +23,19 @@ void main() async {
Future<void> init() async { Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop()) {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(1024, 768),
center: true,
skipTaskbar: false,
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
await windowManager.setPreventClose(true);
});
}
final controller = Get.put(AppController()); final controller = Get.put(AppController());
try { try {
+47
View File
@@ -0,0 +1,47 @@
import 'dart:io';
class FileExplorer {
static Future<void> openAndSelectFile(String filePath) async {
if (!await FileSystemEntity.isDirectory(filePath) &&
!await FileSystemEntity.isFile(filePath)) {
return;
}
if (await FileSystemEntity.isDirectory(filePath)) {
if (Platform.isWindows) {
Process.run('explorer.exe', [filePath]);
} else if (Platform.isMacOS || Platform.isLinux) {
Process.run('xdg-open', [filePath]);
}
} else if (await FileSystemEntity.isFile(filePath)) {
if (Platform.isWindows) {
Process.run('explorer.exe', ['/select,', filePath]);
} else if (Platform.isMacOS) {
Process.run('open', ['-R', filePath]);
} else if (Platform.isLinux) {
if (await _isUbuntuOrDebian()) {
Process.run('xdg-open', [filePath]);
} else if (await _isCentOS()) {
Process.run('nautilus', ['--select', filePath]);
}
}
}
}
static Future<bool> _isUbuntuOrDebian() async {
final result = await Process.run('lsb_release', ['-i']);
if (result.exitCode != 0) {
return false;
}
final output = result.stdout.toString().toLowerCase();
return output.contains('ubuntu') || output.contains('debian');
}
static Future<bool> _isCentOS() async {
final result = await Process.run('cat', ['/etc/os-release']);
if (result.exitCode != 0) {
return false;
}
final output = result.stdout.toString().toLowerCase();
return output.contains('centos');
}
}
+8 -5
View File
@@ -3,12 +3,11 @@ import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
class Util { class Util {
static String buildAbsPath(String dir, String path, String name) { static String safeDir(String path) {
return (dir == "" ? "" : "$dir/") + buildPath(path, name); if (path == "." || path == "./" || path == ".\\") {
return "";
} }
return path;
static String buildPath(String path, String name) {
return (path == "" ? "" : "$path/") + name;
} }
static String fmtByte(int byte) { static String fmtByte(int byte) {
@@ -42,6 +41,10 @@ class Util {
return Platform.isWindows || Platform.isLinux || Platform.isMacOS; return Platform.isWindows || Platform.isLinux || Platform.isMacOS;
} }
static isWindows() {
return !kIsWeb && Platform.isWindows;
}
static isMacos() { static isMacos() {
return !kIsWeb && Platform.isMacOS; return !kIsWeb && Platform.isMacOS;
} }
Loaded 50 of 55 files, more files were not shown because too many files have changed in this diff. Show more