Compare commits

...
34 Commits
Author SHA1 Message Date
Levi 06c7fe7a22 fix: avoid hanging the main thread on repeat start (#584) 2024-06-20 18:59:50 +08:00
Levi 8b3b5d4394 fix: javascript engine xhr polyfill (#581) 2024-06-15 11:40:12 +08:00
liwei 2ee73ab619 fix: it_it translate 2024-06-11 14:37:39 +08:00
albanobattistella 15413dbc95 feat: Create it_it.dart (#576) 2024-06-11 14:23:24 +08:00
Levi c7c43fac80 fix: snap dir pick depend (#558) 2024-06-11 12:35:57 +08:00
247b 4283bdfa5a feat :tamil translation is added (#563) 2024-05-27 09:24:45 +08:00
liwei 328ba8592c build: update go-ieproxy depend 2024-05-23 08:57:02 +08:00
er2de2 a191a4140d feat: Update pl_pl.dart (#554) 2024-05-23 08:53:48 +08:00
Levi 3521837964 release: v1.5.7 2024-05-17 19:59:31 +08:00
Levi b30cd78f86 ci: use special flutter version on snap (#543) 2024-05-17 19:57:40 +08:00
Levi 0583e4aaa6 feat: remember delete task option (#542) 2024-05-17 18:43:21 +08:00
Levi a89c55b5c4 feat: improve Android download directory choose (#541) 2024-05-17 18:38:35 +08:00
Levi 68c40d7f3f feat: add batch create and server info APIs (#540) 2024-05-17 15:19:10 +08:00
Levi 42a0b89d95 fix: bt request extra type convert (#535) 2024-05-16 13:02:40 +08:00
liwei 1860f534cd fix: pl_pl i18n 2024-05-16 12:58:05 +08:00
er2de2 5c2d2ad7b4 feat: add Polish translation (#534) 2024-05-16 12:55:33 +08:00
Levi db6352ada8 feat: add show item on system tray (#533) 2024-05-15 20:41:57 +08:00
Levi d1a5ed8b3d feat: support launch at startup (#532) 2024-05-14 19:23:29 +08:00
Levi be9583be0f ci: fix ios framework build (#525) 2024-05-08 18:06:58 +08:00
Levi d2c70b077b feat: check API port is in use (#524) 2024-05-07 19:19:57 +08:00
Levi 53b3eea4fc feat: add extension store links for edge and firefox (#520) 2024-05-06 18:36:53 +08:00
Levi 54347f41b6 feat: support double click to open task file (#516) 2024-05-02 13:22:57 +08:00
Levi cea454b015 feat: support system proxy (#515) 2024-05-01 18:53:12 +08:00
Levi 070db64ca9 fix: http request cookie incremented indefinitely on retries (#514) 2024-05-01 17:45:59 +08:00
Levi 6a8956b04d docs: fix linux download link 2024-05-01 04:16:56 +00:00
Deepansh Singh Arya 057504bb81 feat: updated iOS icons (#512) 2024-05-01 10:48:35 +08:00
madoka773 470b488824 ci: build gopeed with libayatana-appindicator (#508) 2024-04-26 15:41:47 +08:00
madoka773 c8c9df1ae8 ci: build linux with ubuntu latest (#501) 2024-04-23 08:57:23 +08:00
invictus1032 33d787c74e docs: fix Linux download links (#465)
Fix filename discrepancies for .deb and .AppImage
2024-04-12 14:45:09 +08:00
liwei 67338a5e83 docs: add browser extension link 2024-04-03 17:52:37 +08:00
Levi 723697192d fix: command panic before resolve resource (#451) 2024-04-03 16:56:20 +08:00
Zack 4294d07d6a fix: Fix the issue of installing apk files(Android) (#448) (#449) 2024-04-03 09:23:05 +08:00
WINZORT 3936fd786f feat: Update tr_tr.dart (#445) 2024-04-01 09:07:44 +08:00
ilkeiii 45197e9a3b fix: docker entrypoint_sh (#434) 2024-03-30 12:35:00 +08:00
84 changed files with 1374 additions and 403 deletions

No files matched your search

+45 -17
View File
@@ -136,7 +136,7 @@ jobs:
overwrite: true
build-macos-arm64-lib:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: ubuntu-latest
runs-on: macos-latest
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
@@ -147,16 +147,14 @@ jobs:
env:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
go install src.techknowlogick.com/xgo@latest
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
mv libgopeed-*.dylib libgopeed.dylib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
- uses: actions/upload-artifact@v3
with:
name: macos-arm64-lib
path: libgopeed.dylib
build-macos:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-latest
runs-on: macos-13
needs: [ get-release, build-macos-arm64-lib ]
steps:
- uses: actions/checkout@v3
@@ -171,7 +169,7 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg
run: |
python3 -m pip install setuptools
python3 -m pip install setuptools --break-system-packages
npm install -g appdmg
- uses: actions/download-artifact@v3
with:
@@ -215,7 +213,7 @@ jobs:
overwrite: true
build-linux:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
runs-on: ubuntu-20.04
runs-on: ubuntu-latest
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
@@ -228,9 +226,7 @@ jobs:
- run: |
sudo apt update -y
# flutter build dependencies
sudo apt install -y ninja-build libgtk-3-dev libappindicator3-dev
# rpm build dependencies
sudo apt install -y rpm patchelf
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
# appimage build dependencies
sudo apt install -y libfuse2 locate
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
@@ -262,9 +258,6 @@ jobs:
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Setup LXD
uses: canonical/[email protected]
with:
@@ -302,20 +295,55 @@ jobs:
apps:
gopeed:
command: gopeed
command: bin/gopeed
extensions: [gnome]
plugs:
- network
- home
slots:
- dbus-gopeed
parts:
flutter-git:
source: https://github.com/flutter/flutter.git
source-tag: $FLUTTER_VERSION
source-depth: 1
plugin: nil
override-build: |
mkdir -p \$CRAFT_PART_INSTALL/usr/bin
mkdir -p \$CRAFT_PART_INSTALL/usr/libexec
cp -r \$CRAFT_PART_SRC \$CRAFT_PART_INSTALL/usr/libexec/flutter
ln -s \$CRAFT_PART_INSTALL/usr/libexec/flutter/bin/flutter \$CRAFT_PART_INSTALL/usr/bin/flutter
ln -s \$SNAPCRAFT_PART_INSTALL/usr/libexec/flutter/bin/dart \$SNAPCRAFT_PART_INSTALL/usr/bin/dart
\$CRAFT_PART_INSTALL/usr/bin/flutter doctor
build-packages:
- clang
- cmake
- curl
- libgtk-3-dev
- ninja-build
- unzip
- xz-utils
- zip
override-prime: ''
zenity:
plugin: nil
stage-packages:
- zenity
gopeed:
after: [flutter-git,zenity]
source: .
plugin: flutter
plugin: nil
stage-packages:
- libgtk-3-dev
- libappindicator3-dev
flutter-target: lib/main.dart
override-build: |
# work around pub get stack overflow # https://github.com/dart-lang/sdk/issues/51068#issuecomment-1396588253
set +e
dart pub get
set -eux
flutter build linux --release -v
mkdir -p \$CRAFT_PART_INSTALL/bin/
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
EOF
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
@@ -383,7 +411,7 @@ jobs:
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
gomobile bind -tags nosqlite,netgo -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
mkdir Payload
+1
View File
@@ -21,6 +21,7 @@ The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n
You only need to add the corresponding language file in this directory.
Please refer to `en_us.dart` for translation.
words prefixed with `@` are not meant to be translated.
## flutter development
+6 -2
View File
@@ -53,11 +53,11 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Link</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
</tr>
<tr>
<td>Android</td>
@@ -92,6 +92,10 @@ use `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
## Browser Extension
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## Donate
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
+2 -2
View File
@@ -53,11 +53,11 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Link</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Link</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td>
</tr>
<tr>
<td>Android</td>
+2 -2
View File
@@ -53,11 +53,11 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">Liên kết</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">Liên kết</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td>
</tr>
<tr>
<td>Android</td>
+6 -2
View File
@@ -53,11 +53,11 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">前往</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
</tr>
<tr>
<td>Android</td>
@@ -92,6 +92,10 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
## 浏览器扩展
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## 赞助
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
+2 -2
View File
@@ -53,11 +53,11 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
</tr>
<tr>
<td><code>DEB</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux.deb">前往</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td>
</tr>
<tr>
<td><code>AppImage</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">前往</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td>
</tr>
<tr>
<td>Android</td>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 309 KiB

+3
View File
@@ -65,6 +65,9 @@ var (
func printProgress(task *download.Task, title string) {
var rate float64
if task.Meta.Res == nil {
task = emptyTask
}
if task.Meta.Res.Size <= 0 {
rate = 0
} else {
+1 -1
View File
@@ -1,6 +1,6 @@
#!/bin/bash
chown -R ${PUID}:${PGID} /app/Download
chown -R ${PUID}:${PGID} /app
umask ${UMASK}
+6 -5
View File
@@ -12,9 +12,11 @@ require (
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/mattn/go-ieproxy v0.0.12
github.com/pkg/errors v0.9.1
github.com/rs/zerolog v1.31.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
go.etcd.io/bbolt v1.3.8
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
golang.org/x/sync v0.6.0
@@ -96,15 +98,14 @@ require (
github.com/stretchr/testify v1.8.4 // indirect
github.com/tidwall/btree v1.7.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad // indirect
go.opentelemetry.io/otel v1.22.0 // indirect
go.opentelemetry.io/otel/metric v1.22.0 // indirect
go.opentelemetry.io/otel/trace v1.22.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.17.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
+22 -9
View File
@@ -5,6 +5,7 @@ crawshaw.io/sqlite v0.3.2/go.mod h1:igAO5JulrQ1DbdZdtVq48mnZUBAPOeFzer7VhDWNtW4=
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
@@ -23,9 +24,11 @@ github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 h1:byYvvbfSo3+9efR4IeReh77gVs4PnNDR3AMOE9NJ7a0=
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0/go.mod h1:q37NoqncT41qKc048STsifIt69LfUJ8SrWWcz/yam5k=
github.com/alecthomas/assert/v2 v2.0.0-alpha3 h1:pcHeMvQ3OMstAWgaeaXIAL8uzB9xMm2zlxt+/4ml8lk=
github.com/alecthomas/assert/v2 v2.0.0-alpha3/go.mod h1:+zD0lmDXTeQj7TgDgCt0ePWxb0hMC1G+PGTsTCv1B9o=
github.com/alecthomas/atomic v0.1.0-alpha2 h1:dqwXmax66gXvHhsOS4pGPZKqYOlTkapELkLb3MNdlH8=
github.com/alecthomas/atomic v0.1.0-alpha2/go.mod h1:zD6QGEyw49HIq19caJDc2NMXAy8rNi9ROrxtMXATfyI=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
@@ -86,6 +89,7 @@ github.com/anacrolix/upnp v0.1.3/go.mod h1:Qyhbqo69gwNWvEk1xNTXsS5j7hMHef9hdr984
github.com/anacrolix/utp v0.2.0 h1:65Cdmr6q9WSw2KsM+rtJFu7rqDzLl2bdysf4KlNPcFI=
github.com/anacrolix/utp v0.2.0/go.mod h1:HGk4GYQw1O/3T1+yhqT/F6EcBd+AAwlo9dYErNy7mj8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -139,6 +143,7 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
@@ -146,9 +151,11 @@ github.com/felixge/httpsnoop v1.0.1/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw
github.com/frankban/quicktest v1.9.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y=
github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY=
github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4=
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
github.com/glycerine/go-unsnap-stream v0.0.0-20190901134440-81cf024a9e0a/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
@@ -160,6 +167,7 @@ github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmS
github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4=
github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8=
github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo=
github.com/go-git/go-git/v5 v5.8.1 h1:Zo79E4p7TRk0xoRgMq0RShiTHGKcKI4+DI6BfJc/Q+A=
github.com/go-git/go-git/v5 v5.8.1/go.mod h1:FHFuoD6yGz5OSKEBK+aWN9Oah0q54Jxl0abmj6GnqAo=
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
@@ -212,6 +220,7 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
@@ -233,6 +242,7 @@ github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/huandu/xstrings v1.0.0/go.mod h1:4qWG/gcEcfX4z/mBDHJ++3ReCw9ibxbsNJbcucJdbSo=
github.com/huandu/xstrings v1.2.0/go.mod h1:DvyZB1rfVYsBIigL8HwpZgxHwXozlTgGqn63UyNX5k4=
@@ -270,6 +280,8 @@ github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A=
github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-ieproxy v0.0.12 h1:OZkUFJC3ESNZPQ+6LzC3VJIFSnreeFLQyqvBWtvfL2M=
github.com/mattn/go-ieproxy v0.0.12/go.mod h1:Vn+N61199DAnVeTgaF8eoB9PvLO8P3OBnG95ENh7B7c=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
@@ -454,8 +466,8 @@ golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI=
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220428152302-39d4317da171/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a h1:Q8/wZp0KX97QFTc2ywcOE0YRjZPVIx+MXInMzdvQqcA=
@@ -500,8 +512,8 @@ golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ=
golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac=
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -556,8 +568,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y=
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -570,7 +582,8 @@ golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo=
golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE=
golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw=
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -585,8 +598,8 @@ golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk=
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+3 -3
View File
@@ -1,14 +1,14 @@
package controller
import (
"net/url"
"github.com/GopeedLab/gopeed/pkg/base"
"os"
"path/filepath"
)
type Controller struct {
GetConfig func(v any) bool
ProxyUrl *url.URL
GetConfig func(v any) bool
ProxyConfig *base.DownloaderProxyConfig
FileController
//ContextDialer() (proxy.Dialer, error)
}
+4 -8
View File
@@ -10,7 +10,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"net/http"
"path/filepath"
"strings"
"sync"
@@ -69,9 +68,7 @@ func (f *Fetcher) initClient() (err error) {
cfg.Bep20 = fmt.Sprintf("-GP%s-", parseBep20())
cfg.ExtendedHandshakeClientVersion = fmt.Sprintf("Gopeed %s", base.Version)
cfg.ListenPort = f.config.ListenPort
if f.ctl.ProxyUrl != nil {
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
}
cfg.HTTPProxy = f.ctl.ProxyConfig.ToHandler()
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
@@ -86,10 +83,6 @@ func (f *Fetcher) initClient() (err error) {
}
func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
if err := f.addTorrent(req); err != nil {
return err
}
@@ -243,6 +236,9 @@ func (f *Fetcher) updateRes() {
}
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return
}
if err = f.initClient(); err != nil {
return
}
+10 -5
View File
@@ -8,8 +8,6 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"net/url"
"os"
"reflect"
"testing"
@@ -60,7 +58,14 @@ func TestFetcher_ResolveWithProxy(t *testing.T) {
proxyListener := test.StartSocks5Server(usr, pwd)
defer proxyListener.Close()
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
doResolve(t, buildConfigFetcher(&base.DownloaderProxyConfig{
Enable: true,
System: false,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
Usr: usr,
Pwd: pwd,
}))
}
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
@@ -100,7 +105,7 @@ func buildFetcher() fetcher.Fetcher {
return fetcher
}
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
func buildConfigFetcher(proxyConfig *base.DownloaderProxyConfig) fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build()
newController := controller.NewController()
mockCfg := config{
@@ -114,7 +119,7 @@ func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
}
return true
}
newController.ProxyUrl = proxyUrl
newController.ProxyConfig = proxyConfig
fetcher.Setup(newController)
return fetcher
}
+9 -9
View File
@@ -280,10 +280,6 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
chunk := f.chunks[index]
chunk.retryTimes = 0
httpReq, err := f.buildRequest(ctx, f.meta.Req)
if err != nil {
return err
}
var (
client = f.buildClient()
buf = make([]byte, 8192)
@@ -314,8 +310,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
}
var (
resp *http.Response
httpReq *http.Request
resp *http.Response
)
httpReq, err = f.buildRequest(ctx, f.meta.Req)
if err != nil {
return
}
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
@@ -450,12 +451,11 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
}
func (f *Fetcher) buildClient() *http.Client {
transport := &http.Transport{}
transport := &http.Transport{
Proxy: f.ctl.ProxyConfig.ToHandler(),
}
// Cookie handle
jar, _ := cookiejar.New(nil)
if f.ctl.ProxyUrl != nil {
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
}
return &http.Client{
Transport: transport,
Jar: jar,
+5 -2
View File
@@ -8,7 +8,6 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"net"
"testing"
"time"
@@ -338,7 +337,11 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
fetcher := downloadReady(httpListener, 4, t)
ctl := controller.NewController()
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
ctl.ProxyConfig = &base.DownloaderProxyConfig{
Enable: true,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
}
fetcher.Setup(ctl)
err := fetcher.Start()
if err != nil {
+5
View File
@@ -24,6 +24,11 @@ const (
Dir = "./"
BuildFile = Dir + BuildName
ExternalDownloadUrl = "https://raw.githubusercontent.com/GopeedLab/gopeed/v1.5.6/_docs/img/banner.png"
ExternalDownloadName = "banner.png"
ExternalDownloadSize = 26416
//ExternalDownloadMd5 = "c67c6e3cae79a95342485676571e8a5c"
DownloadName = "download.data"
DownloadRename = "download (1).data"
DownloadFile = Dir + DownloadName
+88
View File
@@ -3,7 +3,10 @@ package base
import (
"fmt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/mattn/go-ieproxy"
"golang.org/x/exp/slices"
"net/http"
"net/url"
"time"
)
@@ -128,3 +131,88 @@ func ParseOptsExtra[E any](opts *Options) error {
opts.Extra = &t
return nil
}
// DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"`
}
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 {
cfg.MaxRunning = 5
}
if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{}
}
return cfg
}
type DownloaderProxyConfig struct {
Enable bool `json:"enable"`
// System is the flag that use system proxy
System bool `json:"system"`
Scheme string `json:"scheme"`
Host string `json:"host"`
Usr string `json:"usr"`
Pwd string `json:"pwd"`
}
func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, error) {
if cfg == nil || cfg.Enable == false {
return nil
}
if cfg.System {
ieproxy.ReloadConf()
return ieproxy.GetProxyFunc()
}
if cfg.Scheme == "" || cfg.Host == "" {
return nil
}
return http.ProxyURL(util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd))
}
// ToUrl returns the proxy url, just for git clone
func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
if cfg == nil || cfg.Enable == false {
return nil
}
if cfg.System {
ieproxy.ReloadConf()
static := ieproxy.GetConf().Static
if static.Active && len(static.Protocols) > 0 {
// If only one protocol, use it
if len(static.Protocols) == 1 {
for _, v := range static.Protocols {
return parseUrlSafe(v)
}
}
// Check https
if v, ok := static.Protocols["https"]; ok {
return parseUrlSafe(v)
}
// Check http
if v, ok := static.Protocols["http"]; ok {
return parseUrlSafe(v)
}
}
return nil
}
if cfg.Scheme == "" || cfg.Host == "" {
return nil
}
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
}
func parseUrlSafe(rawUrl string) *url.URL {
u, err := url.Parse(rawUrl)
if err != nil {
return nil
}
return u
}
+17 -5
View File
@@ -110,7 +110,7 @@ func (d *Downloader) Setup() error {
return err
}
// load config from storage
var cfg DownloaderStoreConfig
var cfg base.DownloaderStoreConfig
exist, err := d.storage.Get(bucketConfig, "config", &cfg)
if err != nil {
return err
@@ -118,7 +118,7 @@ func (d *Downloader) Setup() error {
if exist {
d.cfg.DownloaderStoreConfig = &cfg
} else {
d.cfg.DownloaderStoreConfig = &DownloaderStoreConfig{
d.cfg.DownloaderStoreConfig = &base.DownloaderStoreConfig{
FirstLoad: true,
}
}
@@ -225,7 +225,7 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
ctl.GetConfig = func(v any) bool {
return d.getProtocolConfig(fetcher.Name(), v)
}
ctl.ProxyUrl = d.cfg.ProxyUrl()
ctl.ProxyConfig = d.cfg.Proxy
fetcher.Setup(ctl)
}
@@ -301,6 +301,18 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
return d.doCreate(fetcher, opts)
}
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) {
taskIds := make([]string, 0)
for _, req := range reqs {
taskId, err := d.CreateDirect(req, opts)
if err != nil {
return nil, err
}
taskIds = append(taskIds, taskId)
}
return taskIds, nil
}
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
d.fetcherMapLock.RLock()
fetcher, ok := d.fetcherCache[rrId]
@@ -603,11 +615,11 @@ func (d *Downloader) GetTasksByStatues(statues []base.Status) []*Task {
return tasks
}
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
func (d *Downloader) GetConfig() (*base.DownloaderStoreConfig, error) {
return d.cfg.DownloaderStoreConfig, nil
}
func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
func (d *Downloader) PutConfig(v *base.DownloaderStoreConfig) error {
d.cfg.DownloaderStoreConfig = v
return d.storage.Put(bucketConfig, "config", v)
}
+98 -37
View File
@@ -4,6 +4,7 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"os"
"sync"
"testing"
"time"
@@ -83,39 +84,95 @@ func TestDownloader_Create(t *testing.T) {
}
}
func TestDownloader_CreateWithProxy(t *testing.T) {
// No proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
return nil
})
// Disable proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
proxyCfg.Enable = false
return proxyCfg
})
// Invalid proxy scheme
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
proxyCfg.Scheme = ""
return proxyCfg
})
// Invalid proxy host
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
proxyCfg.Host = ""
return proxyCfg
})
// Use proxy without auth
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
return proxyCfg
})
// Use proxy with auth
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
return proxyCfg
func TestDownloader_CreateDirectBatch(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
reqs := make([]*base.Request, 0)
for i := 0; i < 6; i++ {
req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}
reqs = append(reqs, req)
}
_, err := downloader.CreateDirectBatch(reqs, &base.Options{
Path: test.Dir,
Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
})
if err != nil {
t.Fatal(err)
}
tasks := downloader.GetTasks()
if len(tasks) != len(reqs) {
t.Errorf("CreateDirectBatch() got = %v, want %v", len(tasks), len(reqs))
}
}
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
httpListener := test.StartTestFileServer()
defer httpListener.Close()
func TestDownloader_CreateWithProxy(t *testing.T) {
// No proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return nil
}, nil)
// Disable proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Enable = false
return proxyCfg
}, nil)
// Enable system proxy but not set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.System = true
return proxyCfg
}, nil)
// Enable proxy but error proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
os.Setenv("HTTP_PROXY", "http://127.0.0.1:1234")
os.Setenv("HTTPS_PROXY", "http://127.0.0.1:1234")
proxyCfg.System = true
return proxyCfg
}, func(err error) {
if err == nil {
t.Fatal("doTestDownloaderCreateWithProxy() got = nil, want error")
}
})
// Enable system proxy and set proxy environment variable
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
os.Setenv("HTTP_PROXY", proxyCfg.ToUrl().String())
os.Setenv("HTTPS_PROXY", proxyCfg.ToUrl().String())
proxyCfg.System = true
return proxyCfg
}, nil)
// Invalid proxy scheme
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Scheme = ""
return proxyCfg
}, nil)
// Invalid proxy host
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
proxyCfg.Host = ""
return proxyCfg
}, nil)
// Use proxy without auth
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg
}, nil)
// Use proxy with auth
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig {
return proxyCfg
}, nil)
}
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *base.DownloaderProxyConfig) *base.DownloaderProxyConfig, errHandler func(err error)) {
usr, pwd := "", ""
if auth {
usr, pwd = "admin", "123"
@@ -128,7 +185,7 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
t.Fatal(err)
}
defer downloader.Clear()
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&base.DownloaderProxyConfig{
Enable: true,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
@@ -137,20 +194,24 @@ func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig f
})
req := &base.Request{
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
URL: test.ExternalDownloadUrl,
}
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
if errHandler == nil {
t.Fatal(err)
}
errHandler(err)
return
}
want := &base.Resource{
Size: test.BuildSize,
Size: test.ExternalDownloadSize,
Range: true,
Files: []*base.FileInfo{
{
Name: test.BuildName,
Name: test.ExternalDownloadName,
Path: "",
Size: test.BuildSize,
Size: test.ExternalDownloadSize,
},
},
}
@@ -285,7 +346,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
}
storeCfg := &DownloaderStoreConfig{
storeCfg := &base.DownloaderStoreConfig{
DownloadDir: "./downloads",
ProtocolConfig: map[string]any{
"http": map[string]any{
+7 -3
View File
@@ -3,6 +3,7 @@ package engine
import (
_ "embed"
"errors"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
@@ -11,7 +12,6 @@ import (
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
gojaurl "github.com/dop251/goja_nodejs/url"
"net/url"
"time"
)
@@ -110,7 +110,7 @@ func (e *Engine) Close() {
}
type Config struct {
ProxyURL *url.URL
ProxyConfig *base.DownloaderProxyConfig
}
func NewEngine(cfg *Config) *Engine {
@@ -135,7 +135,7 @@ func NewEngine(cfg *Config) *Engine {
if err := formdata.Enable(runtime); err != nil {
return
}
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
if err := xhr.Enable(runtime, cfg.ProxyConfig.ToHandler()); err != nil {
return
}
if _, err := runtime.RunString(polyfillScript); err != nil {
@@ -145,6 +145,10 @@ func NewEngine(cfg *Config) *Engine {
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
return
}
// polyfill window
if err := runtime.Set("window", runtime.GlobalObject()); err != nil {
return
}
// polyfill window.location
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
return
+51 -5
View File
@@ -7,10 +7,10 @@ import (
"errors"
"fmt"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja"
"io"
"net"
@@ -80,6 +80,30 @@ async function testFormData(file){
return await resp.json();
}
function testHeader(){
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('GET', host+'/header');
xhr.setRequestHeader('X-Gopeed-Test', 'test1');
xhr.setRequestHeader('x-gopeed-test', 'test2');
xhr.setRequestHeader('x-Gopeed-test', 'test3');
xhr.onload = function(){
const testHeader1 = xhr.getResponseHeader("X-Gopeed-Test");
const testHeader2 = xhr.getResponseHeader("x-gopeed-test");
const testHeader3 = xhr.getResponseHeader("x-Gopeed-test");
const expect = 'test1, test2, test3';
const all = xhr.getAllResponseHeaders();
if(testHeader1 === expect && testHeader2 === expect && testHeader3 === expect
&& all.includes('X-Gopeed-Test: '+expect)){
resolve();
}else{
reject();
}
};
xhr.send();
});
}
function testProgress(){
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
@@ -190,14 +214,19 @@ function testTimeout(){
}
}()
_, err = callTestFun(engine, "testHeader")
if err != nil {
t.Fatal("header test failed", err)
}
_, err = callTestFun(engine, "testProgress")
if err != nil {
t.Fatal("progress test failed")
t.Fatal("progress test failed", err)
}
_, err = callTestFun(engine, "testAbort")
if err != nil {
t.Fatal("abort test failed")
t.Fatal("abort test failed", err)
}
_, err = callTestFun(engine, "testTimeout")
@@ -217,8 +246,16 @@ func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
proxyListener := test.StartSocks5Server(usr, pwd)
defer proxyListener.Close()
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
engine := NewEngine(&Config{
ProxyConfig: &base.DownloaderProxyConfig{
Enable: true,
System: false,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
Usr: usr,
Pwd: pwd,
},
})
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
t.Fatal(err)
@@ -320,6 +357,15 @@ func startServer() net.Listener {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
mux.HandleFunc("/header", func(w http.ResponseWriter, r *http.Request) {
for k, v := range r.Header {
if strings.HasPrefix(k, "X-Gopeed") {
w.Header().Set(k, strings.Join(v, ", "))
}
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})
mux.HandleFunc("/text", func(w http.ResponseWriter, r *http.Request) {
buf, _ := io.ReadAll(r.Body)
w.WriteHeader(http.StatusOK)
+18 -20
View File
@@ -2,6 +2,7 @@ package xhr
import (
"bytes"
"errors"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/util"
@@ -11,6 +12,7 @@ import (
"net"
"net/http"
"net/url"
"strings"
"time"
)
@@ -117,10 +119,10 @@ type XMLHttpRequestUpload struct {
type XMLHttpRequest struct {
method string
url string
requestHeaders map[string]string
responseHeaders map[string]string
requestHeaders http.Header
responseHeaders http.Header
aborted bool
proxyUrl *url.URL
proxyHandler func(r *http.Request) (*url.URL, error)
Upload *XMLHttpRequestUpload `json:"upload"`
Timeout int `json:"timeout"`
@@ -136,13 +138,13 @@ type XMLHttpRequest struct {
func (xhr *XMLHttpRequest) Open(method, url string) {
xhr.method = method
xhr.url = url
xhr.requestHeaders = make(map[string]string)
xhr.responseHeaders = make(map[string]string)
xhr.requestHeaders = make(http.Header)
xhr.responseHeaders = make(http.Header)
xhr.doReadystatechange(1)
}
func (xhr *XMLHttpRequest) SetRequestHeader(key, value string) {
xhr.requestHeaders[key] = value
xhr.requestHeaders.Add(key, value)
}
func (xhr *XMLHttpRequest) Send(data goja.Value) {
@@ -193,18 +195,15 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
xhr.callOnerror()
return
}
req.Header = xhr.requestHeaders
if contentType != "" {
req.Header.Set("Content-Type", contentType)
}
if contentLength > 0 {
req.ContentLength = contentLength
}
for k, v := range xhr.requestHeaders {
req.Header.Set(k, v)
}
transport := &http.Transport{}
if xhr.proxyUrl != nil {
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
transport := &http.Transport{
Proxy: xhr.proxyHandler,
}
client := &http.Client{
Transport: transport,
@@ -213,7 +212,8 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
resp, err := client.Do(req)
if err != nil {
// handle timeout error
if err, ok := err.(net.Error); ok && err.Timeout() {
var ne net.Error
if errors.As(err, &ne) && ne.Timeout() {
if xhr.Timeout > 0 {
xhr.Upload.callOntimeout()
xhr.callOntimeout()
@@ -229,9 +229,7 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
if !xhr.aborted {
xhr.Upload.callOnload()
}
for k, v := range resp.Header {
xhr.responseHeaders[k] = v[0]
}
xhr.responseHeaders = resp.Header
xhr.Status = resp.StatusCode
xhr.StatusText = resp.Status
xhr.doReadystatechange(2)
@@ -260,7 +258,7 @@ func (xhr *XMLHttpRequest) Abort() {
}
func (xhr *XMLHttpRequest) GetResponseHeader(key string) string {
return xhr.responseHeaders[key]
return strings.Join(xhr.responseHeaders.Values(key), ", ")
}
func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
@@ -268,7 +266,7 @@ func (xhr *XMLHttpRequest) GetAllResponseHeaders() string {
for k, v := range xhr.responseHeaders {
buf.WriteString(k)
buf.WriteString(": ")
buf.WriteString(v)
buf.WriteString(strings.Join(v, ", "))
buf.WriteString("\r\n")
}
return buf.String()
@@ -313,7 +311,7 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
return data.String()
}
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
func Enable(runtime *goja.Runtime, proxyHandler func(r *http.Request) (*url.URL, error)) error {
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
if len(call.Arguments) < 1 {
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
@@ -327,7 +325,7 @@ func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
})
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
instance := &XMLHttpRequest{
proxyUrl: proxyUrl,
proxyHandler: proxyHandler,
Upload: &XMLHttpRequestUpload{
EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)),
+2 -2
View File
@@ -199,7 +199,7 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
return nil, err
}
proxyOptions := transport.ProxyOptions{}
proxyUrl := d.cfg.ProxyUrl()
proxyUrl := d.cfg.DownloaderStoreConfig.Proxy.ToUrl()
if proxyUrl != nil {
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
proxyOptions.Username = proxyUrl.User.Username()
@@ -341,7 +341,7 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
req.Labels = make(map[string]string)
}
engine := engine.NewEngine(&engine.Config{
ProxyURL: d.cfg.ProxyUrl(),
ProxyConfig: d.cfg.Proxy,
})
defer engine.Close()
err = engine.Runtime.Set("gopeed", gopeed)
+1 -1
View File
@@ -238,7 +238,7 @@ func TestDownloader_Extension_OnError(t *testing.T) {
select {
case err = <-errCh:
break
case <-time.After(time.Second * 10):
case <-time.After(time.Second * 30):
err = errors.New("timeout")
}
+1 -42
View File
@@ -7,9 +7,7 @@ import (
"github.com/GopeedLab/gopeed/internal/protocol/http"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"net/url"
"sync"
"time"
)
@@ -87,7 +85,7 @@ type DownloaderConfig struct {
ProductionMode bool
*DownloaderStoreConfig
*base.DownloaderStoreConfig
}
func (cfg *DownloaderConfig) Init() *DownloaderConfig {
@@ -108,42 +106,3 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
}
return cfg
}
// DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"`
}
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 {
cfg.MaxRunning = 5
}
if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{}
}
return cfg
}
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
if cfg.Proxy == nil {
return nil
}
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
return nil
}
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
}
type DownloaderProxyConfig struct {
Enable bool `json:"enable"`
Scheme string `json:"scheme"`
Host string `json:"host"`
Usr string `json:"usr"`
Pwd string `json:"pwd"`
}
+3 -1
View File
@@ -69,7 +69,9 @@ func (n *MemStorage) Setup(buckets []string) error {
func (n *MemStorage) Put(bucket string, key string, v any) error {
n.lock.Lock()
defer n.lock.Unlock()
memData[bucket][key] = v
if bucketData, ok := memData[bucket]; ok {
bucketData[key] = v
}
return nil
}
+30 -2
View File
@@ -8,8 +8,20 @@ import (
"io"
"net/http"
"net/url"
"runtime"
)
func Info(w http.ResponseWriter, r *http.Request) {
info := map[string]any{
"version": base.Version,
"runtime": runtime.Version(),
"os": runtime.GOOS,
"arch": runtime.GOARCH,
}
info["inDocker"] = base.InDocker == "true"
WriteJson(w, model.NewOkResult(info))
}
func Resolve(w http.ResponseWriter, r *http.Request) {
var req base.Request
if ReadJson(r, w, &req) {
@@ -45,6 +57,22 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
}
}
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskBatch
if ReadJson(r, w, &req) {
if len(req.Reqs) == 0 {
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
return
}
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt)
if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(taskIds))
}
}
func PauseTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
taskId := vars["id"]
@@ -142,7 +170,7 @@ func GetConfig(w http.ResponseWriter, r *http.Request) {
}
func PutConfig(w http.ResponseWriter, r *http.Request) {
var cfg download.DownloaderStoreConfig
var cfg base.DownloaderStoreConfig
if ReadJson(r, w, &cfg) {
if err := Downloader.PutConfig(&cfg); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
@@ -309,7 +337,7 @@ func writeError(w http.ResponseWriter, msg string) {
w.Write([]byte(msg))
}
func getServerConfig() *download.DownloaderStoreConfig {
func getServerConfig() *base.DownloaderStoreConfig {
cfg, _ := Downloader.GetConfig()
return cfg
}
-7
View File
@@ -1,7 +0,0 @@
package model
type Command struct {
Protocol string `json:"protocol"`
Action string `json:"action"`
Params any `json:"params"`
}
+5
View File
@@ -7,3 +7,8 @@ type CreateTask struct {
Req *base.Request `json:"req"`
Opt *base.Options `json:"opt"`
}
type CreateTaskBatch struct {
Reqs []*base.Request `json:"reqs"`
Opt *base.Options `json:"opt"`
}
+20 -7
View File
@@ -19,31 +19,42 @@ import (
)
var (
srv *http.Server
srv *http.Server
runningPort int
Downloader *download.Downloader
)
func Start(startCfg *model.StartConfig) (int, error) {
srv, listener, err := BuildServer(startCfg)
func Start(startCfg *model.StartConfig) (port int, err error) {
// avoid repeat start
if srv != nil {
return runningPort, nil
}
var listener net.Listener
srv, listener, err = BuildServer(startCfg)
if err != nil {
return 0, err
return
}
go func() {
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
if err := srv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
panic(err)
}
}()
port := 0
if addr, ok := listener.Addr().(*net.TCPAddr); ok {
port = addr.Port
runningPort = port
}
return port, nil
return
}
func Stop() {
defer func() {
srv = nil
}()
if srv != nil {
if err := srv.Shutdown(context.TODO()); err != nil {
Downloader.Logger.Warn().Err(err).Msg("shutdown server failed")
@@ -88,8 +99,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}
var r = mux.NewRouter()
r.Methods(http.MethodGet).Path("/api/v1/info").HandlerFunc(Info)
r.Methods(http.MethodPost).Path("/api/v1/resolve").HandlerFunc(Resolve)
r.Methods(http.MethodPost).Path("/api/v1/tasks").HandlerFunc(CreateTask)
r.Methods(http.MethodPost).Path("/api/v1/tasks/batch").HandlerFunc(CreateTaskBatch)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/pause").HandlerFunc(PauseTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/{id}/continue").HandlerFunc(ContinueTask)
r.Methods(http.MethodPut).Path("/api/v1/tasks/pause").HandlerFunc(PauseAllTask)
+29 -2
View File
@@ -63,6 +63,18 @@ var (
}
)
func TestInfo(t *testing.T) {
matchKeys := []string{"version", "runtime", "os", "arch", "inDocker"}
doTest(func() {
resp := httpRequestCheckOk[map[string]any](http.MethodGet, "/api/v1/info", nil)
for _, key := range matchKeys {
if _, ok := resp[key]; !ok {
t.Errorf("Info() missing key = %v", key)
}
}
})
}
func TestResolve(t *testing.T) {
doTest(func() {
resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
@@ -125,6 +137,21 @@ func TestCreateDirectTask(t *testing.T) {
})
}
func TestCreateDirectTaskBatch(t *testing.T) {
doTest(func() {
reqs := make([]*base.Request, 0)
for i := 0; i < 6; i++ {
reqs = append(reqs, createReq.Req)
}
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{
Reqs: reqs,
})
if len(taskIds) != len(reqs) {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
}
})
}
func TestCreateDirectTaskWithResoled(t *testing.T) {
doTest(func() {
var wg sync.WaitGroup
@@ -324,7 +351,7 @@ func TestGetTasks(t *testing.T) {
func TestGetAndPutConfig(t *testing.T) {
doTest(func() {
cfg := httpRequestCheckOk[*download.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
cfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
cfg.DownloadDir = "./download"
cfg.Extra = map[string]any{
"serverConfig": &Config{
@@ -335,7 +362,7 @@ func TestGetAndPutConfig(t *testing.T) {
}
httpRequestCheckOk[any](http.MethodPut, "/api/v1/config", cfg)
newCfg := httpRequestCheckOk[*download.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
newCfg := httpRequestCheckOk[*base.DownloaderStoreConfig](http.MethodGet, "/api/v1/config", nil)
if !test.JsonEqual(cfg, newCfg) {
t.Errorf("GetAndPutConfig() got = %v, want %v", test.ToJson(newCfg), test.ToJson(cfg))
}
+12
View File
@@ -2,6 +2,7 @@ package util
import (
"encoding/base64"
"net/http"
"net/url"
"regexp"
"strings"
@@ -51,3 +52,14 @@ func BuildProxyUrl(scheme, host, usr, pwd string) *url.URL {
Host: host,
}
}
// ProxyUrlToHandler gets the proxy handler from the proxy url.
func ProxyUrlToHandler(proxyUrl *url.URL) func(*http.Request) (*url.URL, error) {
if proxyUrl == nil {
return nil
}
if proxyUrl.Scheme == "system" {
return http.ProxyFromEnvironment
}
return http.ProxyURL(proxyUrl)
}
@@ -2,6 +2,7 @@
package="com.gopeed">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application
android:name="${applicationName}"
@@ -13,6 +13,7 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 029EF293EAA92EB3D18BBF19 /* libresolv.tbd */; };
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D081C25B294826C0006EB10B /* libc++.tbd */; };
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; };
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
@@ -34,6 +35,7 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@@ -63,6 +65,7 @@
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
B44F54F47E581A39546303C9 /* libresolv.tbd in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -130,6 +133,7 @@
D0E623A729482D030001185E /* Libgopeed.xcframework */,
D081C25B294826C0006EB10B /* libc++.tbd */,
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */,
);
name = Frameworks;
sourceTree = "<group>";
Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 714 B

After

Width:  |  Height:  |  Size: 890 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 7.7 KiB

@@ -17,6 +17,7 @@ class DownloaderConfig {
factory DownloaderConfig.fromJson(Map<String, dynamic> json) =>
_$DownloaderConfigFromJson(json);
Map<String, dynamic> toJson() => _$DownloaderConfigToJson(this);
}
@@ -29,6 +30,7 @@ class ProtocolConfig {
factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
}
@@ -47,6 +49,7 @@ class HttpConfig {
factory HttpConfig.fromJson(Map<String, dynamic> json) =>
_$HttpConfigFromJson(json);
Map<String, dynamic> toJson() => _$HttpConfigToJson(this);
}
@@ -59,25 +62,34 @@ class BtConfig {
factory BtConfig.fromJson(Map<String, dynamic> json) =>
_$BtConfigFromJson(json);
Map<String, dynamic> toJson() => _$BtConfigToJson(this);
}
@JsonSerializable(explicitToJson: true)
class ExtraConfig {
String themeMode = '';
String locale = '';
String themeMode;
String locale;
bool lastDeleteTaskKeep;
ExtraConfigBt bt = ExtraConfigBt();
ExtraConfig();
ExtraConfig({
this.themeMode = '',
this.locale = '',
this.lastDeleteTaskKeep = false,
});
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
}
@JsonSerializable()
class ProxyConfig {
bool enable;
bool system;
String scheme;
String host;
String usr;
@@ -85,6 +97,7 @@ class ProxyConfig {
ProxyConfig({
this.enable = false,
this.system = false,
this.scheme = '',
this.host = '',
this.usr = '',
@@ -110,5 +123,6 @@ class ExtraConfigBt {
factory ExtraConfigBt.fromJson(Map<String, dynamic> json) =>
_$ExtraConfigBtFromJson(json);
Map<String, dynamic> toJson() => _$ExtraConfigBtToJson(this);
}
@@ -60,20 +60,23 @@ Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
'trackers': instance.trackers,
};
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig()
..themeMode = json['themeMode'] as String
..locale = json['locale'] as String
..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
themeMode: json['themeMode'] as String? ?? '',
locale: json['locale'] as String? ?? '',
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
<String, dynamic>{
'themeMode': instance.themeMode,
'locale': instance.locale,
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
'bt': instance.bt.toJson(),
};
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
enable: json['enable'] as bool? ?? false,
system: json['system'] as bool? ?? false,
scheme: json['scheme'] as String? ?? '',
host: json['host'] as String? ?? '',
usr: json['usr'] as String? ?? '',
@@ -83,6 +86,7 @@ ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig(
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
<String, dynamic>{
'enable': instance.enable,
'system': instance.system,
'scheme': instance.scheme,
'host': instance.host,
'usr': instance.usr,
@@ -1,13 +1,13 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:gopeed/database/entity.dart';
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:path_provider/path_provider.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:uri_to_file/uri_to_file.dart';
@@ -18,7 +18,9 @@ import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../core/common/start_config.dart';
import '../../../../database/database.dart';
import '../../../../database/entity.dart';
import '../../../../i18n/message.dart';
import '../../../../main.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
@@ -73,6 +75,7 @@ final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
class AppController extends GetxController with WindowListener, TrayListener {
static StartConfig? _defaultStartConfig;
final autoStartup = false.obs;
final startConfig = StartConfig().obs;
final runningPort = 0.obs;
final downloaderConfig = DownloaderConfig().obs;
@@ -98,6 +101,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
_initTrackerUpdate().onError((error, stackTrace) =>
logger.w("initTrackerUpdate error", error, stackTrace));
_initLaunchAtStartup().onError((error, stackTrace) =>
logger.w("initLaunchAtStartup error", error, stackTrace));
}
@override
@@ -114,6 +120,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
}
// According to the system_manager document, make sure to call setState once on the onWindowFocus event.
@override
void onWindowFocus() {
refresh();
}
@override
void onTrayIconMouseDown() {
windowManager.show();
@@ -185,6 +197,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
await trayManager.setIcon('assets/tray_icon/icon.png');
}
final menu = Menu(items: [
MenuItem(
label: "show".tr,
onClick: (menuItem) async => {
await windowManager.show(),
},
),
MenuItem.separator(),
MenuItem(
label: "create".tr,
onClick: (menuItem) async => {
@@ -192,7 +211,6 @@ class AppController extends GetxController with WindowListener, TrayListener {
await Get.rootDelegate.offAndToNamed(Routes.CREATE),
},
),
MenuItem.separator(),
MenuItem(
label: "startAll".tr,
onClick: (menuItem) async => {continueAllTasks()},
@@ -274,7 +292,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
Future<void> _toCreate(Uri uri) async {
final path = (uri.scheme == "magnet" || uri.scheme == "http" || uri.scheme == "https")
final path = (uri.scheme == "magnet" ||
uri.scheme == "http" ||
uri.scheme == "https")
? uri.toString()
: (await toFile(uri.toString())).path;
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
@@ -305,22 +325,24 @@ class AppController extends GetxController with WindowListener, TrayListener {
return _defaultStartConfig!;
}
Future<void> loadStartConfig() async {
Future<StartConfig> loadStartConfig() async {
final defaultCfg = await _initDefaultStartConfig();
final saveCfg = Database.instance.getStartConfig();
startConfig.value.network = saveCfg?.network ?? defaultCfg.network;
startConfig.value.address = saveCfg?.address ?? defaultCfg.address;
startConfig.value.apiToken = saveCfg?.apiToken ?? defaultCfg.apiToken;
return startConfig.value;
}
Future<void> loadDownloaderConfig() async {
Future<DownloaderConfig> loadDownloaderConfig() async {
try {
downloaderConfig.value = await getConfig();
} catch (e) {
logger.w("load downloader config fail", e);
logger.w("load downloader config fail", e, StackTrace.current);
downloaderConfig.value = DownloaderConfig();
}
await _initDownloaderConfig();
return downloaderConfig.value;
}
Future<void> trackerUpdate() async {
@@ -413,12 +435,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isAndroid()) {
final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
if (downloadDir != null) {
config.downloadDir = '$downloadDir/Gopeed';
} else {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
}
config.downloadDir = (await getExternalStorageDirectory())?.path ??
(await getApplicationDocumentsDirectory()).path;
} else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else {
@@ -427,6 +445,17 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
}
Future<void> _initLaunchAtStartup() async {
if (!Util.isWindows() && !Util.isLinux()) {
return;
}
launchAtStartup.setup(
appName: packageInfo.appName,
appPath: Platform.resolvedExecutable,
args: ['--${Args.flagHidden}']);
autoStartup.value = await launchAtStartup.isEnabled();
}
Future<void> saveConfig() async {
Database.instance.saveStartConfig(StartConfigEntity(
network: startConfig.value.network,
@@ -17,6 +17,7 @@ import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
import '../../../views/compact_checkbox.dart';
import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart';
@@ -300,7 +301,7 @@ class CreateView extends GetView<CreateController> {
controller: _btTrackerController,
maxLines: 5,
decoration: InputDecoration(
labelText: 'Trakers',
labelText: 'Trackers',
hintText: 'addTrackerHit'.tr,
)),
],
@@ -321,23 +322,13 @@ class CreateView extends GetView<CreateController> {
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
controller.directDownload.value =
!controller.directDownload.value;
},
child: Row(children: [
Obx(() => Checkbox(
value:
controller.directDownload.value,
onChanged: (bool? value) {
controller.directDownload.value =
value ?? false;
},
)),
Text('directDownload'.tr),
]),
),
CompactCheckbox(
label: 'directDownload'.tr,
value: controller.directDownload.value,
onChanged: (bool? value) {
controller.directDownload.value =
value ?? false;
}),
TextButton(
onPressed: () {
controller.showAdvanced.value =
@@ -415,7 +406,7 @@ class CreateView extends GetView<CreateController> {
}
}
/*
/*
Check if is direct download, there has two ways to direct download
1. Direct download option is checked
2. Muli line urls
@@ -1,15 +1,19 @@
import 'dart:async';
import 'dart:io';
import 'package:badges/badges.dart' as badges;
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../i18n/message.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/message.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
@@ -33,13 +37,22 @@ class SettingView extends GetView<SettingController> {
final startCfg = appController.startConfig;
Timer? timer;
debounceSave({bool needRestart = false}) {
var completer = Completer<void>();
Future<bool> debounceSave(
{Future<String> Function()? check, bool needRestart = false}) {
var completer = Completer<bool>();
timer?.cancel();
timer = Timer(const Duration(milliseconds: 1000), () {
timer = Timer(const Duration(milliseconds: 1000), () async {
if (check != null) {
final checkResult = await check();
if (checkResult.isNotEmpty) {
showErrorMessage(checkResult);
completer.complete(false);
return;
}
}
appController
.saveConfig()
.then(completer.complete)
.then((_) => completer.complete(true))
.onError(completer.completeError);
if (needRestart) {
showMessage('tip'.tr, 'effectAfterRestart'.tr);
@@ -66,6 +79,7 @@ class SettingView extends GetView<SettingController> {
return DirectorySelector(
controller: downloadDirController,
showLabel: false,
showAndoirdToggle: true,
);
});
final buildMaxRunning = _buildConfigItem(
@@ -97,13 +111,57 @@ class SettingView extends GetView<SettingController> {
buildBrowserExtension() {
return ListTile(
title: Text('browserExtension'.tr),
subtitle: const OpenInNew(
text: "Chrome",
url:
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
));
subtitle: const Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
OpenInNew(
text: "Chrome",
url:
"https://chromewebstore.google.com/detail/gopeed/mijpgljlfcapndmchhjffkpckknofcnd",
),
SizedBox(width: 10),
OpenInNew(
text: "Edge",
url:
"https://microsoftedge.microsoft.com/addons/detail/dkajnckekendchdleoaenoophcobooce",
),
SizedBox(width: 10),
OpenInNew(
text: "Firefox",
url:
"https://addons.mozilla.org/zh-CN/firefox/addon/gopeed-extension",
),
],
).paddingOnly(top: 5));
}
// Currently auto startup only support Windows and Linux
final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
? () => null
: _buildConfigItem('launchAtStartup'.tr, () {
return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value: appController.autoStartup.value,
onChanged: (bool value) async {
try {
if (value) {
await launchAtStartup.enable();
} else {
await launchAtStartup.disable();
}
appController.autoStartup.value = value;
} catch (e) {
showErrorMessage(e);
logger.e('launchAtStartup fail', e);
}
},
),
);
});
// http config items start
final httpConfig = downloaderCfg.value.protocolConfig.http;
final buildHttpUa =
@@ -408,29 +466,53 @@ class SettingView extends GetView<SettingController> {
}
// advanced config proxy items start
final proxy = downloaderCfg.value.proxy;
final buildProxy = _buildConfigItem(
'proxy',
() => downloaderCfg.value.proxy.enable
? '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}'
: 'notSet'.tr,
() {
switch (proxy.proxyMode) {
case ProxyModeEnum.noProxy:
return 'noProxy'.tr;
case ProxyModeEnum.systemProxy:
return 'systemProxy'.tr;
case ProxyModeEnum.customProxy:
return '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}';
}
},
(Key key) {
final proxy = downloaderCfg.value.proxy;
final mode = SizedBox(
width: 150,
child: DropdownButtonFormField<ProxyModeEnum>(
value: proxy.proxyMode,
onChanged: (value) async {
if (value != null && value != proxy.proxyMode) {
proxy.proxyMode = value;
downloaderCfg.update((val) {
val!.proxy = proxy;
});
final switcher = Switch(
value: proxy.enable,
onChanged: (bool value) async {
if (value != proxy.enable) {
downloaderCfg.update((val) {
val!.proxy.enable = value;
});
await debounceSave();
}
},
await debounceSave();
}
},
items: [
DropdownMenuItem<ProxyModeEnum>(
value: ProxyModeEnum.noProxy,
child: Text('noProxy'.tr),
),
DropdownMenuItem<ProxyModeEnum>(
value: ProxyModeEnum.systemProxy,
child: Text('systemProxy'.tr),
),
DropdownMenuItem<ProxyModeEnum>(
value: ProxyModeEnum.customProxy,
child: Text('customProxy'.tr),
),
],
),
);
final scheme = SizedBox(
width: 100,
width: 150,
child: DropdownButtonFormField<String>(
value: proxy.scheme,
onChanged: (value) async {
@@ -478,7 +560,7 @@ class SettingView extends GetView<SettingController> {
ipController.addListener(updateAddress);
portController.addListener(updateAddress);
final server = [
final server = Row(children: [
Flexible(
child: TextFormField(
controller: ipController,
@@ -503,12 +585,12 @@ class SettingView extends GetView<SettingController> {
],
),
),
];
]);
final usrController = TextEditingController(text: proxy.usr);
final pwdController = TextEditingController(text: proxy.pwd);
final auth = [
final auth = Row(children: [
Flexible(
child: TextFormField(
controller: usrController,
@@ -528,20 +610,21 @@ class SettingView extends GetView<SettingController> {
),
),
),
];
]);
List<Widget> customView() {
if (proxy.proxyMode != ProxyModeEnum.customProxy) {
return [];
}
return [scheme, server, auth];
}
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([
switcher,
scheme,
Row(
children: server,
),
Row(
children: auth,
),
mode,
...customView(),
]),
),
);
@@ -596,11 +679,41 @@ class SettingView extends GetView<SettingController> {
final ipController = TextEditingController(text: ip);
final portController = TextEditingController(text: port);
updateAddress() async {
if (ipController.text.isEmpty || portController.text.isEmpty) {
return;
}
final newAddress = '${ipController.text}:${portController.text}';
if (newAddress != startCfg.value.address) {
startCfg.value.address = newAddress;
await debounceSave(needRestart: true);
final saved = await debounceSave(
check: () async {
// Check if address already in use
final configIp = ipController.text;
final configPort = int.parse(portController.text);
if (configPort == 0) {
return '';
}
try {
final socket = await Socket.connect(configIp, configPort,
timeout: const Duration(seconds: 3));
socket.close();
return 'portInUse'
.trParams({'port': configPort.toString()});
} catch (e) {
return '';
}
},
needRestart: true);
// If save failed, restore the old address
if (!saved) {
final oldAddress =
(await appController.loadStartConfig()).address;
startCfg.update((val) async {
val!.address = oldAddress;
});
}
}
}
@@ -700,7 +813,8 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([
buildDownloadDir(),
buildMaxRunning(),
buildBrowserExtension()
buildBrowserExtension(),
buildAutoStartup(),
]),
)),
const Text('HTTP'),
@@ -857,3 +971,37 @@ class SettingView extends GetView<SettingController> {
}
}
}
enum ProxyModeEnum {
noProxy,
systemProxy,
customProxy,
}
extension ProxyMode on ProxyConfig {
ProxyModeEnum get proxyMode {
if (!enable) {
return ProxyModeEnum.noProxy;
}
if (system) {
return ProxyModeEnum.systemProxy;
}
return ProxyModeEnum.customProxy;
}
set proxyMode(ProxyModeEnum value) {
switch (value) {
case ProxyModeEnum.noProxy:
enable = false;
break;
case ProxyModeEnum.systemProxy:
enable = true;
system = true;
break;
case ProxyModeEnum.customProxy:
enable = true;
system = false;
break;
}
}
}
@@ -89,7 +89,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
return [
IconButton(
icon:
const Icon(Icons.play_circle),
const Icon(Icons.open_in_new),
onPressed: () {
launchUrl(Uri.parse(accessUrl),
webOnlyWindowName:
@@ -106,12 +106,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
IconButton(
icon: const Icon(Icons.play_circle),
onPressed: () async {
print(filePath);
try {
await OpenFilex.open(filePath);
} catch (e) {
print(e);
}
await OpenFilex.open(filePath);
}),
IconButton(
icon: const Icon(Icons.share),
@@ -1,5 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path/path.dart' as path;
import 'package:styled_widget/styled_widget.dart';
@@ -11,6 +12,7 @@ import '../../util/file_icon.dart';
import '../../util/icons.dart';
import '../../util/message.dart';
import '../../util/util.dart';
import '../modules/app/controllers/app_controller.dart';
import '../routes/app_pages.dart';
class BuildTaskListView extends GetView {
@@ -57,8 +59,12 @@ class BuildTaskListView extends GetView {
return task.status == Status.running;
}
bool isFolderTask() {
return task.meta.res?.name.isNotEmpty ?? false;
}
String buildExplorerUrl(Task task) {
if (task.meta.res!.name.isEmpty) {
if (!isFolderTask()) {
return path.join(Util.safeDir(task.meta.opts.path),
Util.safeDir(task.meta.res!.files[0].path), fileName(task.meta));
} else {
@@ -68,7 +74,7 @@ class BuildTaskListView extends GetView {
}
Future<void> showDeleteDialog(String id) {
final keep = true.obs;
final appController = Get.find<AppController>();
final context = Get.context!;
@@ -78,11 +84,14 @@ class BuildTaskListView extends GetView {
builder: (_) => AlertDialog(
title: Text('deleteTask'.tr),
content: Obx(() => CheckboxListTile(
value: keep.value,
value: appController
.downloaderConfig.value.extra.lastDeleteTaskKeep,
title: Text('deleteTaskTip'.tr,
style: context.textTheme.bodyLarge),
onChanged: (v) {
keep.value = v!;
appController.downloaderConfig.update((val) {
val!.extra.lastDeleteTaskKeep = v!;
});
})),
actions: [
TextButton(
@@ -96,7 +105,10 @@ class BuildTaskListView extends GetView {
),
onPressed: () async {
try {
await deleteTask(id, !keep.value);
final force = !appController
.downloaderConfig.value.extra.lastDeleteTaskKeep;
await appController.saveConfig();
await deleteTask(id, force);
Get.back();
} catch (e) {
showErrorMessage(e);
@@ -107,18 +119,22 @@ class BuildTaskListView extends GetView {
));
}
toTaskFilesView() {
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
}
List<Widget> buildActions() {
final list = <Widget>[];
if (isDone()) {
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () {
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
toTaskFilesView();
},
));
} else {
@@ -176,14 +192,23 @@ class BuildTaskListView extends GetView {
elevation: 4.0,
child: InkWell(
onTap: () {},
onDoubleTap: () {
if (isDone()) {
if (isFolderTask()) {
toTaskFilesView();
} else {
OpenFilex.open(buildExplorerUrl(task));
}
}
},
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
ListTile(
title: Text(fileName(task.meta)),
leading: (task.meta.res?.name.isNotEmpty ?? false
leading: isFolderTask()
? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))]))),
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])),
Row(
children: [
Expanded(
@@ -0,0 +1,66 @@
import 'package:flutter/material.dart';
class CompactCheckbox extends StatefulWidget {
final String label;
final bool value;
final ValueChanged<bool?>? onChanged;
final double? scale;
final TextStyle? textStyle;
const CompactCheckbox({
super.key,
required this.label,
required this.value,
this.onChanged,
this.scale,
this.textStyle,
});
@override
State<CompactCheckbox> createState() => _CompactCheckboxState();
}
class _CompactCheckboxState extends State<CompactCheckbox> {
late bool _value;
@override
void initState() {
super.initState();
_value = widget.value;
}
valueChanged(bool? value) {
setState(() {
_value = value!;
});
widget.onChanged?.call(_value);
}
@override
Widget build(BuildContext context) {
final checkbox = Checkbox(
value: _value,
onChanged: valueChanged,
);
return TextButton(
onPressed: () {
valueChanged(!_value);
},
child: Row(
children: [
widget.scale == null
? checkbox
: Transform.scale(
scale: widget.scale,
child: checkbox,
),
Text(
widget.label,
style: widget.textStyle,
),
],
),
);
}
}
@@ -1,16 +1,26 @@
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.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:toggle_switch/toggle_switch.dart';
import '../../util/mac_secure_util.dart';
import '../../util/message.dart';
import '../../util/util.dart';
class DirectorySelector extends StatefulWidget {
final TextEditingController controller;
final bool showLabel;
final bool showAndoirdToggle;
const DirectorySelector(
{Key? key, required this.controller, this.showLabel = true})
{Key? key,
required this.controller,
this.showLabel = true,
this.showAndoirdToggle = false})
: super(key: key);
@override
@@ -20,6 +30,68 @@ class DirectorySelector extends StatefulWidget {
class _DirectorySelectorState extends State<DirectorySelector> {
@override
Widget build(BuildContext context) {
Widget? buildSelectWidget() {
if (Util.isDesktop()) {
return IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
}
});
}
// After Android 11, access to external storage is increasingly restricted, so it no longer supports selecting the download directory. However, if you do not download in external storage, all downloaded files will be deleted after the application is uninstalled.
// Fortunately, so far, most Android devices can still access the system download directory.
// For the sake of user experience, it is decided to only support selecting the application's internal directory and the system download directory. Also, a test for file write permission is performed when selecting the system download directory. If it cannot be written, selection is not allowed.
if (Util.isAndroid() && widget.showAndoirdToggle) {
final isSwitchToDownloadDir =
widget.controller.text.endsWith('/Gopeed');
return ToggleSwitch(
initialLabelIndex: isSwitchToDownloadDir ? 1 : 0,
totalSwitches: 2,
icons: const [Icons.home, Icons.download],
customWidths: const [50, 50],
onToggle: (index) async {
if (index == 0) {
widget.controller.text =
(await getExternalStorageDirectory())?.path ??
(await getApplicationDocumentsDirectory()).path;
} else {
widget.controller.text =
'${(await DownloadsPath.downloadsDirectory())!.path}/Gopeed';
}
},
cancelToggle: (index) async {
if (index == 0) {
return false;
}
// Check write permission
final downloadDir =
(await DownloadsPath.downloadsDirectory())?.path;
if (downloadDir == null) {
return true;
}
final fileRandomeName =
"test_${DateTime.now().millisecondsSinceEpoch}.tmp";
final testFile = File('$downloadDir/Gopeed/$fileRandomeName');
try {
await testFile.create(recursive: true);
await testFile.writeAsString('test');
await testFile.delete();
return false;
} catch (e) {
showErrorMessage(e);
return true;
}
},
).marginOnly(left: 10);
}
return null;
}
return Row(
children: [
Expanded(
@@ -35,17 +107,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
},
)),
!Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
? IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) {
widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
}
})
: null
buildSelectWidget()
].where((e) => e != null).map((e) => e!).toList(),
);
}
-9
View File
@@ -54,15 +54,6 @@ class Database {
_startConfig, (json) => StartConfigEntity.fromJson(json));
}
void saveLastRunningConfig(StartConfigEntity entity) {
save<StartConfigEntity>(_lastRunningConfig, entity);
}
StartConfigEntity? getLastRunningConfig() {
return get<StartConfigEntity>(
_lastRunningConfig, (json) => StartConfigEntity.fromJson(json));
}
/// Patch non-null fields with the original value
void saveWindowState(WindowStateEntity entity) {
final state = getWindowState();
+6
View File
@@ -58,7 +58,9 @@ const enUS = {
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'Port [@port] is in use, please change the port',
'effectAfterRestart': 'Effect after restart',
'show': 'Show',
'startAll': 'Start All',
'pauseAll': 'Pause All',
'deleteTask': 'Delete Task',
@@ -82,6 +84,9 @@ const enUS = {
'serviceText': 'Running',
'network': 'Network',
'proxy': 'Proxy',
'noProxy': 'No Proxy',
'systemProxy': 'System Proxy',
'customProxy': 'Custom Proxy',
'server': 'Server',
'username': 'Username',
'password': 'Password',
@@ -89,5 +94,6 @@ const enUS = {
'thanksDesc':
'Thanks to all the contributors who have helped build and develop the Gopeed community!',
'browserExtension': 'Browser Extension',
'launchAtStartup': 'Launch at Startup',
},
};
+100
View File
@@ -0,0 +1,100 @@
const itIT = {
'it_IT': {
'label': 'Italiano',
'error': 'Errore',
'tip': 'Suggerimento',
'confirm': 'Conferma',
'cancel': 'Annullla',
'on': 'Acceso',
'off': 'Spento',
'selectAll': 'Seleziona tutto',
'task': 'Attività',
'downloading': 'scaricamento in corso',
'downloaded': 'scaricato',
'setting': 'Impostazioni',
'donate': 'Donazione',
'exit': 'Uscita',
'create': 'Crea attività',
'directDownload': 'Scaricamento diretto',
'advancedOptions': 'Opzioni avanzate',
'downloadLink': 'Link di scaricamento',
'downloadLinkValid': 'Inserisci il collegamento per lo scaricamento',
'downloadLinkHit':
'Inserisci il collegamento per il download, HTTP/HTTPS/MAGNET supported@append',
'downloadLinkHitDesktop':
', oppure trascina qui direttamente il file torrent',
'download': 'Download',
'noFileSelected': 'Seleziona almeno un file per continuare.',
'noStoragePermission': "È richiesta l'autorizzazione di archiviazione",
'selectFile': 'Selezione file',
'rename': 'Rinomina',
'basic': 'Basico',
'advanced': 'Avanzato',
'general': 'Generale',
'downloadDir': 'Directory di scaricamento',
'downloadDirValid': 'Seleziona la directory di scaricamento',
'connections': 'Connessioni',
'useServerCtime': "Utilizza l'ora del server per la creazione dei file",
'maxRunning': 'Numero massimo di attività in esecuzione',
'items': '@count elementi',
'subscribeTracker': 'Iscriviti al tracker',
'subscribeFail':
'Iscrizione non riuscita, controlla la rete o riprova più tardir',
'update': 'Aggiornaento',
'updateDaily': 'Aggiornamento quotidiano',
'lastUpdate': 'Ultimo aggiornamento: @time',
'addTracker': 'Aggiungi Tracker',
'addTrackerHit': "Inserisci l'URL del server del tracker, uno per riga",
'ui': 'UI',
'theme': 'Tema',
'themeSystem': 'Sistema',
'themeLight': 'Chiaro',
'themeDark': 'Scuro',
'locale': 'Lingua',
'about': 'Informazioni',
'homepage': 'Homepage',
'version': 'Versione',
'protocol': 'Protocollo',
'port': 'Porta',
'apiToken': 'Token API',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'La porta [@port] è in uso, per favore cambia la porta',
'effectAfterRestart': 'Effetto dopo il riavvio',
'show': 'Mostra',
'startAll': 'Avvia tutti',
'pauseAll': 'Mtti in pausa tutti',
'deleteTask': 'Elimina attività',
'deleteTaskTip': 'Conserva i file scaricati',
'delete': 'Elimina',
'newVersionTitle': 'Scopri la nuova versione @version',
'newVersionUpdate': 'Aggiorna ora',
'newVersionLater': 'Dopo',
'extensions': 'Estensioni',
'extensionInstallUrl': 'Installa URL',
'extensionInstallSuccess': 'Installato con successo',
'extensionUpdateSuccess': 'Aggiornato con successo',
'extensionDelete': 'Elimina estensione',
'extensionAlreadyLatest': "È già l'ultima versione",
'extensionFind': 'Trova estensioni',
'extensionDevelop': 'Sviluppare estensioni',
'history': 'Cronologia',
'clearHistory': 'Pulisci cronologia',
'noHistoryFound': 'Nessuna cronologia trovata',
'serviceTitle': 'Scarica il servizio',
'serviceText': 'In esecuzione',
'network': 'Rete',
'proxy': 'Proxy',
'noProxy': 'Nessun Proxy',
'systemProxy': 'Sistema Proxy',
'customProxy': 'Proxy personalizzato',
'server': 'Server',
'username': 'Nome utente',
'password': 'Password',
'thanks': 'Ringraziamenti',
'thanksDesc':
'Grazie a tutti i contributori che hanno contribuito a costruire e sviluppare la comunità Gopeed!',
'browserExtension': 'Estensione del browser',
'launchAtStartup': "Lancia all'avvio",
},
};
+98
View File
@@ -0,0 +1,98 @@
const plPL = {
'pl_PL': {
'label': 'Polski',
'error': 'Błąd',
'tip': 'Tip',
'confirm': 'Potwierdź',
'cancel': 'Anuluj',
'on': 'Włącz',
'off': 'Wyłącz',
'selectAll': 'Zaznacz wszystko',
'task': 'Zadania',
'downloading': 'Pobierane',
'downloaded': 'Pobrane',
'setting': 'Ustawienia',
'donate': 'Wesprzyj',
'exit': 'Wyjście',
'create': 'Utwórz zadanie',
'directDownload': 'Pobieranie bezpośrednie',
'advancedOptions': 'Zaawansowane',
'downloadLink': 'Pobierz z linku',
'downloadLinkValid': 'Otwórz link pobierania',
'downloadLinkHit':
'Otwórz link pobierania, HTTP/HTTPS/MAGNET wspierane@append',
'downloadLinkHitDesktop': ', lub przenieś bezpośrednio plik torrent',
'download': 'Pobierz',
'noFileSelected': 'Wybierz co najmniej jeden plik aby kontynuować.',
'noStoragePermission': 'Przyznaj uprawnienia do zapisu',
'selectFile': 'Wybierz plik',
'rename': 'Zmień nazwę',
'basic': 'Zwykły',
'advanced': 'Zaawansowane',
'general': 'Ogólne',
'downloadDir': 'Folder Pobierania',
'downloadDirValid': 'Wskaż folder pobierania',
'connections': 'Połączenia',
'useServerCtime': 'Użyj czasu serwera do utworzenia pliku',
'maxRunning': 'Maksymalna liczba zadań',
'items': '@count items',
'subscribeTracker': 'Subskrybuj tracker',
'subscribeFail': 'Błąd subskrypcji, sprawdź łączność z internetem',
'update': 'Aktualizacja',
'updateDaily': 'Aktualizuj codziennie',
'lastUpdate': 'Ostatnia aktualizacja: @time',
'addTracker': 'Dodaj tracker',
'addTrackerHit': 'Wpisz adres trackera w jednej linii',
'ui': 'Wygląd',
'theme': 'Schemat kolorów',
'themeSystem': 'Systemowy',
'themeLight': 'Jasny',
'themeDark': 'Ciemny',
'locale': 'Język',
'about': 'Info',
'homepage': 'Strona WWW',
'version': 'Wersja',
'protocol': 'Protokół',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'Port [@port] w użyciu, proszę zmień port',
'effectAfterRestart': 'Zmiany po restarcie',
'show': 'Pokaż',
'startAll': 'Zacznij wszystkie',
'pauseAll': 'Zatrzymaj wszystkie',
'deleteTask': 'Usuń zadanie',
'deleteTaskTip': 'Zachowaj pobrane pliki',
'delete': 'Usuń',
'newVersionTitle': 'Sprawdź aktualizację @version',
'newVersionUpdate': 'Aktualizuj teraz',
'newVersionLater': 'Później',
'extensions': 'Wtyczki',
'extensionInstallUrl': 'Instaluj z url',
'extensionInstallSuccess': 'Instalacja zakończona',
'extensionUpdateSuccess': 'Aktualizacja zakończona',
'extensionDelete': 'Usuń wtyczkę',
'extensionAlreadyLatest': 'Wtyczka w aktualnej wersji',
'extensionFind': 'Wyszukaj wtyczkę',
'extensionDevelop': 'Stwórz wtyczkę',
'history': 'Historia',
'clearHistory': 'Wyczyść historię',
'noHistoryFound': 'Brak historii',
'serviceTitle': 'Usługa pobierania',
'serviceText': 'Trwa',
'network': 'Sieć',
'proxy': 'Proxy',
'noProxy': 'Bez Proxy',
'systemProxy': 'Systemowe Proxy',
'customProxy': 'Własne Proxy',
'server': 'Serwer',
'username': 'Użytkownik',
'password': 'Hasło',
'thanks': 'Dzięki',
'thanksDesc':
'Dziękuję wszystkim za pomoc przy powstawaniu aplikacji Gopeed!',
'browserExtension': 'Wtyczka dla przeglądarki',
'launchAtStartup': 'Uruchom na starcie',
},
};
+99
View File
@@ -0,0 +1,99 @@
const taTA = {
'ta_TA': {
'label': 'தமிழ்',
'error': 'பிழை',
'tip': 'குறிப்பு',
'confirm': 'சரி',
'cancel': 'ரத்துசெய்',
'on': 'On',
'off': 'Off',
'selectAll': 'அனைத்தையும் தேர்வுசெய்',
'task': 'பணிகள்',
'downloading': 'பதிவிறக்குகிறது',
'downloaded': 'பதிவிறக்கம் செய்யப்பட்டது',
'setting': 'அமைப்புகள் ',
'donate': 'நன்கொடை',
'exit': 'வெளியேறு',
'create': 'பணியை உருவாக்கு',
'directDownload': 'நேரடி பதிவிறக்கம்',
'advancedOptions': 'மேம்பட்ட விருப்பங்கள்',
'downloadLink': 'தரவிறக்க இணைப்பு',
'downloadLinkValid': 'பதிவிறக்க இணைப்பை உள்ளிடவும்',
'downloadLinkHit':
'பதிவிறக்க இணைப்பை உள்ளிடவும், HTTP/HTTPS/MAGNET ஆகியவற்றை ஏற்கும்@append',
'downloadLinkHitDesktop': ', அல்லது torrent கோப்பை நேரடியாக இங்கே இழுக்கவும்',
'download': 'பதிவிறக்கு',
'noFileSelected': 'குறைந்தபட்சம் ஒரு கோப்பைத் தேர்ந்தெடுக்கவும்.',
'noStoragePermission': 'சேமிப்பக அனுமதி தேவை',
'selectFile': 'கோப்பைத் தேர்ந்தெடுக்கவும்',
'rename': 'பெயறைமாற்று',
'basic': 'அடிப்படை',
'advanced': 'மேம்பட்ட',
'general': 'பொதுவான',
'downloadDir': 'பதிவிரக்குமிடம்',
'downloadDirValid': 'பதிவிரக்குமிடத்தை தேர்வு செயுங்கள்',
'connections': 'தொடர்புகள்',
'useServerCtime': 'Use server time for file creation',
'maxRunning': 'அதிகபட்ச இயங்கும் பணிகள்',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
'Subscribe failed, please check network or try again later',
'update': 'புதுப்பிக்கவும்',
'updateDaily': 'தினமும் புதுப்பிக்கவும்',
'lastUpdate': 'கடைசியாக புதுப்பிக்கப்பட்டது: @time',
'addTracker': 'Trackerஐ சேர்',
'addTrackerHit': 'Please enter the tracker server url, one per line',
'ui': 'UI',
'theme': 'தீம்',
'themeSystem': 'இயல்புநிலை',
'themeLight': 'வெண்மையானநிலை',
'themeDark': 'இருண்டநிலை',
'locale': 'மொழி',
'about': 'எங்களை பற்றி',
'homepage': 'முகப்புப்பக்கம்',
'version': 'பதிப்பு',
'protocol': 'நெறிமுறை',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'Port [@port] is in use, please change the port',
'effectAfterRestart': 'Effect after restart',
'show': 'காட்டு',
'startAll': 'அனைத்தையும் தொடங்கு',
'pauseAll': 'அனைத்தையும் நிறுத்திவை',
'deleteTask': 'பணியை நீக்கு',
'deleteTaskTip': 'பதிவிறக்கம் செய்யப்பட்ட கோப்புகளை வைத்திரு',
'delete': 'நீக்கு',
'newVersionTitle': 'புதிய பதிப்பைக் கண்டறியவும் @version',
'newVersionUpdate': 'இப்பொழுது புதுப்பி',
'newVersionLater': 'பின்னர்',
'extensions': 'நீட்டிப்புகள்',
'extensionInstallUrl': 'நிறுவப்படும் URL',
'extensionInstallSuccess': 'வெற்றிகரமாக நிறுவப்பட்டது',
'extensionUpdateSuccess': 'வெற்றிகரமாக புதுப்பிக்கப்பட்டது',
'extensionDelete': 'நீட்டிப்பை நீக்கு',
'extensionAlreadyLatest': 'இது ஏற்கனவே சமீபத்திய பதிப்பாகும்',
'extensionFind': 'நீட்டிப்புகளைக் கண்டறியவும்',
'extensionDevelop': 'நீட்டிப்புகளை உருவாக்கவும்',
'history': 'வரலாறு',
'clearHistory': 'உள்ளீடுகளை நீக்கு',
'noHistoryFound': 'உள்ளீடுகள் இல்லை',
'serviceTitle': 'பதிவிறக்க சேவை',
'serviceText': 'இயக்கத்தில் உள்ளது',
'network': 'வலைப்பின்னல்',
'proxy': 'பதிலி',
'noProxy': 'பதிலி இல்லாமல் ',
'systemProxy': 'இயல்புநிலை பதிலி',
'customProxy': 'மாற்றியமைக்கப்பட்ட பதிலி',
'server': 'சேவையகம்',
'username': 'பயனர் பெயர்',
'password': 'கடவுச்சொல்',
'thanks': 'நன்றி',
'thanksDesc':
'Gopeed சமூகத்தை உருவாக்கவும் மற்றும் மேம்படுத்த உதவிய அனைத்து பங்களிப்பாளர்களுக்கும் நன்றி!',
'browserExtension': 'உலாவி நீட்டிப்பு',
'launchAtStartup': 'தொடக்கத்தின் போது இயங்கு',
},
};
+13 -13
View File
@@ -20,7 +20,7 @@ const trTR = {
'downloadLink': 'İndirme bağlantısı',
'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
'downloadLinkHit':
'Lütfen indirme bağlantısını girin, HTTP/HTTPS/MAGNET supported@append',
'Lütfen indirme bağlantısını girin, HTTP/HTTPS/MAGNET desteklenir@append',
'downloadLinkHitDesktop':
', veya torrent dosyasını doğrudan buraya sürükleyin',
'download': 'İndir',
@@ -35,18 +35,18 @@ const trTR = {
'downloadDirValid': 'Lütfen indirme dizinini seçin',
'connections': 'Bağlantılar',
'useServerCtime': 'Dosya oluşturma için sunucu saatini kullan',
'maxRunning': 'Maksimum çalışan görevler',
'items': '@count say',
'maxRunning': 'Maksimum çalışan görev sayısı',
'items': '@count tane',
'subscribeTracker': 'İzleyiciye abone ol',
'subscribeFail':
'Abone olunamadı, lütfen ağı kontrol edin veya daha sonra tekrar deneyin',
'update': 'Güncelleme',
'updateDaily': 'Günlük güncelleme',
'lastUpdate': 'Son güncelleme: @time',
'update': 'Güncelle',
'updateDaily': 'Günlük olarak güncelle',
'lastUpdate': 'Son güncellenme tarihi: @time',
'addTracker': 'İzleyici ekle',
'addTrackerHit':
'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin',
'ui': 'UI',
'ui': 'Arayüz',
'theme': 'Tema',
'themeSystem': 'Sistem',
'themeLight': 'Açık',
@@ -58,9 +58,9 @@ const trTR = {
'protocol': 'Protokol',
'port': 'Bağlantı noktası',
'apiToken': 'API anahtarı',
'notSet': 'AYARLAMA',
'set': 'AYARLA',
'effectAfterRestart': 'Yeniden başlatma sonrası etki eder',
'notSet': 'AYARLI',
'set': 'AYARLI DEĞİL',
'effectAfterRestart': 'Yeniden başlatma sonrası etki edecektir',
'startAll': 'Hepsini başlat',
'pauseAll': 'Hepsini durdur',
'deleteTask': 'Görevleri sil',
@@ -75,8 +75,8 @@ const trTR = {
'extensionUpdateSuccess': 'Başarıyla güncellendi',
'extensionDelete': 'Uzantıyı sil',
'extensionAlreadyLatest': 'Bu zaten en son sürüm',
'extensionFind': 'Uzantıları ara',
'extensionDevelop': 'Uzantıları Geliştirin',
'extensionFind': 'Uzantı ara',
'extensionDevelop': 'Uzantı geliştir',
'history': 'Geçmiş',
'clearHistory': 'Geçmişi temizle',
'noHistoryFound': 'Geçmiş bulunamadı',
@@ -90,6 +90,6 @@ const trTR = {
'thanks': 'Teşekkürler',
'thanksDesc':
'Gopeed topluluğunun oluşmasına ve gelişmesine yardımcı olan tüm katılımcılara teşekkürler!',
'browserExtension': 'Tarayıcı Uzantısı',
'browserExtension': 'Tarayıcı uzantısı',
},
};
+6
View File
@@ -56,7 +56,9 @@ const zhCN = {
'apiToken': '接口令牌',
'notSet': '未设置',
'set': '已设置',
'portInUse': '端口[@port]已被占用,请更换端口',
'effectAfterRestart': '此配置项将在重启应用后生效',
'show': '显示',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务',
@@ -80,11 +82,15 @@ const zhCN = {
'serviceText': '运行中',
'network': '网络',
'proxy': '代理',
'noProxy': '不使用代理',
'systemProxy': '系统代理',
'customProxy': '自定义代理',
'server': '服务器',
'username': '用户名',
'password': '密码',
'thanks': '鸣谢',
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
'browserExtension': '浏览器扩展',
'launchAtStartup': '开机自动运行',
}
};
+6
View File
@@ -56,7 +56,9 @@ const zhTW = {
'apiToken': 'API令牌',
'notSet': '未設定',
'set': '已設定',
'portInUse': '端口[@port]已被占用,請更換端口',
'effectAfterRestart': '重啟後生效',
'show': '顯示',
'startAll': '全部開始',
'pauseAll': '全部暫停',
'deleteTask': '刪除任務',
@@ -80,11 +82,15 @@ const zhTW = {
'serviceText': '執行中',
'network': '網路',
'proxy': '代理',
'noProxy': '不使用代理',
'systemProxy': '系統代理',
'customProxy': '自定義代理',
'server': '伺服器',
'username': '名稱',
'password': '密碼',
'thanks': '感謝',
'thanksDesc': '感謝所有為 Gopeed 社區建設添磚加瓦的貢獻者們!',
'browserExtension': '瀏覽器擴充程式',
'launchAtStartup': '開機自動運行',
}
};
+11 -5
View File
@@ -1,13 +1,16 @@
import 'package:get/get.dart';
import 'package:gopeed/i18n/langs/fa_ir.dart';
import 'package:gopeed/i18n/langs/ja_jp.dart';
import 'package:gopeed/i18n/langs/zh_tw.dart';
import 'langs/en_us.dart';
import 'langs/fa_ir.dart';
import 'langs/it_it.dart';
import 'langs/ja_jp.dart';
import 'langs/pl_pl.dart';
import 'langs/ru_ru.dart';
import 'langs/zh_cn.dart';
import 'langs/vi_vn.dart';
import 'langs/ta_ta.dart';
import 'langs/tr_tr.dart';
import 'langs/vi_vn.dart';
import 'langs/zh_cn.dart';
import 'langs/zh_tw.dart';
final messages = _Messages();
@@ -22,6 +25,9 @@ class _Messages extends Translations {
...faIR,
...jaJP,
...viVN,
...taTA,
...trTR,
...plPL,
...itIT,
};
}
+30 -30
View File
@@ -1,6 +1,6 @@
import 'package:args/args.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart';
@@ -9,7 +9,6 @@ import 'app/modules/app/controllers/app_controller.dart';
import 'app/modules/app/views/app_view.dart';
import 'core/libgopeed_boot.dart';
import 'database/database.dart';
import 'database/entity.dart';
import 'i18n/message.dart';
import 'util/locale_manager.dart';
import 'util/log_util.dart';
@@ -17,14 +16,28 @@ import 'util/mac_secure_util.dart';
import 'util/package_info.dart';
import 'util/util.dart';
void main() async {
await init();
class Args {
static const flagHidden = "hidden";
bool hidden = false;
Args.parse(List<String> args) {
final parser = ArgParser();
parser.addFlag(flagHidden);
final results = parser.parse(args);
hidden = results.flag(flagHidden);
}
}
void main(List<String> arguments) async {
final args = Args.parse(arguments);
await init(args);
onStart();
runApp(const AppView());
}
Future<void> init() async {
Future<void> init(Args args) async {
WidgetsFlutterBinding.ensureInitialized();
await Util.initStorageDir();
await Database.instance.init();
@@ -37,8 +50,10 @@ Future<void> init() async {
skipTaskbar: false,
);
await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show();
await windowManager.focus();
if (!args.hidden) {
await windowManager.show();
await windowManager.focus();
}
await windowManager.setPreventClose(true);
// windows_manager has a bug where when window to be maximized, it will be unmaximized immediately, so can't implement this feature currently.
// https://github.com/leanflutter/window_manager/issues/412
@@ -50,27 +65,18 @@ Future<void> init() async {
initLogger();
try {
await initPackageInfo();
} catch (e) {
logger.e("init package info fail", e);
}
final controller = Get.put(AppController());
try {
await controller.loadStartConfig();
final startCfg = controller.startConfig.value;
// When the app is closed by swiping on the mobile, the backend server is actually still running, don't need to start again.
final isRunning =
Util.isMobile() && (await FlutterForegroundTask.isRunningService);
if (!isRunning) {
controller.runningPort.value =
await LibgopeedBoot.instance.start(startCfg);
api.init(
startCfg.network, controller.runningAddress(), startCfg.apiToken);
Database.instance.saveLastRunningConfig(StartConfigEntity(
network: startCfg.network,
address: controller.runningAddress(),
apiToken: startCfg.apiToken));
} else {
final lastRunningConfig = Database.instance.getLastRunningConfig()!;
api.init(lastRunningConfig.network, lastRunningConfig.address,
lastRunningConfig.apiToken);
}
controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
api.init(startCfg.network, controller.runningAddress(), startCfg.apiToken);
} catch (e) {
logger.e("libgopeed init fail", e);
}
@@ -81,12 +87,6 @@ Future<void> init() async {
} catch (e) {
logger.e("load config fail", e);
}
try {
await initPackageInfo();
} catch (e) {
logger.e("init package info fail", e);
}
}
Future<void> onStart() async {
+1 -1
View File
@@ -52,7 +52,7 @@ class Util {
static Future<void> initStorageDir() async {
var storageDir = "";
if (Util.isWindows()) {
storageDir = "./";
storageDir = File(Platform.resolvedExecutable).parent.path;
} else if (!Util.isWeb()) {
if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
+2 -1
View File
@@ -48,7 +48,8 @@ static void my_application_activate(GApplication* application) {
}
gtk_window_set_default_size(window, 1280, 720);
gtk_widget_show(GTK_WIDGET(window));
//gtk_widget_show(GTK_WIDGET(window));
gtk_widget_realize(GTK_WIDGET(window));
g_autoptr(FlDartProject) project = fl_dart_project_new();
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
@@ -13,8 +13,8 @@ essential: false
icon: assets/icon/icon.svg
dependencies:
- libappindicator3-1 | libayatana-appindicator3-1
- gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1
- libayatana-appindicator3-1
- gir1.2-ayatanaappindicator3-0.1
keywords:
- Application
@@ -1,24 +0,0 @@
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
@@ -1,5 +1,6 @@
import Cocoa
import FlutterMacOS
import window_manager
class MainFlutterWindow: NSWindow {
override func awakeFromNib() {
@@ -12,4 +13,9 @@ class MainFlutterWindow: NSWindow {
super.awakeFromNib()
}
override public func order(_ place: NSWindow.OrderingMode, relativeTo otherWin: Int) {
super.order(place, relativeTo: otherWin)
hiddenWindowAtLaunch()
}
}
+27 -3
View File
@@ -34,13 +34,13 @@ packages:
source: hosted
version: "3.4.10"
args:
dependency: transitive
dependency: "direct main"
description:
name: args
sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596
sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a"
url: "https://pub.dev"
source: hosted
version: "2.4.2"
version: "2.5.0"
async:
dependency: transitive
description:
@@ -493,6 +493,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.7.1"
launch_at_startup:
dependency: "direct main"
description:
name: launch_at_startup
sha256: "93fc5638e088290004fae358bae691486673d469957d461d9dae5b12248593eb"
url: "https://pub.dev"
source: hosted
version: "0.2.2"
lecle_downloads_path_provider:
dependency: "direct main"
description:
@@ -994,6 +1002,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.1"
toggle_switch:
dependency: "direct main"
description:
name: toggle_switch
sha256: dca04512d7c23ed320d6c5ede1211a404f177d54d353bf785b07d15546a86ce5
url: "https://pub.dev"
source: hosted
version: "2.3.0"
tray_manager:
dependency: "direct main"
description:
@@ -1130,6 +1146,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.4"
win32_registry:
dependency: transitive
description:
name: win32_registry
sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
window_manager:
dependency: "direct main"
description:
+4 -1
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.5.6+1
version: 1.5.7+1
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -64,6 +64,9 @@ dependencies:
tray_manager: ^0.2.1
lecle_downloads_path_provider: ^0.0.2+8
hive: ^2.2.3
launch_at_startup: ^0.2.2
args: ^2.5.0
toggle_switch: ^2.3.0
dev_dependencies:
flutter_test:
sdk: flutter
@@ -28,7 +28,6 @@ bool FlutterWindow::OnCreate() {
SetChildContent(flutter_controller_->view()->GetNativeWindow());
flutter_controller_->engine()->SetNextFrameCallback([&]() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is