Compare commits

..
6 Commits
33 changed files with 417 additions and 194 deletions

No files matched your search

+26 -18
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,11 +202,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 }}
- 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
@@ -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:
+10 -6
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,11 +64,12 @@ 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
+4 -1
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{
Connections: 8, Extra: http.OptsExtra{
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"
) )
+154 -68
View File
@@ -9,6 +9,7 @@ import (
gonanoid "github.com/matoous/go-nanoid/v2" gonanoid "github.com/matoous/go-nanoid/v2"
"os" "os"
"path" "path"
"sort"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -36,15 +37,15 @@ 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
} }
func NewDownloader(cfg *DownloaderConfig) *Downloader { func NewDownloader(cfg *DownloaderConfig) *Downloader {
@@ -54,12 +55,13 @@ 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{},
} }
for _, f := range cfg.FetchBuilders { for _, f := range cfg.FetchBuilders {
for _, p := range f.Schemes() { for _, p := range f.Schemes() {
@@ -74,6 +76,21 @@ func (d *Downloader) Setup() error {
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig}); err != nil { 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 +111,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 +127,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 +137,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 +155,7 @@ func (d *Downloader) Setup() error {
}() }()
} }
} }
time.Sleep(time.Millisecond * time.Duration(d.refreshInterval)) time.Sleep(time.Millisecond * time.Duration(d.cfg.RefreshInterval))
} }
}() }()
return nil return nil
@@ -185,7 +204,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 +220,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,13 +232,11 @@ 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)
@@ -253,36 +272,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 +301,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,24 +312,24 @@ 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())
d.emit(EventKeyContinue, task)
} }
return return d.doContinue(task)
} }
func (d *Downloader) Delete(id string, force bool) (err error) { func (d *Downloader) Delete(id string, force bool) (err error) {
@@ -371,6 +375,11 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
} }
} }
d.emit(EventKeyDelete, task) 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 +389,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 +435,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 +481,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 +510,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)
} }
+3 -7
View File
@@ -170,8 +170,7 @@ 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{
"connections": 4, "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) 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))
+19 -13
View File
@@ -70,7 +70,9 @@ 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
Storage Storage
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
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
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
Extra map[string]any `json:"extra"` // Extra is the extra config 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
} }
+2 -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
@@ -195,6 +195,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
} }
+3 -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
+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()
} }
+1
View File
@@ -24,6 +24,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",
+1
View File
@@ -24,6 +24,7 @@
"downloadDir": "Папка загрузки", "downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки", "downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений", "connections": "Соединений",
"maxRunning": "Максимальное количество активных задач",
"items": "@подсчет элементов", "items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера", "subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже", "subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
+1
View File
@@ -24,6 +24,7 @@
"downloadDir": "下载目录", "downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录", "downloadDirValid": "请选择下载目录",
"connections": "连接数", "connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 条", "items": "@count 条",
"subscribeTracker": "订阅 Tracker", "subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试", "subscribeFail": "订阅失败,请检查网络或稍后重试",
+5 -9
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,15 +72,12 @@ 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 {
throw Exception(result);
}
} else { } else {
return null as T; throw Exception(result);
} }
} on DioError catch (e) { } on DioError catch (e) {
throw Exception(Result(code: 1000, msg: e.message)); throw Exception(Result(code: 1000, msg: e.message));
@@ -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',
}; };
@@ -2,7 +2,6 @@ import 'package:desktop_drop/desktop_drop.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';
@@ -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,11 +1,11 @@
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_explorer.dart';
import '../../../util/file_icon.dart'; import '../../../util/file_icon.dart';
import '../../../util/icons.dart'; import '../../../util/icons.dart';
import '../../../util/message.dart'; import '../../../util/message.dart';
@@ -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 {
await pauseTask(task.id); try {
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 {
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)]), : 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() style: context.textTheme.titleSmall),
: Text("${Util.fmtByte(task.progress.speed)} / s",
style: context.textTheme.titleSmall),
...buildActions() ...buildActions()
], ],
), ),
+4
View File
@@ -38,6 +38,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';
@@ -97,6 +98,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':
@@ -210,6 +212,7 @@ class Locales {
'downloadDir': 'Папка загрузки', 'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки', 'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений', 'connections': 'Соединений',
'maxRunning': 'Максимальное количество активных задач',
'items': '@подсчет элементов', 'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера', 'subscribeTracker': 'Подпись трекера',
'subscribeFail': 'subscribeFail':
@@ -267,6 +270,7 @@ class Locales {
'downloadDir': '下载目录', 'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录', 'downloadDirValid': '请选择下载目录',
'connections': '连接数', 'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 条', 'items': '@count 条',
'subscribeTracker': '订阅 Tracker', 'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试', 'subscribeFail': '订阅失败,请检查网络或稍后重试',
+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');
}
}
+5 -6
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 "";
}
static String buildPath(String path, String name) { return path;
return (path == "" ? "" : "$path/") + name;
} }
static String fmtByte(int byte) { static String fmtByte(int byte) {
+23 -15
View File
@@ -269,10 +269,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: d090ae03df98b0247b82e5928f44d1b959867049d18d73635e2e0bc3f49542b9 sha256: c7a8e25ca60e7f331b153b0cb3d405828f18d3e72a6fa1d9440c86556fffc877
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.2.5" version: "5.3.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@@ -285,10 +285,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fluent_ui name: fluent_ui
sha256: "55294f8f8b1aab6ac7a2be529b1dcb4affcff3e3176e5e50e5f1918a40050df7" sha256: "334144996f2699bb9dd678a9fe9a8e65a2e0b89bda9328ed4f0d49a8768f1416"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.4.0" version: "4.5.1"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -320,10 +320,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "60fc7b78455b94e6de2333d2f95196d32cf5c22f4b0b0520a628804cb463503b" sha256: "96af49aa6b57c10a312106ad6f71deed5a754029c24789bbf620ba784f0bd0b0"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.7" version: "2.0.14"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -399,7 +399,7 @@ packages:
source: hosted source: hosted
version: "3.2.2" version: "3.2.2"
intl: intl:
dependency: "direct main" dependency: transitive
description: description:
name: intl name: intl
sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91"
@@ -510,6 +510,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.2.0" 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"
meta: meta:
dependency: transitive dependency: transitive
description: description:
@@ -538,10 +546,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: f619162573096d428ccde2e33f92e05b5a179cd6f0e3120c1005f181bee8ed16 sha256: d39e8fbff4c5aef4592737e25ad6ac500df006ce7a7a8e1f838ce1256e167542
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.2" version: "4.0.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -610,10 +618,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: bcabbe399d4042b8ee687e17548d5d3f527255253b4a639f5f8d2094a9c2b45c sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.3" version: "2.1.6"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -634,10 +642,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: dbf0f707c78beedc9200146ad3cb0ab4d5da13c246336987be6940f026500d3a sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.3" version: "2.1.4"
pool: pool:
dependency: transitive dependency: transitive
description: description:
@@ -983,10 +991,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "1952a663c0e34fbde55916010d54bbb249bf5f2583113c497602f0ee01c6faa4" sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.0" version: "4.1.4"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
+4 -4
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. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.3.0+1 version: 1.3.1+1
environment: environment:
sdk: ">=2.17.6 <3.0.0" sdk: ">=2.17.6 <3.0.0"
@@ -49,8 +49,7 @@ dependencies:
shared_preferences: ^2.0.15 shared_preferences: ^2.0.15
logger: ^1.3.0 logger: ^1.3.0
desktop_drop: ^0.3.3 desktop_drop: ^0.3.3
intl: ^0.17.0 package_info_plus: ^4.0.0
package_info_plus: ^3.0.2
fluent_ui: ^4.4.0 fluent_ui: ^4.4.0
path: any path: any
badges: ^3.0.3 badges: ^3.0.3
@@ -65,6 +64,7 @@ dev_dependencies:
# rules and activating additional ones. # rules and activating additional ones.
flutter_lints: ^2.0.0 flutter_lints: ^2.0.0
ffigen: ^6.1.2 ffigen: ^6.1.2
# build json model: flutter pub run build_runner build --delete-conflicting-outputs
build_runner: ^2.2.1 build_runner: ^2.2.1
json_serializable: ^6.3.2 json_serializable: ^6.3.2
flutter_launcher_icons: flutter_launcher_icons:
@@ -131,4 +131,4 @@ ffigen:
output: 'lib/core/ffi/libgopeed_bind.dart' output: 'lib/core/ffi/libgopeed_bind.dart'
headers: headers:
entry-points: 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 // 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. // 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:flutter_test/flutter_test.dart';
import 'package:gopeed/main.dart';
void main() { void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {}); testWidgets('Counter increments smoke test', (WidgetTester tester) async {});