Compare commits

..
12 Commits
55 changed files with 986 additions and 262 deletions

No files matched your search

+27 -19
View File
@@ -3,6 +3,10 @@ name: build
on:
workflow_dispatch:
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
permissions:
contents: write
@@ -24,11 +28,12 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -123,10 +128,9 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- name: Build
run: |
docker pull techknowlogick/xgo:go-1.19.x
@@ -142,14 +146,15 @@ jobs:
needs: [get-release, build-macos-arm64-lib]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-node@v3
with:
node-version: 16
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg
run: npm install -g appdmg
- uses: actions/download-artifact@v3
@@ -197,14 +202,15 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
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
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -256,15 +262,16 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: actions/setup-java@v3
with:
distribution: "zulu"
java-version: "11"
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -292,11 +299,12 @@ jobs:
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
+5 -2
View File
@@ -16,6 +16,9 @@ on:
- "go.sum"
- "Dockerfile"
env:
GO_VERSION: 1.19
permissions:
contents: write
@@ -28,9 +31,9 @@ jobs:
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
go-version: ${{ env.GO_VERSION }}
- uses: release-drafter/release-drafter@v5
id: create_release
env:
+11 -7
View File
@@ -26,6 +26,10 @@ on:
- "go.mod"
- "go.sum"
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
jobs:
# lint:
# name: Lint
@@ -45,10 +49,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v3
@@ -61,14 +64,15 @@ jobs:
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
- uses: actions/setup-go@v4
with:
go-version: "^1.19"
cache: true
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
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
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
+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
```
## 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
[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
```
## 感谢
### 贡献者
<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) 协议开源。
+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

+4 -1
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
)
func main() {
@@ -16,7 +17,9 @@ func main() {
}
}).
Create(&base.Options{
Connections: 8,
Extra: http.OptsExtra{
Connections: 8,
},
})
if err != nil {
panic(err)
+9 -2
View File
@@ -13,9 +13,17 @@ import (
const progressWidth = 20
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
wg.Add(1)
_, err := download.Boot().
URL(args.url).
Listener(func(event *download.Event) {
@@ -46,7 +54,6 @@ func main() {
if err != nil {
panic(err)
}
wg.Add(1)
wg.Wait()
}
+4 -6
View File
@@ -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)
}
}
+2 -1
View File
@@ -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"
)
+187 -65
View File
@@ -7,8 +7,10 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"math"
"os"
"path"
"sort"
"strings"
"sync"
"sync/atomic"
@@ -36,15 +38,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 +56,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 +77,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 +112,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 +128,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 +138,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 +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
@@ -185,7 +205,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 +221,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 +233,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 +273,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 +302,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,22 +313,57 @@ 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 {
})
if err != nil {
return
}
return d.doContinue(task)
}
func (d *Downloader) PauseAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return
}
}
}
return
}
func (d *Downloader) ContinueAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
runningCount := 0
continueTasks := make([]*Task, 0)
for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning {
runningCount++
} else if task.Status != base.DownloadStatusDone {
continueTasks = append(continueTasks, task)
}
}
// calculate how many tasks can be continued, can't exceed maxRunning
continueCount := int(math.Min(float64(d.cfg.MaxRunning-runningCount), float64(len(continueTasks))))
for i := 0; i < continueCount; i++ {
if err = d.doContinue(continueTasks[i]); err != nil {
return
}
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyContinue, task)
}
return
}
@@ -371,6 +411,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 +425,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 +471,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 +517,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 +546,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)
}
+3 -7
View File
@@ -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))
+19 -13
View File
@@ -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
}
+18 -2
View File
@@ -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
@@ -74,6 +74,22 @@ func ContinueTask(w http.ResponseWriter, r *http.Request) {
WriteJson(w, model.NewNilResult())
}
func PauseAllTask(w http.ResponseWriter, r *http.Request) {
if err := Downloader.PauseAll(); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func ContinueAllTask(w http.ResponseWriter, r *http.Request) {
if err := Downloader.ContinueAll(); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
}
func DeleteTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
@@ -195,6 +211,6 @@ func writeError(w http.ResponseWriter, msg string) {
}
func getServerConfig() *download.DownloaderStoreConfig {
_, cfg, _ := Downloader.GetConfig()
cfg, _ := Downloader.GetConfig()
return cfg
}
+4 -1
View File
@@ -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
}
+5 -2
View File
@@ -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
@@ -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.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/continue").HandlerFunc(ContinueAllTask)
r.Methods(http.MethodDelete).Path("/api/v1/tasks/{id}").HandlerFunc(DeleteTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks/{id}").HandlerFunc(GetTask)
r.Methods(http.MethodGet).Path("/api/v1/tasks").HandlerFunc(GetTasks)
+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) {
doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
@@ -275,6 +308,16 @@ func doTest(handler func()) {
Stop()
}()
defer Downloader.Clear()
defer func() {
tasks := Downloader.GetTasks()
taskIds := make([]string, len(tasks))
for i, task := range tasks {
taskIds[i] = task.ID
}
for _, id := range taskIds {
Downloader.Delete(id, true)
}
}()
taskReq.URL = "http://" + fileListener.Addr().String() + "/" + test.BuildName
handler()
}
+6 -1
View File
@@ -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()
}
@@ -12,9 +12,54 @@
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:exported="true"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:launchMode="singleInstance"
android:theme="@style/LaunchTheme"
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
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
+5
View File
@@ -9,6 +9,8 @@
"downloading": "downloading",
"downloaded": "downloaded",
"setting": "Setting",
"donate": "Donate",
"exit": "Exit",
"create": "Create Task",
"downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link",
@@ -24,6 +26,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",
@@ -47,6 +50,8 @@
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "Start All",
"pauseAll": "Pause All",
"deleteTask": "Delete Task",
"deleteTaskTip": "Keep downloaded files",
"delete": "Delete",
+4
View File
@@ -7,6 +7,8 @@
"selectAll": "انتخاب همه",
"task": "کار",
"setting": "تنظیمات",
"donate": "کمک مالی",
"exit": "خروج",
"create": "ایجاد کار",
"downloading": "دانلود کردن",
"downloaded": "دانلود شدن",
@@ -45,6 +47,8 @@
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "شروع همه",
"pauseAll": "توقف همه",
"deleteTask": "پاک کردن کار",
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
"delete": "پاک کردن",
+5
View File
@@ -9,6 +9,8 @@
"downloading": "Загрузка",
"downloaded": "Загружено",
"setting": "Настройки",
"donate": "Пожертвовать",
"exit": "Выход",
"create": "Создать задачу",
"downloadLink": "Ссылка для скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
@@ -24,6 +26,7 @@
"downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений",
"maxRunning": "Максимальное количество активных задач",
"items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
@@ -47,6 +50,8 @@
"notSet": "Не установлено",
"set": "Установлено",
"effectAfterRestart": "Эффект после перезагрузки",
"startAll": "Запустить все",
"pauseAll": "Приостановить все",
"deleteTask": "Удалить задачу",
"deleteTaskTip": "Сохранить загруженные файлы",
"delete": "Удалить",
+5
View File
@@ -9,6 +9,8 @@
"downloading": "下载中",
"downloaded": "已完成",
"setting": "设置",
"donate": "打赏",
"exit": "退出",
"create": "创建任务",
"downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接",
@@ -24,6 +26,7 @@
"downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录",
"connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 条",
"subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试",
@@ -47,6 +50,8 @@
"notSet": "未设置",
"set": "已设置",
"effectAfterRestart": "此配置项将在重启应用后生效",
"startAll": "全部开始",
"pauseAll": "全部暂停",
"deleteTask": "删除任务",
"deleteTaskTip": "保留已下载的文件",
"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

+13 -9
View File
@@ -1,4 +1,3 @@
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
@@ -73,15 +72,12 @@ Future<T> _parse<T>(
) async {
try {
var resp = await fetch();
if (fromJsonT != null) {
final result = Result<T>.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<T>.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));
@@ -115,6 +111,14 @@ Future<void> continueTask(String id) async {
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
}
Future<void> pauseAllTasks() async {
return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null);
}
Future<void> continueAllTasks() async {
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
}
Future<void> deleteTask(String id, bool force) async {
return _parse(
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
@@ -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();
@@ -9,6 +9,7 @@ part of 'downloader_config.dart';
DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
DownloaderConfig()
..downloadDir = json['downloadDir'] as String
..maxRunning = json['maxRunning'] as int
..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] 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) =>
<String, dynamic>{
'downloadDir': instance.downloadDir,
'maxRunning': instance.maxRunning,
'protocolConfig': instance.protocolConfig.toJson(),
'extra': instance.extra.toJson(),
};
+1 -3
View File
@@ -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 {
+1
View File
@@ -28,6 +28,7 @@ const _$StatusEnumMap = {
Status.ready: 'ready',
Status.running: 'running',
Status.pause: 'pause',
Status.wait: 'wait',
Status.error: 'error',
Status.done: 'done',
};
@@ -1,10 +1,16 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:path_provider/path_provider.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/model/downloader_config.dart';
@@ -12,7 +18,9 @@ import '../../../../core/common/start_config.dart';
import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network";
const _startConfigAddress = "start.address";
@@ -55,13 +63,153 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
return ret;
});
class AppController extends GetxController {
class AppController extends GetxController with WindowListener, TrayListener {
static StartConfig? _defaultStartConfig;
final startConfig = StartConfig().obs;
final runningPort = 0.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() {
if (startConfig.value.network == 'unix') {
return startConfig.value.address;
@@ -5,4 +5,5 @@ class CreateController extends GetxController {
final RxList fileInfos = [].obs;
final RxList selectedIndexes = [].obs;
final RxList openedFolders = [].obs;
final isResolving = false.obs;
}
@@ -1,8 +1,8 @@
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/result.dart';
import 'package:rounded_loading_button/rounded_loading_button.dart';
import '../../../../api/api.dart';
@@ -12,8 +12,8 @@ import '../../../../api/model/request.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
import '../../../views/views/directory_selector.dart';
import '../../../views/views/file_list_view.dart';
import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart';
import '../controllers/create_controller.dart';
@@ -27,6 +27,14 @@ class CreateView extends GetView<CreateController> {
@override
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(
appBar: AppBar(
leading: IconButton(
@@ -46,21 +54,42 @@ class CreateView extends GetView<CreateController> {
autovalidateMode: AutovalidateMode.always,
child: Column(
children: [
TextFormField(
autofocus: true,
controller: _urlController,
minLines: 1,
maxLines: 30,
decoration: InputDecoration(
hintText: _hitText(),
hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link)),
validator: (v) {
return v!.trim().isNotEmpty
? null
: 'downloadLinkValid'.tr;
}),
Row(
children: [
Expanded(
child: TextFormField(
autofocus: true,
controller: _urlController,
minLines: 1,
maxLines: 30,
decoration: InputDecoration(
hintText: _hitText(),
hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link),
suffixIcon: IconButton(
onPressed: _urlController.clear,
icon: const Icon(Icons.clear),
),
),
validator: (v) {
return v!.trim().isNotEmpty
? null
: '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(
child: Padding(
padding: const EdgeInsets.all(15.0),
@@ -71,21 +100,7 @@ class CreateView extends GetView<CreateController> {
),
child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary,
onPressed: () async {
try {
_confirmController.start();
if (_resolveFormKey.currentState!.validate()) {
final rr = await resolve(Request(
url: _urlController.text,
));
await _showResolveDialog(rr);
}
} catch (e) {
showErrorMessage(e);
} finally {
_confirmController.reset();
}
},
onPressed: _doResolve,
controller: _confirmController,
child: Text('confirm'.tr),
),
@@ -99,6 +114,27 @@ class CreateView extends GetView<CreateController> {
);
}
Future<void> _doResolve() async {
if (controller.isResolving.value) {
return;
}
controller.isResolving.value = true;
try {
_confirmController.start();
if (_resolveFormKey.currentState!.validate()) {
final rr = await resolve(Request(
url: _urlController.text,
));
await _showResolveDialog(rr);
}
} catch (e) {
showErrorMessage(e);
} finally {
_confirmController.reset();
controller.isResolving.value = false;
}
}
String _hitText() {
return 'downloadLinkHit'.trParams({
'append': Util.isDesktop() ? 'downloadLinkHitDesktop'.tr : '',
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../../../views/views/responsive_builder.dart';
import '../../../views/responsive_builder.dart';
import '../controllers/home_controller.dart';
class HomeView extends GetView<HomeController> {
@@ -11,6 +11,15 @@ class HomeView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
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(
// extendBody: true,
body: Row(
@@ -1,21 +1,21 @@
import 'dart:async';
import 'dart:convert';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../../util/message.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:badges/badges.dart' as badges;
import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/message.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../views/views/check_list_view.dart';
import '../../../views/views/directory_selector.dart';
import '../../../views/views/outlined_button_loading.dart';
import '../../../views/check_list_view.dart';
import '../../../views/directory_selector.dart';
import '../../../views/outlined_button_loading.dart';
import '../../app/controllers/app_controller.dart';
import '../controllers/setting_controller.dart';
@@ -67,11 +67,37 @@ class SettingView extends GetView<SettingController> {
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<SettingController> {
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<SettingController> {
);
});
final buildBtTrackers = _buildConfigItem(
'addTracker'.tr,
'addTracker',
() => 'items'
.trParams({'count': btExtConfig.customTrackers.length.toString()}),
(Key key) {
@@ -298,7 +324,7 @@ class SettingView extends GetView<SettingController> {
// 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<SettingController> {
);
},
);
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<SettingController> {
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<SettingController> {
Text('general'.tr),
Card(
child: Column(
children: _addDivider([
buildDownloadDir(),
]),
children: _addDivider(
[buildDownloadDir(), buildMaxRunning()]),
)),
const Text('HTTP'),
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';
@@ -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
]);
}
@@ -1,7 +1,7 @@
import 'package:flutter/material.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';
class TaskDownloadedView extends GetView<TaskDownloadedController> {
@@ -1,7 +1,7 @@
import 'package:flutter/material.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';
class TaskDownloadingView extends GetView<TaskDownloadingController> {
@@ -1,16 +1,16 @@
import 'dart:io';
import 'package:flutter/material.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/model/task.dart';
import '../../../util/file_icon.dart';
import '../../../util/icons.dart';
import '../../../util/message.dart';
import '../../../util/util.dart';
import '../../routes/app_pages.dart';
import '../../api/api.dart';
import '../../api/model/task.dart';
import '../../util/file_explorer.dart';
import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/message.dart';
import '../../util/util.dart';
import '../routes/app_pages.dart';
class BuildTaskListView extends GetView {
final List<Task> tasks;
@@ -53,6 +53,18 @@ class BuildTaskListView extends GetView {
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) {
final keep = true.obs;
@@ -100,9 +112,7 @@ class BuildTaskListView extends GetView {
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
final file = File(Util.buildAbsPath(task.meta.opts.path,
task.meta.res.files[0].path, task.meta.res.files[0].name));
await launchUrl(file.parent.uri);
await FileExplorer.openAndSelectFile(buildExplorerUrl(task));
},
));
}
@@ -111,14 +121,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);
}
},
));
}
@@ -186,16 +204,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()
],
),
@@ -2,8 +2,8 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../util/mac_secure_util.dart';
import '../../../util/util.dart';
import '../../util/mac_secure_util.dart';
import '../../util/util.dart';
class DirectorySelector extends StatefulWidget {
final TextEditingController controller;
@@ -35,19 +35,17 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
},
)),
!Util.isDesktop()
? null
: IconButton(
Util.isDesktop() || Util.isAndroid()
? IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
if (GetPlatform.isDesktop) {
var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
}
var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
}
})
: null
].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:flutter/material.dart';
import 'package:get/get.dart';
import '../../../api/model/resource.dart';
import '../../api/model/resource.dart';
import 'package:path/path.dart' as path;
import '../../../util/file_icon.dart';
import '../../../util/icons.dart';
import '../../../util/util.dart';
import '../../modules/app/controllers/app_controller.dart';
import '../../modules/create/controllers/create_controller.dart';
import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/util.dart';
import '../modules/app/controllers/app_controller.dart';
import '../modules/create/controllers/create_controller.dart';
class FileListView extends GetView {
final List<FileInfo> files;
+24
View File
@@ -23,6 +23,8 @@ class LocaleKeys {
static const downloading = 'downloading';
static const downloaded = 'downloaded';
static const setting = 'setting';
static const donate = 'donate';
static const exit = 'exit';
static const create = 'create';
static const downloadLink = 'downloadLink';
static const downloadLinkValid = 'downloadLinkValid';
@@ -38,6 +40,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';
@@ -61,6 +64,8 @@ class LocaleKeys {
static const notSet = 'notSet';
static const set = 'set';
static const effectAfterRestart = 'effectAfterRestart';
static const startAll = 'startAll';
static const pauseAll = 'pauseAll';
static const deleteTask = 'deleteTask';
static const deleteTaskTip = 'deleteTaskTip';
static const delete = 'delete';
@@ -81,6 +86,8 @@ class Locales {
'downloading': 'downloading',
'downloaded': 'downloaded',
'setting': 'Setting',
'donate': 'Donate',
'exit': 'Exit',
'create': 'Create Task',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link',
@@ -97,6 +104,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':
@@ -121,6 +129,8 @@ class Locales {
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'Start All',
'pauseAll': 'Pause All',
'deleteTask': 'Delete Task',
'deleteTaskTip': 'Keep downloaded files',
'delete': 'Delete',
@@ -137,6 +147,8 @@ class Locales {
'selectAll': 'انتخاب همه',
'task': 'کار',
'setting': 'تنظیمات',
'donate': 'کمک مالی',
'exit': 'خروج',
'create': 'ایجاد کار',
'downloading': 'دانلود کردن',
'downloaded': 'دانلود شدن',
@@ -176,6 +188,8 @@ class Locales {
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'شروع همه',
'pauseAll': 'توقف همه',
'deleteTask': 'پاک کردن کار',
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
'delete': 'پاک کردن',
@@ -194,6 +208,8 @@ class Locales {
'downloading': 'Загрузка',
'downloaded': 'Загружено',
'setting': 'Настройки',
'donate': 'Пожертвовать',
'exit': 'Выход',
'create': 'Создать задачу',
'downloadLink': 'Ссылка для скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
@@ -210,6 +226,7 @@ class Locales {
'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений',
'maxRunning': 'Максимальное количество активных задач',
'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера',
'subscribeFail':
@@ -234,6 +251,8 @@ class Locales {
'notSet': 'Не установлено',
'set': 'Установлено',
'effectAfterRestart': 'Эффект после перезагрузки',
'startAll': 'Запустить все',
'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить задачу',
'deleteTaskTip': 'Сохранить загруженные файлы',
'delete': 'Удалить',
@@ -252,6 +271,8 @@ class Locales {
'downloading': '下载中',
'downloaded': '已完成',
'setting': '设置',
'donate': '打赏',
'exit': '退出',
'create': '创建任务',
'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接',
@@ -267,6 +288,7 @@ class Locales {
'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录',
'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 条',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
@@ -290,6 +312,8 @@ class Locales {
'notSet': '未设置',
'set': '已设置',
'effectAfterRestart': '此配置项将在重启应用后生效',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务',
'deleteTaskTip': '保留已下载的文件',
'delete': '删除',
+15
View File
@@ -1,6 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
import 'api/api.dart' as api;
import 'app/modules/app/controllers/app_controller.dart';
@@ -11,6 +12,7 @@ import 'util/localeManager.dart';
import 'util/log_util.dart';
import 'util/mac_secure_util.dart';
import 'util/package_info.dart';
import 'util/util.dart';
void main() async {
await init();
@@ -21,6 +23,19 @@ void main() async {
Future<void> init() async {
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());
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');
}
}
+9 -6
View File
@@ -3,12 +3,11 @@ import 'dart:io';
import 'package:flutter/foundation.dart';
class Util {
static String buildAbsPath(String dir, String path, String name) {
return (dir == "" ? "" : "$dir/") + buildPath(path, name);
}
static String buildPath(String path, String name) {
return (path == "" ? "" : "$path/") + name;
static String safeDir(String path) {
if (path == "." || path == "./" || path == ".\\") {
return "";
}
return path;
}
static String fmtByte(int byte) {
@@ -42,6 +41,10 @@ class Util {
return Platform.isWindows || Platform.isLinux || Platform.isMacOS;
}
static isWindows() {
return !kIsWeb && Platform.isWindows;
}
static isMacos() {
return !kIsWeb && Platform.isMacOS;
}
+13
View File
@@ -4,6 +4,19 @@ import FlutterMacOS
@NSApplicationMain
class AppDelegate: FlutterAppDelegate {
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return false
}
override func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
if !flag {
for window in NSApp.windows {
if !window.isVisible {
window.setIsVisible(true)
}
window.makeKeyAndOrderFront(self)
NSApp.activate(ignoringOtherApps: true)
}
}
return true
}
}
+79 -15
View File
@@ -17,6 +17,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.0.0"
app_links:
dependency: "direct main"
description:
name: app_links
sha256: d572dcdff49c4cfcfa6f315e2683e518ec6eb54e084d01e51d9631a4dcc1b5e8
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.4.2"
archive:
dependency: transitive
description:
@@ -269,10 +277,10 @@ packages:
dependency: "direct main"
description:
name: file_picker
sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9
sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
url: "https://pub.flutter-io.cn"
source: hosted
version: "5.2.5"
version: "5.3.0"
fixnum:
dependency: transitive
description:
@@ -285,10 +293,10 @@ packages:
dependency: "direct main"
description:
name: fluent_ui
sha256: "55294f8f8b1aab6ac7a2be529b1dcb4affcff3e3176e5e50e5f1918a40050df7"
sha256: "334144996f2699bb9dd678a9fe9a8e65a2e0b89bda9328ed4f0d49a8768f1416"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.4.0"
version: "4.5.1"
flutter:
dependency: "direct main"
description: flutter
@@ -320,10 +328,10 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b"
sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.0.7"
version: "2.0.14"
flutter_test:
dependency: "direct dev"
description: flutter
@@ -399,7 +407,7 @@ packages:
source: hosted
version: "3.2.2"
intl:
dependency: "direct main"
dependency: transitive
description:
name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
@@ -510,6 +518,22 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
math_expressions:
dependency: transitive
description:
name: math_expressions
sha256: "3576593617c3870d75728a751f6ec6e606706d44e363f088ac394b5a28a98064"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.4.0"
menu_base:
dependency: transitive
description:
name: menu_base
sha256: "820368014a171bd1241030278e6c2617354f492f5c703d7b7d4570a6b8b84405"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.1"
meta:
dependency: transitive
description:
@@ -538,10 +562,10 @@ packages:
dependency: "direct main"
description:
name: package_info_plus
sha256: f619162573096d428ccde2e33f92e05b5a179cd6f0e3120c1005f181bee8ed16
sha256: d39e8fbff4c5aef4592737e25ad6ac500df006ce7a7a8e1f838ce1256e167542
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.2"
version: "4.0.0"
package_info_plus_platform_interface:
dependency: transitive
description:
@@ -610,10 +634,10 @@ packages:
dependency: transitive
description:
name: path_provider_windows
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c
sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
version: "2.1.6"
petitparser:
dependency: transitive
description:
@@ -634,10 +658,10 @@ packages:
dependency: transitive
description:
name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.3"
version: "2.1.4"
pool:
dependency: transitive
description:
@@ -702,6 +726,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.27.5"
screen_retriever:
dependency: transitive
description:
name: screen_retriever
sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.6"
scroll_pos:
dependency: transitive
description:
@@ -790,6 +822,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.2"
shortid:
dependency: transitive
description:
name: shortid
sha256: d0b40e3dbb50497dad107e19c54ca7de0d1a274eb9b4404991e443dadb9ebedb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.1.2"
sky_engine:
dependency: transitive
description: flutter
@@ -883,6 +923,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.0"
tray_manager:
dependency: "direct main"
description:
name: tray_manager
sha256: b1975a05e0c6999e983cf9a58a6a098318c896040ccebac5398a3cc9e43b9c69
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
typed_data:
dependency: transitive
description:
@@ -891,6 +939,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.3.1"
uri_to_file:
dependency: "direct main"
description:
name: uri_to_file
sha256: "84afd633b1492fc465c768141e1a29edd519061bf99935b6b4d0d5de8ec7c108"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
url_launcher:
dependency: "direct main"
description:
@@ -983,10 +1039,18 @@ packages:
dependency: transitive
description:
name: win32
sha256: "1952a663c0e34fbde55916010d54bbb249bf5f2583113c497602f0ee01c6faa4"
sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.0.0"
version: "4.1.4"
window_manager:
dependency: "direct main"
description:
name: window_manager
sha256: "95096fede562cbb65f30d38b62d819a458f59ba9fe4a317f6cee669710f6676b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.4"
xdg_directories:
dependency: transitive
description:
+10 -7
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.3.0+1
version: 1.3.2+1
environment:
sdk: ">=2.17.6 <3.0.0"
@@ -49,11 +49,14 @@ dependencies:
shared_preferences: ^2.0.15
logger: ^1.3.0
desktop_drop: ^0.3.3
intl: ^0.17.0
package_info_plus: ^3.0.2
package_info_plus: ^4.0.0
fluent_ui: ^4.4.0
path: any
badges: ^3.0.3
app_links: ^3.4.2
uri_to_file: ^0.2.0
window_manager: ^0.3.4
tray_manager: ^0.2.0
dev_dependencies:
flutter_test:
sdk: flutter
@@ -65,6 +68,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:
@@ -83,9 +87,8 @@ flutter:
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - macos/Frameworks/libgopeed.dylib
# - macos/lib/libgopeed.dylib
assets:
- assets/tray_icon/
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/assets-and-images/#resolution-aware
@@ -131,4 +134,4 @@ ffigen:
output: 'lib/core/ffi/libgopeed_bind.dart'
headers:
entry-points:
- 'include/libgopeed.h'
- 'include/libgopeed.h'
-2
View File
@@ -5,10 +5,8 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:gopeed/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {});
+6
View File
@@ -7,6 +7,12 @@
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
_In_ wchar_t *command_line, _In_ int show_command) {
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"gopeed");
if (hwnd != NULL) {
::ShowWindow(hwnd, SW_NORMAL);
::SetForegroundWindow(hwnd);
return EXIT_FAILURE;
}
// Attach to console when present (e.g., 'flutter run') or create a
// new console when running with a debugger.
if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) {