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' label: 'doc'
- title: '🚀 CI/CD' - title: '🚀 CI/CD'
label: 'ci' label: 'ci'
- title: '🌎 Internationalization'
label: 'translation'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver: version-resolver:
major: major:
+17 -111
View File
@@ -223,126 +223,32 @@ jobs:
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- run: | - run: |
sudo apt-get update -y sudo apt update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev # 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 - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
mkdir -p build/dist flutter_distributor package --platform linux --targets appimage,deb
cd dist/*
# Build portable mv gopeed-*-linux.AppImage Gopeed-${VERSION}-linux-x86_64.AppImage
tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./ mv gopeed-*-linux.deb Gopeed-${VERSION}-linux-amd64.deb
# 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
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/dist/* asset_path: ui/flutter/dist/*/*
overwrite: true overwrite: true
build-snap: build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
@@ -405,7 +311,7 @@ jobs:
plugin: flutter plugin: flutter
stage-packages: stage-packages:
- libgtk-3-dev - libgtk-3-dev
- libayatana-appindicator3-dev - libappindicator3-dev
flutter-target: lib/main.dart flutter-target: lib/main.dart
EOF EOF
+1 -1
View File
@@ -4,7 +4,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
ui/flutter/dist/
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
+1 -1
View File
@@ -16,7 +16,7 @@ flutter 项目即可运行。
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。 Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
请注意以 `en_US.json` 为参照进行翻译。 请注意以 `en_us.dart` 为参照进行翻译。
## flutter开发 ## flutter开发
+1 -1
View File
@@ -15,7 +15,7 @@
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。 Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
請以 `en_US.json` 作為參照。 請以 `en_us.dart` 作為參照。
## flutter開發 ## flutter開發
+1
View File
@@ -17,5 +17,6 @@ FROM alpine:3.14.2
LABEL maintainer="monkeyWie" LABEL maintainer="monkeyWie"
WORKDIR /app WORKDIR /app
COPY --from=go /app/dist/gopeed ./ COPY --from=go /app/dist/gopeed ./
VOLUME ["/app/storage"]
EXPOSE 9999 EXPOSE 9999
ENTRYPOINT ["./gopeed"] 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). 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 ## Install
@@ -47,9 +47,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### Start directly #### Start directly
```bash ```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 #### Using Docker Compose
```bash ```bash
@@ -160,7 +162,6 @@ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web 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 build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
``` ```
## Credits ## Credits
+3 -2
View File
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接始める #### 直接始める
```bash ```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 の使用 #### Docker Compose の使用
```bash ```bash
@@ -159,7 +161,6 @@ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web 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 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)来实现更多的功能。 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 ```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 #### 使用 Docker Compose
```bash ```bash
@@ -159,7 +161,6 @@ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web 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 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 ```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 #### 使用 Docker Compose
```bash ```bash
@@ -159,7 +161,6 @@ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web 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 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/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs" "io/fs"
"os"
"path/filepath"
) )
//go:embed dist/* //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{ cfg := &model.StartConfig{
Network: "tcp", Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port), Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt, Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken, ApiToken: *args.ApiToken,
ProductionMode: true, ProductionMode: true,
WebEnable: true, WebEnable: true,
+3 -2
View File
@@ -4,8 +4,9 @@ services:
gopeed: gopeed:
container_name: gopeed container_name: gopeed
ports: ports:
- 9999:9999 # HTTP port (host:container) - "9999:9999" # HTTP port (host:container)
image: liwei2633/gopeed image: liwei2633/gopeed
volumes: 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 restart: unless-stopped
+2
View File
@@ -21,6 +21,8 @@ type Fetcher interface {
Pause() error Pause() error
Close() error Close() error
// Stats refreshes health statistics and returns the latest information
Stats() any
// Meta returns the meta information of the download. // Meta returns the meta information of the download.
Meta() *FetcherMeta Meta() *FetcherMeta
// Progress returns the progress of the download. // Progress returns the progress of the download.
+11
View File
@@ -128,6 +128,7 @@ func (f *Fetcher) Start() (err error) {
file.Download() file.Download()
} }
} }
return return
} }
@@ -188,6 +189,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta 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 { func (f *Fetcher) Progress() fetcher.Progress {
if !f.torrentReady.Load() { if !f.torrentReady.Load() {
return f.progress return f.progress
+22 -18
View File
@@ -18,6 +18,7 @@ import (
"os" "os"
"path" "path"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -104,8 +105,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
Range: false, Range: false,
Files: []*base.FileInfo{}, Files: []*base.FileInfo{},
} }
if base.HttpCodePartialContent == httpResp.StatusCode { 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)) {
// 返回206响应码表示支持断点下载 // 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
res.Range = true res.Range = true
// 解析资源大小: bytes 0-1000/1001 => 1001 // 解析资源大小: bytes 0-1000/1001 => 1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
@@ -217,6 +218,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta return f.meta
} }
func (f *Fetcher) Stats() any {
// todo http download health
return &fhttp.Stats{}
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0) p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 { if len(f.chunks) > 0 {
@@ -306,26 +312,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent { if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status) err = NewRequestError(resp.StatusCode, resp.Status)
return err return err
} }
return nil // Http request success, reset retry times
}() chunk.retryTimes = 0
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()
for { for {
n, err := resp.Body.Read(buf) n, err := resp.Body.Read(buf)
if n > 0 { if n > 0 {
@@ -342,7 +335,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return err 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 chunk is completed, reset other not complete chunks retry times
if err == nil { if err == nil {
for _, c := range f.chunks { for _, c := range f.chunks {
+2
View File
@@ -16,10 +16,12 @@ const (
HttpCodePartialContent = 206 HttpCodePartialContent = 206
HttpHeaderRange = "Range" HttpHeaderRange = "Range"
HttpHeaderAcceptRanges = "Accept-Ranges"
HttpHeaderContentLength = "Content-Length" HttpHeaderContentLength = "Content-Length"
HttpHeaderContentRange = "Content-Range" HttpHeaderContentRange = "Content-Range"
HttpHeaderContentDisposition = "Content-Disposition" HttpHeaderContentDisposition = "Content-Disposition"
HttpHeaderUserAgent = "Usr-Agent" HttpHeaderUserAgent = "Usr-Agent"
HttpHeaderBytes = "bytes"
HttpHeaderRangeFormat = "bytes=%d-%d" HttpHeaderRangeFormat = "bytes=%d-%d"
) )
+63 -34
View File
@@ -6,8 +6,11 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2" gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"github.com/virtuald/go-paniclog" "github.com/virtuald/go-paniclog"
"os" "os"
"path/filepath" "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.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")) d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
if cfg.ProductionMode { if cfg.ProductionMode {
@@ -225,7 +229,10 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return return
} }
res := d.triggerOnResolve(req) res, err := d.triggerOnResolve(req)
if err != nil {
return
}
if res != nil && len(res.Files) > 0 { if res != nil && len(res.Files) > 0 {
rr = &ResolveResult{ rr = &ResolveResult{
Res: res, Res: res,
@@ -525,9 +532,6 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
Task: task, Task: task,
Err: err, 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) { func (d *Downloader) watch(task *Task) {
err := task.fetcher.Wait() err := task.fetcher.Wait()
if err != nil { if err != nil {
task.Status = base.DownloadStatusError d.doOnError(task, err)
d.emit(EventKeyError, task, err) return
} 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)
} }
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() 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 { 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.fetcher = fetcher
task.Meta = fetcher.Meta() task.Meta = fetcher.Meta()
task.Progress = &Progress{} task.Progress = &Progress{}
task.Status = base.DownloadStatusReady
initTask(task) initTask(task)
if err = fetcher.Create(opts); err != nil { if err = fetcher.Create(opts); err != nil {
return return
@@ -697,25 +730,18 @@ func (d *Downloader) doStart(task *Task) (err error) {
} }
} }
cloneTask := task.clone()
isCreate := task.Status == base.DownloadStatusReady isCreate := task.Status == base.DownloadStatusReady
task.Status = base.DownloadStatusRunning
doStart := func() error { doStart := func() error {
task.lock.Lock() task.lock.Lock()
defer task.lock.Unlock() defer task.lock.Unlock()
req := d.triggerOnStart(cloneTask) d.triggerOnStart(task)
if req != nil { task.updateStatus(base.DownloadStatusRunning)
task.Meta.Req = req
task.fetcher.Meta().Req = req
}
if task.Meta.Res == nil { if task.Meta.Res == nil {
err := task.fetcher.Resolve(task.Meta.Req) err := task.fetcher.Resolve(task.Meta.Req)
if err != nil { if err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
return err return err
} }
task.Meta.Res = task.fetcher.Meta().Res task.Meta.Res = task.fetcher.Meta().Res
@@ -724,8 +750,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
if isCreate { if isCreate {
d.checkDuplicateLock.Lock() d.checkDuplicateLock.Lock()
defer d.checkDuplicateLock.Unlock() 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. // check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" { if task.Meta.Res.Name != "" {
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
fullDirPath := task.Meta.FolderPath() fullDirPath := task.Meta.FolderPath()
newName, err := util.CheckDuplicateAndRename(fullDirPath) newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil { if err != nil {
@@ -733,6 +761,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
} }
task.Meta.Opts.Name = newName task.Meta.Opts.Name = newName
} else { } else {
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
fullFilePath := task.Meta.SingleFilepath() fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath) newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil { if err != nil {
@@ -758,7 +787,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
go func() { go func() {
err := doStart() err := doStart()
if err != nil { 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) { func (d *Downloader) doPause(task *Task) (err error) {
err = func() error { err = func() error {
if task.Status != base.DownloadStatusDone { if task.Status != base.DownloadStatusDone {
task.Status = base.DownloadStatusPause task.updateStatus(base.DownloadStatusPause)
task.timer.Pause() task.timer.Pause()
if task.fetcher != nil { if task.fetcher != nil {
if err := task.fetcher.Pause(); err != nil { if err := task.fetcher.Pause(); err != nil {
+9 -1
View File
@@ -3,6 +3,7 @@ package engine
import ( import (
_ "embed" _ "embed"
"errors" "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/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
@@ -125,6 +126,9 @@ func NewEngine(cfg *Config) *Engine {
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime) vm.Enable(runtime)
gojaurl.Enable(runtime) gojaurl.Enable(runtime)
if err := gojaerror.Enable(runtime); err != nil {
return
}
if err := file.Enable(runtime); err != nil { if err := file.Enable(runtime); err != nil {
return return
} }
@@ -167,7 +171,11 @@ func resolveResult(value goja.Value) (any, error) {
case goja.PromiseStateFulfilled: case goja.PromiseStateFulfilled:
return p.Result().Export(), nil return p.Result().Export(), nil
case goja.PromiseStateRejected: 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 return export, nil
+13
View File
@@ -7,7 +7,9 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/GopeedLab/gopeed/internal/test" "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" "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/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"io" "io"
@@ -20,6 +22,7 @@ import (
) )
func TestPolyfill(t *testing.T) { func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "MessageError")
doTestPolyfill(t, "XMLHttpRequest") doTestPolyfill(t, "XMLHttpRequest")
doTestPolyfill(t, "Blob") doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData") doTestPolyfill(t, "FormData")
@@ -27,6 +30,16 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "__gopeed_create_vm") 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) { func TestFetch(t *testing.T) {
server := startServer() server := startServer()
defer server.Close() 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 ( import (
"bytes" "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/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"io" "io"
"mime/multipart" "mime/multipart"
@@ -316,7 +316,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error { func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object { progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
if len(call.Arguments) < 1 { 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{ instance := &ProgressEvent{
Type: call.Argument(0).String(), 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/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download/engine" "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/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5"
@@ -35,7 +37,7 @@ type ActivationEvent string
const ( const (
EventOnResolve ActivationEvent = "onResolve" EventOnResolve ActivationEvent = "onResolve"
EventOnStart ActivationEvent = "onStart" EventOnStart ActivationEvent = "onStart"
//EventOnError ActivationEvent = "onError" EventOnError ActivationEvent = "onError"
//EventOnDone ActivationEvent = "onDone" //EventOnDone ActivationEvent = "onDone"
) )
@@ -238,8 +240,8 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
return &ext, nil return &ext, nil
} }
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) { func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, err error) {
doTrigger(d, err = doTrigger(d,
EventOnResolve, EventOnResolve,
req, req,
&OnResolveContext{ &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()) gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
return 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) ctx.Res.CalcSize(nil)
} }
res = ctx.Res res = ctx.Res
@@ -260,12 +266,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
return return
} }
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) { func (d *Downloader) triggerOnStart(task *Task) {
doTrigger(d, doTrigger(d,
EventOnStart, EventOnStart,
task.Meta.Req, task.Meta.Req,
&OnStartContext{ &OnStartContext{
Task: task, Task: NewExtensionTask(d, task),
}, },
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) { func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
// Validate request structure // 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()) gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
return return
} }
req = ctx.Task.Meta.Req
} }
}, },
) )
return 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 // init extension global object
gopeed := &Instance{ gopeed := &Instance{
Events: make(InstanceEvents), 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) gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
return 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 { func (d *Downloader) ExtensionPath(ext *Extension) string {
@@ -577,10 +601,10 @@ func (h InstanceEvents) OnStart(fn goja.Callable) {
h.register(EventOnStart, fn) h.register(EventOnStart, fn)
} }
//func (h InstanceEvents) OnError(fn goja.Callable) { func (h InstanceEvents) OnError(fn goja.Callable) {
// h.register(HookEventOnError, fn) h.register(EventOnError, fn)
//} }
//
//func (h InstanceEvents) OnDone(fn goja.Callable) { //func (h InstanceEvents) OnDone(fn goja.Callable) {
// h.register(HookEventOnDone, fn) // h.register(HookEventOnDone, fn)
//} //}
@@ -649,7 +673,33 @@ type OnResolveContext struct {
} }
type OnStartContext 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 { func parseSettings(settings []*Setting) map[string]any {
+66 -20
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/dop251/goja" "github.com/dop251/goja"
"os" "os"
"testing" "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) { downloadAndCheck := func(req *base.Request) {
setupDownloader(func(downloader *Downloader) { setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil { 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) task := downloader.GetTask(id)
if task.Meta.Req.URL != "https://github.com" { 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" { 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 // label match
downloadAndCheck(&base.Request{ downloadAndCheck(&base.Request{
URL: "https://xxx.com", URL: "https://test.com",
Labels: map[string]string{ Labels: map[string]string{
"test": "true", "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) { func TestDownloader_Extension_Errors(t *testing.T) {
setupDownloader(func(downloader *Downloader) { setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil { 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") 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) { 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) { func TestDownloader_Extension_Logger(t *testing.T) {
logger := logger.NewLogger(false, "") logger := logger.NewLogger(false, "")
il := newInstanceLogger(&Extension{ il := newInstanceLogger(&Extension{
+8
View File
@@ -24,6 +24,7 @@ type Task struct {
Status base.Status `json:"status"` Status base.Status `json:"status"`
Progress *Progress `json:"progress"` Progress *Progress `json:"progress"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
fetcherBuilder fetcher.FetcherBuilder fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher fetcher fetcher.Fetcher
@@ -41,9 +42,15 @@ func NewTask() *Task {
ID: id, ID: id,
Status: base.DownloadStatusReady, Status: base.DownloadStatusReady,
CreatedAt: time.Now(), 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 { func (t *Task) clone() *Task {
return &Task{ return &Task{
ID: t.ID, ID: t.ID,
@@ -51,6 +58,7 @@ func (t *Task) clone() *Task {
Status: t.Status, Status: t.Status,
Progress: t.Progress, Progress: t.Progress,
CreatedAt: t.CreatedAt, CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
} }
} }
@@ -1,5 +1,6 @@
gopeed.events.onResolve(async function (ctx) { gopeed.events.onResolve(async function (ctx) {
const aaa = {}; const aaa = {};
// access undefined property
gopeed.logger.info(aaa.bbb.ccc); gopeed.logger.info(aaa.bbb.ccc);
ctx.res = { 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.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.url = "https://github.com";
ctx.task.meta.req.labels['modified'] = 'true'; ctx.task.meta.req.labels['modified'] = 'true';
}); });
+9
View File
@@ -3,3 +3,12 @@ package bt
type ReqExtra struct { type ReqExtra struct {
Trackers []string `json:"trackers"` 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 { type OptsExtra struct {
Connections int `json:"connections"` 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 ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors"
"net" "net"
"net/http" "net/http"
"os" "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( srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}), handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
+2
View File
@@ -204,12 +204,14 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
// continue all // continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil) 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) tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
if len(tasks) != cfg.MaxRunning { if len(tasks) != cfg.MaxRunning {
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning) t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
} }
// pause all // pause all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil) 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) tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
if len(tasks) != total { if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", 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 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() @JsonSerializable()
class OptsExtraHttp { class OptsExtraHttp {
int connections = 0; int connections = 0;
bool autoTorrent = false;
OptsExtraHttp(); OptsExtraHttp();
+4 -1
View File
@@ -32,9 +32,12 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
} }
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) => 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) => Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{ <String, dynamic>{
'connections': instance.connections, 'connections': instance.connections,
'autoTorrent': instance.autoTorrent,
}; };
+2
View File
@@ -13,6 +13,7 @@ class Task {
Status status; Status status;
Progress progress; Progress progress;
DateTime createdAt; DateTime createdAt;
DateTime updatedAt;
Task({ Task({
required this.id, required this.id,
@@ -20,6 +21,7 @@ class Task {
required this.status, required this.status,
required this.progress, required this.progress,
required this.createdAt, required this.createdAt,
required this.updatedAt,
}); });
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json); 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']), status: $enumDecode(_$StatusEnumMap, json['status']),
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>), progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['createdAt'] as String), createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
); );
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
@@ -20,6 +21,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'status': _$StatusEnumMap[instance.status]!, 'status': _$StatusEnumMap[instance.status]!,
'progress': instance.progress.toJson(), 'progress': instance.progress.toJson(),
'createdAt': instance.createdAt.toIso8601String(), 'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
}; };
const _$StatusEnumMap = { const _$StatusEnumMap = {
@@ -6,6 +6,7 @@ import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.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:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
@@ -21,6 +22,7 @@ import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../../util/window_util.dart' as windows_util;
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network"; const _startConfigNetwork = "start.network";
@@ -49,7 +51,12 @@ const allTrackerCdns = [
[ [
"https://ghproxy.com/https://raw.githubusercontent.com", "https://ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)" 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, final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
key: (v) => v as String, key: (v) => v as String,
@@ -118,6 +125,26 @@ class AppController extends GetxController with WindowListener, TrayListener {
trayManager.popUpContextMenu(); 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 { Future<void> _initDeepLinks() async {
// currently only support android // currently only support android
if (!Util.isAndroid()) { if (!Util.isAndroid()) {
@@ -391,9 +418,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (Util.isDesktop()) { if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./"; config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isAndroid()) { } else if (Util.isAndroid()) {
config.downloadDir = (await getExternalStorageDirectory())?.path ?? final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
(await getApplicationDocumentsDirectory()).path; if (downloadDir != null) {
return; config.downloadDir = '$downloadDir/Gopeed';
} else {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
}
} else if (Util.isIOS()) { } else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path; config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else { } else {
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -14,22 +15,24 @@ class AppView extends GetView<AppController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final config = controller.downloaderConfig.value; final config = controller.downloaderConfig.value;
return GetMaterialApp.router( return WithForegroundTask(
useInheritedMediaQuery: true, child: GetMaterialApp.router(
debugShowCheckedModeBanner: false, useInheritedMediaQuery: true,
theme: GopeedTheme.light, debugShowCheckedModeBanner: false,
darkTheme: GopeedTheme.dark, theme: GopeedTheme.light,
themeMode: ThemeMode.values.byName(config.extra.themeMode), darkTheme: GopeedTheme.dark,
translations: messages, themeMode: ThemeMode.values.byName(config.extra.themeMode),
locale: toLocale(config.extra.locale), translations: messages,
fallbackLocale: fallbackLocale, locale: toLocale(config.extra.locale),
localizationsDelegates: const [ fallbackLocale: fallbackLocale,
GlobalMaterialLocalizations.delegate, localizationsDelegates: const [
GlobalWidgetsLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], GlobalCupertinoLocalizations.delegate,
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(), ],
getPages: AppPages.routes, 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/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.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:path/path.dart' as path;
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';
import '../../../../api/model/create_task.dart'; import '../../../../api/model/create_task.dart';
import '../../../../api/model/options.dart'; import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart'; import '../../../../api/model/request.dart';
import '../../../../api/model/resolve_result.dart'; import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/resource.dart';
import '../../../../util/input_formatter.dart'; import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
@@ -21,6 +19,8 @@ import '../../../routes/app_pages.dart';
import '../../../views/directory_selector.dart'; import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart'; import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../../history/controller/history_controller.dart';
import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart'; import '../controllers/create_controller.dart';
class CreateView extends GetView<CreateController> { class CreateView extends GetView<CreateController> {
@@ -409,15 +409,20 @@ class CreateView extends GetView<CreateController> {
try { try {
_confirmController.start(); _confirmController.start();
if (_confirmFormKey.currentState!.validate()) { if (_confirmFormKey.currentState!.validate()) {
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty final isWebFileChosen =
Util.isWeb() && controller.fileDataUri.isNotEmpty;
final submitUrl = isWebFileChosen
? controller.fileDataUri.value ? controller.fileDataUri.value
: _urlController.text; : _urlController.text;
final urls = Util.textToLines(submitUrl); final urls = Util.textToLines(submitUrl);
// Add url to the history // Add url to the history
for (final url in urls) { if (!isWebFileChosen) {
_historyController.addHistory(url); for (final url in urls) {
_historyController.addHistory(url);
}
} }
/* /*
Check if is direct download, there has two ways to direct download Check if is direct download, there has two ways to direct download
1. Direct download option is checked 1. Direct download option is checked
@@ -433,7 +438,7 @@ class CreateView extends GetView<CreateController> {
name: isMultiLine ? "" : _renameController.text, name: isMultiLine ? "" : _renameController.text,
path: _pathController.text, path: _pathController.text,
selectFiles: [], selectFiles: [],
extra: parseReqOpts()))); extra: parseReqOptsExtra())));
})); }));
Get.rootDelegate.offNamed(Routes.TASK); Get.rootDelegate.offNamed(Routes.TASK);
} else { } else {
@@ -471,11 +476,10 @@ class CreateView extends GetView<CreateController> {
return reqExtra; return reqExtra;
} }
Object? parseReqOpts() { Object? parseReqOptsExtra() {
return _connectionsController.text.isEmpty return OptsExtraHttp()
? null ..connections = int.tryParse(_connectionsController.text) ?? 0
: (OptsExtraHttp() ..autoTorrent = true;
..connections = int.parse(_connectionsController.text));
} }
String _hitText() { String _hitText() {
@@ -541,7 +545,7 @@ class CreateView extends GetView<CreateController> {
showMessage('tip'.tr, 'noFileSelected'.tr); showMessage('tip'.tr, 'noFileSelected'.tr);
return; return;
} }
final optExtra = parseReqOpts(); final optExtra = parseReqOptsExtra();
if (createFormKey.currentState!.validate()) { if (createFormKey.currentState!.validate()) {
if (rr.id.isEmpty) { if (rr.id.isEmpty) {
// from extension resolve result // from extension resolve result
@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
@@ -12,7 +11,7 @@ class HomeView extends GetView<HomeController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) { return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
switch (currentRoute?.location) { switch (currentRoute?.uri.path) {
case Routes.EXTENSION: case Routes.EXTENSION:
controller.currentIndex.value = 1; controller.currentIndex.value = 1;
break; break;
@@ -24,141 +23,139 @@ class HomeView extends GetView<HomeController> {
break; break;
} }
return WithForegroundTask( return Scaffold(
child: Scaffold( // extendBody: true,
// extendBody: true, body: Row(
body: Row( // crossAxisAlignment: CrossAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ !ResponsiveBuilder.isNarrow(context)
!ResponsiveBuilder.isNarrow(context) ? NavigationRail(
? NavigationRail( extended: true,
extended: true, labelType: NavigationRailLabelType.none,
labelType: NavigationRailLabelType.none, minExtendedWidth: 170,
minExtendedWidth: 170, groupAlignment: 0,
groupAlignment: 0, // useIndicator: false,
// useIndicator: false, onDestinationSelected: (int index) {
onDestinationSelected: (int index) { controller.currentIndex.value = index;
controller.currentIndex.value = index; switch (index) {
switch (index) { case 0:
case 0: delegate.offAndToNamed(Routes.TASK);
delegate.offAndToNamed(Routes.TASK); break;
break; case 1:
case 1: delegate.offAndToNamed(Routes.EXTENSION);
delegate.offAndToNamed(Routes.EXTENSION); break;
break; case 2:
case 2: delegate.offAndToNamed(Routes.SETTING);
delegate.offAndToNamed(Routes.SETTING); break;
break; }
} },
}, destinations: [
destinations: [ NavigationRailDestination(
NavigationRailDestination( icon: const Icon(Icons.task),
icon: const Icon(Icons.task), selectedIcon: const Icon(Icons.task),
selectedIcon: const Icon(Icons.task), label: Text('task'.tr),
label: Text('task'.tr), ),
), NavigationRailDestination(
NavigationRailDestination( icon: const Icon(Icons.extension),
icon: const Icon(Icons.extension), selectedIcon: const Icon(Icons.extension),
selectedIcon: const Icon(Icons.extension), label: Text('extensions'.tr),
label: Text('extensions'.tr), ),
), NavigationRailDestination(
NavigationRailDestination( icon: const Icon(Icons.settings),
icon: const Icon(Icons.settings), selectedIcon: const Icon(Icons.settings),
selectedIcon: const Icon(Icons.settings), label: Text('setting'.tr),
label: Text('setting'.tr), ),
), ],
], selectedIndex: controller.currentIndex.value,
selectedIndex: controller.currentIndex.value, leading: const Icon(Icons.menu),
leading: const Icon(Icons.menu), // trailing: const Icon(Icons.info_outline),
// trailing: const Icon(Icons.info_outline), )
) : const SizedBox.shrink(),
: const SizedBox.shrink(), Expanded(
Expanded( child: GetRouterOutlet(
child: GetRouterOutlet( initialRoute: Routes.TASK,
initialRoute: Routes.TASK, // anchorRoute: '/',
// anchorRoute: '/', // filterPages: (afterAnchor) {
// filterPages: (afterAnchor) { // logger.w(afterAnchor);
// logger.w(afterAnchor); // logger.w(afterAnchor.take(1));
// logger.w(afterAnchor.take(1)); // return afterAnchor.take(1);
// return afterAnchor.take(1); // },
// }, ))
)) ]),
]), bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
bottomNavigationBar: ResponsiveBuilder.isNarrow(context) ? BottomNavigationBar(
? BottomNavigationBar( items: <BottomNavigationBarItem>[
items: <BottomNavigationBarItem>[ BottomNavigationBarItem(
BottomNavigationBarItem( icon: const Icon(Icons.task),
icon: const Icon(Icons.task), label: 'task'.tr,
label: 'task'.tr, ),
), BottomNavigationBarItem(
BottomNavigationBarItem( icon: const Icon(Icons.extension),
icon: const Icon(Icons.extension), label: 'extensions'.tr,
label: 'extensions'.tr, ),
), BottomNavigationBarItem(
BottomNavigationBarItem( icon: const Icon(Icons.settings),
icon: const Icon(Icons.settings), label: 'setting'.tr,
label: 'setting'.tr, ),
), ],
], currentIndex: controller.currentIndex.value,
currentIndex: controller.currentIndex.value, // selectedItemColor: Get.theme.highlightColor,
// selectedItemColor: Get.theme.highlightColor, onTap: (index) {
onTap: (index) { controller.currentIndex.value = index;
controller.currentIndex.value = index; switch (index) {
switch (index) { case 0:
case 0: delegate.offAndToNamed(Routes.TASK);
delegate.offAndToNamed(Routes.TASK); break;
break; case 1:
case 1: delegate.offAndToNamed(Routes.EXTENSION);
delegate.offAndToNamed(Routes.EXTENSION); break;
break; case 2:
case 2: delegate.offAndToNamed(Routes.SETTING);
delegate.offAndToNamed(Routes.SETTING); break;
break; }
} },
}, )
) // StylishBottomBar(
// StylishBottomBar( // option: AnimatedBarOptions(
// option: AnimatedBarOptions( // iconSize: 32,
// iconSize: 32, // barAnimation: BarAnimation.blink,
// barAnimation: BarAnimation.blink, // iconStyle: IconStyle.Default,
// iconStyle: IconStyle.Default, // opacity: 0.3,
// opacity: 0.3, // ),
// ), // items: [
// items: [ // BottomBarItem(
// BottomBarItem( // icon: const Icon(Icons.file_download),
// icon: const Icon(Icons.file_download), // selectedColor: Get.theme.primaryColor,
// selectedColor: Get.theme.primaryColor, // title: Text('downloading'.tr)),
// title: Text('downloading'.tr)), // BottomBarItem(
// BottomBarItem( // icon: const Icon(Icons.done),
// icon: const Icon(Icons.done), // selectedColor: Get.theme.primaryColor,
// selectedColor: Get.theme.primaryColor, // title: Text('downloaded'.tr)),
// title: Text('downloaded'.tr)), // BottomBarItem(
// BottomBarItem( // icon: const Icon(Icons.settings),
// icon: const Icon(Icons.settings), // selectedColor: Get.theme.primaryColor,
// selectedColor: Get.theme.primaryColor, // title: Text('setting'.tr)),
// title: Text('setting'.tr)), // ],
// ], // // hasNotch: true,
// // hasNotch: true, // currentIndex: controller.currentIndex.value,
// currentIndex: controller.currentIndex.value, // onTap: (index) {
// onTap: (index) { // switch (index) {
// switch (index) { // case 0:
// case 0: // delegate.toNamed(Routes.DOWNLOADING);
// delegate.toNamed(Routes.DOWNLOADING); // controller.currentIndex.value = 0;
// controller.currentIndex.value = 0; // break;
// break; // case 1:
// case 1: // delegate.toNamed(Routes.DOWNLOADED);
// delegate.toNamed(Routes.DOWNLOADED); // controller.currentIndex.value = 1;
// controller.currentIndex.value = 1; // break;
// break; // case 2:
// case 2: // delegate.toNamed(Routes.SETTING);
// delegate.toNamed(Routes.SETTING); // controller.currentIndex.value = 2;
// controller.currentIndex.value = 2; // break;
// break; // }
// } // },
// }, // )
// ) : const SizedBox.shrink(),
: 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 // advanced config proxy items start
final buildProxy = _buildConfigItem( final buildProxy = _buildConfigItem(
'proxy', 'proxy',
@@ -387,45 +399,43 @@ class SettingView extends GetView<SettingController> {
}, },
); );
final items = <Widget>[ final scheme = SizedBox(
SizedBox( width: 100,
width: 150, child: DropdownButtonFormField<String>(
child: DropdownButtonFormField<String>( value: proxy.scheme,
value: proxy.scheme, onChanged: (value) async {
onChanged: (value) async { if (value != null && value != proxy.scheme) {
if (value != null && value != proxy.scheme) { proxy.scheme = value;
proxy.scheme = value;
await debounceSave(); await debounceSave();
} }
}, },
items: const [ items: const [
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: 'http', value: 'http',
child: Text('HTTP'), child: Text('HTTP'),
), ),
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: 'https', value: 'https',
child: Text('HTTPS'), child: Text('HTTPS'),
), ),
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: 'socks5', value: 'socks5',
child: Text('SOCKS5'), child: Text('SOCKS5'),
), ),
], ],
), ),
) );
];
final arr = proxy.host.split(':'); final arr = proxy.host.split(':');
var ip = ''; var host = '';
var port = ''; var port = '';
if (arr.length > 1) { if (arr.length > 1) {
ip = arr[0]; host = arr[0];
port = arr[1]; port = arr[1];
} }
final ipController = TextEditingController(text: ip); final ipController = TextEditingController(text: host);
final portController = TextEditingController(text: port); final portController = TextEditingController(text: port);
updateAddress() async { updateAddress() async {
final newAddress = '${ipController.text}:${portController.text}'; final newAddress = '${ipController.text}:${portController.text}';
@@ -438,25 +448,18 @@ class SettingView extends GetView<SettingController> {
ipController.addListener(updateAddress); ipController.addListener(updateAddress);
portController.addListener(updateAddress); portController.addListener(updateAddress);
items.addAll([ final server = [
const Padding(padding: EdgeInsets.only(left: 20)), Flexible(
SizedBox(
width: 150,
child: TextFormField( child: TextFormField(
controller: ipController, controller: ipController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'IP', labelText: 'server'.tr,
contentPadding: EdgeInsets.all(0.0), contentPadding: const EdgeInsets.all(0.0),
), ),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
), ),
), ),
const Padding(padding: EdgeInsets.only(left: 10)), const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: portController, controller: portController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -470,40 +473,29 @@ class SettingView extends GetView<SettingController> {
], ],
), ),
), ),
]); ];
final usrController = TextEditingController(text: proxy.usr); final usrController = TextEditingController(text: proxy.usr);
final pwdController = TextEditingController(text: proxy.pwd); final pwdController = TextEditingController(text: proxy.pwd);
final auth = [ final auth = [
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: usrController, controller: usrController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'username'.tr, labelText: 'username'.tr,
contentPadding: const EdgeInsets.all(0.0), contentPadding: const EdgeInsets.all(0.0),
), ),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
), ),
), ),
const Padding(padding: EdgeInsets.only(left: 10)), const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: pwdController, controller: pwdController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'password'.tr, labelText: 'password'.tr,
contentPadding: const EdgeInsets.all(0.0), 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, crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([ children: _addPadding([
switcher, switcher,
scheme,
Row( Row(
children: items, children: server,
), ),
Row( Row(
children: auth, children: auth,
@@ -534,7 +527,7 @@ class SettingView extends GetView<SettingController> {
(Key key) { (Key key) {
final items = <Widget>[ final items = <Widget>[
SizedBox( SizedBox(
width: 150, width: 80,
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: startCfg.value.network, value: startCfg.value.network,
onChanged: Util.isDesktop() onChanged: Util.isDesktop()
@@ -585,8 +578,7 @@ class SettingView extends GetView<SettingController> {
portController.addListener(updateAddress); portController.addListener(updateAddress);
items.addAll([ items.addAll([
const Padding(padding: EdgeInsets.only(left: 20)), const Padding(padding: EdgeInsets.only(left: 20)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: ipController, controller: ipController,
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -600,8 +592,7 @@ class SettingView extends GetView<SettingController> {
), ),
), ),
const Padding(padding: EdgeInsets.only(left: 10)), const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: portController, controller: portController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -710,6 +701,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([ children: _addDivider([
buildHomepage(), buildHomepage(),
buildVersion(), buildVersion(),
buildThanks(),
]), ]),
)), )),
]), ]),
@@ -3,5 +3,6 @@ import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
import '../../../../api/model/task.dart'; import '../../../../api/model/task.dart';
class TaskDownloadedController extends TaskListController { 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.pause,
Status.wait, Status.wait,
Status.error 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/api.dart';
import '../../../../api/model/task.dart'; import '../../../../api/model/task.dart';
enum SortDirection { asc, desc }
abstract class TaskListController extends GetxController { abstract class TaskListController extends GetxController {
List<Status> statuses; 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 tasks = <Task>[].obs;
final isRunning = false.obs; final isRunning = false.obs;
@@ -48,13 +46,7 @@ abstract class TaskListController extends GetxController {
getTasksState() async { getTasksState() async {
final tasks = await getTasks(statuses); final tasks = await getTasks(statuses);
// sort tasks by create time // sort tasks by create time
tasks.sort((a, b) { tasks.sort(compare);
if (sortDirection == SortDirection.asc) {
return a.createdAt.compareTo(b.createdAt);
} else {
return b.createdAt.compareTo(a.createdAt);
}
});
this.tasks.value = tasks; this.tasks.value = tasks;
} }
} }
@@ -35,7 +35,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr; return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
}, },
)), )),
!Util.isWeb() && !Util.isIOS() !Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
? IconButton( ? IconButton(
icon: const Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: () async { onPressed: () async {
+4
View File
@@ -79,7 +79,11 @@ const enUS = {
'serviceText': 'Running', 'serviceText': 'Running',
'network': 'Network', 'network': 'Network',
'proxy': 'Proxy', 'proxy': 'Proxy',
'server': 'Server',
'username': 'Username', 'username': 'Username',
'password': 'Password', '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 عنوان: کشف نسخه جدید', 'newVersionTitle': '@version عنوان: کشف نسخه جدید',
'newVersionUpdate': 'بروزرسانی', 'newVersionUpdate': 'بروزرسانی',
'newVersionLater': 'بعداً', 'newVersionLater': 'بعداً',
'thanks': 'تشکر',
'thanksDesc':
'از تمامی افرادی که در ساخت و توسعه‌ی جامعه‌ی Gopeed مشارکت کرده‌اند، سپاسگزاری می‌کنیم!',
} }
}; };
+2
View File
@@ -59,5 +59,7 @@ const jaJP = {
'newVersionTitle': '新しいバージョン @version を発見する', 'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': 'アップデート', 'newVersionUpdate': 'アップデート',
'newVersionLater': '後で', 'newVersionLater': '後で',
'thanks': '感謝',
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
} }
}; };
+3
View File
@@ -73,5 +73,8 @@ const ruRU = {
'history': 'История', 'history': 'История',
'clearHistory': 'Очистить историю', 'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена', 'noHistoryFound': 'История не найдена',
'thanks': 'Благодарности',
'thanksDesc':
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
} }
}; };
+3
View File
@@ -77,7 +77,10 @@ const zhCN = {
'serviceText': '运行中', 'serviceText': '运行中',
'network': '网络', 'network': '网络',
'proxy': '代理', 'proxy': '代理',
'server': '服务器',
'username': '用户名', 'username': '用户名',
'password': '密码', 'password': '密码',
'thanks': '鸣谢',
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
} }
}; };
+14 -7
View File
@@ -24,7 +24,7 @@ const zhTW = {
'noStoragePermission': '沒有存儲權限', 'noStoragePermission': '沒有存儲權限',
'selectFile': '選擇文件', 'selectFile': '選擇文件',
'rename': '重命名', 'rename': '重命名',
'basic': '基礎', 'basic': '一般',
'advanced': '進階', 'advanced': '進階',
'general': '通用設定', 'general': '通用設定',
'downloadDir': '下載目録', 'downloadDir': '下載目録',
@@ -59,21 +59,28 @@ const zhTW = {
'deleteTask': '刪除任務', 'deleteTask': '刪除任務',
'deleteTaskTip': '保留已經下載的文件', 'deleteTaskTip': '保留已經下載的文件',
'delete': '刪除', 'delete': '刪除',
'newVersionTitle': '發現新版本 @version', 'newVersionTitle': '現新版本 @version 已發佈',
'newVersionUpdate': '立刻更新', 'newVersionUpdate': '下載',
'newVersionLater': '稍後更新', 'newVersionLater': '取消',
'extensions': '外掛程序', 'extensions': '擴充程序',
'extensionInstallUrl': '安裝URL', 'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝成功', 'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成', 'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝', 'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本', 'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得外掛', 'extensionFind': '取得擴充程序',
'extensionDevelop': '開發外掛', 'extensionDevelop': '開發擴充程序',
'history': '歷史記錄', 'history': '歷史記錄',
'clearHistory': '清空記錄', 'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄', 'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務', 'serviceTitle': '下載服務',
'serviceText': '執行中', '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/mac_secure_util.dart';
import 'util/package_info.dart'; import 'util/package_info.dart';
import 'util/util.dart'; import 'util/util.dart';
import 'util/window_util.dart' as windows_util;
void main() async { void main() async {
await init(); await init();
@@ -25,15 +26,21 @@ Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop()) { if (Util.isDesktop()) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
const windowOptions = WindowOptions( final windowState = await windows_util.loadState();
size: Size(800, 600), final windowOptions = WindowOptions(
size: Size(windowState.width, windowState.height),
center: true, center: true,
skipTaskbar: false, skipTaskbar: false,
); );
windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setPreventClose(true); 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; 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 Icon=com.gopeed.Gopeed
Type=Application Type=Application
Categories=Utility;Internet 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 dependency: transitive
description: description:
name: collection name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.17.1" version: "1.18.0"
context_menus: context_menus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -441,10 +441,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.18.0" version: "0.18.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -477,6 +477,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.7.1" 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: lint:
dependency: transitive dependency: transitive
description: description:
@@ -537,18 +545,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.12.15" version: "0.12.16"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.2.0" version: "0.5.0"
math_expressions: math_expressions:
dependency: transitive dependency: transitive
description: description:
@@ -569,10 +577,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -894,10 +902,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
sprintf: sprintf:
dependency: transitive dependency: transitive
description: description:
@@ -910,18 +918,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.11.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@@ -958,10 +966,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.5.1" version: "0.6.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -973,12 +981,11 @@ packages:
tray_manager: tray_manager:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: tray_manager
ref: "fix/libayatana-set-icon-deprecation" sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225"
resolved-ref: "056da3fef20e75877e8d2a4ae73d93f51a0f77cc" url: "https://pub.flutter-io.cn"
url: "https://github.com/provokateurin/tray_manager.git" source: hosted
source: git version: "0.2.1"
version: "0.2.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1083,6 +1090,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" 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: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@@ -1140,5 +1155,5 @@ packages:
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
sdks: sdks:
dart: ">=3.0.0 <4.0.0" dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.10.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. # 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.5.1+1 version: 1.5.3+1
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@@ -56,16 +56,14 @@ dependencies:
app_links: ^3.4.2 app_links: ^3.4.2
uri_to_file: ^0.2.0 uri_to_file: ^0.2.0
window_manager: ^0.3.4 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 autoscale_tabbarview: ^1.0.2
share_plus: ^7.1.0 share_plus: ^7.1.0
flutter_form_builder: ^9.1.1 flutter_form_builder: ^9.1.1
form_builder_validators: ^9.0.0 form_builder_validators: ^9.0.0
flutter_foreground_task: ^6.1.2 flutter_foreground_task: ^6.1.2
open_filex: ^4.3.4 open_filex: ^4.3.4
tray_manager: ^0.2.1
lecle_downloads_path_provider: ^0.0.2+8
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter