mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
23
Commits
feat/system_proxy
...
2.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
06c7fe7a22 | ||
|
|
8b3b5d4394 | ||
|
|
2ee73ab619 | ||
|
|
15413dbc95 | ||
|
|
c7c43fac80 | ||
|
|
4283bdfa5a | ||
|
|
328ba8592c | ||
|
|
a191a4140d | ||
|
|
3521837964 | ||
|
|
b30cd78f86 | ||
|
|
0583e4aaa6 | ||
|
|
a89c55b5c4 | ||
|
|
68c40d7f3f | ||
|
|
42a0b89d95 | ||
|
|
1860f534cd | ||
|
|
5c2d2ad7b4 | ||
|
|
db6352ada8 | ||
|
|
d1a5ed8b3d | ||
|
|
be9583be0f | ||
|
|
d2c70b077b | ||
|
|
53b3eea4fc | ||
|
|
54347f41b6 | ||
|
|
cea454b015 |
No files matched your search
+43
-13
@@ -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:
|
||||
@@ -260,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:
|
||||
@@ -300,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
|
||||
@@ -381,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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -451,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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"`
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package model
|
||||
|
||||
type Command struct {
|
||||
Protocol string `json:"protocol"`
|
||||
Action string `json:"action"`
|
||||
Params any `json:"params"`
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>";
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
};
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
@@ -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': 'தொடக்கத்தின் போது இயங்கு',
|
||||
},
|
||||
};
|
||||
@@ -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': '开机自动运行',
|
||||
}
|
||||
};
|
||||
@@ -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': '開機自動運行',
|
||||
}
|
||||
};
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user