Compare commits

...
29 Commits
Author SHA1 Message Date
liwei 195dfac74b test 2024-01-19 14:31:03 +08:00
liwei 1c1fab2002 test 2024-01-19 11:54:36 +08:00
madoka773 abccbabc78 fix: wrong debian installed size (#335) 2024-01-19 11:17:50 +08:00
Levi c26eb26b8f fix: default download path on mobile (#337) 2024-01-19 11:12:27 +08:00
Levi 74beea6044 feat: auto create torrent task for http download (#334) 2024-01-18 18:38:44 +08:00
Levi e548d7e0c9 feat: save window size and resize on startup (#332) 2024-01-18 15:00:05 +08:00
madoka773 a996d57914 ci: fix debian 12 depends (#329) 2024-01-18 11:16:50 +08:00
Levi f7dc60ad50 fix: proxy setting only numeric input (#328) 2024-01-18 09:17:06 +08:00
Levi 8425a96ac9 feat: add mirror.ghproxy.com trackers subscribe cdn (#330) 2024-01-17 22:09:37 +08:00
Tracey Lesley 99f4e8c895 feat: add download stats info (#323) 2024-01-17 21:59:01 +08:00
Elliot cc0f61b527 feat: improve zh-TW translation (#327) 2024-01-17 18:16:40 +08:00
Levi 922ef969a6 feat: add thanks item for contributors (#325) 2024-01-16 19:02:20 +08:00
Tracey Lesley 3a542054fc fix: compatible with not follow the http range response standard 2024-01-14 09:58:14 +08:00
Levi 895a8d2ee4 fix: foreground task scope (#314) 2024-01-05 21:49:21 +08:00
madoka773 29d0f5ebcf ci: use flutter distributor (#313) 2024-01-04 12:46:16 +08:00
Levi 31228e3edf release: v1.5.3 2023-12-15 19:23:16 +08:00
Levi 31a32d7e14 fix: handle api panic (#302) 2023-12-15 19:20:56 +08:00
Levi 65fd6fb489 fix: http pause handle (#303) 2023-12-15 19:15:38 +08:00
Levi 953b878a89 fix: adapt mobile proxy settings ui (#301) 2023-12-15 19:09:48 +08:00
Elliot ae4ca3da58 feat: improve zh-TW translation (#300) 2023-12-14 18:06:49 +08:00
Levi d09bd180af release: v1.5.2 2023-12-13 09:38:34 +08:00
liwei 3d24e353c5 docs: docker data path mounted 2023-12-13 08:57:57 +08:00
Levi ce7489b0c0 feat: support volume storage dir on docker (#291) 2023-12-13 08:51:22 +08:00
Levi 2b5091ba21 feat: improve extension engine (#298)
1. support onError event
2. add built-in MessageError
2023-12-12 19:00:42 +08:00
Levi ff32971aca fix: repair invalid filename (#296) 2023-12-12 18:40:36 +08:00
Levi f2af61f9aa fix: sort of completed tasks (#297) 2023-12-12 18:36:40 +08:00
Elliot d935b9d631 feat: improve zh_TW translate (#287) 2023-12-04 23:01:49 +08:00
liwei ae66d1d83d docs: fix contributing 2023-12-04 13:41:27 +08:00
madoka773 aa91ca1d50 ci: linux build use libappindicator (#286) 2023-12-04 13:36:27 +08:00
69 changed files with 990 additions and 514 deletions

No files matched your search

+2
View File
@@ -19,6 +19,8 @@ categories:
label: 'doc'
- title: '🚀 CI/CD'
label: 'ci'
- title: '🌎 Internationalization'
label: 'translation'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver:
major:
+17 -111
View File
@@ -223,126 +223,32 @@ jobs:
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
sudo apt update -y
# flutter build dependencies
sudo apt install -y ninja-build libgtk-3-dev libappindicator3-dev
# rpm build dependencies
sudo apt install -y rpm patchelf
# appimage build dependencies
sudo apt install -y libfuse2 locate
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
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
cd ui/flutter
flutter build linux
mkdir -p build/dist
# Build portable
tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./
# Build deb package
mkdir -p debian/gopeed/opt/gopeed
mkdir -p debian/gopeed/usr/share/applications
mkdir -p debian/gopeed/usr/share/icons/hicolor/scalable/apps
mkdir -p debian/gopeed/DEBIAN
cp -r build/linux/x64/release/bundle/* debian/gopeed/opt/gopeed/
cp assets/icon/icon.svg debian/gopeed/usr/share/icons/hicolor/scalable/apps/com.gopeed.Gopeed.svg
cat>debian/gopeed/DEBIAN/postinst<<EOF
#!/bin/bash
ln -sf /opt/gopeed/gopeed /usr/bin/gopeed
EOF
cat>debian/gopeed/DEBIAN/postrm<<EOF
#!/bin/bash
rm -rf /usr/bin/gopeed
EOF
chmod 0755 debian/gopeed/DEBIAN/postinst
chmod 0755 debian/gopeed/DEBIAN/postrm
cat>debian/gopeed/DEBIAN/control<<EOF
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cp linux/assets/com.gopeed.Gopeed.desktop debian/gopeed/usr/share/applications/gopeed.desktop
dpkg-deb --build debian/gopeed
mv debian/gopeed.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
# Build AppImage
sudo apt-get -y install libfuse2
wget -O appimage-builder https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder
mkdir AppDir
cp -r build/linux/x64/release/bundle/* AppDir
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
cp assets/icon/icon.svg AppDir/usr/share/icons/hicolor/scalable/apps/gopeed.svg
cat>AppImageBuilder.yml<<EOF
version: 1
AppDir:
path: ${PWD}/AppDir
app_info:
id: com.gopeed.gopeed
name: Gopeed
icon: gopeed
version: ${VERSION:1}
exec: gopeed
exec_args: \$@
apt:
arch: amd64
allow_unauthenticated: true
sources:
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted
universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main
restricted universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports
main restricted universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted
universe multiverse
include: []
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: x86_64
update-information: guess
EOF
./appimage-builder --skip-tests --recipe AppImageBuilder.yml
mv Gopeed-*-x86_64.AppImage build/dist/Gopeed-$VERSION-linux-amd64.AppImage
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
flutter_distributor package --platform linux --targets appimage,deb
cd dist/*
mv gopeed-*-linux.AppImage Gopeed-${VERSION}-linux-x86_64.AppImage
mv gopeed-*-linux.deb Gopeed-${VERSION}-linux-amd64.deb
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/dist/*
asset_path: ui/flutter/dist/*/*
overwrite: true
build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
@@ -405,7 +311,7 @@ jobs:
plugin: flutter
stage-packages:
- libgtk-3-dev
- libayatana-appindicator3-dev
- libappindicator3-dev
flutter-target: lib/main.dart
EOF
+1 -1
View File
@@ -4,7 +4,7 @@
*.dll
*.so
*.dylib
ui/flutter/dist/
# Test binary, built with `go test -c`
*.test
+1 -1
View File
@@ -16,7 +16,7 @@ flutter 项目即可运行。
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
请注意以 `en_US.json` 为参照进行翻译。
请注意以 `en_us.dart` 为参照进行翻译。
## flutter开发
+1 -1
View File
@@ -15,7 +15,7 @@
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
請以 `en_US.json` 作為參照。
請以 `en_us.dart` 作為參照。
## flutter開發
+1
View File
@@ -17,5 +17,6 @@ FROM alpine:3.14.2
LABEL maintainer="monkeyWie"
WORKDIR /app
COPY --from=go /app/dist/gopeed ./
VOLUME ["/app/storage"]
EXPOSE 9999
ENTRYPOINT ["./gopeed"]
+4 -3
View File
@@ -16,7 +16,7 @@
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
## Install
@@ -47,9 +47,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### Start directly
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[More details for docker](https://docs.gopeed.com/install.html#docker-installation)
#### Using Docker Compose
```bash
@@ -160,7 +162,6 @@ cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## Credits
+3 -2
View File
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接始める
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[Dockerの使用については](https://docs.gopeed.com/install.html#docker-installation)
#### Docker Compose の使用
```bash
@@ -159,7 +161,6 @@ cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感謝
+4 -3
View File
@@ -15,7 +15,7 @@
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## 安装
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接运行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[更多关于 Docker 的使用](https://docs.gopeed.com/zh/install.html#docker-%E5%AE%89%E8%A3%85)
#### 使用 Docker Compose
```bash
@@ -159,7 +161,6 @@ cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感谢
+3 -2
View File
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接執行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[更多關於 Docker 的使用](https://docs.gopeed.com/zh/install.html#docker-%E5%AE%89%E8%A3%85)
#### 使用 Docker Compose
```bash
@@ -159,7 +161,6 @@ cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感謝
+9
View File
@@ -9,6 +9,8 @@ import (
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
"os"
"path/filepath"
)
//go:embed dist/*
@@ -29,10 +31,17 @@ func main() {
}
}
exe, err := os.Executable()
if err != nil {
panic(err)
}
dir := filepath.Dir(exe)
cfg := &model.StartConfig{
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken,
ProductionMode: true,
WebEnable: true,
+3 -2
View File
@@ -4,8 +4,9 @@ services:
gopeed:
container_name: gopeed
ports:
- 9999:9999 # HTTP port (host:container)
- "9999:9999" # HTTP port (host:container)
image: liwei2633/gopeed
volumes:
- ~/Downloads:/root/Downloads # replace with your prefer path
- ~/gopeed/Downloads:/root/Downloads # mount download path
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
restart: unless-stopped
+2
View File
@@ -21,6 +21,8 @@ type Fetcher interface {
Pause() error
Close() error
// Stats refreshes health statistics and returns the latest information
Stats() any
// Meta returns the meta information of the download.
Meta() *FetcherMeta
// Progress returns the progress of the download.
+11
View File
@@ -128,6 +128,7 @@ func (f *Fetcher) Start() (err error) {
file.Download()
}
}
return
}
@@ -188,6 +189,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Stats() any {
stats := f.torrent.Stats()
baseStats := &bt.Stats{
TotalPeers: stats.TotalPeers,
ActivePeers: stats.ActivePeers,
ConnectedSeeders: stats.ConnectedSeeders,
}
return baseStats
}
func (f *Fetcher) Progress() fetcher.Progress {
if !f.torrentReady.Load() {
return f.progress
+22 -18
View File
@@ -18,6 +18,7 @@ import (
"os"
"path"
"strconv"
"strings"
"time"
)
@@ -104,8 +105,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
Range: false,
Files: []*base.FileInfo{},
}
if base.HttpCodePartialContent == httpResp.StatusCode {
// 返回206响应码表示支持断点下载
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
res.Range = true
// 解析资源大小: bytes 0-1000/1001 => 1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
@@ -217,6 +218,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Stats() any {
// todo http download health
return &fhttp.Stats{}
}
func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 {
@@ -306,26 +312,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status)
return err
}
return nil
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second)
continue
}
// Http request success, reset retry times
chunk.retryTimes = 0
err = func() error {
defer resp.Body.Close()
// Http request success, reset retry times
chunk.retryTimes = 0
for {
n, err := resp.Body.Read(buf)
if n > 0 {
@@ -342,7 +335,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return err
}
}
return nil
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second)
continue
}
// if chunk is completed, reset other not complete chunks retry times
if err == nil {
for _, c := range f.chunks {
+2
View File
@@ -16,10 +16,12 @@ const (
HttpCodePartialContent = 206
HttpHeaderRange = "Range"
HttpHeaderAcceptRanges = "Accept-Ranges"
HttpHeaderContentLength = "Content-Length"
HttpHeaderContentRange = "Content-Range"
HttpHeaderContentDisposition = "Content-Disposition"
HttpHeaderUserAgent = "Usr-Agent"
HttpHeaderBytes = "bytes"
HttpHeaderRangeFormat = "bytes=%d-%d"
)
+63 -34
View File
@@ -6,8 +6,11 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"github.com/virtuald/go-paniclog"
"os"
"path/filepath"
@@ -91,6 +94,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
}
}
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
if cfg.ProductionMode {
@@ -225,7 +229,10 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return
}
res := d.triggerOnResolve(req)
res, err := d.triggerOnResolve(req)
if err != nil {
return
}
if res != nil && len(res.Files) > 0 {
rr = &ResolveResult{
Res: res,
@@ -525,9 +532,6 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
Task: task,
Err: err,
})
if eventKey == EventKeyError {
d.emit(EventKeyFinally, task, err)
}
}
}
@@ -572,26 +576,56 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
func (d *Downloader) watch(task *Task) {
err := task.fetcher.Wait()
if err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
} else {
task.Progress.Used = task.timer.Used()
if task.Meta.Res.Size == 0 {
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
used = 1
}
totalSize := task.Meta.Res.Size
task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.Status = base.DownloadStatusDone
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
d.doOnError(task, err)
return
}
task.Progress.Used = task.timer.Used()
if task.Meta.Res.Size == 0 {
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
used = 1
}
totalSize := task.Meta.Res.Size
task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.updateStatus(base.DownloadStatusDone)
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
downloadFilePath := task.Meta.SingleFilepath()
if e.AutoTorrent && strings.HasSuffix(downloadFilePath, ".torrent") {
go func() {
_, err2 := d.CreateDirect(
&base.Request{
URL: downloadFilePath,
},
&base.Options{
Path: task.Meta.Opts.Path,
SelectFiles: make([]int, 0),
})
if err2 != nil {
d.Logger.Error().Err(err2).Msgf("auto create torrent task failed, task id: %s", task.ID)
}
}()
}
}
}
func (d *Downloader) doOnError(task *Task, err error) {
d.Logger.Warn().Err(err).Msgf("task download failed, task id: %s", task.ID)
task.updateStatus(base.DownloadStatusError)
d.triggerOnError(task, err)
if task.Status == base.DownloadStatusError {
d.emit(EventKeyError, task, err)
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
}
}
func (d *Downloader) restoreFetcher(task *Task) error {
@@ -658,7 +692,6 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
task.fetcher = fetcher
task.Meta = fetcher.Meta()
task.Progress = &Progress{}
task.Status = base.DownloadStatusReady
initTask(task)
if err = fetcher.Create(opts); err != nil {
return
@@ -697,25 +730,18 @@ func (d *Downloader) doStart(task *Task) (err error) {
}
}
cloneTask := task.clone()
isCreate := task.Status == base.DownloadStatusReady
task.Status = base.DownloadStatusRunning
doStart := func() error {
task.lock.Lock()
defer task.lock.Unlock()
req := d.triggerOnStart(cloneTask)
if req != nil {
task.Meta.Req = req
task.fetcher.Meta().Req = req
}
d.triggerOnStart(task)
task.updateStatus(base.DownloadStatusRunning)
if task.Meta.Res == nil {
err := task.fetcher.Resolve(task.Meta.Req)
if err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
return err
}
task.Meta.Res = task.fetcher.Meta().Res
@@ -724,8 +750,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
if isCreate {
d.checkDuplicateLock.Lock()
defer d.checkDuplicateLock.Unlock()
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
// check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" {
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
fullDirPath := task.Meta.FolderPath()
newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil {
@@ -733,6 +761,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
}
task.Meta.Opts.Name = newName
} else {
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil {
@@ -758,7 +787,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
go func() {
err := doStart()
if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
d.doOnError(task, err)
}
}()
@@ -768,7 +797,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
func (d *Downloader) doPause(task *Task) (err error) {
err = func() error {
if task.Status != base.DownloadStatusDone {
task.Status = base.DownloadStatusPause
task.updateStatus(base.DownloadStatusPause)
task.timer.Pause()
if task.fetcher != nil {
if err := task.fetcher.Pause(); err != nil {
+9 -1
View File
@@ -3,6 +3,7 @@ package engine
import (
_ "embed"
"errors"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
@@ -125,6 +126,9 @@ func NewEngine(cfg *Config) *Engine {
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime)
gojaurl.Enable(runtime)
if err := gojaerror.Enable(runtime); err != nil {
return
}
if err := file.Enable(runtime); err != nil {
return
}
@@ -167,7 +171,11 @@ func resolveResult(value goja.Value) (any, error) {
case goja.PromiseStateFulfilled:
return p.Result().Export(), nil
case goja.PromiseStateRejected:
return nil, errors.New(p.Result().String())
if err, ok := p.Result().Export().(error); ok {
return nil, err
} else {
return nil, errors.New(p.Result().String())
}
}
}
return export, nil
+13
View File
@@ -7,7 +7,9 @@ import (
"errors"
"fmt"
"github.com/GopeedLab/gopeed/internal/test"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja"
"io"
@@ -20,6 +22,7 @@ import (
)
func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "MessageError")
doTestPolyfill(t, "XMLHttpRequest")
doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData")
@@ -27,6 +30,16 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "__gopeed_create_vm")
}
func TestError(t *testing.T) {
engine := NewEngine(nil)
_, err := engine.RunString(`
throw new MessageError('test');
`)
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); !ok {
t.Fatalf("expect MessageError, but got %v", me)
}
}
func TestFetch(t *testing.T) {
server := startServer()
defer server.Close()
@@ -0,0 +1,29 @@
package error
import (
"github.com/dop251/goja"
)
type MessageError struct {
Message string `json:"message"`
}
func (e *MessageError) Error() string {
return e.Message
}
func Enable(runtime *goja.Runtime) error {
messageError := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
var message string
if len(call.Arguments) > 0 {
message = call.Arguments[0].String()
}
instance := &MessageError{
Message: message,
}
instanceValue := runtime.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
return runtime.Set("MessageError", messageError)
}
-9
View File
@@ -1,9 +0,0 @@
package inject
import (
"github.com/dop251/goja"
)
func ThrowTypeError(vm *goja.Runtime, msg string) {
panic(vm.NewTypeError(msg))
}
+2 -2
View File
@@ -2,9 +2,9 @@ package xhr
import (
"bytes"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja"
"io"
"mime/multipart"
@@ -316,7 +316,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
if len(call.Arguments) < 1 {
inject.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
}
instance := &ProgressEvent{
Type: call.Argument(0).String(),
+24
View File
@@ -0,0 +1,24 @@
package util
import (
"github.com/dop251/goja"
)
func ThrowTypeError(vm *goja.Runtime, msg string) {
panic(vm.NewTypeError(msg))
}
func AssertError[T error](err error) (t T, r bool) {
if err == nil {
return
}
if e, ok := err.(T); ok {
return e, true
}
if e, ok := err.(*goja.Exception); ok {
if ee, okk := e.Value().Export().(T); okk {
return ee, true
}
}
return
}
+64 -14
View File
@@ -6,6 +6,8 @@ import (
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download/engine"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja"
"github.com/go-git/go-git/v5"
@@ -35,7 +37,7 @@ type ActivationEvent string
const (
EventOnResolve ActivationEvent = "onResolve"
EventOnStart ActivationEvent = "onStart"
//EventOnError ActivationEvent = "onError"
EventOnError ActivationEvent = "onError"
//EventOnDone ActivationEvent = "onDone"
)
@@ -238,8 +240,8 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
return &ext, nil
}
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
doTrigger(d,
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, err error) {
err = doTrigger(d,
EventOnResolve,
req,
&OnResolveContext{
@@ -252,6 +254,10 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
return
}
ctx.Res.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
for _, file := range ctx.Res.Files {
file.Name = util.ReplaceInvalidFilename(file.Name)
}
ctx.Res.CalcSize(nil)
}
res = ctx.Res
@@ -260,12 +266,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
return
}
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
func (d *Downloader) triggerOnStart(task *Task) {
doTrigger(d,
EventOnStart,
task.Meta.Req,
&OnStartContext{
Task: task,
Task: NewExtensionTask(d, task),
},
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
// Validate request structure
@@ -274,14 +280,25 @@ func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
return
}
req = ctx.Task.Meta.Req
}
},
)
return
}
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) {
func (d *Downloader) triggerOnError(task *Task, err error) {
doTrigger(d,
EventOnError,
task.Meta.Req,
&OnErrorContext{
Task: NewExtensionTask(d, task),
Error: err,
},
nil,
)
}
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
// init extension global object
gopeed := &Instance{
Events: make(InstanceEvents),
@@ -343,13 +360,20 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
return
}
handler(ext, gopeed, ctx)
if handler != nil {
handler(ext, gopeed, ctx)
}
}
}()
}
}
}
return
// Only return MessageError
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); ok {
return me
}
return nil
}
func (d *Downloader) ExtensionPath(ext *Extension) string {
@@ -577,10 +601,10 @@ func (h InstanceEvents) OnStart(fn goja.Callable) {
h.register(EventOnStart, fn)
}
//func (h InstanceEvents) OnError(fn goja.Callable) {
// h.register(HookEventOnError, fn)
//}
//
func (h InstanceEvents) OnError(fn goja.Callable) {
h.register(EventOnError, fn)
}
//func (h InstanceEvents) OnDone(fn goja.Callable) {
// h.register(HookEventOnDone, fn)
//}
@@ -649,7 +673,33 @@ type OnResolveContext struct {
}
type OnStartContext struct {
Task *Task `json:"task"`
Task *ExtensionTask `json:"task"`
}
type OnErrorContext struct {
Task *ExtensionTask `json:"task"`
Error error `json:"error"`
}
type ExtensionTask struct {
*Task
download *Downloader
}
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
return &ExtensionTask{
Task: task.clone(),
download: download,
}
}
func (t *ExtensionTask) Continue() error {
return t.download.Continue(t.ID)
}
func (t *ExtensionTask) Pause() error {
return t.download.Pause(t.ID)
}
func parseSettings(settings []*Setting) map[string]any {
+66 -20
View File
@@ -4,6 +4,7 @@ import (
"errors"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/dop251/goja"
"os"
"testing"
@@ -165,7 +166,7 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
})
}
func TestDownloader_ExtensionByOnStart(t *testing.T) {
func TestDownloader_Extension_OnStart(t *testing.T) {
downloadAndCheck := func(req *base.Request) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
@@ -192,10 +193,10 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
}
task := downloader.GetTask(id)
if task.Meta.Req.URL != "https://github.com" {
panic("extension on start modify url error")
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
}
if task.Meta.Req.Labels["modified"] != "true" {
panic("extension on start modify label error")
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
}
})
}
@@ -207,13 +208,54 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
// label match
downloadAndCheck(&base.Request{
URL: "https://xxx.com",
URL: "https://test.com",
Labels: map[string]string{
"test": "true",
},
})
}
func TestDownloader_Extension_OnError(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_error", false); err != nil {
t.Fatal(err)
}
errCh := make(chan error, 1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyFinally {
errCh <- event.Err
}
})
id, err := downloader.CreateDirect(&base.Request{
URL: "https://github.com/gopeed/test/404",
Labels: map[string]string{
"test": "true",
},
}, nil)
if err != nil {
t.Fatal(err)
}
select {
case err = <-errCh:
break
case <-time.After(time.Second * 10):
err = errors.New("timeout")
}
if err != nil {
panic("extension on error download error: " + err.Error())
}
// extension on error modify url and continue download
task := downloader.GetTask(id)
if task.Meta.Req.URL != "https://github.com" {
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
}
if task.Status != base.DownloadStatusDone {
t.Fatalf("except status is done, actual: %s", task.Status)
}
})
}
func TestDownloader_Extension_Errors(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
@@ -244,6 +286,26 @@ func TestDownloader_Extension_Errors(t *testing.T) {
t.Fatal("function error catch failed")
}
})
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/message_error", false); err != nil {
t.Fatal(err)
}
_, err := downloader.Resolve(&base.Request{
URL: "https://github.com/test",
})
if err == nil {
t.Fatalf("except error, but got nil")
}
me, ok := err.(*gojaerror.MessageError)
if !ok {
t.Fatalf("except MessageError type, but got %s", err)
}
want := "test"
if me.Error() != want {
t.Fatalf("except MessageError message %s, but got %s", want, me.Message)
}
})
}
func TestDownloader_Extension_Settings(t *testing.T) {
@@ -336,22 +398,6 @@ func TestDownloader_DeleteExtension(t *testing.T) {
})
}
func TestDownloader_Extension_OnResolve(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false)
if err != nil {
t.Fatal(err)
}
if err := downloader.DeleteExtension(installedExt.Identity); err != nil {
t.Fatal(err)
}
extensions := downloader.GetExtensions()
if len(extensions) != 0 {
t.Fatal("extension delete fail")
}
})
}
func TestDownloader_Extension_Logger(t *testing.T) {
logger := logger.NewLogger(false, "")
il := newInstanceLogger(&Extension{
+8
View File
@@ -24,6 +24,7 @@ type Task struct {
Status base.Status `json:"status"`
Progress *Progress `json:"progress"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
@@ -41,9 +42,15 @@ func NewTask() *Task {
ID: id,
Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
func (t *Task) updateStatus(status base.Status) {
t.UpdatedAt = time.Now()
t.Status = status
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
@@ -51,6 +58,7 @@ func (t *Task) clone() *Task {
Status: t.Status,
Progress: t.Progress,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
@@ -1,5 +1,6 @@
gopeed.events.onResolve(async function (ctx) {
const aaa = {};
// access undefined property
gopeed.logger.info(aaa.bbb.ccc);
ctx.res = {
@@ -0,0 +1,3 @@
gopeed.events.onResolve(async function (ctx) {
throw new MessageError("test");
});
@@ -0,0 +1,16 @@
{
"name": "message-error",
"title": "gopeed extension message error test",
"version": "0.0.1",
"scripts": [
{
"event": "onResolve",
"match": {
"urls": [
"*://github.com/*"
]
},
"entry": "index.js"
}
]
}
+8
View File
@@ -0,0 +1,8 @@
gopeed.events.onError(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
gopeed.logger.info("error", ctx.error);
ctx.task.meta.req.url = "https://github.com";
ctx.task.pause();
ctx.task.continue();
});
+16
View File
@@ -0,0 +1,16 @@
{
"name": "on-error",
"title": "gopeed extension on error event test",
"version": "0.0.1",
"scripts": [
{
"event": "onError",
"match": {
"labels": [
"test"
]
},
"entry": "index.js"
}
]
}
+1
View File
@@ -1,4 +1,5 @@
gopeed.events.onStart(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
ctx.task.meta.req.url = "https://github.com";
ctx.task.meta.req.labels['modified'] = 'true';
});
+9
View File
@@ -3,3 +3,12 @@ package bt
type ReqExtra struct {
Trackers []string `json:"trackers"`
}
// Stats for download
type Stats struct {
// bt stats
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
TotalPeers int `json:"totalPeers"`
ActivePeers int `json:"activePeers"`
ConnectedSeeders int `json:"connectedSeeders"`
}
+8
View File
@@ -8,4 +8,12 @@ type ReqExtra struct {
type OptsExtra struct {
Connections int `json:"connections"`
// AutoTorrent when task download complete, and it is a .torrent file, it will be auto create a new task for the torrent file
AutoTorrent bool `json:"autoTorrent"`
}
// Stats for download
type Stats struct {
// http stats
// health indicators of http
}
+16
View File
@@ -3,11 +3,13 @@ package rest
import (
"context"
"encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"net"
"net/http"
"os"
@@ -131,6 +133,20 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
})
}
// recover panic
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
err := errors.WithStack(fmt.Errorf("%v", v))
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
}
}()
h.ServeHTTP(w, r)
})
})
srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
+2
View File
@@ -204,12 +204,14 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
// continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
time.Sleep(time.Millisecond * 100)
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)
time.Sleep(time.Millisecond * 100)
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
+11
View File
@@ -183,3 +183,14 @@ func IsExistsFile(path string) bool {
}
return false
}
// ReplaceInvalidFilename replace invalid path characters
func ReplaceInvalidFilename(path string) string {
if path == "" {
return ""
}
for _, char := range invalidPathChars {
path = strings.ReplaceAll(path, char, "_")
}
return path
}
+6
View File
@@ -0,0 +1,6 @@
//go:build !windows
// +build !windows
package util
var invalidPathChars = []string{`/`, `:`}
+47
View File
@@ -178,3 +178,50 @@ func TestIsExistsFile(t *testing.T) {
})
}
}
func TestReplaceInvalidFilename(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
{
name: "blank",
args: args{
path: "",
},
want: "",
},
{
name: "normal",
args: args{
path: "test.txt",
},
want: "test.txt",
},
{
name: "case1",
args: args{
path: "te/st.txt",
},
want: "te_st.txt",
},
{
name: "case2",
args: args{
path: "te/st:.txt",
},
want: "te_st_.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ReplaceInvalidFilename(tt.args.path); got != tt.want {
t.Errorf("ReplaceInvalidFilename() = %v, want %v", got, tt.want)
}
})
}
}
+3
View File
@@ -0,0 +1,3 @@
package util
var invalidPathChars = []string{`\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`}
+1
View File
@@ -0,0 +1 @@
output: dist/
+1
View File
@@ -25,6 +25,7 @@ class Options {
@JsonSerializable()
class OptsExtraHttp {
int connections = 0;
bool autoTorrent = false;
OptsExtraHttp();
+4 -1
View File
@@ -32,9 +32,12 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
}
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp()..connections = json['connections'] as int;
OptsExtraHttp()
..connections = json['connections'] as int
..autoTorrent = json['autoTorrent'] as bool;
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{
'connections': instance.connections,
'autoTorrent': instance.autoTorrent,
};
+2
View File
@@ -13,6 +13,7 @@ class Task {
Status status;
Progress progress;
DateTime createdAt;
DateTime updatedAt;
Task({
required this.id,
@@ -20,6 +21,7 @@ class Task {
required this.status,
required this.progress,
required this.createdAt,
required this.updatedAt,
});
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
+2
View File
@@ -12,6 +12,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
status: $enumDecode(_$StatusEnumMap, json['status']),
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
@@ -20,6 +21,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'status': _$StatusEnumMap[instance.status]!,
'progress': instance.progress.toJson(),
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
};
const _$StatusEnumMap = {
@@ -6,6 +6,7 @@ import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:tray_manager/tray_manager.dart';
@@ -21,6 +22,7 @@ import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
import '../../../../util/window_util.dart' as windows_util;
import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network";
@@ -49,7 +51,12 @@ const allTrackerCdns = [
[
"https://ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
]
],
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
[
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
],
];
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
key: (v) => v as String,
@@ -118,6 +125,26 @@ class AppController extends GetxController with WindowListener, TrayListener {
trayManager.popUpContextMenu();
}
@override
void onWindowMaximize() {
windows_util.saveState(isMaximized: true);
}
@override
void onWindowUnmaximize() {
windows_util.saveState(isMaximized: false);
}
final _windowsResizeSave = Util.debounce(() async {
final size = await windowManager.getSize();
windows_util.saveState(width: size.width, height: size.height);
}, 500);
@override
void onWindowResize() {
_windowsResizeSave();
}
Future<void> _initDeepLinks() async {
// currently only support android
if (!Util.isAndroid()) {
@@ -391,9 +418,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isAndroid()) {
config.downloadDir = (await getExternalStorageDirectory())?.path ??
(await getApplicationDocumentsDirectory()).path;
return;
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
if (downloadDir != null) {
config.downloadDir = '$downloadDir/Gopeed';
} else {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
}
} else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else {
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
@@ -14,22 +15,24 @@ class AppView extends GetView<AppController> {
@override
Widget build(BuildContext context) {
final config = controller.downloaderConfig.value;
return GetMaterialApp.router(
useInheritedMediaQuery: true,
debugShowCheckedModeBanner: false,
theme: GopeedTheme.light,
darkTheme: GopeedTheme.dark,
themeMode: ThemeMode.values.byName(config.extra.themeMode),
translations: messages,
locale: toLocale(config.extra.locale),
fallbackLocale: fallbackLocale,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
getPages: AppPages.routes,
return WithForegroundTask(
child: GetMaterialApp.router(
useInheritedMediaQuery: true,
debugShowCheckedModeBanner: false,
theme: GopeedTheme.light,
darkTheme: GopeedTheme.dark,
themeMode: ThemeMode.values.byName(config.extra.themeMode),
translations: messages,
locale: toLocale(config.extra.locale),
fallbackLocale: fallbackLocale,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
getPages: AppPages.routes,
),
);
}
}
@@ -4,16 +4,14 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
import 'package:gopeed/app/modules/history/views/history_view.dart';
import 'package:path/path.dart' as path;
import 'package:rounded_loading_button/rounded_loading_button.dart';
import '../../../../api/api.dart';
import '../../../../api/model/create_task.dart';
import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart';
import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/resource.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
@@ -21,6 +19,8 @@ import '../../../routes/app_pages.dart';
import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart';
import '../../history/controller/history_controller.dart';
import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart';
class CreateView extends GetView<CreateController> {
@@ -409,15 +409,20 @@ class CreateView extends GetView<CreateController> {
try {
_confirmController.start();
if (_confirmFormKey.currentState!.validate()) {
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
final isWebFileChosen =
Util.isWeb() && controller.fileDataUri.isNotEmpty;
final submitUrl = isWebFileChosen
? controller.fileDataUri.value
: _urlController.text;
final urls = Util.textToLines(submitUrl);
// Add url to the history
for (final url in urls) {
_historyController.addHistory(url);
if (!isWebFileChosen) {
for (final url in urls) {
_historyController.addHistory(url);
}
}
/*
Check if is direct download, there has two ways to direct download
1. Direct download option is checked
@@ -433,7 +438,7 @@ class CreateView extends GetView<CreateController> {
name: isMultiLine ? "" : _renameController.text,
path: _pathController.text,
selectFiles: [],
extra: parseReqOpts())));
extra: parseReqOptsExtra())));
}));
Get.rootDelegate.offNamed(Routes.TASK);
} else {
@@ -471,11 +476,10 @@ class CreateView extends GetView<CreateController> {
return reqExtra;
}
Object? parseReqOpts() {
return _connectionsController.text.isEmpty
? null
: (OptsExtraHttp()
..connections = int.parse(_connectionsController.text));
Object? parseReqOptsExtra() {
return OptsExtraHttp()
..connections = int.tryParse(_connectionsController.text) ?? 0
..autoTorrent = true;
}
String _hitText() {
@@ -541,7 +545,7 @@ class CreateView extends GetView<CreateController> {
showMessage('tip'.tr, 'noFileSelected'.tr);
return;
}
final optExtra = parseReqOpts();
final optExtra = parseReqOptsExtra();
if (createFormKey.currentState!.validate()) {
if (rr.id.isEmpty) {
// from extension resolve result
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
@@ -12,7 +11,7 @@ class HomeView extends GetView<HomeController> {
@override
Widget build(BuildContext context) {
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
switch (currentRoute?.location) {
switch (currentRoute?.uri.path) {
case Routes.EXTENSION:
controller.currentIndex.value = 1;
break;
@@ -24,141 +23,139 @@ class HomeView extends GetView<HomeController> {
break;
}
return WithForegroundTask(
child: Scaffold(
// extendBody: true,
body: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
!ResponsiveBuilder.isNarrow(context)
? NavigationRail(
extended: true,
labelType: NavigationRailLabelType.none,
minExtendedWidth: 170,
groupAlignment: 0,
// useIndicator: false,
onDestinationSelected: (int index) {
controller.currentIndex.value = index;
switch (index) {
case 0:
delegate.offAndToNamed(Routes.TASK);
break;
case 1:
delegate.offAndToNamed(Routes.EXTENSION);
break;
case 2:
delegate.offAndToNamed(Routes.SETTING);
break;
}
},
destinations: [
NavigationRailDestination(
icon: const Icon(Icons.task),
selectedIcon: const Icon(Icons.task),
label: Text('task'.tr),
),
NavigationRailDestination(
icon: const Icon(Icons.extension),
selectedIcon: const Icon(Icons.extension),
label: Text('extensions'.tr),
),
NavigationRailDestination(
icon: const Icon(Icons.settings),
selectedIcon: const Icon(Icons.settings),
label: Text('setting'.tr),
),
],
selectedIndex: controller.currentIndex.value,
leading: const Icon(Icons.menu),
// trailing: const Icon(Icons.info_outline),
)
: const SizedBox.shrink(),
Expanded(
child: GetRouterOutlet(
initialRoute: Routes.TASK,
// anchorRoute: '/',
// filterPages: (afterAnchor) {
// logger.w(afterAnchor);
// logger.w(afterAnchor.take(1));
// return afterAnchor.take(1);
// },
))
]),
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
? BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Icons.task),
label: 'task'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.extension),
label: 'extensions'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.settings),
label: 'setting'.tr,
),
],
currentIndex: controller.currentIndex.value,
// selectedItemColor: Get.theme.highlightColor,
onTap: (index) {
controller.currentIndex.value = index;
switch (index) {
case 0:
delegate.offAndToNamed(Routes.TASK);
break;
case 1:
delegate.offAndToNamed(Routes.EXTENSION);
break;
case 2:
delegate.offAndToNamed(Routes.SETTING);
break;
}
},
)
// StylishBottomBar(
// option: AnimatedBarOptions(
// iconSize: 32,
// barAnimation: BarAnimation.blink,
// iconStyle: IconStyle.Default,
// opacity: 0.3,
// ),
// items: [
// BottomBarItem(
// icon: const Icon(Icons.file_download),
// selectedColor: Get.theme.primaryColor,
// title: Text('downloading'.tr)),
// BottomBarItem(
// icon: const Icon(Icons.done),
// selectedColor: Get.theme.primaryColor,
// title: Text('downloaded'.tr)),
// BottomBarItem(
// icon: const Icon(Icons.settings),
// selectedColor: Get.theme.primaryColor,
// title: Text('setting'.tr)),
// ],
// // hasNotch: true,
// currentIndex: controller.currentIndex.value,
// onTap: (index) {
// switch (index) {
// case 0:
// delegate.toNamed(Routes.DOWNLOADING);
// controller.currentIndex.value = 0;
// break;
// case 1:
// delegate.toNamed(Routes.DOWNLOADED);
// controller.currentIndex.value = 1;
// break;
// case 2:
// delegate.toNamed(Routes.SETTING);
// controller.currentIndex.value = 2;
// break;
// }
// },
// )
: const SizedBox.shrink(),
),
return Scaffold(
// extendBody: true,
body: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
!ResponsiveBuilder.isNarrow(context)
? NavigationRail(
extended: true,
labelType: NavigationRailLabelType.none,
minExtendedWidth: 170,
groupAlignment: 0,
// useIndicator: false,
onDestinationSelected: (int index) {
controller.currentIndex.value = index;
switch (index) {
case 0:
delegate.offAndToNamed(Routes.TASK);
break;
case 1:
delegate.offAndToNamed(Routes.EXTENSION);
break;
case 2:
delegate.offAndToNamed(Routes.SETTING);
break;
}
},
destinations: [
NavigationRailDestination(
icon: const Icon(Icons.task),
selectedIcon: const Icon(Icons.task),
label: Text('task'.tr),
),
NavigationRailDestination(
icon: const Icon(Icons.extension),
selectedIcon: const Icon(Icons.extension),
label: Text('extensions'.tr),
),
NavigationRailDestination(
icon: const Icon(Icons.settings),
selectedIcon: const Icon(Icons.settings),
label: Text('setting'.tr),
),
],
selectedIndex: controller.currentIndex.value,
leading: const Icon(Icons.menu),
// trailing: const Icon(Icons.info_outline),
)
: const SizedBox.shrink(),
Expanded(
child: GetRouterOutlet(
initialRoute: Routes.TASK,
// anchorRoute: '/',
// filterPages: (afterAnchor) {
// logger.w(afterAnchor);
// logger.w(afterAnchor.take(1));
// return afterAnchor.take(1);
// },
))
]),
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
? BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Icons.task),
label: 'task'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.extension),
label: 'extensions'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.settings),
label: 'setting'.tr,
),
],
currentIndex: controller.currentIndex.value,
// selectedItemColor: Get.theme.highlightColor,
onTap: (index) {
controller.currentIndex.value = index;
switch (index) {
case 0:
delegate.offAndToNamed(Routes.TASK);
break;
case 1:
delegate.offAndToNamed(Routes.EXTENSION);
break;
case 2:
delegate.offAndToNamed(Routes.SETTING);
break;
}
},
)
// StylishBottomBar(
// option: AnimatedBarOptions(
// iconSize: 32,
// barAnimation: BarAnimation.blink,
// iconStyle: IconStyle.Default,
// opacity: 0.3,
// ),
// items: [
// BottomBarItem(
// icon: const Icon(Icons.file_download),
// selectedColor: Get.theme.primaryColor,
// title: Text('downloading'.tr)),
// BottomBarItem(
// icon: const Icon(Icons.done),
// selectedColor: Get.theme.primaryColor,
// title: Text('downloaded'.tr)),
// BottomBarItem(
// icon: const Icon(Icons.settings),
// selectedColor: Get.theme.primaryColor,
// title: Text('setting'.tr)),
// ],
// // hasNotch: true,
// currentIndex: controller.currentIndex.value,
// onTap: (index) {
// switch (index) {
// case 0:
// delegate.toNamed(Routes.DOWNLOADING);
// controller.currentIndex.value = 0;
// break;
// case 1:
// delegate.toNamed(Routes.DOWNLOADED);
// controller.currentIndex.value = 1;
// break;
// case 2:
// delegate.toNamed(Routes.SETTING);
// controller.currentIndex.value = 2;
// break;
// }
// },
// )
: const SizedBox.shrink(),
);
});
}
@@ -365,6 +365,18 @@ class SettingView extends GetView<SettingController> {
);
}
buildThanks() {
const thankPage =
'https://github.com/GopeedLab/gopeed/graphs/contributors';
return ListTile(
title: Text('thanks'.tr),
subtitle: Text('thanksDesc'.tr),
onTap: () {
launchUrl(Uri.parse(thankPage), mode: LaunchMode.externalApplication);
},
);
}
// advanced config proxy items start
final buildProxy = _buildConfigItem(
'proxy',
@@ -387,45 +399,43 @@ class SettingView extends GetView<SettingController> {
},
);
final items = <Widget>[
SizedBox(
width: 150,
child: DropdownButtonFormField<String>(
value: proxy.scheme,
onChanged: (value) async {
if (value != null && value != proxy.scheme) {
proxy.scheme = value;
final scheme = SizedBox(
width: 100,
child: DropdownButtonFormField<String>(
value: proxy.scheme,
onChanged: (value) async {
if (value != null && value != proxy.scheme) {
proxy.scheme = value;
await debounceSave();
}
},
items: const [
DropdownMenuItem<String>(
value: 'http',
child: Text('HTTP'),
),
DropdownMenuItem<String>(
value: 'https',
child: Text('HTTPS'),
),
DropdownMenuItem<String>(
value: 'socks5',
child: Text('SOCKS5'),
),
],
),
)
];
await debounceSave();
}
},
items: const [
DropdownMenuItem<String>(
value: 'http',
child: Text('HTTP'),
),
DropdownMenuItem<String>(
value: 'https',
child: Text('HTTPS'),
),
DropdownMenuItem<String>(
value: 'socks5',
child: Text('SOCKS5'),
),
],
),
);
final arr = proxy.host.split(':');
var ip = '';
var host = '';
var port = '';
if (arr.length > 1) {
ip = arr[0];
host = arr[0];
port = arr[1];
}
final ipController = TextEditingController(text: ip);
final ipController = TextEditingController(text: host);
final portController = TextEditingController(text: port);
updateAddress() async {
final newAddress = '${ipController.text}:${portController.text}';
@@ -438,25 +448,18 @@ class SettingView extends GetView<SettingController> {
ipController.addListener(updateAddress);
portController.addListener(updateAddress);
items.addAll([
const Padding(padding: EdgeInsets.only(left: 20)),
SizedBox(
width: 150,
final server = [
Flexible(
child: TextFormField(
controller: ipController,
decoration: const InputDecoration(
labelText: 'IP',
contentPadding: EdgeInsets.all(0.0),
decoration: InputDecoration(
labelText: 'server'.tr,
contentPadding: const EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
),
),
const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 150,
Flexible(
child: TextFormField(
controller: portController,
decoration: InputDecoration(
@@ -470,40 +473,29 @@ class SettingView extends GetView<SettingController> {
],
),
),
]);
];
final usrController = TextEditingController(text: proxy.usr);
final pwdController = TextEditingController(text: proxy.pwd);
final auth = [
SizedBox(
width: 150,
Flexible(
child: TextFormField(
controller: usrController,
decoration: InputDecoration(
labelText: 'username'.tr,
contentPadding: const EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
),
),
const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 150,
Flexible(
child: TextFormField(
controller: pwdController,
decoration: InputDecoration(
labelText: 'password'.tr,
contentPadding: const EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 0, max: 65535),
],
),
),
];
@@ -513,8 +505,9 @@ class SettingView extends GetView<SettingController> {
crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([
switcher,
scheme,
Row(
children: items,
children: server,
),
Row(
children: auth,
@@ -534,7 +527,7 @@ class SettingView extends GetView<SettingController> {
(Key key) {
final items = <Widget>[
SizedBox(
width: 150,
width: 80,
child: DropdownButtonFormField<String>(
value: startCfg.value.network,
onChanged: Util.isDesktop()
@@ -585,8 +578,7 @@ class SettingView extends GetView<SettingController> {
portController.addListener(updateAddress);
items.addAll([
const Padding(padding: EdgeInsets.only(left: 20)),
SizedBox(
width: 150,
Flexible(
child: TextFormField(
controller: ipController,
decoration: const InputDecoration(
@@ -600,8 +592,7 @@ class SettingView extends GetView<SettingController> {
),
),
const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 150,
Flexible(
child: TextFormField(
controller: portController,
decoration: InputDecoration(
@@ -710,6 +701,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([
buildHomepage(),
buildVersion(),
buildThanks(),
]),
)),
]),
@@ -3,5 +3,6 @@ import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
import '../../../../api/model/task.dart';
class TaskDownloadedController extends TaskListController {
TaskDownloadedController() : super([Status.done], SortDirection.asc);
TaskDownloadedController()
: super([Status.done], (a, b) => b.updatedAt.compareTo(a.updatedAt));
}
@@ -9,5 +9,5 @@ class TaskDownloadingController extends TaskListController {
Status.pause,
Status.wait,
Status.error
], SortDirection.desc);
], (a, b) => b.createdAt.compareTo(a.createdAt));
}
@@ -5,13 +5,11 @@ import 'package:get/get.dart';
import '../../../../api/api.dart';
import '../../../../api/model/task.dart';
enum SortDirection { asc, desc }
abstract class TaskListController extends GetxController {
List<Status> statuses;
SortDirection sortDirection;
int Function(Task a, Task b) compare;
TaskListController(this.statuses, this.sortDirection);
TaskListController(this.statuses, this.compare);
final tasks = <Task>[].obs;
final isRunning = false.obs;
@@ -48,13 +46,7 @@ abstract class TaskListController extends GetxController {
getTasksState() async {
final tasks = await getTasks(statuses);
// sort tasks by create time
tasks.sort((a, b) {
if (sortDirection == SortDirection.asc) {
return a.createdAt.compareTo(b.createdAt);
} else {
return b.createdAt.compareTo(a.createdAt);
}
});
tasks.sort(compare);
this.tasks.value = tasks;
}
}
@@ -35,7 +35,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
},
)),
!Util.isWeb() && !Util.isIOS()
!Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
? IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
+4
View File
@@ -79,7 +79,11 @@ const enUS = {
'serviceText': 'Running',
'network': 'Network',
'proxy': 'Proxy',
'server': 'Server',
'username': 'Username',
'password': 'Password',
'thanks': 'Thanks',
'thanksDesc':
'Thanks to all the contributors who have helped build and develop the Gopeed community!',
},
};
+3
View File
@@ -57,5 +57,8 @@ const faIR = {
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
'newVersionUpdate': 'بروزرسانی',
'newVersionLater': 'بعداً',
'thanks': 'تشکر',
'thanksDesc':
'از تمامی افرادی که در ساخت و توسعه‌ی جامعه‌ی Gopeed مشارکت کرده‌اند، سپاسگزاری می‌کنیم!',
}
};
+2
View File
@@ -59,5 +59,7 @@ const jaJP = {
'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': 'アップデート',
'newVersionLater': '後で',
'thanks': '感謝',
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
}
};
+3
View File
@@ -73,5 +73,8 @@ const ruRU = {
'history': 'История',
'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена',
'thanks': 'Благодарности',
'thanksDesc':
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
}
};
+3
View File
@@ -77,7 +77,10 @@ const zhCN = {
'serviceText': '运行中',
'network': '网络',
'proxy': '代理',
'server': '服务器',
'username': '用户名',
'password': '密码',
'thanks': '鸣谢',
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
}
};
+14 -7
View File
@@ -24,7 +24,7 @@ const zhTW = {
'noStoragePermission': '沒有存儲權限',
'selectFile': '選擇文件',
'rename': '重命名',
'basic': '基礎',
'basic': '一般',
'advanced': '進階',
'general': '通用設定',
'downloadDir': '下載目録',
@@ -59,21 +59,28 @@ const zhTW = {
'deleteTask': '刪除任務',
'deleteTaskTip': '保留已經下載的文件',
'delete': '刪除',
'newVersionTitle': '發現新版本 @version',
'newVersionUpdate': '立刻更新',
'newVersionLater': '稍後更新',
'extensions': '外掛程序',
'newVersionTitle': '現新版本 @version 已發佈',
'newVersionUpdate': '下載',
'newVersionLater': '取消',
'extensions': '擴充程序',
'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得外掛',
'extensionDevelop': '開發外掛',
'extensionFind': '取得擴充程序',
'extensionDevelop': '開發擴充程序',
'history': '歷史記錄',
'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務',
'serviceText': '執行中',
'network': '網路',
'proxy': '代理',
'server': '伺服器',
'username': '名稱',
'password': '密碼',
'thanks': '鳴謝',
'thanksDesc': '感謝所有為 Gopeed 社群建設添磚加瓦的貢獻者們!'
}
};
+10 -3
View File
@@ -13,6 +13,7 @@ import 'util/log_util.dart';
import 'util/mac_secure_util.dart';
import 'util/package_info.dart';
import 'util/util.dart';
import 'util/window_util.dart' as windows_util;
void main() async {
await init();
@@ -25,15 +26,21 @@ Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop()) {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(800, 600),
final windowState = await windows_util.loadState();
final windowOptions = WindowOptions(
size: Size(windowState.width, windowState.height),
center: true,
skipTaskbar: false,
);
windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
await windowManager.setPreventClose(true);
// windows_manager has a bug where when window to be maximized, it will be unmaximized immediately, so can't implement this feature currently.
// https://github.com/leanflutter/window_manager/issues/412
// if (windowState.isMaximized) {
// await windowManager.maximize();
// }
});
}
+8
View File
@@ -143,4 +143,12 @@ class Util {
}
return completer.future;
}
static void Function() debounce(Function() fn, int ms) {
Timer? timer;
return () {
timer?.cancel();
timer = Timer(Duration(milliseconds: ms), fn);
};
}
}
+39
View File
@@ -0,0 +1,39 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
class WindowState {
late bool isMaximized;
late double width;
late double height;
WindowState(this.isMaximized, this.width, this.height);
}
const String _windowStateIsMaximized = "window.isMaximized";
const String _windowStateWidth = "window.width";
const String _windowStateHeight = "window.height";
/// Save window state to storage.
void saveState({bool? isMaximized, double? width, double? height}) async {
final prefs = await SharedPreferences.getInstance();
if (isMaximized != null) {
await prefs.setBool(_windowStateIsMaximized, isMaximized);
}
if (width != null) {
await prefs.setDouble(_windowStateWidth, width);
}
if (height != null) {
await prefs.setDouble(_windowStateHeight, height);
}
}
/// Load window state from storage.
Future<WindowState> loadState() async {
final prefs = await SharedPreferences.getInstance();
return WindowState(
prefs.getBool(_windowStateIsMaximized) ?? false,
prefs.getDouble(_windowStateWidth) ?? 800,
prefs.getDouble(_windowStateHeight) ?? 600,
);
}
@@ -11,4 +11,4 @@ Exec=gopeed
Icon=com.gopeed.Gopeed
Type=Application
Categories=Utility;Internet
Keywords=Bittorrent;Downloader;
Keywords=Bittorrent;Downloader;
@@ -0,0 +1,17 @@
display_name: Gopeed
icon: assets/icon/icon.svg
keywords:
- Application
- DownloadManager
- Network
- Utility
generic_name: Download Manager
categories:
- Network
- Utility
startup_notify: true
@@ -0,0 +1,31 @@
display_name: Gopeed
package_name: gopeed
maintainer:
name: monkeyWie
email: [email protected]
co_authors:
- name: madoka773
email: [email protected]
priority: optional
section: x11
installed_size: 52360
essential: false
icon: assets/icon/icon.svg
dependencies:
- libappindicator3-1 | libayatana-appindicator3-1
- gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1
keywords:
- Application
- DownloadManager
- Network
- Utility
generic_name: Download Manager
categories:
- Network
- Utility
startup_notify: true
@@ -0,0 +1,24 @@
display_name: Gopeed
icon: assets/icon/icon.svg
summary: A modern download manager that supports all platforms. Built with Golang and Flutter.
group: Applications/Internet
vendor: monkeyWie
packager: madoka773
packagerEmail: [email protected]
license: GPL-3.0-or-later
url: https://github.com/GopeedLab/gopeed
description: A modern download manager that supports all platforms. Built with Golang and Flutter.
keywords:
- Application
- DownloadManager
- Network
- Utility
generic_name: Download Manager
categories:
- Network
- Utility
startup_notify: true
+40 -25
View File
@@ -181,10 +181,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.17.1"
version: "1.18.0"
context_menus:
dependency: "direct main"
description:
@@ -441,10 +441,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.18.0"
version: "0.18.1"
io:
dependency: transitive
description:
@@ -477,6 +477,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.7.1"
lecle_downloads_path_provider:
dependency: "direct main"
description:
name: lecle_downloads_path_provider
sha256: "111f6d6d21e18acddda515eba8fd445658aa14defcbd59513d6d1b02b9ed47a0"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.2+8"
lint:
dependency: transitive
description:
@@ -537,18 +545,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
version: "0.5.0"
math_expressions:
dependency: transitive
description:
@@ -569,10 +577,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.10.0"
mime:
dependency: transitive
description:
@@ -894,10 +902,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.10.0"
sprintf:
dependency: transitive
description:
@@ -910,18 +918,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.0"
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
version: "2.1.2"
stream_transform:
dependency: transitive
description:
@@ -958,10 +966,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1"
version: "0.6.1"
timing:
dependency: transitive
description:
@@ -973,12 +981,11 @@ packages:
tray_manager:
dependency: "direct main"
description:
path: "."
ref: "fix/libayatana-set-icon-deprecation"
resolved-ref: "056da3fef20e75877e8d2a4ae73d93f51a0f77cc"
url: "https://github.com/provokateurin/tray_manager.git"
source: git
version: "0.2.0"
name: tray_manager
sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.1"
typed_data:
dependency: transitive
description:
@@ -1083,6 +1090,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.0"
web_socket_channel:
dependency: transitive
description:
@@ -1140,5 +1155,5 @@ packages:
source: hosted
version: "2.1.1"
sdks:
dart: ">=3.0.0 <4.0.0"
dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.10.0"
+3 -5
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.5.1+1
version: 1.5.3+1
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -56,16 +56,14 @@ dependencies:
app_links: ^3.4.2
uri_to_file: ^0.2.0
window_manager: ^0.3.4
tray_manager:
git:
url: https://github.com/provokateurin/tray_manager.git
ref: fix/libayatana-set-icon-deprecation
autoscale_tabbarview: ^1.0.2
share_plus: ^7.1.0
flutter_form_builder: ^9.1.1
form_builder_validators: ^9.0.0
flutter_foreground_task: ^6.1.2
open_filex: ^4.3.4
tray_manager: ^0.2.1
lecle_downloads_path_provider: ^0.0.2+8
dev_dependencies:
flutter_test:
sdk: flutter