mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 02:50:27 +00:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
195dfac74b | ||
|
|
1c1fab2002 | ||
|
|
abccbabc78 | ||
|
|
c26eb26b8f | ||
|
|
74beea6044 | ||
|
|
e548d7e0c9 | ||
|
|
a996d57914 | ||
|
|
f7dc60ad50 | ||
|
|
8425a96ac9 | ||
|
|
99f4e8c895 | ||
|
|
cc0f61b527 | ||
|
|
922ef969a6 | ||
|
|
3a542054fc | ||
|
|
895a8d2ee4 | ||
|
|
29d0f5ebcf | ||
|
|
31228e3edf | ||
|
|
31a32d7e14 | ||
|
|
65fd6fb489 | ||
|
|
953b878a89 | ||
|
|
ae4ca3da58 | ||
|
|
d09bd180af | ||
|
|
3d24e353c5 | ||
|
|
ce7489b0c0 | ||
|
|
2b5091ba21 | ||
|
|
ff32971aca | ||
|
|
f2af61f9aa | ||
|
|
d935b9d631 | ||
|
|
ae66d1d83d | ||
|
|
aa91ca1d50 |
No files matched your search
@@ -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
@@ -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
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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开发
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
|
||||||
|
|
||||||
|
|
||||||
請以 `en_US.json` 作為參照。
|
請以 `en_us.dart` 作為參照。
|
||||||
|
|
||||||
## flutter開發
|
## flutter開發
|
||||||
|
|
||||||
|
|||||||
@@ -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"]
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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,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
@@ -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
|
||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package inject
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/dop251/goja"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ThrowTypeError(vm *goja.Runtime, msg string) {
|
|
||||||
panic(vm.NewTypeError(msg))
|
|
||||||
}
|
|
||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
|
||||||
@@ -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,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';
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,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
|
||||||
}
|
}
|
||||||
@@ -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"}),
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
//go:build !windows
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package util
|
||||||
|
|
||||||
|
var invalidPathChars = []string{`/`, `:`}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
var invalidPathChars = []string{`\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
output: dist/
|
||||||
@@ -25,6 +25,7 @@ class Options {
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class OptsExtraHttp {
|
class OptsExtraHttp {
|
||||||
int connections = 0;
|
int connections = 0;
|
||||||
|
bool autoTorrent = false;
|
||||||
|
|
||||||
OptsExtraHttp();
|
OptsExtraHttp();
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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!',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -57,5 +57,8 @@ const faIR = {
|
|||||||
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
||||||
'newVersionUpdate': 'بروزرسانی',
|
'newVersionUpdate': 'بروزرسانی',
|
||||||
'newVersionLater': 'بعداً',
|
'newVersionLater': 'بعداً',
|
||||||
|
'thanks': 'تشکر',
|
||||||
|
'thanksDesc':
|
||||||
|
'از تمامی افرادی که در ساخت و توسعهی جامعهی Gopeed مشارکت کردهاند، سپاسگزاری میکنیم!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -59,5 +59,7 @@ const jaJP = {
|
|||||||
'newVersionTitle': '新しいバージョン @version を発見する',
|
'newVersionTitle': '新しいバージョン @version を発見する',
|
||||||
'newVersionUpdate': 'アップデート',
|
'newVersionUpdate': 'アップデート',
|
||||||
'newVersionLater': '後で',
|
'newVersionLater': '後で',
|
||||||
|
'thanks': '感謝',
|
||||||
|
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -73,5 +73,8 @@ const ruRU = {
|
|||||||
'history': 'История',
|
'history': 'История',
|
||||||
'clearHistory': 'Очистить историю',
|
'clearHistory': 'Очистить историю',
|
||||||
'noHistoryFound': 'История не найдена',
|
'noHistoryFound': 'История не найдена',
|
||||||
|
'thanks': 'Благодарности',
|
||||||
|
'thanksDesc':
|
||||||
|
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -77,7 +77,10 @@ const zhCN = {
|
|||||||
'serviceText': '运行中',
|
'serviceText': '运行中',
|
||||||
'network': '网络',
|
'network': '网络',
|
||||||
'proxy': '代理',
|
'proxy': '代理',
|
||||||
|
'server': '服务器',
|
||||||
'username': '用户名',
|
'username': '用户名',
|
||||||
'password': '密码',
|
'password': '密码',
|
||||||
|
'thanks': '鸣谢',
|
||||||
|
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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 社群建設添磚加瓦的貢獻者們!'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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();
|
||||||
|
// }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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
@@ -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"
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in new issue
Block a user