Compare commits

...
71 Commits
Author SHA1 Message Date
liwei 09bdb5584a ci: fix mac build 2023-04-01 21:29:57 +08:00
liwei 36f5c9569e ci: debug 2023-04-01 21:21:09 +08:00
liwei 3d9edc5133 ci: debug 2023-04-01 21:10:41 +08:00
liwei ed08c05532 ci: fix 2023-04-01 20:58:33 +08:00
liwei 9bd8befcb8 fix: mac universal binary build 2023-04-01 20:49:51 +08:00
liwei 7ba87e48fc fix: mac build 2023-04-01 20:31:55 +08:00
Li Wei af5f739630 ci: macos universal binaries build (#103) 2023-04-01 20:13:08 +08:00
liwei 5558cac263 fix: task view ui 2023-04-01 19:43:47 +08:00
liwei 4128e718e9 ci: fix web build 2023-04-01 17:05:51 +08:00
liwei 6d017b18f5 fix: version check 2023-04-01 16:53:43 +08:00
liwei 35eb089178 ci: fix web build 2023-04-01 16:37:42 +08:00
liwei b5f78f0fbe ci: fix docker build 2023-04-01 16:32:57 +08:00
liwei 628d73f90d ci: fix android build 2023-04-01 16:15:34 +08:00
liwei 7d2d84d844 fix: home page menu icon 2023-04-01 16:09:36 +08:00
liwei 1cbe58d760 ci: fix macos arm build 2023-04-01 15:56:07 +08:00
liwei c77a4094a3 fix: logger file output 2023-04-01 15:52:12 +08:00
Li Wei 33f6325bf7 feat: new version check (#102) 2023-04-01 15:33:58 +08:00
Li Wei 701ea59035 feat: smooth speed calculation (#99) 2023-03-09 10:35:10 +08:00
pharaoh2012 e77441f0de docs: correct the official website link (#98) 2023-03-07 11:01:18 +08:00
Li Wei dd46e7a3eb fix: crash caused by non-resumable http download resources (#97) 2023-03-06 21:09:31 +08:00
liwei f42838810a refactor: common message dialog method 2023-03-06 21:02:24 +08:00
liwei 7fdba12376 fix: tracker cdn retry issue 2023-03-06 17:07:00 +08:00
liwei 20130ae6d0 fix: error code 2023-03-05 15:37:04 +08:00
liwei 4598e9b351 ci: fix release action permissions 2023-03-05 12:10:32 +08:00
Li Wei 2e639e0fef feat: api result add code (#96) 2023-03-05 11:54:39 +08:00
liwei dbbbc204fb doc: add official website link 2023-03-04 13:37:20 +08:00
liwei caab121cc2 ci: fix flutter linux build tool 2023-03-02 14:05:22 +08:00
Li Wei c637b36094 ci: flutter build check (#95) 2023-03-02 13:57:00 +08:00
Neginegiandwuke2022 38ba901956 fix: fluent system theme mode (#94)
* Fix: fluent theme mode fix

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-28 01:53:37 +08:00
Neginegiandwuke2022 8bae1a5850 feat: complete tree view (#93)
* feat: complete tree view

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-23 21:34:39 +08:00
Neginegiandwuke2022 56375c0edb fix: treeview,style,file picker (#91)
* fix: tree view,style,file picker

Co-authored-by: wuke2022 <[email protected]>
2023-02-22 20:06:28 +08:00
Neginegiandwuke2022 5b69472ac8 fix: style, performance ,loading bug (#90)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

* fix:locale fix

* fix: task list stack progress style
fix: translation
fix: tree view
fix: confirm button loading
remove: stylish_bottom_bar(dark mode not supported)

* fix: task list stack progress style
fix: translation
fix: tree view
fix: confirm button loading
remove: stylish_bottom_bar(dark mode not supported)

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-21 20:17:17 +08:00
Neginegiandwuke2022 dbc9c4c121 fix docs and locales (#86)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

* fix:locale fix

* docs: add detailed bash for build and dev

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-19 23:47:28 +08:00
Neginegiandwuke2022 c0e24ce869 fix locale in get-cli standard (#81)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-19 21:22:20 +08:00
Neginegiandwuke2022 74c24ad860 Refactor project in get-cli standard (#80)
* refactor project in get-cli standard

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* responsive view,treeView,file select
downloaded and downloading pages
file extension icons
fix some minor bugs

* use local trackers when github is not connectable
upgrade stylish_bottom_bar to 1.0.0
responsive view for home_view
treeview for file_list_vie
fluent ui integrating

* improve responsive view.
treeView add light theme.fix overflow bugs

* bottom bar index fix

* refactor project in get-cli standard

---------

Co-authored-by: wuke2022 <[email protected]>
2023-02-19 20:06:28 +08:00
wuke2022 9b1ed15fea feat: ui improvement
flutter 3.7 compatibility
responsive view
beautiful progress bar
select file in treeview
downlading and downloaded page
2023-02-16 11:34:42 +08:00
liwei a875a9258e refactor: change repo url 2023-02-11 21:37:05 +08:00
liwei 0d2ac10af8 fix: rename logic 2023-02-10 21:40:16 +08:00
liwei 978b8bc7cb docs: add full name 2023-02-10 16:20:55 +08:00
liwei 50c3713c90 docs: donate link hash 2023-02-10 16:16:00 +08:00
liwei 2639b6bc65 docs: badge sort 2023-02-10 16:09:01 +08:00
liwei 8b29ceb151 docs: add donate badge 2023-02-10 16:07:43 +08:00
liwei e1c14ad9f8 docs: add donate info 2023-02-10 15:42:29 +08:00
liwei e4f86097e8 fix: bt resolve 2023-02-10 15:27:15 +08:00
Li Wei 0515ce6081 feat: auto rename on duplicate (#76) 2023-02-10 15:18:31 +08:00
liwei 048554df4d ci: fix 2023-02-08 17:29:23 +08:00
Li Wei 2d02486ca6 ci: windows setup add chinese (#75) 2023-02-08 17:18:39 +08:00
liwei 3523f5855a fix: api ok nil response 2023-02-08 14:59:59 +08:00
liwei aa9577a2e0 fix: ignore broken task data 2023-02-08 14:53:49 +08:00
liwei efc9dcca0b fix: download dir config 2023-02-07 23:53:50 +08:00
liwei b062336c09 docs: update CONTRIBUTING.md 2023-02-07 23:30:26 +08:00
liwei baf88a5cb7 ci: build only manual trigger 2023-02-07 18:28:14 +08:00
liwei e208014d34 ci: fix windows 2023-02-07 18:24:03 +08:00
liwei 3ecedd60fd ci: fix 2023-02-07 15:33:02 +08:00
liwei 6fd68e4752 ci: fix input 2023-02-07 15:13:51 +08:00
Li Wei c2910b0e8c ci: get last release include draft (#74) 2023-02-07 15:10:31 +08:00
Li Wei 8ed51787ad feat: add cdn for tracker subscribe urls (#73) 2023-02-07 14:18:59 +08:00
liwei 2e5fd3017c fix: bt fetcher restore 2023-02-06 22:21:25 +08:00
Li Wei 027d29e834 fix: windows version (#72) 2023-02-06 21:06:56 +08:00
Li Wei 1a3c8c026b feat: reuse fetcher (#71) 2023-02-06 20:58:13 +08:00
Persian Prince 17cb59c606 feat: add farsi languages (#70) 2023-02-03 21:35:23 +08:00
liwei e3828c2982 ci: fix 2022-12-17 13:09:15 +08:00
liwei 8cef0bb727 ci: fix android sign 2022-12-17 10:58:32 +08:00
Li Wei 21ec1e4539 fix: follow android 11 storage permissions (#66) 2022-12-16 21:43:06 +08:00
levi dda44cc770 fix: docker tag 2022-12-16 15:24:41 +08:00
levi 78911b2aa9 ci: fix docker build 2022-12-16 15:10:34 +08:00
Li Wei 4935dcd0e6 ci: fix 2022-12-16 15:02:33 +08:00
Li Wei e8fe6ad550 ci: docker supports more platforms (#64) 2022-12-16 14:56:56 +08:00
Li Wei db4003ad4f fix: speed calculate (#63) 2022-12-16 14:53:07 +08:00
Li Wei 0a4be165a5 fix: http download fail (#61) 2022-12-14 12:12:39 +08:00
levi c62867e3ef chore: fix docker-compose command 2022-12-13 15:00:32 +08:00
139 changed files with 6327 additions and 1910 deletions

No files matched your search

+27
View File
@@ -0,0 +1,27 @@
## Donate
Thank you for your support! 🙏
<div style="text-align: center;">
<table>
<tr>
<th>微信</th>
<th>支付宝</th>
<th>PayPal</th>
<th>Ko-fi</th>
</tr>
<tr>
<td><img src="/.donate/qr/wechatpay.jpg" alt="wechatpay" width="300" /></td>
<td><img src="/.donate/qr/alipay.jpg" alt="alipay" width="300" /></td>
<td><a href="https://paypal.me/monkeyWie" target="_blank">Click Me</a></td>
<td><a href="https://ko-fi.com/gopeed" target="_blank">Click Me</a></td>
</table>
</div>
### Crypto
- **BTC**: 35jYkPN8bgkh7AWjt3B1VgMQHqtGYcvF6p
- **ETH:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
- **Matic:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
- **BNB:** 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
- **USDT(ERC20)**: 0x928a7fd2398c941Ac6713704e606Fa414595e3A0
Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
#github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
#patreon: # Replace with a single Patreon username
#open_collective: # Replace with a single Open Collective username
#ko_fi: # Replace with a single Ko-fi username
#tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
#community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
#liberapay: # Replace with a single Liberapay username
#issuehunt: # Replace with a single IssueHunt username
#otechie: # Replace with a single Otechie username
#custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
# github: monkeyWie
ko_fi: gopeed
custom: https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate
+6 -6
View File
@@ -1,8 +1,8 @@
<!-- <!--
请根据以下模版提issue Please use the following template to create an issue.
--> -->
### 问题描述(必要) ### Description(required)
### 版本号(必要) ### App Version(required)
### 操作系统(必要) ### OS Version(required)
### 相关截图 ### Snapshots
### 相关日志 ### Log
+2 -2
View File
@@ -1,5 +1,5 @@
name-template: 'v1.2.0 🌈' name-template: 'v$NEXT_PATCH_VERSION 🌈'
tag-template: 'v1.2.0' tag-template: 'v$NEXT_PATCH_VERSION'
categories: categories:
- title: '🆕 Features' - title: '🆕 Features'
labels: labels:
+68 -42
View File
@@ -2,8 +2,10 @@ name: build
on: on:
workflow_dispatch: workflow_dispatch:
release:
types: [published] permissions:
contents: write
jobs: jobs:
get-release: get-release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -12,9 +14,11 @@ jobs:
upload_url: ${{ steps.get-release.outputs.upload_url }} upload_url: ${{ steps.get-release.outputs.upload_url }}
steps: steps:
- uses: actions/checkout@v3 - uses: monkeyWie/get-latest-release@v1
- uses: thebritican/[email protected]
id: get-release id: get-release
with:
myToken: ${{ github.token }}
build-windows: build-windows:
runs-on: windows-latest runs-on: windows-latest
needs: [get-release] needs: [get-release]
@@ -29,7 +33,10 @@ jobs:
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build windows flutter build windows
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin" $mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
@@ -43,7 +50,7 @@ jobs:
cp $system\vcruntime140_1.dll $release cp $system\vcruntime140_1.dll $release
New-Item -Path build\windows\Output -ItemType Directory New-Item -Path build\windows\Output -ItemType Directory
Compress-Archive -Path "$release*" -DestinationPath build\windows\Output\Gopeed-$env:VERSION-windows-amd64.zip Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$env:VERSION-windows-amd64-portable.zip"
cd build/windows cd build/windows
echo @" echo @"
@@ -77,9 +84,12 @@ jobs:
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
LanguageDetectionMethod=uilanguage
ShowLanguageDialog=yes
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "chinesesimplified"; MessagesFile: "compiler:Languages\ChineseSimplified.isl"
[Tasks] [Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
@@ -88,6 +98,9 @@ jobs:
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[UninstallDelete]
Type: filesandordirs; Name: "{app}"
[Icons] [Icons]
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
@@ -96,7 +109,9 @@ jobs:
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
"@ > setup.iss "@ > setup.iss
iscc.exe setup.iss iscc.exe setup.iss
mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64-install.exe" mv "Output\gopeed.exe" "Output\Gopeed-$env:VERSION-windows-amd64.exe"
Compress-Archive -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe" -DestinationPath "Output\Gopeed-$env:VERSION-windows-amd64.zip"
Remove-Item -Path "Output\Gopeed-$env:VERSION-windows-amd64.exe"
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -112,20 +127,9 @@ jobs:
with: with:
go-version: "^1.19" go-version: "^1.19"
cache: true cache: true
- run: mkdir -p ~/image-cache
- id: image-cache
uses: actions/cache@v3
with:
path: ~/image-cache
key: image-cache-xgo-v1.19
- if: steps.image-cache.outputs.cache-hit != 'true'
run: |
docker pull techknowlogick/xgo:go-1.19.x
docker save -o ~/image-cache/xgo.tar techknowlogick/xgo:go-1.19.x
- if: steps.image-cache.outputs.cache-hit == 'true'
run: docker load -i ~/image-cache/xgo.tar
- name: Build - name: Build
run: | run: |
docker pull techknowlogick/xgo:go-1.19.x
go install src.techknowlogick.com/xgo@latest go install src.techknowlogick.com/xgo@latest
xgo --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed . xgo --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
mv libgopeed-*.dylib libgopeed.dylib mv libgopeed-*.dylib libgopeed.dylib
@@ -151,14 +155,23 @@ jobs:
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: macos-arm64-lib name: macos-arm64-lib
path: ui/flutter/macos/Frameworks path: ui/flutter/lib-arm64
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
run: | run: |
PROJECT_DIR=$(pwd) PROJECT_DIR=$(pwd)
cd ui/flutter # amd64 lib
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
# universal binary
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/amd64-lib
cp ui/flutter/lib-arm64/libgopeed.dylib ui/flutter/macos/Frameworks/arm64-lib
cd ui/flutter/macos/Frameworks
lipo -create -output libgopeed.dylib amd64-lib arm64-lib
cd $PROJECT_DIR/ui/flutter
flutter build macos flutter build macos
cd build/macos/Build/Products/Release cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF cat>appdmg.json<<EOF
@@ -172,14 +185,7 @@ jobs:
} }
EOF EOF
mkdir dist mkdir dist
appdmg appdmg.json dist/Gopeed-$VERSION-macos-arm64.dmg appdmg appdmg.json dist/Gopeed-$VERSION-macos.dmg
# amd64
cd $PROJECT_DIR
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o bin/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop
cp bin/libgopeed.dylib ui/flutter/build/macos/Build/Products/Release/Gopeed.app/Contents/Frameworks/
cd ui/flutter/build/macos/Build/Products/Release
appdmg appdmg.json dist/Gopeed-$VERSION-macos-amd64.dmg
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -203,7 +209,7 @@ jobs:
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux flutter build linux
mkdir -p debian/gui mkdir -p debian/gui
@@ -262,12 +268,16 @@ jobs:
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
APK_KEYSTORE : ${{ secrets.APK_KEYSTORE }}
APK_KEY_PASSWORD : ${{ secrets.APK_KEY_PASSWORD }}
APK_STORE_PASSWORD : ${{ secrets.APK_STORE_PASSWORD }}
run: | run: |
mkdir ui/flutter/android/app/libs
go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/monkeyWie/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
flutter build apk flutter build apk
mkdir dist mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-$VERSION-android.apk
@@ -301,9 +311,13 @@ jobs:
export CGO_ENABLED=0 export CGO_ENABLED=0
for goos in "${goos_arr[@]}"; do for goos in "${goos_arr[@]}"; do
for goarch in "${goarch_arr[@]}"; do for goarch in "${goarch_arr[@]}"; do
name=gopeed-web-$VERSION-$goos-$goarch goos_name=$goos
if [ $goos = "darwin" ]; then
goos_name="macos"
fi
name=gopeed-web-$VERSION-$goos_name-$goarch
dir="dist/$name/" dir="dist/$name/"
(GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/monkeyWie/gopeed/cmd/web \ (GOOS=$goos GOARCH=$goarch go build -tags nosqlite,web -ldflags="-s -w" -o $dir github.com/GopeedLab/gopeed/cmd/web \
&& cd $dir \ && cd $dir \
&& file=$(ls -AU | head -1) \ && file=$(ls -AU | head -1) \
&& mkdir $name \ && mkdir $name \
@@ -324,13 +338,25 @@ jobs:
needs: [get-release] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Build - name: Set up QEMU
uses: docker/setup-qemu-action@v2
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: liwei2633
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v2
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
run: | with:
tag=liwei2633/gopeed:${VERSION:1} push: true
docker build -t $tag . platforms: |
docker login -u liwei2633 -p ${{ secrets.DOCKER_PASSWORD }} linux/386
docker push $tag linux/amd64
docker tag $tag liwei2633/gopeed:latest linux/arm64
docker push liwei2633/gopeed:latest tags: |
liwei2633/gopeed:latest
liwei2633/gopeed:${{ env.VERSION }}
+3
View File
@@ -16,6 +16,9 @@ on:
- "go.sum" - "go.sum"
- "Dockerfile" - "Dockerfile"
permissions:
contents: write
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+10 -4
View File
@@ -41,7 +41,7 @@ jobs:
# go get -u golang.org/x/lint/golint # go get -u golang.org/x/lint/golint
# golint -set_exit_status ./... # golint -set_exit_status ./...
test: test:
name: Test name: Test check
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -54,9 +54,8 @@ jobs:
- uses: codecov/codecov-action@v3 - uses: codecov/codecov-action@v3
with: with:
files: ./coverage.txt files: ./coverage.txt
fail_ci_if_error: true
build: build:
name: Build name: Build check
runs-on: ubuntu-latest runs-on: ubuntu-latest
# needs: [lint, test] # needs: [lint, test]
needs: [test] needs: [test]
@@ -66,5 +65,12 @@ jobs:
with: with:
go-version: "^1.19" go-version: "^1.19"
cache: true cache: true
- uses: subosito/flutter-action@v2
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev
- name: Build - name: Build
run: go build github.com/monkeyWie/gopeed/cmd/gopeed run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
+54
View File
@@ -0,0 +1,54 @@
# Gopeed contributors guide
Firstly, thank you for your interest in contributing to Gopeed. This guide will help you better
participate in the development of Gopeed.
## Branch description
This project only has one main branch, namely the `main` branch. If you want to participate in the
development of Gopeed, please fork this project first, and then develop in your fork project. After
development is completed, submit a PR to this project and merge it into the `main` branch.
## Local development
It is recommended to develop and debug through the web. First, start the backend service, and start
it by the command line `go run cmd/api/main.go`, the default port of the service is `9999`, and then
start the front-end flutter project in `debug` mode to run.
## Translation
The internationalization files of Gopeed are located in the `ui/flutter/assets/locales` directory.
You only need to add the corresponding language file in this directory.
Generate locales after you edit locales:
```
get generate locales
```
Please refer to `en_us.dart` for translation.
## flutter development
Don't forget to run`dart format ./ui/flutter`before you commit to keep your code in standard dart format
Turn on build_runner watcher if you want to edit api/models:
```
flutter pub run build_runner watch
```
get-cli commands usages:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+48
View File
@@ -0,0 +1,48 @@
# Gopeed 贡献指南
首先感谢您对贡献代码感兴趣,这份指南将帮助您更好的参与到 Gopeed 的开发中来。
## 分支说明
本项目只有一个主分支,即 `main` 分支,如果您想要参与到 Gopeed 的开发中来,请先 fork 本项目,然后在您的 fork 项目中进行开发,开发完成后再向本项目提交
PR,合并到 `main` 分支。
## 本地开发
建议通过 web 端进行开发调试,首先启动后端服务,通过命令行 `go run cmd/api/main.go` 启动 ,服务启动默认端口为 `9999`,然后以 `debug` 模式启动前端
flutter 项目即可运行。
## 翻译
Gopeed 的国际化文件位于 `ui/flutter/assets/locales` 目录下,只需要在该目录下添加对应的语言文件即可。
编辑locales后请运行以下命令:
```
get generate locales
```
请注意以 `en_US.json` 为参照进行翻译。
## flutter开发
每次提交前请务必`dart format ./ui/flutter`
如果要编辑api/models,请打开build_runner watcher:
```
flutter pub run build_runner watch
```
适用get-cli 命令:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+2 -2
View File
@@ -1,4 +1,4 @@
FROM cirrusci/flutter:3.3.7 AS flutter FROM cirrusci/flutter:3.7.7 AS flutter
WORKDIR /app WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./ COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get RUN flutter pub get
@@ -11,7 +11,7 @@ COPY ./go.mod ./go.sum ./
RUN go mod download RUN go mod download
COPY . . COPY . .
COPY --from=flutter /app/build/web ./cmd/web/dist COPY --from=flutter /app/build/web ./cmd/web/dist
RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/monkeyWie/gopeed/cmd/web RUN CGO_ENABLED=0 go build -tags nosqlite,web -ldflags="-s -w" -o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
FROM alpine:3.14.2 FROM alpine:3.14.2
LABEL maintainer="monkeyWie" LABEL maintainer="monkeyWie"
+29 -33
View File
@@ -1,6 +1,7 @@
[![Test Status](https://github.com/monkeyWie/gopeed/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/monkeyWie/gopeed.svg?style=flat-square)](https://github.com/monkeyWie/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg?style=flat-square)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) ![](_docs/img/banner.png)
@@ -9,9 +10,11 @@
## Introduction ## Introduction
Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which supports (HTTP, BitTorrent, Magnet) protocol downloads and supports all platforms. Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms.
## Installation Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
## Install
**Supported platforms** **Supported platforms**
@@ -23,7 +26,7 @@ Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which support
- [x] web - [x] web
- [x] docker - [x] docker
[To Release](https://github.com/monkeyWie/gopeed/releases/latest) [Download](https://github.com/GopeedLab/gopeed/releases/latest)
> Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal > Tips: If the macos open failed, please execute the `xattr -d com.apple.quarantine /Applications/Gopeed.app` command in the terminal
@@ -32,7 +35,7 @@ Gopeed is a high-speed downloader developed by `Golang`+`Flutter`, which support
use `go install`: use `go install`:
```bash ```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
### Docker ### Docker
@@ -45,31 +48,20 @@ docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
#### Using Docker Compose #### Using Docker Compose
```yaml
version: '3'
services:
gopeed:
container_name: gopeed
ports:
- 9999:9999
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download
restart: unless-stopped
```
Use the command below to start container:
```bash ```bash
docker compose up -d docker-compose up -d
``` ```
#### Access Gopeed #### Access Gopeed
When the docker container is running, you can access the web page through `http://localhost:9999`. When the docker container is running, you can access the web page through `http://localhost:9999`.
> Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host. > Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
## Donate
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
## Showcase ## Showcase
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
@@ -83,7 +75,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Environment ### Environment
1. Golang 1.19+ 1. Golang 1.19+
2. Flutter 3.0+ 2. Flutter 3.7+
### Clone ### Clone
@@ -91,6 +83,10 @@ This project is divided into two parts, the front end uses `flutter`, the back e
git clone [email protected]:monkeyWie/gopeed.git git clone [email protected]:monkeyWie/gopeed.git
``` ```
### Contributing
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
### Build ### Build
#### Desktop #### Desktop
@@ -102,7 +98,7 @@ command:
- windows - windows
```bash ```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build windows flutter build windows
``` ```
@@ -110,7 +106,7 @@ flutter build windows
- macos - macos
```bash ```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build macos flutter build macos
``` ```
@@ -118,7 +114,7 @@ flutter build macos
- linux - linux
```bash ```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux flutter build linux
``` ```
@@ -129,6 +125,7 @@ Same as before, you also need to prepare the `cgo` environment, and then install
```bash ```bash
go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init gomobile init
``` ```
@@ -137,15 +134,13 @@ command:
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/monkeyWie/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
#### Web #### Web
Web platform communicates directly with the backend http server, no additional environment is required.
command: command:
```bash ```bash
@@ -154,8 +149,9 @@ flutter build web
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/monkeyWie/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
```` go run cmd/web/main.go
```
## License ## License
+28 -33
View File
@@ -1,6 +1,7 @@
[![Test Status](https://github.com/monkeyWie/gopeed/workflows/test/badge.svg)](https://github.com/monkeyWie/gopeed/actions?query=workflow%3Atest) [![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/monkeyWie/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/monkeyWie/gopeed) [![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/monkeyWie/gopeed.svg?style=flat-square)](https://github.com/monkeyWie/gopeed/releases) [![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg?style=flat-square)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB) [![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png) ![](_docs/img/banner.png)
@@ -9,9 +10,11 @@
## 介绍 ## 介绍
Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。 Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。
## 下载 访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
## 安装
**已支持平台** **已支持平台**
@@ -23,7 +26,7 @@ Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、
- [x] web - [x] web
- [x] docker - [x] docker
[点击前往](https://github.com/monkeyWie/gopeed/releases/latest) [前往下载](https://github.com/GopeedLab/gopeed/releases/latest)
> 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令 > 注:macos 版本运行如果提示损坏,请在终端执行 `xattr -d com.apple.quarantine /Applications/Gopeed.app` 命令
@@ -32,7 +35,7 @@ Gopeed 是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、
使用`go install`安装: 使用`go install`安装:
```bash ```bash
go install github.com/monkeyWie/gopeed/cmd/gopeed go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
### Docker ### Docker
@@ -45,24 +48,8 @@ docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
#### 使用 Docker Compose #### 使用 Docker Compose
```yaml
version: '3'
services:
gopeed:
container_name: gopeed
ports:
- 9999:9999
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download
restart: unless-stopped
```
输入以下指令启动容器:
```bash ```bash
docker compose up -d docker-compose up -d
``` ```
#### 访问服务 #### 访问服务
@@ -70,6 +57,10 @@ docker compose up -d
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。 当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。 > 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
## 打赏
如果觉得项目对你有帮助,请考虑[打赏](/.donate/index.md#donate)以支持这个项目的发展,非常感谢!
## 界面展示 ## 界面展示
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
@@ -83,7 +74,7 @@ docker compose up -d
### 环境要求 ### 环境要求
1. Golang 1.19+ 1. Golang 1.19+
2. Flutter 3.0+ 2. Flutter 3.7+
### 克隆项目 ### 克隆项目
@@ -91,6 +82,10 @@ docker compose up -d
git clone [email protected]:monkeyWie/gopeed.git git clone [email protected]:monkeyWie/gopeed.git
``` ```
### 贡献代码
请参考[贡献指南](CONTRIBUTING_zh-CN.md)
### 编译 ### 编译
#### 桌面端 #### 桌面端
@@ -102,7 +97,7 @@ git clone [email protected]:monkeyWie/gopeed.git
- windows - windows
```bash ```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build windows flutter build windows
``` ```
@@ -110,7 +105,7 @@ flutter build windows
- macos - macos
```bash ```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build macos flutter build macos
``` ```
@@ -118,7 +113,7 @@ flutter build macos
- linux - linux
```bash ```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/monkeyWie/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux flutter build linux
``` ```
@@ -129,6 +124,7 @@ flutter build linux
```bash ```bash
go install golang.org/x/mobile/cmd/gomobile@latest go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init gomobile init
``` ```
@@ -137,14 +133,12 @@ gomobile init
- android - android
```bash ```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/monkeyWie/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build apk flutter build apk
``` ```
#### Web 端(推荐本地调试使用) #### Web 端
Web 端直接与后端 http 服务通讯,不需要额外准备环境。
构建命令: 构建命令:
@@ -154,7 +148,8 @@ flutter build web
cd ../../ cd ../../
rm -rf cmd/web/dist rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/monkeyWie/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
``` ```
## 开源许可 ## 开源许可
+2 -2
View File
@@ -2,8 +2,8 @@ package main
import ( import (
"fmt" "fmt"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
) )
func main() { func main() {
+2 -2
View File
@@ -3,8 +3,8 @@ package main
import "C" import "C"
import ( import (
"encoding/json" "encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
) )
func main() {} func main() {}
+2 -2
View File
@@ -4,8 +4,8 @@ package libgopeed
import "C" import "C"
import ( import (
"encoding/json" "encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest"
"github.com/monkeyWie/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
) )
func Start(cfg string) (int, error) { func Start(cfg string) (int, error) {
+3 -16
View File
@@ -2,28 +2,15 @@ package main
import ( import (
_ "embed" _ "embed"
"fmt" "github.com/GopeedLab/gopeed/cmd"
"github.com/monkeyWie/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"net/http"
) )
//go:embed banner.txt
var banner string
func main() { func main() {
cfg := &model.StartConfig{ cfg := &model.StartConfig{
Network: "tcp", Network: "tcp",
Address: "127.0.0.1:9999", Address: "127.0.0.1:9999",
Storage: model.StorageBolt, Storage: model.StorageBolt,
} }
fmt.Println(banner) cmd.Start(cfg)
srv, listener, err := rest.BuildServer(cfg)
if err != nil {
panic(err)
}
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
}
} }
File renamed without changes.
+6 -6
View File
@@ -2,10 +2,10 @@ package main
import ( import (
"fmt" "fmt"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/monkeyWie/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"strings" "strings"
"sync" "sync"
) )
@@ -56,10 +56,10 @@ var (
) )
func printProgress(task *download.Task, title string) { func printProgress(task *download.Task, title string) {
rate := float64(task.Progress.Downloaded) / float64(task.Res.Size) rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
completeWidth := int(progressWidth * rate) completeWidth := int(progressWidth * rate)
speed := util.ByteFmt(task.Progress.Speed) speed := util.ByteFmt(task.Progress.Speed)
totalSize := util.ByteFmt(task.Res.Size) totalSize := util.ByteFmt(task.Meta.Res.Size)
sb.WriteString(fmt.Sprintf("\r%s [", title)) sb.WriteString(fmt.Sprintf("\r%s [", title))
for i := 0; i < progressWidth; i++ { for i := 0; i < progressWidth; i++ {
if i < completeWidth { if i < completeWidth {
+42
View File
@@ -0,0 +1,42 @@
package cmd
import (
_ "embed"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"net/http"
"os"
"path/filepath"
)
//go:embed banner.txt
var banner string
func Start(cfg *model.StartConfig) {
fmt.Println(banner)
srv, listener, err := rest.BuildServer(cfg)
if err != nil {
panic(err)
}
exist, _, err := rest.Downloader.GetConfig()
if err != nil {
panic(err)
}
if !exist {
// Set default download dir to user download dir
userDir, err := os.UserHomeDir()
if err == nil {
downloadDir := filepath.Join(userDir, "Downloads")
rest.Downloader.PutConfig(&download.DownloaderStoreConfig{
DownloadDir: downloadDir,
})
}
}
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
}
}
-7
View File
@@ -1,7 +0,0 @@
_______ ______ .______ _______ _______ _______
/ _____| / __ \ | _ \ | ____|| ____|| \
| | __ | | | | | |_) | | |__ | |__ | .--. |
| | |_ | | | | | | ___/ | __| | __| | | | |
| |__| | | `--' | | | | |____ | |____ | '--' |
\______| \______/ | _| |_______||_______||_______/
+3 -16
View File
@@ -5,16 +5,11 @@ package main
import ( import (
"embed" "embed"
"fmt" "github.com/GopeedLab/gopeed/cmd"
"github.com/monkeyWie/gopeed/pkg/rest" "github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"io/fs" "io/fs"
"net/http"
) )
//go:embed banner.txt
var banner string
//go:embed dist/* //go:embed dist/*
var dist embed.FS var dist embed.FS
@@ -30,13 +25,5 @@ func main() {
WebEnable: true, WebEnable: true,
WebFS: sub, WebFS: sub,
} }
fmt.Println(banner) cmd.Start(cfg)
srv, listener, err := rest.BuildServer(cfg)
if err != nil {
panic(err)
}
fmt.Printf("Server start success on http://%s\n", listener.Addr().String())
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
panic(err)
}
} }
+2 -5
View File
@@ -1,9 +1,10 @@
module github.com/monkeyWie/gopeed module github.com/GopeedLab/gopeed
go 1.19 go 1.19
require ( require (
github.com/anacrolix/torrent v1.46.0 github.com/anacrolix/torrent v1.46.0
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0 github.com/matoous/go-nanoid/v2 v2.0.0
go.etcd.io/bbolt v1.3.6 go.etcd.io/bbolt v1.3.6
@@ -39,7 +40,6 @@ require (
github.com/felixge/httpsnoop v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect
github.com/google/btree v1.0.1 // indirect github.com/google/btree v1.0.1 // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/handlers v1.5.1 // indirect
github.com/gorilla/websocket v1.4.2 // indirect github.com/gorilla/websocket v1.4.2 // indirect
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
github.com/lispad/go-generics-tools v1.0.0 // indirect github.com/lispad/go-generics-tools v1.0.0 // indirect
@@ -66,11 +66,8 @@ require (
github.com/tidwall/btree v1.3.1 // indirect github.com/tidwall/btree v1.3.1 // indirect
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect
golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect golang.org/x/exp v0.0.0-20220428152302-39d4317da171 // indirect
golang.org/x/mobile v0.0.0-20220928052126-fa6bcb076835 // indirect
golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57 // indirect
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023 // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
) )
+30 -10
View File
@@ -1,29 +1,48 @@
package fetcher package fetcher
import ( import (
"github.com/monkeyWie/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"path"
) )
// Fetcher defines the interface for a download protocol. // Fetcher defines the interface for a download protocol.
// One fetcher for each download task
type Fetcher interface { type Fetcher interface {
// Name return the name of the protocol. // Name return the name of the protocol.
Name() string Name() string
Setup(ctl *controller.Controller) error Setup(ctl *controller.Controller) error
// Resolve resource info from request // Resolve resource info from request
Resolve(req *base.Request) (res *base.Resource, err error) Resolve(req *base.Request) error
// Create ready to download, but not started // Create ready to download, but not started
Create(res *base.Resource, opts *base.Options) (err error) Create(opts *base.Options) error
Start() (err error) Start() error
Pause() (err error) Pause() error
Continue() (err error) Continue() error
Close() (err error) Close() error
// Meta returns the meta information of the download.
Meta() *FetcherMeta
// Progress returns the progress of the download. // Progress returns the progress of the download.
Progress() Progress Progress() Progress
// Wait for the download to complete, this method will block until the download is done. // Wait for the download to complete, this method will block until the download is done.
Wait() (err error) Wait() error
}
// FetcherMeta defines the meta information of a fetcher.
type FetcherMeta struct {
Req *base.Request `json:"req"`
Res *base.Resource `json:"res"`
Opts *base.Options `json:"opts"`
}
func (m *FetcherMeta) Filepath(file *base.FileInfo) string {
finalName := m.Opts.Name
if finalName == "" {
finalName = file.Name
}
return path.Join(m.Opts.Path, m.Res.RootDir, finalName)
} }
// FetcherBuilder defines the interface for a fetcher builder. // FetcherBuilder defines the interface for a fetcher builder.
@@ -36,11 +55,12 @@ type FetcherBuilder interface {
// Store fetcher // Store fetcher
Store(fetcher Fetcher) (any, error) Store(fetcher Fetcher) (any, error)
// Restore fetcher // Restore fetcher
Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) Fetcher) Restore() (v any, f func(meta *FetcherMeta, v any) Fetcher)
} }
type DefaultFetcher struct { type DefaultFetcher struct {
Ctl *controller.Controller Ctl *controller.Controller
Meta *FetcherMeta
DoneCh chan error DoneCh chan error
} }
+83 -72
View File
@@ -1,13 +1,14 @@
package bt package bt
import ( import (
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent" "github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo" "github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage" "github.com/anacrolix/torrent/storage"
"github.com/monkeyWie/gopeed/internal/controller" "path"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"path/filepath" "path/filepath"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -17,11 +18,10 @@ import (
var client *torrent.Client var client *torrent.Client
type Fetcher struct { type Fetcher struct {
Ctl *controller.Controller ctl *controller.Controller
torrent *torrent.Torrent torrent *torrent.Torrent
res *base.Resource meta *fetcher.FetcherMeta
opts *base.Options
ready atomic.Bool ready atomic.Bool
progress fetcher.Progress progress fetcher.Progress
@@ -34,7 +34,10 @@ func (f *Fetcher) Name() string {
} }
func (f *Fetcher) Setup(ctl *controller.Controller) (err error) { func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
f.Ctl = ctl f.ctl = ctl
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
var once sync.Once var once sync.Once
once.Do(func() { once.Do(func() {
cfg := torrent.NewDefaultClientConfig() cfg := torrent.NewDefaultClientConfig()
@@ -66,50 +69,17 @@ func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
return return
} }
func (f *Fetcher) Wait() (err error) { func (f *Fetcher) Resolve(req *base.Request) error {
for { if err := f.addTorrent(req.URL); err != nil {
if f.ready.Load() { return err
done := true
for _, selectIndex := range f.opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
if file.BytesCompleted() < file.Length() {
done = false
break
}
}
if done {
// remove unselected files
for i, file := range f.torrent.Files() {
selected := false
for _, selectIndex := range f.opts.SelectFiles {
if i == selectIndex {
selected = true
break
}
}
if !selected {
util.SafeRemove(filepath.Join(f.opts.Path, file.Path()))
}
}
break
}
}
time.Sleep(time.Millisecond * 500)
}
return nil
}
func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
if err = f.addTorrent(req.URL, true); err != nil {
return
} }
defer f.torrent.Drop() defer f.torrent.Drop()
res = &base.Resource{ res := &base.Resource{
Req: req, Name: f.torrent.Name(),
Name: f.torrent.Name(), Range: true,
Range: true, RootDir: f.torrent.Name(),
Files: make([]*base.FileInfo, len(f.torrent.Files())), Files: make([]*base.FileInfo, len(f.torrent.Files())),
Hash: f.torrent.InfoHash().String(), Hash: f.torrent.InfoHash().String(),
} }
for i, file := range f.torrent.Files() { for i, file := range f.torrent.Files() {
res.Files[i] = &base.FileInfo{ res.Files[i] = &base.FileInfo{
@@ -119,36 +89,39 @@ func (f *Fetcher) Resolve(req *base.Request) (res *base.Resource, err error) {
} }
res.Size += file.Length() res.Size += file.Length()
} }
return f.meta.Req = req
f.meta.Res = res
return nil
} }
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) (err error) { func (f *Fetcher) Create(opts *base.Options) (err error) {
f.res = res f.meta.Opts = opts
f.opts = opts if len(opts.SelectFiles) == 0 {
if len(f.opts.SelectFiles) == 0 { opts.SelectFiles = make([]int, 0)
f.opts.SelectFiles = make([]int, 0)
for i := range f.torrent.Files() { for i := range f.torrent.Files() {
f.opts.SelectFiles = append(f.opts.SelectFiles, i) opts.SelectFiles = append(opts.SelectFiles, i)
} }
} }
if f.opts.Path != "" { if opts.Path != "" {
f.torrentPaths[res.Hash] = f.opts.Path f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
} }
f.progress = make(fetcher.Progress, len(f.opts.SelectFiles)) f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
f.ready.Store(false) f.ready.Store(false)
return nil return nil
} }
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
if err = f.addTorrent(f.res.Req.URL, false); err != nil { if !f.ready.Load() {
return if err = f.addTorrent(f.meta.Req.URL); err != nil {
return
}
} }
files := f.torrent.Files() files := f.torrent.Files()
if len(f.opts.SelectFiles) == len(files) { if len(f.meta.Opts.SelectFiles) == len(files) {
f.torrent.DownloadAll() f.torrent.DownloadAll()
} else { } else {
for _, selectIndex := range f.opts.SelectFiles { for _, selectIndex := range f.meta.Opts.SelectFiles {
file := files[selectIndex] file := files[selectIndex]
file.Download() file.Download()
} }
@@ -171,19 +144,56 @@ func (f *Fetcher) Close() (err error) {
return nil return nil
} }
func (f *Fetcher) Wait() (err error) {
for {
if f.ready.Load() {
done := true
for _, selectIndex := range f.meta.Opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
if file.BytesCompleted() < file.Length() {
done = false
break
}
}
if done {
// remove unselected files
for i, file := range f.torrent.Files() {
selected := false
for _, selectIndex := range f.meta.Opts.SelectFiles {
if i == selectIndex {
selected = true
break
}
}
if !selected {
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
}
}
break
}
}
time.Sleep(time.Millisecond * 500)
}
return nil
}
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
if !f.ready.Load() { if !f.ready.Load() {
return f.progress return f.progress
} }
for i := range f.progress { for i := range f.progress {
selectIndex := f.opts.SelectFiles[i] selectIndex := f.meta.Opts.SelectFiles[i]
file := f.torrent.Files()[selectIndex] file := f.torrent.Files()[selectIndex]
f.progress[i] = file.BytesCompleted() f.progress[i] = file.BytesCompleted()
} }
return f.progress return f.progress
} }
func (f *Fetcher) addTorrent(url string, resolve bool) (err error) { func (f *Fetcher) addTorrent(url string) (err error) {
schema := util.ParseSchema(url) schema := util.ParseSchema(url)
if schema == "MAGNET" { if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(url) f.torrent, err = client.AddMagnet(url)
@@ -194,7 +204,7 @@ func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
return return
} }
var cfg config var cfg config
exist, err := f.Ctl.GetConfig(&cfg) exist, err := f.ctl.GetConfig(&cfg)
if err != nil { if err != nil {
return return
} }
@@ -206,9 +216,6 @@ func (f *Fetcher) addTorrent(url string, resolve bool) (err error) {
f.torrent.AddTrackers(announceList) f.torrent.AddTrackers(announceList)
} }
<-f.torrent.GotInfo() <-f.torrent.GotInfo()
if resolve {
return
}
f.ready.Store(true) f.ready.Store(true)
return return
} }
@@ -230,6 +237,10 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
return nil, nil return nil, nil
} }
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) { func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return nil, nil return nil, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
return &Fetcher{
meta: meta,
}
}
} }
+11 -11
View File
@@ -2,10 +2,10 @@ package bt
import ( import (
"encoding/json" "encoding/json"
"github.com/monkeyWie/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/internal/test" "github.com/GopeedLab/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"reflect" "reflect"
"testing" "testing"
) )
@@ -19,7 +19,7 @@ func TestFetcher_Config(t *testing.T) {
} }
func doResolve(t *testing.T, fetcher fetcher.Fetcher) { func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
res, err := fetcher.Resolve(&base.Request{ err := fetcher.Resolve(&base.Request{
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent", URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
}) })
if err != nil { if err != nil {
@@ -27,10 +27,10 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
} }
want := &base.Resource{ want := &base.Resource{
Req: res.Req, Name: "ubuntu-22.04-live-server-amd64.iso",
Name: "ubuntu-22.04-live-server-amd64.iso", Size: 1466714112,
Size: 1466714112, Range: true,
Range: true, RootDir: "ubuntu-22.04-live-server-amd64.iso",
Files: []*base.FileInfo{ Files: []*base.FileInfo{
{ {
Name: "ubuntu-22.04-live-server-amd64.iso", Name: "ubuntu-22.04-live-server-amd64.iso",
@@ -39,8 +39,8 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
}, },
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed", Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
} }
if !reflect.DeepEqual(want, res) { if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", res, want) t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
} }
} }
+61 -49
View File
@@ -4,10 +4,10 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"github.com/monkeyWie/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/internal/fetcher"
fhttp "github.com/monkeyWie/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util" fhttp "github.com/GopeedLab/gopeed/pkg/protocol/http"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"io" "io"
"mime" "mime"
@@ -16,9 +16,7 @@ import (
"net/url" "net/url"
"os" "os"
"path" "path"
"path/filepath"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -51,10 +49,10 @@ func newChunk(begin int64, end int64) *chunk {
} }
type Fetcher struct { type Fetcher struct {
*fetcher.DefaultFetcher ctl *controller.Controller
doneCh chan error
res *base.Resource meta *fetcher.FetcherMeta
opts *base.Options
status base.Status status base.Status
chunks []*chunk chunks []*chunk
@@ -68,25 +66,34 @@ func (f *Fetcher) Name() string {
return "http" return "http"
} }
func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) { func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
f.ctl = ctl
f.doneCh = make(chan error, 1)
f.pauseCh = make(chan interface{}, 1)
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
return
}
func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil { if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
return nil, err return err
} }
httpReq, err := buildRequest(nil, req) httpReq, err := buildRequest(nil, req)
if err != nil { if err != nil {
return nil, err return err
} }
client := buildClient() client := buildClient()
// 只访问一个字节,测试资源是否支持Range请求 // 只访问一个字节,测试资源是否支持Range请求
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0)) httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
httpResp, err := client.Do(httpReq) httpResp, err := client.Do(httpReq)
if err != nil { if err != nil {
return nil, err return err
} }
// 拿到响应头就关闭,不用加defer // 拿到响应头就关闭,不用加defer
httpResp.Body.Close() httpResp.Body.Close()
res := &base.Resource{ res := &base.Resource{
Req: req,
Range: false, Range: false,
Files: []*base.FileInfo{}, Files: []*base.FileInfo{},
} }
@@ -98,7 +105,7 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if contentTotal != "" { if contentTotal != "" {
parse, err := strconv.ParseInt(contentTotal, 10, 64) parse, err := strconv.ParseInt(contentTotal, 10, 64)
if err != nil { if err != nil {
return nil, err return err
} }
res.Size = parse res.Size = parse
} }
@@ -108,12 +115,12 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
if contentLength != "" { if contentLength != "" {
parse, err := strconv.ParseInt(contentLength, 10, 64) parse, err := strconv.ParseInt(contentLength, 10, 64)
if err != nil { if err != nil {
return nil, err return err
} }
res.Size = parse res.Size = parse
} }
} else { } else {
return nil, NewRequestError(httpResp.StatusCode, httpResp.Status) return NewRequestError(httpResp.StatusCode, httpResp.Status)
} }
file := &base.FileInfo{ file := &base.FileInfo{
Size: res.Size, Size: res.Size,
@@ -127,36 +134,37 @@ func (f *Fetcher) Resolve(req *base.Request) (*base.Resource, error) {
} }
} }
// Get file filePath by URL // Get file filePath by URL
if file.Name == "" && strings.Count(req.URL, "/") > 2 { if file.Name == "" {
file.Name = filepath.Base(req.URL) file.Name = path.Base(httpReq.URL.Path)
} }
// unknown file filePath // unknown file filePath
if file.Name == "" { if file.Name == "" || file.Name == "/" || file.Name == "." {
file.Name = "unknown" file.Name = "unknown"
} }
res.Files = append(res.Files, file) res.Files = append(res.Files, file)
res.Name = file.Name res.Name = file.Name
return res, nil f.meta.Req = req
f.meta.Res = res
return nil
} }
func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error { func (f *Fetcher) Create(opts *base.Options) error {
f.res = res f.meta.Opts = opts
f.opts = opts
f.status = base.DownloadStatusReady f.status = base.DownloadStatusReady
if err := base.ParseReqExtra[fhttp.ReqExtra](res.Req); err != nil { if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
return err return err
} }
if err := base.ParseOptsExtra[fhttp.OptsExtra](opts); err != nil { if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
return err return err
} }
if opts.Extra == nil { if opts.Extra == nil {
opts.Extra = &fhttp.ReqExtra{} opts.Extra = &fhttp.OptsExtra{}
} }
extra := opts.Extra.(*fhttp.OptsExtra) extra := opts.Extra.(*fhttp.OptsExtra)
if extra.Connections == 0 { if extra.Connections == 0 {
var cfg config var cfg config
exist, err := f.Ctl.GetConfig(&cfg) exist, err := f.ctl.GetConfig(&cfg)
if err != nil { if err != nil {
return err return err
} }
@@ -172,7 +180,7 @@ func (f *Fetcher) Create(res *base.Resource, opts *base.Options) error {
func (f *Fetcher) Start() (err error) { func (f *Fetcher) Start() (err error) {
// 创建文件 // 创建文件
name := f.filepath() name := f.filepath()
f.file, err = f.Ctl.Touch(name, f.res.Size) f.file, err = f.ctl.Touch(name, f.meta.Res.Size)
if err != nil { if err != nil {
return err return err
} }
@@ -215,6 +223,10 @@ func (f *Fetcher) Close() (err error) {
return return
} }
func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0) p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 { if len(f.chunks) > 0 {
@@ -227,14 +239,18 @@ func (f *Fetcher) Progress() fetcher.Progress {
return p return p
} }
func (f *Fetcher) Wait() (err error) {
return <-f.doneCh
}
func (f *Fetcher) filepath() string { func (f *Fetcher) filepath() string {
return util.Filepath(f.opts.Path, f.res.Files[0].Name, f.opts.Name) return f.meta.Filepath(f.meta.Res.Files[0])
} }
func (f *Fetcher) fetch() { func (f *Fetcher) fetch() {
f.ctx, f.cancel = context.WithCancel(context.Background()) f.ctx, f.cancel = context.WithCancel(context.Background())
eg, _ := errgroup.WithContext(f.ctx) eg, _ := errgroup.WithContext(f.ctx)
for i := 0; i < f.opts.Extra.(*fhttp.OptsExtra).Connections; i++ { for i := 0; i < len(f.chunks); i++ {
i := i i := i
eg.Go(func() error { eg.Go(func() error {
return f.fetchChunk(i) return f.fetchChunk(i)
@@ -253,7 +269,7 @@ func (f *Fetcher) fetch() {
} else { } else {
f.status = base.DownloadStatusDone f.status = base.DownloadStatusDone
} }
f.DoneCh <- err f.doneCh <- err
} }
}() }()
} }
@@ -261,7 +277,7 @@ func (f *Fetcher) fetch() {
func (f *Fetcher) fetchChunk(index int) (err error) { func (f *Fetcher) fetchChunk(index int) (err error) {
chunk := f.chunks[index] chunk := f.chunks[index]
httpReq, err := buildRequest(f.ctx, f.res.Req) httpReq, err := buildRequest(f.ctx, f.meta.Req)
if err != nil { if err != nil {
return err return err
} }
@@ -283,7 +299,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
resp *http.Response resp *http.Response
retry bool retry bool
) )
if f.res.Range { if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange, httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End)) fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else { } else {
@@ -337,7 +353,7 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
return return
} }
isComplete := f.res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 isComplete := f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1
if f.status == base.DownloadStatusPause && !isComplete { if f.status == base.DownloadStatusPause && !isComplete {
chunk.Status = base.DownloadStatusPause chunk.Status = base.DownloadStatusPause
return return
@@ -348,10 +364,10 @@ func (f *Fetcher) fetchChunk(index int) (err error) {
} }
func (f *Fetcher) splitChunk() (chunks []*chunk) { func (f *Fetcher) splitChunk() (chunks []*chunk) {
if f.res.Range { if f.meta.Res.Range {
connections := f.opts.Extra.(*fhttp.OptsExtra).Connections connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
// 每个连接平均需要下载的分块大小 // 每个连接平均需要下载的分块大小
chunkSize := f.res.Size / int64(connections) chunkSize := f.meta.Res.Size / int64(connections)
chunks = make([]*chunk, connections) chunks = make([]*chunk, connections)
for i := 0; i < connections; i++ { for i := 0; i < connections; i++ {
var ( var (
@@ -360,7 +376,7 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
) )
if i == connections-1 { if i == connections-1 {
// 最后一个分块需要保证把文件下载完 // 最后一个分块需要保证把文件下载完
end = f.res.Size - 1 end = f.meta.Res.Size - 1
} else { } else {
end = begin + chunkSize - 1 end = begin + chunkSize - 1
} }
@@ -440,10 +456,7 @@ func (fb *FetcherBuilder) Schemes() []string {
} }
func (fb *FetcherBuilder) Build() fetcher.Fetcher { func (fb *FetcherBuilder) Build() fetcher.Fetcher {
return &Fetcher{ return &Fetcher{}
DefaultFetcher: new(fetcher.DefaultFetcher),
pauseCh: make(chan interface{}),
}
} }
func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) { func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
@@ -453,16 +466,15 @@ func (fb *FetcherBuilder) Store(f fetcher.Fetcher) (data any, err error) {
}, nil }, nil
} }
func (fb *FetcherBuilder) Restore() (v any, f func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher) { func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher) {
return &fetcherData{}, func(res *base.Resource, opts *base.Options, v any) fetcher.Fetcher { return &fetcherData{}, func(meta *fetcher.FetcherMeta, v any) fetcher.Fetcher {
fd := v.(*fetcherData) fd := v.(*fetcherData)
fb := &FetcherBuilder{} fb := &FetcherBuilder{}
fetcher := fb.Build().(*Fetcher) fetcher := fb.Build().(*Fetcher)
fetcher.res = res fetcher.meta = meta
fetcher.opts = opts
fetcher.status = base.DownloadStatusPause fetcher.status = base.DownloadStatusPause
base.ParseReqExtra[fhttp.ReqExtra](res.Req) base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptsExtra[fhttp.OptsExtra](opts) base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Chunks) == 0 { if len(fd.Chunks) == 0 {
fetcher.chunks = fetcher.splitChunk() fetcher.chunks = fetcher.splitChunk()
} else { } else {
+31 -28
View File
@@ -3,11 +3,11 @@ package http
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/monkeyWie/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/internal/test" "github.com/GopeedLab/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"net" "net"
"reflect" "reflect"
"testing" "testing"
@@ -43,15 +43,14 @@ func testResolve(startTestServer func() net.Listener, want *base.Resource, t *te
listener := startTestServer() listener := startTestServer()
defer listener.Close() defer listener.Close()
fetcher := buildFetcher() fetcher := buildFetcher()
res, err := fetcher.Resolve(&base.Request{ err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
res.Req = nil if !reflect.DeepEqual(want, fetcher.meta.Res) {
if !reflect.DeepEqual(want, res) { t.Errorf("Resolve() got = %v, want %v", fetcher.meta.Res, want)
t.Errorf("Resolve() got = %v, want %v", res, want)
} }
} }
@@ -80,6 +79,7 @@ func TestFetcher_DownloadChunked(t *testing.T) {
defer listener.Close() defer listener.Close()
// chunked编码下载 // chunked编码下载
downloadNormal(listener, 1, t) downloadNormal(listener, 1, t)
downloadNormal(listener, 2, t)
} }
func TestFetcher_DownloadPost(t *testing.T) { func TestFetcher_DownloadPost(t *testing.T) {
@@ -114,7 +114,7 @@ func TestFetcher_DownloadResume(t *testing.T) {
} }
func TestFetcher_Config(t *testing.T) { func TestFetcher_Config(t *testing.T) {
fetcher, _, _ := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t) fetcher := doDownloadReady(buildConfigFetcher(), test.StartTestFileServer(), 0, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -130,34 +130,37 @@ func TestFetcher_Config(t *testing.T) {
} }
} }
func downloadReady(listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) { func downloadReady(listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
return doDownloadReady(buildFetcher(), listener, connections, t) return doDownloadReady(buildFetcher(), listener, connections, t)
} }
func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) (fetcher fetcher.Fetcher, res *base.Resource, opts *base.Options) { func doDownloadReady(f fetcher.Fetcher, listener net.Listener, connections int, t *testing.T) fetcher.Fetcher {
fetcher = f err := f.Resolve(&base.Request{
res, err := fetcher.Resolve(&base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
opts = &base.Options{ var extra any = nil
Name: test.DownloadName, if connections > 0 {
Path: test.Dir, extra = http.OptsExtra{
Extra: http.OptsExtra{
Connections: connections, Connections: connections,
}, }
} }
err = fetcher.Create(res, opts) opts := &base.Options{
Name: test.DownloadName,
Path: test.Dir,
Extra: extra,
}
err = f.Create(opts)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return return f
} }
func downloadNormal(listener net.Listener, connections int, t *testing.T) { func downloadNormal(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -174,8 +177,8 @@ func downloadNormal(listener net.Listener, connections int, t *testing.T) {
} }
func downloadPost(listener net.Listener, connections int, t *testing.T) { func downloadPost(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
fetcher.(*Fetcher).res.Req.Extra = &http.ReqExtra{ fetcher.Meta().Req.Extra = &http.ReqExtra{
Method: "POST", Method: "POST",
Header: map[string]string{ Header: map[string]string{
"Authorization": "Bearer 123456", "Authorization": "Bearer 123456",
@@ -198,7 +201,7 @@ func downloadPost(listener net.Listener, connections int, t *testing.T) {
} }
func downloadContinue(listener net.Listener, connections int, t *testing.T) { func downloadContinue(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -223,7 +226,7 @@ func downloadContinue(listener net.Listener, connections int, t *testing.T) {
} }
func downloadError(listener net.Listener, connections int, t *testing.T) { func downloadError(listener net.Listener, connections int, t *testing.T) {
fetcher, _, _ := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -235,7 +238,7 @@ func downloadError(listener net.Listener, connections int, t *testing.T) {
} }
func downloadResume(listener net.Listener, connections int, t *testing.T) { func downloadResume(listener net.Listener, connections int, t *testing.T) {
fetcher, res, opts := downloadReady(listener, connections, t) fetcher := downloadReady(listener, connections, t)
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@@ -251,7 +254,7 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
fetcher.Pause() fetcher.Pause()
_, f := fb.Restore() _, f := fb.Restore()
f(res, opts, data) f(fetcher.Meta(), data)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
+18 -21
View File
@@ -1,28 +1,25 @@
package base package base
import "github.com/monkeyWie/gopeed/pkg/util" import (
"github.com/GopeedLab/gopeed/pkg/util"
)
// 下载请求 // Request download request
type Request struct { type Request struct {
// 下载链接 URL string `json:"url"`
URL string `json:"url"` Extra any `json:"extra"`
// 附加信息
Extra any `json:"extra"`
} }
// 资源信息 // Resource download resource
type Resource struct { type Resource struct {
Req *Request `json:"req"`
// 资源名称
Name string `json:"name"` Name string `json:"name"`
// 资源总大小 Size int64 `json:"size"`
Size int64 `json:"size"` // is support range download
// 是否支持断点下载 Range bool `json:"range"`
Range bool `json:"range"` RootDir string `json:"rootDir"`
// 资源所包含的文件列表 // file list
Files []*FileInfo `json:"files"` Files []*FileInfo `json:"files"`
// 资源hash值 Hash string `json:"hash"`
Hash string `json:"hash"`
} }
type FileInfo struct { type FileInfo struct {
@@ -31,15 +28,15 @@ type FileInfo struct {
Size int64 `json:"size"` Size int64 `json:"size"`
} }
// 下载选项 // Options for download
type Options struct { type Options struct {
// 保存文件名 // Download file name
Name string `json:"name"` Name string `json:"name"`
// 保存目录 // Download file path
Path string `json:"path"` Path string `json:"path"`
// 选择下载的文件下标列表 // Select file indexes to download
SelectFiles []int `json:"selectFiles"` SelectFiles []int `json:"selectFiles"`
// 附加信息 // Extra info for specific fetcher
Extra any `json:"extra"` Extra any `json:"extra"`
} }
+109 -43
View File
@@ -2,10 +2,13 @@ package download
import ( import (
"errors" "errors"
"github.com/monkeyWie/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"os"
"path"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
@@ -13,8 +16,11 @@ import (
) )
const ( const (
bucketTask = "task" // task info bucket
bucketSave = "save" bucketTask = "task"
// task download data bucket
bucketSave = "save"
// downloader config bucket
bucketConfig = "config" bucketConfig = "config"
) )
@@ -31,6 +37,7 @@ type Progress struct {
type Downloader struct { type Downloader struct {
fetchBuilders map[string]fetcher.FetcherBuilder fetchBuilders map[string]fetcher.FetcherBuilder
fetcherMap map[string]fetcher.Fetcher
storage Storage storage Storage
tasks []*Task tasks []*Task
listener Listener listener Listener
@@ -47,7 +54,9 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
cfg.Init() cfg.Init()
d := &Downloader{ d := &Downloader{
fetchBuilders: make(map[string]fetcher.FetcherBuilder), fetchBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherMap: make(map[string]fetcher.Fetcher),
refreshInterval: cfg.RefreshInterval, refreshInterval: cfg.RefreshInterval,
storage: cfg.Storage, storage: cfg.Storage,
lock: &sync.Mutex{}, lock: &sync.Mutex{},
@@ -73,7 +82,13 @@ func (d *Downloader) Setup() error {
if tasks == nil { if tasks == nil {
tasks = make([]*Task, 0) tasks = make([]*Task, 0)
} else { } else {
for _, task := range tasks { for i := len(tasks) - 1; i >= 0; i-- {
task := tasks[i]
// Remove broken tasks
if task.Meta == nil {
tasks = append(tasks[:i], tasks[i+1:]...)
continue
}
initTask(task) initTask(task)
if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError { if task.Status != base.DownloadStatusDone && task.Status != base.DownloadStatusError {
task.Status = base.DownloadStatusPause task.Status = base.DownloadStatusPause
@@ -82,8 +97,9 @@ func (d *Downloader) Setup() error {
} }
d.tasks = tasks d.tasks = tasks
// 每秒统计一次下载速度 // 每个tick统计一次下载速度
go func() { go func() {
for !d.closed.Load() { for !d.closed.Load() {
if len(d.tasks) > 0 { if len(d.tasks) > 0 {
for _, task := range d.tasks { for _, task := range d.tasks {
@@ -102,7 +118,7 @@ func (d *Downloader) Setup() error {
current := task.fetcher.Progress().TotalDownloaded() current := task.fetcher.Progress().TotalDownloaded()
task.Progress.Used = task.timer.Used() task.Progress.Used = task.timer.Used()
task.Progress.Speed = current - task.Progress.Downloaded task.Progress.Speed = task.calcSpeed(current-task.Progress.Downloaded, float64(d.refreshInterval)/1000)
task.Progress.Downloaded = current task.Progress.Downloaded = current
d.emit(EventKeyProgress, task) d.emit(EventKeyProgress, task)
@@ -126,18 +142,16 @@ func (d *Downloader) Setup() error {
return nil return nil
} }
func (d *Downloader) buildFetcher(url string) (fetcher.FetcherBuilder, fetcher.Fetcher, error) { func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
schema := util.ParseSchema(url) schema := util.ParseSchema(url)
fetchBuilder, ok := d.fetchBuilders[schema] fetchBuilder, ok := d.fetchBuilders[schema]
if !ok { if !ok {
fetchBuilder, ok = d.fetchBuilders[util.FileSchema] fetchBuilder, ok = d.fetchBuilders[util.FileSchema]
} }
if ok { if ok {
fetcher := fetchBuilder.Build() return fetchBuilder, nil
d.setupFetcher(fetcher)
return fetchBuilder, fetcher, nil
} }
return nil, nil, errors.New("unsupported protocol") return nil, errors.New("unsupported protocol")
} }
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) { func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
@@ -148,23 +162,48 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
fetcher.Setup(ctl) fetcher.Setup(ctl)
} }
func (d *Downloader) Resolve(req *base.Request) (*base.Resource, error) { func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
_, fetcher, err := d.buildFetcher(req.URL) fb, err := d.parseFb(req.URL)
if err != nil { if err != nil {
return nil, err return
} }
res, err := fetcher.Resolve(req) fetcher := fb.Build()
d.setupFetcher(fetcher)
err = fetcher.Resolve(req)
if err != nil { if err != nil {
return nil, err return
} }
rrId, err := gonanoid.New()
return res, nil if err != nil {
return
}
d.fetcherMap[rrId] = fetcher
rr = &ResolveResult{
ID: rrId,
Res: fetcher.Meta().Res,
}
return
} }
func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId string, err error) { func (d *Downloader) DirectCreate(req *base.Request, opts *base.Options) (taskId string, err error) {
rr, err := d.Resolve(req)
if err != nil {
return
}
return d.Create(rr.ID, opts)
}
func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err error) {
if opts == nil { if opts == nil {
opts = &base.Options{} opts = &base.Options{}
} }
fetcher, ok := d.fetcherMap[rrId]
if !ok {
return "", errors.New("invalid resource id")
}
meta := fetcher.Meta()
meta.Opts = opts
res := meta.Res
if len(opts.SelectFiles) == 0 { if len(opts.SelectFiles) == 0 {
opts.SelectFiles = make([]int, 0) opts.SelectFiles = make([]int, 0)
for i := range res.Files { for i := range res.Files {
@@ -181,20 +220,38 @@ func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId stri
} }
} }
fetcherBuilder, fetcher, err := d.buildFetcher(res.Req.URL) fb, err := d.parseFb(fetcher.Meta().Req.URL)
if err != nil { if err != nil {
return return
} }
err = fetcher.Create(res, opts)
// check if the download file is duplicated and rename it automatically.
files := res.Files
if res.RootDir != "" {
fullDirPath := path.Join(opts.Path, res.RootDir)
newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil {
return "", err
}
res.RootDir = newName
} else if len(files) == 1 {
fullPath := meta.Filepath(files[0])
newName, err := util.CheckDuplicateAndRename(fullPath)
if err != nil {
return "", err
}
opts.Name = newName
}
err = fetcher.Create(opts)
if err != nil { if err != nil {
return return
} }
task := NewTask() task := NewTask()
task.fetcherBuilder = fetcherBuilder task.fetcherBuilder = fb
task.fetcher = fetcher task.fetcher = fetcher
task.Res = res task.Meta = fetcher.Meta()
task.Opts = opts
task.Progress = &Progress{} task.Progress = &Progress{}
task.Status = base.DownloadStatusRunning task.Status = base.DownloadStatusRunning
// calculate select files size // calculate select files size
@@ -217,7 +274,6 @@ func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId stri
} }
d.emit(EventKeyStart, task) d.emit(EventKeyStart, task)
taskId = task.ID taskId = task.ID
go func() { go func() {
err := fetcher.Start() err := fetcher.Start()
if err != nil { if err != nil {
@@ -226,6 +282,7 @@ func (d *Downloader) Create(res *base.Resource, opts *base.Options) (taskId stri
} }
d.watch(task) d.watch(task)
}() }()
delete(d.fetcherMap, rrId)
return return
} }
@@ -255,6 +312,7 @@ func (d *Downloader) Continue(id string) (err error) {
defer task.lock.Unlock() defer task.lock.Unlock()
if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError { if task.Status == base.DownloadStatusPause || task.Status == base.DownloadStatusError {
task.Status = base.DownloadStatusRunning task.Status = base.DownloadStatusRunning
task.Progress.Speed = 0
err = d.restoreFetcher(task) err = d.restoreFetcher(task)
if err != nil { if err != nil {
return return
@@ -300,11 +358,17 @@ func (d *Downloader) Delete(id string, force bool) (err error) {
return return
} }
if force { if force {
names := make([]string, 0) if task.Meta.Res.RootDir != "" {
for _, file := range task.Res.Files { if err = os.RemoveAll(path.Join(task.Meta.Opts.Path, task.Meta.Res.RootDir)); err != nil {
names = append(names, util.Filepath(file.Path, file.Name, task.Opts.Name)) return
}
} else {
for _, file := range task.Meta.Res.Files {
if err = util.SafeRemove(task.Meta.Filepath(file)); err != nil {
return err
}
}
} }
util.SafeRemoveAll(task.Opts.Path, names)
} }
d.emit(EventKeyDelete, task) d.emit(EventKeyDelete, task)
task = nil task = nil
@@ -392,6 +456,7 @@ func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
return true, nil return true, nil
} }
// wait task done
func (d *Downloader) watch(task *Task) { func (d *Downloader) watch(task *Task) {
err := task.fetcher.Wait() err := task.fetcher.Wait()
if err != nil { if err != nil {
@@ -401,7 +466,7 @@ func (d *Downloader) watch(task *Task) {
task.Progress.Used = task.timer.Used() task.Progress.Used = task.timer.Used()
if task.Size == 0 { if task.Size == 0 {
task.Size = task.fetcher.Progress().TotalDownloaded() task.Size = task.fetcher.Progress().TotalDownloaded()
task.Res.Size = task.Size task.Meta.Res.Size = task.Size
} }
used := task.Progress.Used / int64(time.Second) used := task.Progress.Used / int64(time.Second)
if used == 0 { if used == 0 {
@@ -418,30 +483,30 @@ func (d *Downloader) watch(task *Task) {
func (d *Downloader) restoreFetcher(task *Task) error { func (d *Downloader) restoreFetcher(task *Task) error {
if task.fetcher == nil { if task.fetcher == nil {
fetcherBuilder, _, err := d.buildFetcher(task.Res.Req.URL) fb, err := d.parseFb(task.Meta.Req.URL)
if err != nil { if err != nil {
return err return err
} }
task.fetcherBuilder = fetcherBuilder task.fetcherBuilder = fb
err = func() error { err = func() error {
v, f := fetcherBuilder.Restore() v, f := fb.Restore()
if v != nil { if v != nil {
err := d.storage.Pop(bucketSave, task.ID, v) err := d.storage.Pop(bucketSave, task.ID, v)
if err != nil { if err != nil {
return err return err
} }
task.fetcher = f(task.Res, task.Opts, v)
} }
task.fetcher = f(task.Meta, v)
return nil return nil
}() }()
if err != nil { if err != nil {
// TODO log // TODO log
} }
if task.fetcher == nil { if task.fetcher == nil {
task.fetcher = fetcherBuilder.Build() task.fetcher = fb.Build()
} }
d.setupFetcher(task.fetcher) d.setupFetcher(task.fetcher)
task.fetcher.Create(task.Res, task.Opts) task.fetcher.Create(task.Meta.Opts)
go d.watch(task) go d.watch(task)
} }
return nil return nil
@@ -450,6 +515,7 @@ func (d *Downloader) restoreFetcher(task *Task) error {
func initTask(task *Task) { func initTask(task *Task) {
task.timer = &util.Timer{} task.timer = &util.Timer{}
task.lock = &sync.Mutex{} task.lock = &sync.Mutex{}
task.speedArr = make([]int64, 0)
} }
var defaultDownloader = NewDownloader(nil) var defaultDownloader = NewDownloader(nil)
@@ -470,7 +536,7 @@ func (b *boot) Extra(extra interface{}) *boot {
return b return b
} }
func (b *boot) Resolve() (*base.Resource, error) { func (b *boot) Resolve() (*ResolveResult, error) {
return defaultDownloader.Resolve(&base.Request{ return defaultDownloader.Resolve(&base.Request{
URL: b.url, URL: b.url,
Extra: b.extra, Extra: b.extra,
@@ -483,12 +549,12 @@ func (b *boot) Listener(listener Listener) *boot {
} }
func (b *boot) Create(opts *base.Options) (string, error) { func (b *boot) Create(opts *base.Options) (string, error) {
res, err := b.Resolve() rr, err := b.Resolve()
if err != nil { if err != nil {
return "", err return "", err
} }
defaultDownloader.Listener(b.listener) defaultDownloader.Listener(b.listener)
return defaultDownloader.Create(res, opts) return defaultDownloader.Create(rr.ID, opts)
} }
func Boot() *boot { func Boot() *boot {
+10 -11
View File
@@ -1,9 +1,9 @@
package download package download
import ( import (
"github.com/monkeyWie/gopeed/internal/test" "github.com/GopeedLab/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
"reflect" "reflect"
"sync" "sync"
"testing" "testing"
@@ -22,13 +22,12 @@ func TestDownloader_Resolve(t *testing.T) {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
res, err := downloader.Resolve(req) rr, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
want := &base.Resource{ want := &base.Resource{
Name: test.BuildName, Name: test.BuildName,
Req: req,
Size: test.BuildSize, Size: test.BuildSize,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
@@ -39,8 +38,8 @@ func TestDownloader_Resolve(t *testing.T) {
}, },
}, },
} }
if !reflect.DeepEqual(want, res) { if !reflect.DeepEqual(want, rr.Res) {
t.Errorf("Resolve() got = %v, want %v", res, want) t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
} }
} }
@@ -56,7 +55,7 @@ func TestDownloader_Create(t *testing.T) {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
res, err := downloader.Resolve(req) rr, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -68,7 +67,7 @@ func TestDownloader_Create(t *testing.T) {
wg.Done() wg.Done()
} }
}) })
_, err = downloader.Create(res, &base.Options{ _, err = downloader.Create(rr.ID, &base.Options{
Path: test.Dir, Path: test.Dir,
Name: test.DownloadName, Name: test.DownloadName,
Extra: http.OptsExtra{ Extra: http.OptsExtra{
@@ -100,12 +99,12 @@ func TestDownloader_StoreAndRestore(t *testing.T) {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
res, err := downloader.Resolve(req) rr, err := downloader.Resolve(req)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
id, err := downloader.Create(res, &base.Options{ id, err := downloader.Create(rr.ID, &base.Options{
Path: test.Dir, Path: test.Dir,
Name: test.DownloadName, Name: test.DownloadName,
Extra: http.OptsExtra{ Extra: http.OptsExtra{
+65 -5
View File
@@ -1,12 +1,72 @@
package download package download
import ( import (
"github.com/monkeyWie/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
"github.com/monkeyWie/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/internal/protocol/bt" "github.com/GopeedLab/gopeed/internal/protocol/bt"
"github.com/monkeyWie/gopeed/internal/protocol/http" "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"
"sync"
"time"
) )
type ResolveResult struct {
ID string `json:"id"`
Res *base.Resource `json:"res"`
}
type Task struct {
ID string `json:"id"`
Meta *fetcher.FetcherMeta `json:"meta"`
Status base.Status `json:"status"`
Progress *Progress `json:"progress"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
timer *util.Timer
lock *sync.Mutex
speedArr []int64
}
func NewTask() *Task {
id, err := gonanoid.New()
if err != nil {
panic(err)
}
return &Task{
ID: id,
Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
}
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
Meta: t.Meta,
Status: t.Status,
Progress: t.Progress,
Size: t.Size,
CreatedAt: t.CreatedAt,
}
}
func (t *Task) calcSpeed(downloaded int64, usedTime float64) int64 {
t.speedArr = append(t.speedArr, downloaded)
if len(t.speedArr) > 6 {
t.speedArr = t.speedArr[1:]
}
var total int64
for _, v := range t.speedArr {
total += v
}
return int64(float64(total) / float64(len(t.speedArr)) / usedTime)
}
type DownloaderConfig struct { type DownloaderConfig struct {
Controller *controller.Controller Controller *controller.Controller
FetchBuilders []fetcher.FetcherBuilder FetchBuilders []fetcher.FetcherBuilder
@@ -34,7 +94,7 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
} }
if cfg.RefreshInterval == 0 { if cfg.RefreshInterval == 0 {
cfg.RefreshInterval = 1000 cfg.RefreshInterval = 350
} }
return cfg return cfg
} }
-49
View File
@@ -1,49 +0,0 @@
package download
import (
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/monkeyWie/gopeed/internal/fetcher"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/util"
"sync"
"time"
)
type Task struct {
ID string `json:"id"`
Res *base.Resource `json:"res"`
Opts *base.Options `json:"opts"`
Status base.Status `json:"status"`
Progress *Progress `json:"progress"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
timer *util.Timer
lock *sync.Mutex
}
func NewTask() *Task {
id, err := gonanoid.New()
if err != nil {
panic(err)
}
return &Task{
ID: id,
Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
}
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
Res: t.Res,
Opts: t.Opts,
Status: t.Status,
Progress: t.Progress,
Size: t.Size,
CreatedAt: t.CreatedAt,
}
}
+90 -49
View File
@@ -1,12 +1,10 @@
package rest package rest
import ( import (
"compress/gzip" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/monkeyWie/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"github.com/monkeyWie/gopeed/pkg/rest/util"
"io" "io"
"net/http" "net/http"
"net/url" "net/url"
@@ -15,75 +13,107 @@ import (
func Resolve(w http.ResponseWriter, r *http.Request) { func Resolve(w http.ResponseWriter, r *http.Request) {
var req base.Request var req base.Request
if util.ReadJson(w, r, &req) { if ReadJson(r, w, &req) {
resource, err := Downloader.Resolve(&req) rr, err := Downloader.Resolve(&req)
if err != nil { if err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) WriteJson(w, model.NewErrorResult(err.Error()))
return return
} }
util.WriteJsonOk(w, model.NewResultWithData(resource)) WriteJson(w, model.NewOkResult(rr))
} }
} }
func CreateTask(w http.ResponseWriter, r *http.Request) { func CreateTask(w http.ResponseWriter, r *http.Request) {
var req model.CreateTask var req model.CreateTask
if util.ReadJson(w, r, &req) { if ReadJson(r, w, &req) {
taskId, err := Downloader.Create(req.Res, req.Opts) var (
if err != nil { taskId string
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) err error
)
if req.Rid != "" {
taskId, err = Downloader.Create(req.Rid, req.Opts)
} else if req.Req != nil {
taskId, err = Downloader.DirectCreate(req.Req, req.Opts)
} else {
WriteJson(w, model.NewErrorResult("param invalid: rid or req", model.CodeInvalidParam))
return return
} }
util.WriteJsonOk(w, model.NewResultWithData(taskId)) if err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewOkResult(taskId))
} }
} }
func PauseTask(w http.ResponseWriter, r *http.Request) { func PauseTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
taskId := vars["id"] taskId := vars["id"]
if err := Downloader.Pause(taskId); err != nil { if taskId == "" {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return return
} }
util.WriteJsonOk(w, nil) if err := Downloader.Pause(taskId); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
} }
func ContinueTask(w http.ResponseWriter, r *http.Request) { func ContinueTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
taskId := vars["id"] taskId := vars["id"]
if err := Downloader.Continue(taskId); err != nil { if taskId == "" {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return return
} }
util.WriteJsonOk(w, nil) if err := Downloader.Continue(taskId); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
} }
func DeleteTask(w http.ResponseWriter, r *http.Request) { func DeleteTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
taskId := vars["id"] taskId := vars["id"]
force := r.FormValue("force") force := r.FormValue("force")
if err := Downloader.Delete(taskId, force == "true"); err != nil { if taskId == "" {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
return return
} }
util.WriteJsonOk(w, nil) if err := Downloader.Delete(taskId, force == "true"); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return
}
WriteJson(w, model.NewNilResult())
} }
func GetTask(w http.ResponseWriter, r *http.Request) { func GetTask(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r) vars := mux.Vars(r)
taskId := vars["id"] taskId := vars["id"]
task := Downloader.GetTask(taskId) if taskId == "" {
if task == nil { WriteJson(w, model.NewErrorResult("param invalid: id", model.CodeInvalidParam))
util.WriteJson(w, http.StatusNotFound, model.NewResultWithMsg("task not found"))
return return
} }
util.WriteJsonOk(w, model.NewResultWithData(task)) task := Downloader.GetTask(taskId)
if task == nil {
WriteJson(w, model.NewErrorResult("task not found", model.CodeTaskNotFound))
return
}
WriteJson(w, model.NewOkResult(task))
} }
func GetTasks(w http.ResponseWriter, r *http.Request) { func GetTasks(w http.ResponseWriter, r *http.Request) {
status := r.FormValue("status") status := r.FormValue("status")
if status == "" { if status == "" {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("param is required: status")) status = strings.Join([]string{
return string(base.DownloadStatusReady),
string(base.DownloadStatusRunning),
string(base.DownloadStatusPause),
string(base.DownloadStatusError),
string(base.DownloadStatusDone),
}, ",")
} }
statusArr := strings.Split(status, ",") statusArr := strings.Split(status, ",")
tasks := Downloader.GetTasks() tasks := Downloader.GetTasks()
@@ -95,33 +125,33 @@ func GetTasks(w http.ResponseWriter, r *http.Request) {
} }
} }
} }
util.WriteJsonOk(w, model.NewResultWithData(result)) WriteJson(w, model.NewOkResult(result))
} }
func GetConfig(w http.ResponseWriter, r *http.Request) { func GetConfig(w http.ResponseWriter, r *http.Request) {
util.WriteJsonOk(w, model.NewResultWithData(getServerConfig())) WriteJson(w, model.NewOkResult(getServerConfig()))
} }
func PutConfig(w http.ResponseWriter, r *http.Request) { func PutConfig(w http.ResponseWriter, r *http.Request) {
var cfg download.DownloaderStoreConfig var cfg download.DownloaderStoreConfig
if util.ReadJson(w, r, &cfg) { if ReadJson(r, w, &cfg) {
if err := Downloader.PutConfig(&cfg); err != nil { if err := Downloader.PutConfig(&cfg); err != nil {
util.WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error())) WriteJson(w, model.NewErrorResult(err.Error()))
return return
} }
} }
util.WriteJsonOk(w, nil) WriteJson(w, model.NewNilResult())
} }
func DoProxy(w http.ResponseWriter, r *http.Request) { func DoProxy(w http.ResponseWriter, r *http.Request) {
target := r.Header.Get("X-Target-Uri") target := r.Header.Get("X-Target-Uri")
if target == "" { if target == "" {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg("header is required: X-Target-Uri")) writeError(w, "param invalid: X-Target-Uri")
return return
} }
targetUrl, err := url.Parse(target) targetUrl, err := url.Parse(target)
if err != nil { if err != nil {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error())) writeError(w, err.Error())
return return
} }
r.RequestURI = "" r.RequestURI = ""
@@ -129,28 +159,39 @@ func DoProxy(w http.ResponseWriter, r *http.Request) {
r.Host = targetUrl.Host r.Host = targetUrl.Host
resp, err := http.DefaultClient.Do(r) resp, err := http.DefaultClient.Do(r)
if err != nil { if err != nil {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error())) writeError(w, err.Error())
return return
} }
defer resp.Body.Close() defer resp.Body.Close()
w.WriteHeader(resp.StatusCode)
for k, vv := range resp.Header { for k, vv := range resp.Header {
for _, v := range vv { for _, v := range vv {
w.Header().Add(k, v) w.Header().Set(k, v)
} }
} }
var reader io.ReadCloser w.WriteHeader(resp.StatusCode)
switch resp.Header.Get("Content-Encoding") { buf, err := io.ReadAll(resp.Body)
case "gzip": if err != nil {
reader, err = gzip.NewReader(resp.Body) writeError(w, err.Error())
defer reader.Close()
default:
reader = resp.Body
}
if _, err := io.Copy(w, reader); err != nil {
util.WriteJson(w, http.StatusBadRequest, model.NewResultWithMsg(err.Error()))
return return
} }
w.Write(buf)
//var reader io.ReadCloser
//switch resp.Header.Get("Content-Encoding") {
//case "gzip":
// reader, err = gzip.NewReader(resp.Body)
// defer reader.Close()
//default:
// reader = resp.Body
//}
//if _, err := io.Copy(w, resp.Body); err != nil {
// writeError(w, err.Error())
// return
//}
}
func writeError(w http.ResponseWriter, msg string) {
w.WriteHeader(http.StatusInternalServerError)
w.Write([]byte(msg))
} }
func getServerConfig() *download.DownloaderStoreConfig { func getServerConfig() *download.DownloaderStoreConfig {
+34 -8
View File
@@ -1,21 +1,47 @@
package model package model
type RespCode int
const (
CodeOk RespCode = 0
// CodeError is the common error code
CodeError RespCode = 1000
// CodeUnauthorized is the error code for unauthorized
CodeUnauthorized RespCode = 1001
// CodeInvalidParam is the error code for invalid parameter
CodeInvalidParam RespCode = 1002
// CodeTaskNotFound is the error code for task not found
CodeTaskNotFound RespCode = 2001
)
type Result[T any] struct { type Result[T any] struct {
Msg string `json:"msg"` Code RespCode `json:"code"`
Data T `json:"data"` Msg string `json:"msg"`
Data T `json:"data"`
} }
func NewResult[T any](msg string, data T) *Result[T] { func NewOkResult[T any](data T) *Result[T] {
return &Result[T]{ return &Result[T]{
Msg: msg, Code: CodeOk,
Data: data, Data: data,
} }
} }
func NewResultWithMsg(msg string) *Result[any] { func NewNilResult() *Result[any] {
return NewResult[any](msg, nil) return &Result[any]{
Code: CodeOk,
}
} }
func NewResultWithData[T any](data T) *Result[T] { func NewErrorResult(msg string, code ...RespCode) *Result[any] {
return NewResult[T]("", data) // if code is not provided, the default code is CodeError
c := CodeError
if len(code) > 0 {
c = code[0]
}
return &Result[any]{
Code: c,
Msg: msg,
}
} }
+4 -3
View File
@@ -1,8 +1,9 @@
package model package model
import "github.com/monkeyWie/gopeed/pkg/base" import "github.com/GopeedLab/gopeed/pkg/base"
type CreateTask struct { type CreateTask struct {
Res *base.Resource `json:"res"` Rid string `json:"rid"`
Opts *base.Options `json:"opts"` Req *base.Request `json:"req"`
Opts *base.Options `json:"opts"`
} }
+19 -5
View File
@@ -2,12 +2,12 @@ package rest
import ( import (
"context" "context"
"encoding/json"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/monkeyWie/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model"
restUtil "github.com/monkeyWie/gopeed/pkg/rest/util"
"github.com/monkeyWie/gopeed/pkg/util"
"net" "net"
"net/http" "net/http"
) )
@@ -98,7 +98,7 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Use(func(h http.Handler) http.Handler { r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Api-Token") != startCfg.ApiToken { if r.Header.Get("X-Api-Token") != startCfg.ApiToken {
restUtil.WriteJson(w, http.StatusUnauthorized, model.NewResultWithMsg("invalid token")) WriteJson(w, model.NewErrorResult("invalid token", model.CodeUnauthorized))
return return
} }
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
@@ -113,3 +113,17 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
)(r)} )(r)}
return srv, listener, nil return srv, listener, nil
} }
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, model.NewErrorResult(err.Error()))
return false
}
return true
}
func WriteJson(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(v)
}
+34 -22
View File
@@ -6,10 +6,10 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/monkeyWie/gopeed/internal/test" "github.com/GopeedLab/gopeed/internal/test"
"github.com/monkeyWie/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/monkeyWie/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/monkeyWie/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"io" "io"
"net" "net"
"net/http" "net/http"
@@ -34,7 +34,6 @@ var (
} }
taskRes = &base.Resource{ taskRes = &base.Resource{
Name: test.BuildName, Name: test.BuildName,
Req: taskReq,
Size: test.BuildSize, Size: test.BuildSize,
Range: true, Range: true,
Files: []*base.FileInfo{ Files: []*base.FileInfo{
@@ -46,7 +45,7 @@ var (
}, },
} }
createReq = &model.CreateTask{ createReq = &model.CreateTask{
Res: taskRes, Req: taskReq,
Opts: &base.Options{ Opts: &base.Options{
Path: test.Dir, Path: test.Dir,
Name: test.DownloadName, Name: test.DownloadName,
@@ -59,9 +58,9 @@ var (
func TestResolve(t *testing.T) { func TestResolve(t *testing.T) {
doTest(func() { doTest(func() {
resp := httpRequestCheckOk[*base.Resource](http.MethodPost, "/api/v1/resolve", taskReq) resp := httpRequestCheckOk[*download.ResolveResult](http.MethodPost, "/api/v1/resolve", taskReq)
if !test.JsonEqual(taskRes, resp) { if !test.JsonEqual(taskRes, resp.Res) {
t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp), test.ToJson(taskRes)) t.Errorf("Resolve() got = %v, want %v", test.ToJson(resp.Res), test.ToJson(taskRes))
} }
}) })
} }
@@ -148,19 +147,20 @@ func TestPauseAndContinueTask(t *testing.T) {
func TestDeleteTask(t *testing.T) { func TestDeleteTask(t *testing.T) {
doTest(func() { doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq) taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
time.Sleep(time.Millisecond * 200)
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil) httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId, nil)
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil) code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
checkCode(code, http.StatusNotFound) checkCode(code, model.CodeTaskNotFound)
}) })
} }
func TestDeleteTaskForce(t *testing.T) { func TestDeleteTaskForce(t *testing.T) {
doTest(func() { doTest(func() {
taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq) taskId := httpRequestCheckOk[string](http.MethodPost, "/api/v1/tasks", createReq)
time.Sleep(time.Millisecond * 500) time.Sleep(time.Millisecond * 200)
httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil) httpRequestCheckOk[any](http.MethodDelete, "/api/v1/tasks/"+taskId+"?force=true", nil)
code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil) code, _ := httpRequest[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
checkCode(code, http.StatusNotFound) checkCode(code, model.CodeTaskNotFound)
if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(test.DownloadFile); !errors.Is(err, os.ErrNotExist) {
t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist) t.Errorf("DeleteTaskForce() got = %v, want %v", err, os.ErrNotExist)
} }
@@ -179,8 +179,7 @@ func TestGetTasks(t *testing.T) {
httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s", httpRequestCheckOk[string](http.MethodPost, fmt.Sprintf("/api/v1/tasks?status=%s,%s",
base.DownloadStatusReady, base.DownloadStatusRunning), createReq) base.DownloadStatusReady, base.DownloadStatusRunning), createReq)
code, _ := httpRequest[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil) httpRequestCheckOk[[]*download.Task](http.MethodGet, "/api/v1/tasks", nil)
checkCode(code, http.StatusBadRequest)
wg.Wait() wg.Wait()
r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", r := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s",
@@ -219,7 +218,7 @@ func TestGetAndPutConfig(t *testing.T) {
func TestDoProxy(t *testing.T) { func TestDoProxy(t *testing.T) {
doTest(func() { doTest(func() {
code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{ code, respBody := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
"X-Target-Uri": "https://github.com/monkeyWie/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md", "X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/README.md",
}, nil) }, nil)
if code != http.StatusOK { if code != http.StatusOK {
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusOK) t.Errorf("DoProxy() got = %v, want %v", code, http.StatusOK)
@@ -230,6 +229,15 @@ func TestDoProxy(t *testing.T) {
t.Errorf("DoProxy() got = %v, want %v", got, want) t.Errorf("DoProxy() got = %v, want %v", got, want)
} }
}) })
doTest(func() {
code, _ := doHttpRequest0(http.MethodGet, "/api/v1/proxy", map[string]string{
"X-Target-Uri": "https://github.com/GopeedLab/gopeed/raw/695da7ea87d2b455552b709d3cb4d7879484d4d1/NOT_FOUND",
}, nil)
if code != http.StatusNotFound {
t.Errorf("DoProxy() got = %v, want %v", code, http.StatusNotFound)
}
})
} }
func TestAuthorization(t *testing.T) { func TestAuthorization(t *testing.T) {
@@ -245,12 +253,12 @@ func TestAuthorization(t *testing.T) {
}() }()
code, _ := doHttpRequest[any](http.MethodGet, "/api/v1/config", nil, nil) code, _ := doHttpRequest[any](http.MethodGet, "/api/v1/config", nil, nil)
checkCode(code, http.StatusUnauthorized) checkCode(code, model.CodeUnauthorized)
code, _ = doHttpRequest[any](http.MethodGet, "/api/v1/config", map[string]string{ code, _ = doHttpRequest[any](http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken, "X-Api-Token": cfg.ApiToken,
}, nil) }, nil)
checkCode(code, http.StatusOK) checkCode(code, model.CodeOk)
} }
@@ -308,17 +316,21 @@ func doHttpRequest0(method string, path string, headers map[string]string, body
if err != nil { if err != nil {
panic(err) panic(err)
} }
return response.StatusCode, respBody return response.StatusCode, respBody
} }
func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) { func doHttpRequest[T any](method string, path string, headers map[string]string, body any) (int, *model.Result[T]) {
code, respBody := doHttpRequest0(method, path, headers, body) statusCode, respBody := doHttpRequest0(method, path, headers, body)
if statusCode != http.StatusOK {
panic(fmt.Sprintf("http request failed, status code: %d", statusCode))
}
var r model.Result[T] var r model.Result[T]
if err := json.Unmarshal(respBody, &r); err != nil { if err := json.Unmarshal(respBody, &r); err != nil {
panic(err) panic(err)
} }
return code, &r return int(r.Code), &r
} }
func httpRequest[T any](method string, path string, body any) (int, *model.Result[T]) { func httpRequest[T any](method string, path string, body any) (int, *model.Result[T]) {
@@ -332,11 +344,11 @@ func httpRequestCheckOk[T any](method string, path string, body any) T {
} }
func checkOk(code int) { func checkOk(code int) {
checkCode(code, 200) checkCode(code, model.CodeOk)
} }
func checkCode(code int, exceptCode int) { func checkCode(code int, exceptCode model.RespCode) {
if code != exceptCode { if code != int(exceptCode) {
panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode)) panic(fmt.Sprintf("code got = %d, want %d", code, exceptCode))
} }
} }
-25
View File
@@ -1,25 +0,0 @@
package util
import (
"encoding/json"
"github.com/monkeyWie/gopeed/pkg/rest/model"
"net/http"
)
func ReadJson(w http.ResponseWriter, r *http.Request, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, http.StatusInternalServerError, model.NewResultWithMsg(err.Error()))
return false
}
return true
}
func WriteJsonOk(w http.ResponseWriter, v any) {
WriteJson(w, http.StatusOK, v)
}
func WriteJson(w http.ResponseWriter, code int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(code)
json.NewEncoder(w).Encode(v)
}
+45 -27
View File
@@ -2,13 +2,15 @@ package util
import ( import (
"errors" "errors"
"fmt"
"io" "io"
"os" "os"
"path/filepath" syspath "path"
"strings"
) )
func Dir(path string) string { func Dir(path string) string {
dir := filepath.Dir(path) dir := syspath.Dir(path)
if dir == "." { if dir == "." {
return "" return ""
} }
@@ -19,7 +21,7 @@ func Filepath(path string, originName string, customName string) string {
if customName == "" { if customName == "" {
customName = originName customName = originName
} }
return filepath.Join(path, customName) return syspath.Join(path, customName)
} }
// SafeRemove remove file safely, ignoring errors if the path does not exist. // SafeRemove remove file safely, ignoring errors if the path does not exist.
@@ -30,39 +32,55 @@ func SafeRemove(name string) error {
return nil return nil
} }
// SafeRemoveAll remove file and parent directories safely // CheckDuplicateAndRename rename duplicate file, add suffix (1) (2) ...
func SafeRemoveAll(path string, names []string) error { // if file name is a.txt, rename to a (1).txt
for _, name := range names { // if directory name is a, rename to a (1)
err := SafeRemove(filepath.Join(path, name)) // return new name
if err != nil { func CheckDuplicateAndRename(path string) (string, error) {
return err dir := syspath.Dir(path)
name := syspath.Base(path)
// if file not exists, return directly
_, err := os.Stat(path)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return name, nil
} }
if err := safeRemoveParent(path, name); err != nil { return "", err
return err }
index := strings.LastIndex(name, ".")
var nameTpl string
if index == -1 {
nameTpl = name + " (%d)"
} else {
nameTpl = name[:index] + " (%d)" + name[index:]
}
for i := 1; ; i++ {
newName := fmt.Sprintf(nameTpl, i)
newPath := syspath.Join(dir, newName)
if _, err := os.Stat(newPath); os.IsNotExist(err) {
return newName, nil
} }
} }
return nil
} }
func safeRemoveParent(path string, subPath string) error { // GetSingleDir get the top level single folder name,if not exist, return empty string
currPath := filepath.Dir(subPath) func GetSingleDir(paths []string) string {
if currPath == "." { if len(paths) == 0 {
return nil return ""
} }
// if directory is empty, remove it split := strings.Split(paths[0], "/")
dir := filepath.Join(path, filepath.Dir(subPath)) if len(split) == 0 || split[0] == "" {
empty, err := isEmpty(dir) return ""
if err != nil {
return err
} }
if empty { dir := split[0]
err = SafeRemove(dir) for i := 1; i < len(paths); i++ {
if err != nil { if !strings.HasPrefix(paths[i], dir) {
return err return ""
} }
return safeRemoveParent(path, currPath)
} }
return nil return dir
} }
// check directory is empty // check directory is empty
+93 -90
View File
@@ -2,7 +2,6 @@ package util
import ( import (
"os" "os"
"path/filepath"
"testing" "testing"
) )
@@ -87,10 +86,13 @@ func TestFilepath(t *testing.T) {
func TestSafeRemove(t *testing.T) { func TestSafeRemove(t *testing.T) {
name := "test_safe_remove.data" name := "test_safe_remove.data"
_, err := os.Create(name) file, err := os.Create(name)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := file.Close(); err != nil {
t.Fatal(err)
}
if err := SafeRemove(name); err != nil { if err := SafeRemove(name); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -103,96 +105,97 @@ func TestSafeRemove(t *testing.T) {
} }
} }
func TestSafeRemoveAll(t *testing.T) { func TestGetSingleDir(t *testing.T) {
testDir := "test_dir" type args struct {
err := os.Mkdir(testDir, 0755) paths []string
}
tests := []struct {
name string
args args
want string
}{
{
name: "empty",
args: args{
paths: []string{},
},
want: "",
},
{
name: "blank",
args: args{
paths: []string{""},
},
want: "",
},
{
name: "blank multi",
args: args{
paths: []string{"", "a"},
},
want: "",
},
{
name: "multi dir not single",
args: args{
paths: []string{"a", "b"},
},
want: "",
},
{
name: "single dir",
args: args{
paths: []string{"a"},
},
want: "a",
},
{
name: "multi dir single",
args: args{
paths: []string{"a/b", "a/c"},
},
want: "a",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := GetSingleDir(tt.args.paths); got != tt.want {
t.Errorf("IsSingleDir() = %v, want %v", got, tt.want)
}
})
}
}
func TestCheckDuplicateAndRename(t *testing.T) {
doCheckDuplicateAndRename(t, []string{}, "a.txt", "a.txt")
doCheckDuplicateAndRename(t, []string{"a.txt"}, "a.txt", "a (1).txt")
doCheckDuplicateAndRename(t, []string{"a.txt", "a (1).txt"}, "a.txt", "a (2).txt")
doCheckDuplicateAndRename(t, []string{}, "a", "a")
doCheckDuplicateAndRename(t, []string{"a"}, "a", "a (1)")
doCheckDuplicateAndRename(t, []string{"a", "a (1)"}, "a", "a (2)")
}
func doCheckDuplicateAndRename(t *testing.T, exitsPaths []string, path string, except string) {
for _, path := range exitsPaths {
if err := os.MkdirAll(path, 0755); err != nil {
t.Fatal(err)
}
}
defer func() {
for _, path := range exitsPaths {
if err := os.RemoveAll(path); err != nil {
t.Fatal(err)
}
}
}()
got, err := CheckDuplicateAndRename(path)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer os.RemoveAll(testDir) if got != except {
t.Errorf("CheckDuplicateAndRename() = %v, want %v", got, except)
doSafeRemoveAll(t, testDir, []string{
"1.txt",
}, []string{})
doSafeRemoveAll(t, testDir, []string{
"1.txt",
}, []string{
"2.txt",
}, "2.txt")
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{})
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{
"a/b/2.txt",
}, "a/b/2.txt")
doSafeRemoveAll(t, testDir, []string{
"1.txt",
"a/b/c/1.txt",
"a/b/1.txt",
"a/1.txt",
}, []string{
"a/2.txt",
}, "a/2.txt")
}
func doSafeRemoveAll(t *testing.T, path string, downloadNames []string, otherNames []string, exist ...string) {
preCreate(t, path, downloadNames)
preCreate(t, path, otherNames)
if err := SafeRemoveAll(path, downloadNames); err != nil {
t.Fatal(err)
}
if len(exist) == 0 {
for _, name := range downloadNames {
filePath := filepath.Join(path, name)
if isExist(filePath) {
t.Fatalf("file %s should not exist", filePath)
}
subDirPath := filepath.Dir(name)
if subDirPath != "." {
dirPath := filepath.Join(path, subDirPath)
if isExist(dirPath) {
t.Fatalf("dir %s should not exist", dirPath)
}
}
}
return
}
for _, name := range exist {
if !isExist(filepath.Join(path, name)) {
t.Fatalf("file %s should exist", name)
}
} }
} }
func preCreate(t *testing.T, path string, names []string) {
for _, name := range names {
fullPath := filepath.Join(path, name)
if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil {
t.Fatal(err)
}
_, err := os.Create(fullPath)
if err != nil {
t.Fatal(err)
}
}
}
func isExist(path string) bool {
_, err := os.Stat(path)
return err == nil || os.IsExist(err)
}
+2 -1
View File
@@ -29,7 +29,8 @@
.packages .packages
.pub-cache/ .pub-cache/
.pub/ .pub/
/build/ build/
.fvm/
# Web related # Web related
+5 -20
View File
@@ -4,7 +4,7 @@
# This file should be version controlled. # This file should be version controlled.
version: version:
revision: f1875d570e39de09040c8f79aa13cc56baab8db1 revision: 7048ed95a5ad3e43d697e0c397464193991fc230
channel: stable channel: stable
project_type: app project_type: app
@@ -13,26 +13,11 @@ project_type: app
migration: migration:
platforms: platforms:
- platform: root - platform: root
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
- platform: android
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: ios
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: linux
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: macos
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: web
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1
- platform: windows - platform: windows
create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 create_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 base_revision: 7048ed95a5ad3e43d697e0c397464193991fc230
# User provided section # User provided section
-16
View File
@@ -1,16 +0,0 @@
# gopeed
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.
+15 -4
View File
@@ -48,16 +48,27 @@ android {
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 19 minSdkVersion 19
targetSdkVersion flutter.targetSdkVersion targetSdkVersion 29
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
signingConfigs {
release {
keyAlias "upload"
keyPassword System.getenv("APK_KEY_PASSWORD")
storeFile file("upload-keystore.jks")
storePassword System.getenv("APK_STORE_PASSWORD")
}
}
buildTypes { buildTypes {
release { release {
// TODO: Add your own signing config for the release build. if (signingConfigs.release.keyPassword != null) {
// Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release
signingConfig signingConfigs.debug } else {
signingConfig signingConfigs.debug
}
} }
} }
Whitespace-only changes.
@@ -4,9 +4,5 @@
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<application android:requestLegacyExternalStorage="true"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest> </manifest>
@@ -2,11 +2,8 @@
package="com.gopeed"> package="com.gopeed">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application <application
android:requestLegacyExternalStorage="true"
android:name="${applicationName}" android:name="${applicationName}"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="Gopeed"> android:label="Gopeed">
@@ -4,9 +4,5 @@
the Flutter tool needs it to communicate with the running application the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc. to allow setting breakpoints, to provide hot reload, etc.
--> -->
<application android:requestLegacyExternalStorage="true"/>
<uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
</manifest> </manifest>
Binary file not shown.
+57
View File
@@ -0,0 +1,57 @@
{
"label": "English",
"error": "Error",
"tip": "Tip",
"confirm": "Confirm",
"cancel": "Cancel",
"selectAll": "Select All",
"task": "Task",
"downloading": "downloading",
"downloaded": "downloaded",
"setting": "Setting",
"create": "Create Task",
"downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link",
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
"downloadLinkHitDesktop": ", or drag the torrent file here directly",
"download": "Download",
"noFileSelected": "Please select at least one file to continue.",
"noStoragePermission": "Storage permission required",
"selectFile": "Select File",
"basic": "Basic",
"advanced": "Advanced",
"general": "General",
"downloadDir": "Download Directory",
"downloadDirValid": "Please select the download directory",
"connections": "Connections",
"items": "@count items",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
"update": "Update",
"updateDaily": "Update daily",
"lastUpdate": "Last update: @time",
"addTracker": "Add Tracker",
"addTrackerHit": "Please enter the tracker server url, one per line",
"ui": "UI",
"theme": "Theme",
"themeSystem": "System",
"themeLight": "Light",
"themeDark": "Dark",
"locale": "Language",
"about": "About",
"homepage": "Homepage",
"version": "Version",
"protocol": "Protocol",
"port": "Port",
"apiToken": "API Token",
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"deleteTask": "Delete Task",
"deleteTaskTip": "Keep downloaded files",
"delete": "Delete",
"newVersionTitle": "Discover new version @version",
"newVersionUpdate": "Download Now",
"newVersionLater": "Later"
}
+54
View File
@@ -0,0 +1,54 @@
{
"label": "فارسی",
"error": "اررور",
"tip": "Tip",
"confirm": "تایید",
"cancel": "انصراف",
"selectAll": "انتخاب همه",
"task": "کار",
"setting": "تنظیمات",
"create": "ایجاد کار",
"downloading": "دانلود کردن",
"downloaded": "دانلود شدن",
"downloadLink": "لینک دانلود",
"downloadLinkValid": "لطفا لینک دانلود را وارد کنید",
"download": "دانلود",
"noFileSelected": "لطفا حداقل یک فایل را جهت دانلود انتخاب کنید.",
"noStoragePermission": "مجوز ذخیره سازی مورد نیاز است",
"selectFile": "انتخاب فایل",
"basic": "پایه",
"advanced": "پیشرفته",
"general": "عمومی",
"downloadDir": "دایرکتوری دانلود",
"downloadDirValid": "لطفا دایرکتوری دانلود را انتخاب کنید",
"connections": "اتصالات",
"items": "@count آیتم ها",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
"update": "بروزرسانی",
"updateDaily": "بروزرسانی روزانه",
"lastUpdate": "Last update: @time",
"addTracker": "Add Tracker",
"addTrackerHit": "Please enter the tracker server url, one per line",
"ui": "رابط کاربری",
"theme": "پوسته",
"themeSystem": "سیستم",
"themeLight": "روشن",
"themeDark": "تاریک",
"locale": "Language",
"about": "درباره ی",
"homepage": "صفحه اصلی",
"version": "نسخه",
"protocol": "پروتکل",
"port": "پورت",
"apiToken": "API توکن",
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"deleteTask": "پاک کردن کار",
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
"delete": "پاک کردن",
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
"newVersionUpdate": "دریافت اکنون",
"newVersionLater": "بعداً"
}
+56
View File
@@ -0,0 +1,56 @@
{
"label": "Русский(Россия)",
"error": "Ошибка",
"tip": "Подсказка",
"confirm": "Подтвердить",
"cancel": "Отмена",
"selectAll": "Выбрать все",
"task": "Задачи",
"downloading": "Загрузка",
"downloaded": "Загружено",
"setting": "Настройки",
"create": "Создать задачу",
"downloadLink": "Ссылка для скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append",
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
"download": "Скачать",
"noFileSelected": "Файл не выбран",
"noStoragePermission": "Требуется доступ к хранилищу",
"selectFile": "Выберать файл",
"basic": "Основные параметры",
"advanced": "Расширенные",
"general": "Общие",
"downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений",
"items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
"update": "Обновить",
"updateDaily": "Обновлять ежедневно",
"lastUpdate": "Последнее обновление: @время",
"addTracker": "Добавить трекер",
"addTrackerHit": "Пожалуйста, введите url трекера, по одному в строке",
"ui": "UI",
"theme": "Тема",
"themeSystem": "Системная",
"themeLight": "Светлая",
"themeDark": "Темная",
"locale": "Язык",
"about": "О приложении",
"homepage": "Домашняя страница",
"version": "Версия",
"protocol": "Протокол",
"port": "Порт",
"apiToken": "API Токен",
"notSet": "Не установлено",
"set": "Установлено",
"effectAfterRestart": "Эффект после перезагрузки",
"deleteTask": "Удалить задачу",
"deleteTaskTip": "Сохранить загруженные файлы",
"delete": "Удалить",
"newVersionTitle": "Обнаружена новая версия @version",
"newVersionUpdate": "Скачать сейчас",
"newVersionLater": "позже"
}
+56
View File
@@ -0,0 +1,56 @@
{
"label": "中文(简体)",
"error": "错误",
"tip": "提示",
"confirm": "确认",
"cancel": "取消",
"selectAll": "全选",
"task": "任务",
"downloading": "下载中",
"downloaded": "已完成",
"setting": "设置",
"create": "创建任务",
"downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接",
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
"downloadLinkHitDesktop": ",也可以直接拖拽种子文件到此处",
"download": "下载",
"noFileSelected": "请至少选择一个文件下载",
"noStoragePermission": "需要开启存储权限",
"selectFile": "选择文件",
"basic": "基础",
"advanced": "高级",
"general": "通用",
"downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录",
"connections": "连接数",
"items": "@count 条",
"subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试",
"update": "更新",
"updateDaily": "每天自动更新",
"lastUpdate": "上次更新:@time",
"addTracker": "添加 Tracker",
"addTrackerHit": "请输入 tracker 服务器地址,每行一条",
"ui": "界面",
"theme": "主题",
"themeSystem": "跟随系统",
"themeLight": "明亮主题",
"themeDark": "暗黑主题",
"locale": "语言",
"about": "关于",
"homepage": "主页",
"version": "版本",
"protocol": "通讯协议",
"port": "端口",
"apiToken": "接口令牌",
"notSet": "未设置",
"set": "已设置",
"effectAfterRestart": "此配置项将在重启应用后生效",
"deleteTask": "删除任务",
"deleteTaskTip": "保留已下载的文件",
"delete": "删除",
"newVersionTitle": "发现新版本 @version",
"newVersionUpdate": "立即下载",
"newVersionLater": "稍后再说"
}
+1 -1
View File
@@ -1,6 +1,6 @@
/* Code generated by cmd/cgo; DO NOT EDIT. */ /* Code generated by cmd/cgo; DO NOT EDIT. */
/* package github.com/monkeyWie/gopeed/bind/desktop */ /* package github.com/GopeedLab/gopeed/bind/desktop */
#line 1 "cgo-builtin-export-prolog" #line 1 "cgo-builtin-export-prolog"
+21 -13
View File
@@ -1,12 +1,13 @@
import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'package:dio/adapter.dart';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'model/resolve_result.dart';
import '../util/util.dart'; import '../util/util.dart';
import 'model/create_task.dart'; import 'model/create_task.dart';
import 'model/request.dart'; import 'model/request.dart';
import 'model/resource.dart';
import 'model/result.dart'; import 'model/result.dart';
import 'model/task.dart'; import 'model/task.dart';
@@ -33,6 +34,9 @@ class _Client {
} }
} }
dio.options.baseUrl = baseUrl; dio.options.baseUrl = baseUrl;
dio.options.contentType = Headers.jsonContentType;
dio.options.connectTimeout = const Duration(seconds: 5);
dio.options.receiveTimeout = const Duration(seconds: 60);
dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) { dio.interceptors.add(InterceptorsWrapper(onRequest: (options, handler) {
if (apiToken.isNotEmpty) { if (apiToken.isNotEmpty) {
options.headers['X-Api-Token'] = apiToken; options.headers['X-Api-Token'] = apiToken;
@@ -42,7 +46,7 @@ class _Client {
_instance!.dio = dio; _instance!.dio = dio;
if (isUnixSocket) { if (isUnixSocket) {
(_instance!.dio.httpClientAdapter as DefaultHttpClientAdapter) (_instance!.dio.httpClientAdapter as IOHttpClientAdapter)
.onHttpClientCreate = (client) { .onHttpClientCreate = (client) {
client.connectionFactory = client.connectionFactory =
(Uri uri, String? proxyHost, int? proxyPort) { (Uri uri, String? proxyHost, int? proxyPort) {
@@ -70,22 +74,24 @@ Future<T> _parse<T>(
try { try {
var resp = await fetch(); var resp = await fetch();
if (fromJsonT != null) { if (fromJsonT != null) {
return Result<T>.fromJson(resp.data, fromJsonT).data as T; final result = Result<T>.fromJson(resp.data, fromJsonT);
if (result.code == 0) {
return result.data as T;
} else {
throw Exception(result);
}
} else { } else {
return null as T; return null as T;
} }
} on DioError catch (e) { } on DioError catch (e) {
if (e.response == null) { throw Exception(Result(code: 1000, msg: e.message));
throw Exception("Server error");
}
throw Exception(Result.fromJson(e.response?.data, (_) => null).msg);
} }
} }
Future<Resource> resolve(Request request) async { Future<ResolveResult> resolve(Request request) async {
return _parse<Resource>( return _parse<ResolveResult>(
() => _client.dio.post("/api/v1/resolve", data: request), () => _client.dio.post("/api/v1/resolve", data: request),
(data) => Resource.fromJson(data)); (data) => ResolveResult.fromJson(data));
} }
Future<String> createTask(CreateTask createTask) async { Future<String> createTask(CreateTask createTask) async {
@@ -123,11 +129,13 @@ Future<void> putConfig(DownloaderConfig config) async {
return _parse(() => _client.dio.put("/api/v1/config", data: config), null); return _parse(() => _client.dio.put("/api/v1/config", data: config), null);
} }
Future<Response<T>> proxyRequest<T>(String uri, Future<Response<String>> proxyRequest<T>(String uri,
{data, Options? options}) async { {data, Options? options}) async {
options ??= Options(); options ??= Options();
options.headers ??= {}; options.headers ??= {};
options.headers!["X-Target-Uri"] = uri; options.headers!["X-Target-Uri"] = uri;
return _client.dio.request("/api/v1/proxy", data: data, options: options); // add timestamp to avoid cache
return _client.dio.request("/api/v1/proxy?t=${DateTime.now()}",
data: data, options: options);
} }
+8 -4
View File
@@ -1,21 +1,25 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'options.dart'; import 'options.dart';
import 'resource.dart'; import 'request.dart';
part 'create_task.g.dart'; part 'create_task.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class CreateTask { class CreateTask {
Resource res; String? rid;
Request? req;
Options? opts; Options? opts;
CreateTask({ CreateTask({
required this.res, this.rid,
this.req,
this.opts, this.opts,
}); });
factory CreateTask.fromJson(Map<String, dynamic> json) => factory CreateTask.fromJson(
Map<String, dynamic> json,
) =>
_$CreateTaskFromJson(json); _$CreateTaskFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskToJson(this); Map<String, dynamic> toJson() => _$CreateTaskToJson(this);
+7 -4
View File
@@ -7,16 +7,17 @@ part of 'create_task.dart';
// ************************************************************************** // **************************************************************************
CreateTask _$CreateTaskFromJson(Map<String, dynamic> json) => CreateTask( CreateTask _$CreateTaskFromJson(Map<String, dynamic> json) => CreateTask(
res: Resource.fromJson(json['res'] as Map<String, dynamic>), rid: json['rid'] as String?,
req: json['req'] == null
? null
: Request.fromJson(json['req'] as Map<String, dynamic>),
opts: json['opts'] == null opts: json['opts'] == null
? null ? null
: Options.fromJson(json['opts'] as Map<String, dynamic>), : Options.fromJson(json['opts'] as Map<String, dynamic>),
); );
Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) { Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{};
'res': instance.res.toJson(),
};
void writeNotNull(String key, dynamic value) { void writeNotNull(String key, dynamic value) {
if (value != null) { if (value != null) {
@@ -24,6 +25,8 @@ Map<String, dynamic> _$CreateTaskToJson(CreateTask instance) {
} }
} }
writeNotNull('rid', instance.rid);
writeNotNull('req', instance.req?.toJson());
writeNotNull('opts', instance.opts?.toJson()); writeNotNull('opts', instance.opts?.toJson());
return val; return val;
} }
@@ -22,8 +22,8 @@ class ProtocolConfig {
ProtocolConfig(); ProtocolConfig();
factory ProtocolConfig.fromJson(Map<String, dynamic> json) => factory ProtocolConfig.fromJson(Map<String, dynamic>? json) =>
_$ProtocolConfigFromJson(json); json == null ? ProtocolConfig() : _$ProtocolConfigFromJson(json);
Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this); Map<String, dynamic> toJson() => _$ProtocolConfigToJson(this);
} }
@@ -57,8 +57,8 @@ class ExtraConfig {
ExtraConfig(); ExtraConfig();
factory ExtraConfig.fromJson(Map<String, dynamic> json) => factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
_$ExtraConfigFromJson(json); json == null ? ExtraConfig() : _$ExtraConfigFromJson(json);
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this); Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
} }
@@ -10,8 +10,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
DownloaderConfig() DownloaderConfig()
..downloadDir = json['downloadDir'] as String ..downloadDir = json['downloadDir'] as String
..protocolConfig = ProtocolConfig.fromJson( ..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>) json['protocolConfig'] as Map<String, dynamic>?)
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>); ..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?);
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) => Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -64,12 +64,12 @@ ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
..trackerSubscribeUrls = (json['trackerSubscribeUrls'] as List<dynamic>) ..trackerSubscribeUrls = (json['trackerSubscribeUrls'] as List<dynamic>)
.map((e) => e as String) .map((e) => e as String)
.toList() .toList()
..lastTrackerUpdateTime = json['lastTrackerUpdateTime'] == null
? null
: DateTime.parse(json['lastTrackerUpdateTime'] as String)
..subscribeTrackers = (json['subscribeTrackers'] as List<dynamic>) ..subscribeTrackers = (json['subscribeTrackers'] as List<dynamic>)
.map((e) => e as String) .map((e) => e as String)
.toList() .toList()
..lastTrackerUpdateTime = json['lastTrackerUpdateTime'] == null
? null
: DateTime.parse(json['lastTrackerUpdateTime'] as String)
..customTrackers = (json['customTrackers'] as List<dynamic>) ..customTrackers = (json['customTrackers'] as List<dynamic>)
.map((e) => e as String) .map((e) => e as String)
.toList(); .toList();
@@ -77,6 +77,7 @@ ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) { Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
'trackerSubscribeUrls': instance.trackerSubscribeUrls, 'trackerSubscribeUrls': instance.trackerSubscribeUrls,
'subscribeTrackers': instance.subscribeTrackers,
}; };
void writeNotNull(String key, dynamic value) { void writeNotNull(String key, dynamic value) {
@@ -87,7 +88,6 @@ Map<String, dynamic> _$ExtraConfigBtToJson(ExtraConfigBt instance) {
writeNotNull('lastTrackerUpdateTime', writeNotNull('lastTrackerUpdateTime',
instance.lastTrackerUpdateTime?.toIso8601String()); instance.lastTrackerUpdateTime?.toIso8601String());
val['subscribeTrackers'] = instance.subscribeTrackers;
val['customTrackers'] = instance.customTrackers; val['customTrackers'] = instance.customTrackers;
return val; return val;
} }
+22
View File
@@ -0,0 +1,22 @@
import 'options.dart';
import 'request.dart';
import 'resource.dart';
import 'package:json_annotation/json_annotation.dart';
part 'meta.g.dart';
@JsonSerializable(explicitToJson: true)
class Meta {
Request req;
Resource res;
Options opts;
Meta({
required this.req,
required this.res,
required this.opts,
});
factory Meta.fromJson(Map<String, dynamic> json) => _$MetaFromJson(json);
Map<String, dynamic> toJson() => _$MetaToJson(this);
}
+19
View File
@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'meta.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Meta _$MetaFromJson(Map<String, dynamic> json) => Meta(
req: Request.fromJson(json['req'] as Map<String, dynamic>),
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
opts: Options.fromJson(json['opts'] as Map<String, dynamic>),
);
Map<String, dynamic> _$MetaToJson(Meta instance) => <String, dynamic>{
'req': instance.req.toJson(),
'res': instance.res.toJson(),
'opts': instance.opts.toJson(),
};
+13 -1
View File
@@ -2,11 +2,12 @@ import 'package:json_annotation/json_annotation.dart';
part 'options.g.dart'; part 'options.g.dart';
@JsonSerializable() @JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
class Options { class Options {
String name; String name;
String path; String path;
List<int> selectFiles; List<int> selectFiles;
Map<String, dynamic>? extra;
Options({ Options({
required this.name, required this.name,
@@ -19,3 +20,14 @@ class Options {
Map<String, dynamic> toJson() => _$OptionsToJson(this); Map<String, dynamic> toJson() => _$OptionsToJson(this);
} }
@JsonSerializable()
class OptsExtraHttp {
int connections = 0;
OptsExtraHttp();
factory OptsExtraHttp.fromJson(Map<String, dynamic> json) =>
_$OptsExtraHttpFromJson(json);
Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this);
}
+24 -5
View File
@@ -11,10 +11,29 @@ Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
path: json['path'] as String, path: json['path'] as String,
selectFiles: selectFiles:
(json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(), (json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(),
); )..extra = json['extra'] as Map<String, dynamic>?;
Map<String, dynamic> _$OptionsToJson(Options instance) => <String, dynamic>{ Map<String, dynamic> _$OptionsToJson(Options instance) {
'name': instance.name, final val = <String, dynamic>{
'path': instance.path, 'name': instance.name,
'selectFiles': instance.selectFiles, 'path': instance.path,
'selectFiles': instance.selectFiles,
};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('extra', instance.extra);
return val;
}
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp()..connections = json['connections'] as int;
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{
'connections': instance.connections,
}; };
+16 -2
View File
@@ -2,17 +2,31 @@ import 'package:json_annotation/json_annotation.dart';
part 'request.g.dart'; part 'request.g.dart';
@JsonSerializable() @JsonSerializable(explicitToJson: true)
class Request { class Request {
String url; String url;
Map<String, dynamic>? extra; Map<String, dynamic>? extra;
Request({ Request({
required this.url, required this.url,
this.extra,
}); });
factory Request.fromJson(Map<String, dynamic> json) => factory Request.fromJson(Map<String, dynamic> json) =>
_$RequestFromJson(json); _$RequestFromJson(json);
Map<String, dynamic> toJson() => _$RequestToJson(this); Map<String, dynamic> toJson() => _$RequestToJson(this);
} }
@JsonSerializable()
class ReqExtraHttp {
String method = 'GET';
Map<String, String> headers = {};
String body = '';
ReqExtraHttp();
factory ReqExtraHttp.fromJson(Map<String, dynamic> json) =>
_$ReqExtraHttpFromJson(json);
Map<String, dynamic> toJson() => _$ReqExtraHttpToJson(this);
}
+13 -2
View File
@@ -8,8 +8,7 @@ part of 'request.dart';
Request _$RequestFromJson(Map<String, dynamic> json) => Request( Request _$RequestFromJson(Map<String, dynamic> json) => Request(
url: json['url'] as String, url: json['url'] as String,
extra: json['extra'] as Map<String, dynamic>?, )..extra = json['extra'] as Map<String, dynamic>?;
);
Map<String, dynamic> _$RequestToJson(Request instance) { Map<String, dynamic> _$RequestToJson(Request instance) {
final val = <String, dynamic>{ final val = <String, dynamic>{
@@ -25,3 +24,15 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
writeNotNull('extra', instance.extra); writeNotNull('extra', instance.extra);
return val; return val;
} }
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
..method = json['method'] as String
..headers = Map<String, String>.from(json['headers'] as Map)
..body = json['body'] as String;
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
<String, dynamic>{
'method': instance.method,
'headers': instance.headers,
'body': instance.body,
};
@@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
import 'resource.dart';
part 'resolve_result.g.dart';
@JsonSerializable(explicitToJson: true)
class ResolveResult {
String id;
Resource res;
ResolveResult({
required this.id,
required this.res,
});
factory ResolveResult.fromJson(Map<String, dynamic> json) =>
_$ResolveResultFromJson(json);
Map<String, dynamic> toJson() => _$ResolveResultToJson(this);
}
@@ -0,0 +1,19 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'resolve_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ResolveResult _$ResolveResultFromJson(Map<String, dynamic> json) =>
ResolveResult(
id: json['id'] as String,
res: Resource.fromJson(json['res'] as Map<String, dynamic>),
);
Map<String, dynamic> _$ResolveResultToJson(ResolveResult instance) =>
<String, dynamic>{
'id': instance.id,
'res': instance.res.toJson(),
};
+15 -14
View File
@@ -1,45 +1,46 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'request.dart';
part 'resource.g.dart'; part 'resource.g.dart';
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class Resource { class Resource {
Request req;
String name; String name;
int size; int size;
bool range; bool range;
String rootDir;
List<FileInfo> files; List<FileInfo> files;
Map<String, dynamic>? extra; String hash;
Resource({ Resource(
required this.req, {required this.name,
required this.name, required this.size,
required this.size, required this.range,
required this.range, required this.rootDir,
required this.files, required this.files,
this.extra, required this.hash});
});
factory Resource.fromJson(Map<String, dynamic> json) => factory Resource.fromJson(Map<String, dynamic> json) =>
_$ResourceFromJson(json); _$ResourceFromJson(json);
Map<String, dynamic> toJson() => _$ResourceToJson(this); Map<String, dynamic> toJson() => _$ResourceToJson(this);
} }
@JsonSerializable() @JsonSerializable()
class FileInfo { class FileInfo {
String name; // String rootDir;
String path; String path;
String name;
int size; int size;
FileInfo({ FileInfo({
required this.name, // required this.rootDir,
required this.path, required this.path,
required this.name,
required this.size, required this.size,
}); });
factory FileInfo.fromJson(Map<String, dynamic> json) => factory FileInfo.fromJson(Map<String, dynamic> json) =>
_$FileInfoFromJson(json); _$FileInfoFromJson(json);
Map<String, dynamic> toJson() => _$FileInfoToJson(this); Map<String, dynamic> toJson() => _$FileInfoToJson(this);
} }
+12 -22
View File
@@ -7,43 +7,33 @@ part of 'resource.dart';
// ************************************************************************** // **************************************************************************
Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource( Resource _$ResourceFromJson(Map<String, dynamic> json) => Resource(
req: Request.fromJson(json['req'] as Map<String, dynamic>),
name: json['name'] as String, name: json['name'] as String,
size: json['size'] as int, size: json['size'] as int,
range: json['range'] as bool, range: json['range'] as bool,
rootDir: json['rootDir'] as String,
files: (json['files'] as List<dynamic>) files: (json['files'] as List<dynamic>)
.map((e) => FileInfo.fromJson(e as Map<String, dynamic>)) .map((e) => FileInfo.fromJson(e as Map<String, dynamic>))
.toList(), .toList(),
extra: json['extra'] as Map<String, dynamic>?, hash: json['hash'] as String,
); );
Map<String, dynamic> _$ResourceToJson(Resource instance) { Map<String, dynamic> _$ResourceToJson(Resource instance) => <String, dynamic>{
final val = <String, dynamic>{ 'name': instance.name,
'req': instance.req.toJson(), 'size': instance.size,
'name': instance.name, 'range': instance.range,
'size': instance.size, 'rootDir': instance.rootDir,
'range': instance.range, 'files': instance.files.map((e) => e.toJson()).toList(),
'files': instance.files.map((e) => e.toJson()).toList(), 'hash': instance.hash,
}; };
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('extra', instance.extra);
return val;
}
FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo( FileInfo _$FileInfoFromJson(Map<String, dynamic> json) => FileInfo(
name: json['name'] as String,
path: json['path'] as String, path: json['path'] as String,
name: json['name'] as String,
size: json['size'] as int, size: json['size'] as int,
); );
Map<String, dynamic> _$FileInfoToJson(FileInfo instance) => <String, dynamic>{ Map<String, dynamic> _$FileInfoToJson(FileInfo instance) => <String, dynamic>{
'name': instance.name,
'path': instance.path, 'path': instance.path,
'name': instance.name,
'size': instance.size, 'size': instance.size,
}; };
+3
View File
@@ -4,10 +4,12 @@ part 'result.g.dart';
@JsonSerializable(genericArgumentFactories: true) @JsonSerializable(genericArgumentFactories: true)
class Result<T> { class Result<T> {
int code;
String? msg; String? msg;
T? data; T? data;
Result({ Result({
required this.code,
this.msg, this.msg,
this.data, this.data,
}); });
@@ -18,6 +20,7 @@ class Result<T> {
) => ) =>
_$ResultFromJson(json, fromJsonT); _$ResultFromJson(json, fromJsonT);
Map<String, dynamic> toJson() => { Map<String, dynamic> toJson() => {
'code': code,
'msg': msg, 'msg': msg,
'data': data is List 'data': data is List
? (data as dynamic)?.map((e) => e.toJson()).toList() ? (data as dynamic)?.map((e) => e.toJson()).toList()
+4 -1
View File
@@ -11,6 +11,7 @@ Result<T> _$ResultFromJson<T>(
T Function(Object? json) fromJsonT, T Function(Object? json) fromJsonT,
) => ) =>
Result<T>( Result<T>(
code: json['code'] as int,
msg: json['msg'] as String?, msg: json['msg'] as String?,
data: _$nullableGenericFromJson(json['data'], fromJsonT), data: _$nullableGenericFromJson(json['data'], fromJsonT),
); );
@@ -19,7 +20,9 @@ Map<String, dynamic> _$ResultToJson<T>(
Result<T> instance, Result<T> instance,
Object? Function(T value) toJsonT, Object? Function(T value) toJsonT,
) { ) {
final val = <String, dynamic>{}; final val = <String, dynamic>{
'code': instance.code,
};
void writeNotNull(String key, dynamic value) { void writeNotNull(String key, dynamic value) {
if (value != null) { if (value != null) {
+3 -4
View File
@@ -1,5 +1,6 @@
import 'package:json_annotation/json_annotation.dart'; import 'package:json_annotation/json_annotation.dart';
import 'meta.dart';
import 'options.dart'; import 'options.dart';
import 'resource.dart'; import 'resource.dart';
@@ -10,8 +11,7 @@ enum Status { ready, running, pause, error, done }
@JsonSerializable(explicitToJson: true) @JsonSerializable(explicitToJson: true)
class Task { class Task {
String id; String id;
Resource res; Meta meta;
Options opts;
Status status; Status status;
Progress progress; Progress progress;
int size; int size;
@@ -19,8 +19,7 @@ class Task {
Task({ Task({
required this.id, required this.id,
required this.res, required this.meta,
required this.opts,
required this.status, required this.status,
required this.progress, required this.progress,
required this.size, required this.size,
+2 -4
View File
@@ -8,8 +8,7 @@ part of 'task.dart';
Task _$TaskFromJson(Map<String, dynamic> json) => Task( Task _$TaskFromJson(Map<String, dynamic> json) => Task(
id: json['id'] as String, id: json['id'] as String,
res: Resource.fromJson(json['res'] as Map<String, dynamic>), meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
opts: Options.fromJson(json['opts'] as Map<String, dynamic>),
status: $enumDecode(_$StatusEnumMap, json['status']), status: $enumDecode(_$StatusEnumMap, json['status']),
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>), progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
size: json['size'] as int, size: json['size'] as int,
@@ -18,8 +17,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'id': instance.id, 'id': instance.id,
'res': instance.res.toJson(), 'meta': instance.meta.toJson(),
'opts': instance.opts.toJson(),
'status': _$StatusEnumMap[instance.status]!, 'status': _$StatusEnumMap[instance.status]!,
'progress': instance.progress.toJson(), 'progress': instance.progress.toJson(),
'size': instance.size, 'size': instance.size,
@@ -0,0 +1,8 @@
import 'package:get/get.dart';
class ApiProvider extends GetConnect {
@override
void onInit() {
httpClient.baseUrl = 'YOUR-API-URL';
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/app_controller.dart';
class AppBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<AppController>(
() => AppController(),
);
}
}
@@ -1,18 +1,18 @@
import 'dart:convert'; import 'dart:convert';
import 'dart:ui' as ui; import 'dart:ui' as ui;
import 'package:external_path/external_path.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import '../../api/api.dart'; import '../../../../api/api.dart';
import '../../api/model/downloader_config.dart'; import '../../../../api/model/downloader_config.dart';
import '../../core/common/start_config.dart'; import '../../../../core/common/start_config.dart';
import '../../i18n/messages.dart'; import '../../../../generated/locales.g.dart';
import '../../util/log_util.dart'; import '../../../../util/localeManager.dart';
import '../../util/util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/util.dart';
const _startConfigNetwork = "start.network"; const _startConfigNetwork = "start.network";
const _startConfigAddress = "start.address"; const _startConfigAddress = "start.address";
@@ -33,6 +33,27 @@ const allTrackerSubscribeUrls = [
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt', 'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt',
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt', 'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
]; ];
const allTrackerCdns = [
// jsdelivr: https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
["https://cdn.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
// nuaa: https://hub.nuaa.cf/ngosang/trackerslist/raw/master/trackers_all.txt
["https://hub.nuaa.cf", r".*github.com(/.*)"]
];
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
key: (v) => v as String,
value: (v) {
final ret = [v as String];
for (final cdn in allTrackerCdns) {
final reg = RegExp(cdn[1]);
final match = reg.firstMatch(v.toString());
var matchStr = "";
for (var i = 1; i <= match!.groupCount; i++) {
matchStr += match.group(i)!;
}
ret.add("${cdn[0]}$matchStr");
}
return ret;
});
class AppController extends GetxController { class AppController extends GetxController {
static StartConfig? _defaultStartConfig; static StartConfig? _defaultStartConfig;
@@ -96,7 +117,19 @@ class AppController extends GetxController {
final btExtConfig = downloaderConfig.value.extra.bt; final btExtConfig = downloaderConfig.value.extra.bt;
final result = <String>[]; final result = <String>[];
for (var u in btExtConfig.trackerSubscribeUrls) { for (var u in btExtConfig.trackerSubscribeUrls) {
result.addAll(await _fetchTrackers(u)); var allFail = true;
for (var cdn in allTrackerSubscribeUrlCdns[u]!) {
try {
result.addAll(await _fetchTrackers(cdn));
allFail = false;
break;
} catch (e) {
logger.w("subscribe trackers fail, url: $cdn", e);
}
}
if (allFail) {
throw Exception('subscribe trackers fail, network error');
}
} }
btExtConfig.subscribeTrackers.clear(); btExtConfig.subscribeTrackers.clear();
btExtConfig.subscribeTrackers.addAll(result); btExtConfig.subscribeTrackers.addAll(result);
@@ -124,17 +157,25 @@ class AppController extends GetxController {
// if last update time is null or more than 1 day, update trackers // if last update time is null or more than 1 day, update trackers
if (lastUpdateTime == null || if (lastUpdateTime == null ||
lastUpdateTime.difference(DateTime.now()).inDays < 0) { lastUpdateTime.difference(DateTime.now()).inDays < 0) {
await trackerUpdate(); try {
await trackerUpdate();
} catch (e) {
logger.w("tracker update fail", e);
}
} }
} }
Future<List<String>> _fetchTrackers(String subscribeUrl) async { Future<List<String>> _fetchTrackers(String subscribeUrl) async {
final resp = await proxyRequest(subscribeUrl); final resp = await proxyRequest(subscribeUrl);
if (resp.statusCode != 200) { if (resp.statusCode != 200) {
throw Exception('Failed to get trackers'); throw Exception(
'Failed to get trackers, status code: ${resp.statusCode}');
}
if (resp.data == null || resp.data!.isEmpty) {
throw Exception('Failed to get trackers, data is null');
} }
const ls = LineSplitter(); const ls = LineSplitter();
return ls.convert(resp.data).where((e) => e.isNotEmpty).toList(); return ls.convert(resp.data!).where((e) => e.isNotEmpty).toList();
} }
_initDownloaderConfig() async { _initDownloaderConfig() async {
@@ -148,7 +189,7 @@ class AppController extends GetxController {
} }
if (extra.locale.isEmpty) { if (extra.locale.isEmpty) {
final systemLocale = getLocaleKey(ui.window.locale); final systemLocale = getLocaleKey(ui.window.locale);
extra.locale = messages.keys.containsKey(systemLocale) extra.locale = AppTranslation.translations.containsKey(systemLocale)
? systemLocale ? systemLocale
: getLocaleKey(fallbackLocale); : getLocaleKey(fallbackLocale);
} }
@@ -157,22 +198,17 @@ class AppController extends GetxController {
extra.bt.trackerSubscribeUrls.addAll(allTrackerSubscribeUrls); extra.bt.trackerSubscribeUrls.addAll(allTrackerSubscribeUrls);
} }
if (Util.isAndroid()) {
config.downloadDir =
'${await ExternalPath.getExternalStoragePublicDirectory(ExternalPath.DIRECTORY_DOWNLOADS)}/com.gopeed';
return;
}
if (Util.isIOS()) {
config.downloadDir = 'Documents';
return;
}
if (config.downloadDir.isEmpty) { if (config.downloadDir.isEmpty) {
if (Util.isWeb()) {
config.downloadDir = './';
}
if (Util.isDesktop()) { if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./"; config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isAndroid()) {
config.downloadDir = (await getExternalStorageDirectory())?.path ??
(await getApplicationDocumentsDirectory()).path;
return;
} else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else {
config.downloadDir = './';
} }
} }
} }
@@ -0,0 +1,37 @@
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import '../../../../generated/locales.g.dart';
import '../../../../theme/theme.dart';
import '../../../../util/localeManager.dart';
import '../../../routes/app_pages.dart';
import '../controllers/app_controller.dart';
class AppView extends GetView<AppController> {
const AppView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final config = controller.downloaderConfig.value;
return GetMaterialApp.router(
useInheritedMediaQuery: true,
debugShowCheckedModeBanner: false,
theme: GopeedTheme.light,
darkTheme: GopeedTheme.dark,
themeMode: ThemeMode.values.byName(config.extra.themeMode),
// translations: messages,
translationsKeys: AppTranslation.translations,
locale: toLocale(config.extra.locale),
fallbackLocale: fallbackLocale,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales:
AppTranslation.translations.keys.map((e) => toLocale(e)).toList(),
getPages: AppPages.routes,
);
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/create_controller.dart';
class CreateBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<CreateController>(
() => CreateController(),
);
}
}
@@ -0,0 +1,8 @@
import 'package:get/get.dart';
class CreateController extends GetxController {
// final files = [].obs;
final RxList fileInfos = [].obs;
final RxList selectedIndexes = [].obs;
final RxList openedFolders = [].obs;
}
@@ -1,26 +1,28 @@
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart'; import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/result.dart';
import 'package:rounded_loading_button/rounded_loading_button.dart'; import 'package:rounded_loading_button/rounded_loading_button.dart';
import '../../api/api.dart'; import '../../../../api/api.dart';
import '../../api/model/create_task.dart'; import '../../../../api/model/create_task.dart';
import '../../api/model/options.dart'; import '../../../../api/model/options.dart';
import '../../api/model/request.dart'; import '../../../../api/model/request.dart';
import '../../api/model/resource.dart'; import '../../../../util/message.dart';
import '../../routes/router.dart'; import '../../../../util/util.dart';
import '../../util/util.dart'; import '../../../routes/app_pages.dart';
import '../../widget/directory_selector.dart'; import '../../../views/views/directory_selector.dart';
import '../../widget/file_list_view.dart'; import '../../../views/views/file_list_view.dart';
import '../app/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import 'create_controller.dart'; import '../controllers/create_controller.dart';
class CreateView extends GetView<CreateController> { class CreateView extends GetView<CreateController> {
final _resolveFormKey = GlobalKey<FormState>(); final _resolveFormKey = GlobalKey<FormState>();
final _urlController = TextEditingController(); final _urlController = TextEditingController();
final _confirmController = RoundedLoadingButtonController(); final _confirmController = RoundedLoadingButtonController();
CreateView({Key? key}) : super(key: key); CreateView({Key? key}) : super(key: key);
@override @override
@@ -29,9 +31,9 @@ class CreateView extends GetView<CreateController> {
appBar: AppBar( appBar: AppBar(
leading: IconButton( leading: IconButton(
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => Get.rootDelegate.offNamed(Routes.task)), onPressed: () => Get.rootDelegate.offNamed(Routes.TASK)),
actions: [Container()], // actions: [],
title: Text('create.title'.tr), title: Text('create'.tr),
), ),
body: DropTarget( body: DropTarget(
onDragDone: (details) { onDragDone: (details) {
@@ -48,15 +50,16 @@ class CreateView extends GetView<CreateController> {
autofocus: true, autofocus: true,
controller: _urlController, controller: _urlController,
minLines: 1, minLines: 1,
maxLines: 15, maxLines: 30,
decoration: InputDecoration( decoration: InputDecoration(
hintText: _hitText(), hintText: _hitText(),
labelText: 'create.downloadLink'.tr, hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link)), icon: const Icon(Icons.link)),
validator: (v) { validator: (v) {
return v!.trim().isNotEmpty return v!.trim().isNotEmpty
? null ? null
: 'create.downloadLinkValid'.tr; : 'downloadLinkValid'.tr;
}), }),
Center( Center(
child: Padding( child: Padding(
@@ -69,18 +72,18 @@ class CreateView extends GetView<CreateController> {
child: RoundedLoadingButton( child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary, color: Get.theme.colorScheme.secondary,
onPressed: () async { onPressed: () async {
if (_resolveFormKey.currentState!.validate()) { try {
_confirmController.start(); _confirmController.start();
try { if (_resolveFormKey.currentState!.validate()) {
final res = await resolve(Request( final rr = await resolve(Request(
url: _urlController.text, url: _urlController.text,
)); ));
await _showResolveDialog(res); await _showResolveDialog(rr);
} catch (e) {
Get.snackbar('error'.tr, e.toString());
} finally {
_confirmController.reset();
} }
} catch (e) {
showErrorMessage(e);
} finally {
_confirmController.reset();
} }
}, },
controller: _confirmController, controller: _confirmController,
@@ -97,21 +100,20 @@ class CreateView extends GetView<CreateController> {
} }
String _hitText() { String _hitText() {
return 'create.downloadLinkHit'.trParams({ return 'downloadLinkHit'.trParams({
'append': Util.isDesktop() ? 'create.downloadLinkHitDesktop'.tr : '', 'append': Util.isDesktop() ? 'downloadLinkHitDesktop'.tr : '',
}); });
} }
Future<void> _showResolveDialog(Resource res) async { Future<void> _showResolveDialog(ResolveResult rr) async {
final files = rr.res.files;
final appController = Get.find<AppController>(); final appController = Get.find<AppController>();
final setting = appController.downloaderConfig.value;
final createFormKey = GlobalKey<FormState>(); final createFormKey = GlobalKey<FormState>();
final pathController = TextEditingController(text: setting.downloadDir); final pathController = TextEditingController(
text: appController.downloaderConfig.value.downloadDir);
final downloadController = RoundedLoadingButtonController(); final downloadController = RoundedLoadingButtonController();
var fileValues = List.filled(res.files.length, true); return showDialog<void>(
return await showDialog<void>(
context: Get.context!, context: Get.context!,
barrierDismissible: false, barrierDismissible: false,
builder: (_) => AlertDialog( builder: (_) => AlertDialog(
@@ -129,11 +131,7 @@ class CreateView extends GetView<CreateController> {
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
child: Column( child: Column(
children: [ children: [
Expanded( Expanded(child: FileListView(files: files)),
child: FileListView(
files: res.files,
values: fileValues,
)),
DirectorySelector( DirectorySelector(
controller: pathController, controller: pathController,
), ),
@@ -153,7 +151,7 @@ class CreateView extends GetView<CreateController> {
ElevatedButton.styleFrom(shape: const StadiumBorder()) ElevatedButton.styleFrom(shape: const StadiumBorder())
.copyWith( .copyWith(
backgroundColor: MaterialStateProperty.all( backgroundColor: MaterialStateProperty.all(
Get.theme.backgroundColor)), Get.theme.colorScheme.background)),
onPressed: () { onPressed: () {
Get.back(); Get.back();
}, },
@@ -166,43 +164,45 @@ class CreateView extends GetView<CreateController> {
height: Get.theme.buttonTheme.height, height: Get.theme.buttonTheme.height,
), ),
child: RoundedLoadingButton( child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary, color: Get.theme.colorScheme.secondary,
onPressed: () async { onPressed: () async {
try { try {
if (createFormKey.currentState!.validate()) {
downloadController.start(); downloadController.start();
if (Util.isAndroid()) { if (controller.selectedIndexes.isEmpty) {
if (!await Permission.storage.request().isGranted) { showMessage('tip'.tr, 'noFileSelected'.tr);
Get.snackbar('error'.tr, return;
'create.error.noStoragePermission'.tr);
return;
}
} }
await createTask(CreateTask( if (createFormKey.currentState!.validate()) {
res: res, // if (Util.isAndroid()) {
opts: Options( // if (!await Permission.storage.request().isGranted) {
name: '', // Get.snackbar('error'.tr,
path: pathController.text, // 'noStoragePermission'.tr);
selectFiles: fileValues // return;
.asMap() // }
.entries // }
.where((e) => e.value) await createTask(CreateTask(
.map((e) => e.key) rid: rr.id,
.toList(), opts: Options(
))); name: '',
Get.back(); path: pathController.text,
Get.rootDelegate.offNamed(Routes.task); selectFiles: controller.selectedIndexes
.cast<int>())));
Get.back();
Get.rootDelegate.offNamed(Routes.TASK);
}
} catch (e) {
showErrorMessage(e);
} finally {
downloadController.reset();
} }
} catch (e) { },
Get.snackbar('error'.tr, e.toString()); controller: downloadController,
rethrow; child: Text(
} finally { 'download'.tr,
downloadController.reset(); // style: controller.selectedIndexes.isEmpty
} // ? Get.textTheme.disabled
}, // : Get.textTheme.titleSmall
controller: downloadController, )),
child: Text('create.download'.tr),
),
), ),
], ],
)); ));
@@ -0,0 +1,13 @@
import 'package:get/get.dart';
import '../controllers/home_controller.dart';
class HomeBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<HomeController>(
() => HomeController(),
fenix: true,
);
}
}
@@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../routes/app_pages.dart';
import '../../../views/views/responsive_builder.dart';
import '../controllers/home_controller.dart';
class HomeView extends GetView<HomeController> {
const HomeView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
return Scaffold(
// extendBody: true,
body: Row(
// crossAxisAlignment: CrossAxisAlignment.start,
children: [
!ResponsiveBuilder.isNarrow(context)
? NavigationRail(
extended: true,
labelType: NavigationRailLabelType.none,
minExtendedWidth: 170,
groupAlignment: 0,
// useIndicator: false,
onDestinationSelected: (int index) {
switch (index) {
case 0:
controller.currentIndex.value = 0;
delegate.toNamed(Routes.TASK);
break;
case 1:
controller.currentIndex.value = 1;
delegate.toNamed(Routes.SETTING);
break;
}
},
destinations: [
NavigationRailDestination(
icon: const Icon(Icons.list),
selectedIcon: const Icon(Icons.list),
label: Text('task'.tr),
),
NavigationRailDestination(
icon: const Icon(Icons.settings),
selectedIcon: const Icon(Icons.settings),
label: Text('setting'.tr),
),
],
selectedIndex: controller.currentIndex.value,
leading: const Icon(Icons.menu),
// trailing: const Icon(Icons.info_outline),
)
: const SizedBox.shrink(),
Expanded(
child: GetRouterOutlet(
initialRoute: Routes.TASK,
// anchorRoute: '/',
// filterPages: (afterAnchor) {
// logger.w(afterAnchor);
// logger.w(afterAnchor.take(1));
// return afterAnchor.take(1);
// },
))
]),
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
? BottomNavigationBar(
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(
icon: const Icon(Icons.list),
label: 'task'.tr,
),
BottomNavigationBarItem(
icon: const Icon(Icons.settings),
label: 'setting'.tr,
),
],
currentIndex: controller.currentIndex.value,
// selectedItemColor: Get.theme.highlightColor,
onTap: (index) {
switch (index) {
case 0:
controller.currentIndex.value = 0;
delegate.toNamed(Routes.TASK);
break;
case 1:
controller.currentIndex.value = 1;
delegate.toNamed(Routes.SETTING);
break;
}
},
)
// StylishBottomBar(
// option: AnimatedBarOptions(
// iconSize: 32,
// barAnimation: BarAnimation.blink,
// iconStyle: IconStyle.Default,
// opacity: 0.3,
// ),
// items: [
// BottomBarItem(
// icon: const Icon(Icons.file_download),
// selectedColor: Get.theme.primaryColor,
// title: Text('downloading'.tr)),
// BottomBarItem(
// icon: const Icon(Icons.done),
// selectedColor: Get.theme.primaryColor,
// title: Text('downloaded'.tr)),
// BottomBarItem(
// icon: const Icon(Icons.settings),
// selectedColor: Get.theme.primaryColor,
// title: Text('setting'.tr)),
// ],
// // hasNotch: true,
// currentIndex: controller.currentIndex.value,
// onTap: (index) {
// switch (index) {
// case 0:
// delegate.toNamed(Routes.DOWNLOADING);
// controller.currentIndex.value = 0;
// break;
// case 1:
// delegate.toNamed(Routes.DOWNLOADED);
// controller.currentIndex.value = 1;
// break;
// case 2:
// delegate.toNamed(Routes.SETTING);
// controller.currentIndex.value = 2;
// break;
// }
// },
// )
: const SizedBox.shrink(),
);
});
}
}
@@ -0,0 +1,10 @@
import 'package:get/get.dart';
import '../controllers/root_controller.dart';
class RootBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RootController>(() => RootController(), fenix: true);
}
}
@@ -1,4 +1,3 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../api/api.dart';
class RootController extends GetxController {} class RootController extends GetxController {}
@@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../routes/router.dart'; import '../../../routes/app_pages.dart';
import 'root_controller.dart'; import '../controllers/root_controller.dart';
class RootView extends GetView<RootController> { class RootView extends GetView<RootController> {
const RootView({Key? key}) : super(key: key); const RootView({Key? key}) : super(key: key);
@@ -11,14 +11,12 @@ class RootView extends GetView<RootController> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetRouterOutlet.builder( return GetRouterOutlet.builder(
builder: (context, delegate, current) { builder: (context, delegate, current) {
return Scaffold( return GetRouterOutlet(
body: GetRouterOutlet( initialRoute: Routes.HOME,
initialRoute: Routes.home, // anchorRoute: '/',
// anchorRoute: '/', // filterPages: (afterAnchor) {
// filterPages: (afterAnchor) { // return afterAnchor.take(1);
// return afterAnchor.take(1); // },
// },
),
); );
}, },
); );
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/setting_controller.dart';
class SettingBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<SettingController>(
() => SettingController(),
);
}
}
@@ -0,0 +1,44 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:gopeed/api/api.dart';
class SettingController extends GetxController {
final tapStatues = <String, bool>{}.obs;
final latestVersion = "".obs;
@override
void onInit() {
super.onInit();
fetchLatestVersion();
}
// set all tap status to false
void clearTap() {
tapStatues.updateAll((key, value) => false);
}
// set one tap status to true
void onTap(String key) {
clearTap();
tapStatues[key] = true;
}
// fetch latest version
void fetchLatestVersion() async {
String? releaseDataStr;
try {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
} catch (e) {
releaseDataStr = (await proxyRequest(
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
.data;
}
if (releaseDataStr == null) {
return;
}
final releaseData = jsonDecode(releaseDataStr);
latestVersion.value = releaseData["tag_name"].substring(1);
}
}
@@ -4,17 +4,20 @@ import 'dart:convert';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../../util/message.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:badges/badges.dart' as badges;
import '../../i18n/messages.dart'; import '../../../../generated/locales.g.dart';
import '../../util/package_info.dart'; import '../../../../util/localeManager.dart';
import '../../util/util.dart'; import '../../../../util/package_info.dart';
import '../../widget/check_list_view.dart'; import '../../../../util/util.dart';
import '../../widget/directory_selector.dart'; import '../../../views/views/check_list_view.dart';
import '../../widget/outlined_button_loading.dart'; import '../../../views/views/directory_selector.dart';
import '../app/app_controller.dart'; import '../../../views/views/outlined_button_loading.dart';
import 'setting_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../controllers/setting_controller.dart';
const _padding = SizedBox(height: 10); const _padding = SizedBox(height: 10);
final _divider = const Divider().paddingOnly(left: 10, right: 10); final _divider = const Divider().paddingOnly(left: 10, right: 10);
@@ -38,7 +41,7 @@ class SettingView extends GetView<SettingController> {
.then(completer.complete) .then(completer.complete)
.onError(completer.completeError); .onError(completer.completeError);
if (needRestart) { if (needRestart) {
Get.snackbar('tip'.tr, 'setting.effectAfterRestart'.tr); showMessage('tip'.tr, 'effectAfterRestart'.tr);
} }
}); });
return completer.future; return completer.future;
@@ -46,8 +49,7 @@ class SettingView extends GetView<SettingController> {
// download basic config items start // download basic config items start
final buildDownloadDir = _buildConfigItem( final buildDownloadDir = _buildConfigItem(
'setting.downloadDir', () => downloaderCfg.value.downloadDir, 'downloadDir', () => downloaderCfg.value.downloadDir, (Key key) {
(Key key) {
final downloadDirController = final downloadDirController =
TextEditingController(text: downloaderCfg.value.downloadDir); TextEditingController(text: downloaderCfg.value.downloadDir);
downloadDirController.addListener(() async { downloadDirController.addListener(() async {
@@ -69,8 +71,7 @@ class SettingView extends GetView<SettingController> {
// http config items start // http config items start
final httpConfig = downloaderCfg.value.protocolConfig.http; final httpConfig = downloaderCfg.value.protocolConfig.http;
final buildHttpConnections = _buildConfigItem( final buildHttpConnections = _buildConfigItem(
'setting.connections'.tr, () => httpConfig.connections.toString(), 'connections'.tr, () => httpConfig.connections.toString(), (Key key) {
(Key key) {
final connectionsController = final connectionsController =
TextEditingController(text: httpConfig.connections.toString()); TextEditingController(text: httpConfig.connections.toString());
connectionsController.addListener(() async { connectionsController.addListener(() async {
@@ -98,8 +99,8 @@ class SettingView extends GetView<SettingController> {
final btExtConfig = downloaderCfg.value.extra.bt; final btExtConfig = downloaderCfg.value.extra.bt;
final buildBtTrackerSubscribeUrls = _buildConfigItem( final buildBtTrackerSubscribeUrls = _buildConfigItem(
'setting.subscribeTracker'.tr, 'subscribeTracker'.tr,
() => 'setting.items'.trParams( () => 'items'.trParams(
{'count': btExtConfig.trackerSubscribeUrls.length.toString()}), {'count': btExtConfig.trackerSubscribeUrls.length.toString()}),
(Key key) { (Key key) {
final trackerUpdateController = OutlinedButtonLoadingController(); final trackerUpdateController = OutlinedButtonLoadingController();
@@ -127,27 +128,24 @@ class SettingView extends GetView<SettingController> {
try { try {
await appController.trackerUpdate(); await appController.trackerUpdate();
} catch (e) { } catch (e) {
Get.snackbar('error'.tr, 'setting.subscribeFail'.tr); showErrorMessage('subscribeFail'.tr);
} finally { } finally {
trackerUpdateController.stop(); trackerUpdateController.stop();
} }
}, },
controller: trackerUpdateController, controller: trackerUpdateController,
child: Text('setting.update'.tr), child: Text('update'.tr),
), ),
Flexible( Expanded(
child: SizedBox( child: SwitchListTile(
width: 200, controlAffinity: ListTileControlAffinity.leading,
child: SwitchListTile( value: true,
controlAffinity: ListTileControlAffinity.leading, onChanged: (bool value) {},
value: true, title: Text('updateDaily'.tr)),
onChanged: (bool value) {},
title: Text('setting.updateDaily'.tr)),
),
), ),
], ],
), ),
Text('setting.lastUpdate'.trParams({ Text('lastUpdate'.trParams({
'time': btExtConfig.lastTrackerUpdateTime != null 'time': btExtConfig.lastTrackerUpdateTime != null
? DateFormat('yyyy-MM-dd HH:mm:ss') ? DateFormat('yyyy-MM-dd HH:mm:ss')
.format(btExtConfig.lastTrackerUpdateTime!) .format(btExtConfig.lastTrackerUpdateTime!)
@@ -157,8 +155,8 @@ class SettingView extends GetView<SettingController> {
); );
}); });
final buildBtTrackers = _buildConfigItem( final buildBtTrackers = _buildConfigItem(
'setting.addTracker'.tr, 'addTracker'.tr,
() => 'setting.items' () => 'items'
.trParams({'count': btExtConfig.customTrackers.length.toString()}), .trParams({'count': btExtConfig.customTrackers.length.toString()}),
(Key key) { (Key key) {
final trackersController = TextEditingController( final trackersController = TextEditingController(
@@ -171,7 +169,7 @@ class SettingView extends GetView<SettingController> {
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: 5, maxLines: 5,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'setting.addTrackerHit'.tr, hintText: 'addTrackerHit'.tr,
), ),
onChanged: (value) async { onChanged: (value) async {
btExtConfig.customTrackers = ls.convert(value); btExtConfig.customTrackers = ls.convert(value);
@@ -184,7 +182,7 @@ class SettingView extends GetView<SettingController> {
// ui config items start // ui config items start
final buildTheme = _buildConfigItem( final buildTheme = _buildConfigItem(
'setting.theme', 'theme',
() => _getThemeName(downloaderCfg.value.extra.themeMode), () => _getThemeName(downloaderCfg.value.extra.themeMode),
(Key key) => DropdownButton<String>( (Key key) => DropdownButton<String>(
key: key, key: key,
@@ -206,8 +204,9 @@ class SettingView extends GetView<SettingController> {
.toList(), .toList(),
)); ));
final buildLocale = _buildConfigItem( final buildLocale = _buildConfigItem(
'setting.locale', 'locale',
() => messages.keys[downloaderCfg.value.extra.locale]!['label']!, () => AppTranslation
.translations[downloaderCfg.value.extra.locale]!['label']!,
(Key key) => DropdownButton<String>( (Key key) => DropdownButton<String>(
key: key, key: key,
value: downloaderCfg.value.extra.locale, value: downloaderCfg.value.extra.locale,
@@ -221,30 +220,85 @@ class SettingView extends GetView<SettingController> {
await debounceSave(); await debounceSave();
}, },
items: messages.keys.keys items: AppTranslation.translations.keys
.map((e) => DropdownMenuItem<String>( .map((e) => DropdownMenuItem<String>(
value: e, value: e,
child: Text(messages.keys[e]!['label']!), child: Text(AppTranslation.translations[e]!['label']!),
)) ))
.toList(), .toList(),
)); ));
// about config items start // about config items start
buildHomepage() => ListTile( buildHomepage() => ListTile(
title: Text('setting.homepage'.tr), title: Text('homepage'.tr),
subtitle: const Text('https://github.com/monkeyWie/gopeed'), subtitle: const Text('https://github.com/GopeedLab/gopeed'),
onTap: () { onTap: () {
launchUrl(Uri.parse('https://github.com/monkeyWie/gopeed')); launchUrl(Uri.parse('https://github.com/GopeedLab/gopeed'),
mode: LaunchMode.externalApplication);
}, },
); );
buildVersion() => ListTile( buildVersion() {
title: Text('setting.version'.tr), bool isNewVersion(String current, String latest) {
subtitle: Text(packageInfo.version), if (latest == "") {
); return false;
}
final v1Parts = current.split('.');
final v2Parts = latest.split('.');
for (var i = 0; i < 3; i++) {
final v1Part = int.parse(v1Parts[i]);
final v2Part = int.parse(v2Parts[i]);
if (v1Part < v2Part) {
return true;
} else if (v1Part > v2Part) {
return false;
}
}
return false;
}
var hasNewVersion =
isNewVersion(packageInfo.version, controller.latestVersion.value);
return ListTile(
title: hasNewVersion
? badges.Badge(
position: badges.BadgePosition.topStart(start: 36),
child: Text('version'.tr))
: Text('version'.tr),
subtitle: Text(packageInfo.version),
onTap: () {
if (hasNewVersion) {
showDialog(
context: context,
builder: (context) => AlertDialog(
content: Text('newVersionTitle'.trParams(
{'version': controller.latestVersion.value})),
actions: [
TextButton(
onPressed: () {
Get.back();
},
child: Text('newVersionLater'.tr),
),
TextButton(
onPressed: () {
launchUrl(Uri.parse('https://gopeed.com'),
mode: LaunchMode.externalApplication);
},
child: Text('newVersionUpdate'.tr),
),
],
));
}
},
);
}
// advanced config items start // advanced config items start
final buildApiProtocol = _buildConfigItem( final buildApiProtocol = _buildConfigItem(
'setting.protocol'.tr, 'protocol'.tr,
() => startCfg.value.network == 'tcp' () => startCfg.value.network == 'tcp'
? 'TCP ${startCfg.value.address}' ? 'TCP ${startCfg.value.address}'
: 'Unix', : 'Unix',
@@ -322,7 +376,7 @@ class SettingView extends GetView<SettingController> {
child: TextFormField( child: TextFormField(
controller: portController, controller: portController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'setting.port'.tr, labelText: 'port'.tr,
contentPadding: const EdgeInsets.all(0.0), contentPadding: const EdgeInsets.all(0.0),
), ),
keyboardType: TextInputType.number, keyboardType: TextInputType.number,
@@ -342,11 +396,9 @@ class SettingView extends GetView<SettingController> {
); );
}, },
); );
final buildApiToken = _buildConfigItem( final buildApiToken = _buildConfigItem('apiToken'.tr,
'setting.apiToken'.tr, () => startCfg.value.apiToken.isEmpty ? 'notSet'.tr : 'seted'.tr,
() => startCfg.value.apiToken.isEmpty (Key key) {
? 'setting.notSet'.tr
: 'setting.seted'.tr, (Key key) {
final apiTokenController = final apiTokenController =
TextEditingController(text: startCfg.value.apiToken); TextEditingController(text: startCfg.value.apiToken);
apiTokenController.addListener(() async { apiTokenController.addListener(() async {
@@ -379,10 +431,10 @@ class SettingView extends GetView<SettingController> {
bottom: TabBar( bottom: TabBar(
tabs: [ tabs: [
Tab( Tab(
text: 'setting.basic'.tr, text: 'basic'.tr,
), ),
Tab( Tab(
text: 'setting.advanced'.tr, text: 'advanced'.tr,
), ),
], ],
), ),
@@ -393,7 +445,7 @@ class SettingView extends GetView<SettingController> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([ children: _addPadding([
Text('setting.general'.tr), Text('general'.tr),
Card( Card(
child: Column( child: Column(
children: _addDivider([ children: _addDivider([
@@ -415,7 +467,7 @@ class SettingView extends GetView<SettingController> {
buildBtTrackers(), buildBtTrackers(),
]), ]),
)), )),
Text('setting.ui'.tr), Text('ui'.tr),
Card( Card(
child: Column( child: Column(
children: _addDivider([ children: _addDivider([
@@ -423,7 +475,7 @@ class SettingView extends GetView<SettingController> {
buildLocale(), buildLocale(),
]), ]),
)), )),
Text('setting.about'.tr), Text('about'.tr),
Card( Card(
child: Column( child: Column(
children: _addDivider([ children: _addDivider([
@@ -521,22 +573,22 @@ class SettingView extends GetView<SettingController> {
String _getThemeName(String? themeMode) { String _getThemeName(String? themeMode) {
switch (ThemeMode.values.byName(themeMode ?? ThemeMode.system.name)) { switch (ThemeMode.values.byName(themeMode ?? ThemeMode.system.name)) {
case ThemeMode.light: case ThemeMode.light:
return 'setting.themeLight'.tr; return 'themeLight'.tr;
case ThemeMode.dark: case ThemeMode.dark:
return 'setting.themeDark'.tr; return 'themeDark'.tr;
default: default:
return 'setting.themeSystem'.tr; return 'themeSystem'.tr;
} }
} }
} }
class _ConfigItem { // class _ConfigItem {
late String label; // late String label;
late String Function() text; // late String Function() text;
late Widget Function(Key key) inputItem; // late Widget Function(Key key) inputItem;
//
_ConfigItem(this.label, this.text, this.inputItem); // _ConfigItem(this.label, this.text, this.inputItem);
} // }
class _NumericalRangeFormatter extends TextInputFormatter { class _NumericalRangeFormatter extends TextInputFormatter {
final int min; final int min;
@@ -0,0 +1,20 @@
import 'package:get/get.dart';
import '../controllers/task_controller.dart';
import '../controllers/task_downloaded_controller.dart';
import '../controllers/task_downloading_controller.dart';
class TaskBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TaskController>(
() => TaskController(),
);
Get.lazyPut<TaskDownloadingController>(
() => TaskDownloadingController(),
);
Get.lazyPut<TaskDownloadedController>(
() => TaskDownloadedController(),
);
}
}
@@ -0,0 +1,5 @@
import 'package:get/get.dart';
class TaskController extends GetxController {
final tabIndex = 0.obs;
}
@@ -0,0 +1,9 @@
import 'dart:async';
import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
import '../../../../api/model/task.dart';
class TaskDownloadedController extends TaskListController {
TaskDownloadedController() : super([Status.done]);
}
@@ -0,0 +1,7 @@
import '../../../../api/model/task.dart';
import 'task_list_controller.dart';
class TaskDownloadingController extends TaskListController {
TaskDownloadingController()
: super([Status.ready, Status.running, Status.pause, Status.error]);
}
@@ -0,0 +1,48 @@
import 'dart:async';
import 'package:get/get.dart';
import '../../../../api/api.dart';
import '../../../../api/model/task.dart';
abstract class TaskListController extends GetxController {
List<Status> statuses;
TaskListController(this.statuses);
final tasks = <Task>[].obs;
final isRunning = false.obs;
late final Timer _timer;
@override
void onInit() async {
super.onInit();
start();
_timer = Timer.periodic(const Duration(milliseconds: 1000), (timer) async {
if (isRunning.value) {
await getTasksState();
}
});
}
@override
void onClose() {
super.onClose();
_timer.cancel();
}
void start() async {
await getTasksState();
isRunning.value = true;
}
void stop() {
isRunning.value = false;
}
getTasksState() async {
tasks.value = await getTasks(statuses);
}
}
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../views/views/buid_task_list_view.dart';
import '../controllers/task_downloaded_controller.dart';
class TaskDownloadedView extends GetView<TaskDownloadedController> {
const TaskDownloadedView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BuildTaskListView(tasks: controller.tasks);
}
}
@@ -0,0 +1,14 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../views/views/buid_task_list_view.dart';
import '../controllers/task_downloading_controller.dart';
class TaskDownloadingView extends GetView<TaskDownloadingController> {
const TaskDownloadingView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return BuildTaskListView(tasks: controller.tasks);
}
}
@@ -0,0 +1,60 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/controllers/task_downloading_controller.dart';
import '../controllers/task_downloaded_controller.dart';
import 'task_downloading_view.dart';
import 'task_downloaded_view.dart';
import '../controllers/task_controller.dart';
class TaskView extends GetView<TaskController> {
const TaskView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 2,
child: Scaffold(
appBar: PreferredSize(
preferredSize: const Size.fromHeight(56),
child: AppBar(
bottom: TabBar(
tabs: const [
Tab(
icon: Icon(Icons.file_download),
),
Tab(
icon: Icon(Icons.done),
),
],
onTap: (index) {
if (controller.tabIndex.value != index) {
controller.tabIndex.value = index;
final downloadingController =
Get.find<TaskDownloadingController>();
final downloadedController =
Get.find<TaskDownloadedController>();
switch (index) {
case 0:
downloadingController.start();
downloadedController.stop();
break;
case 1:
downloadingController.stop();
downloadedController.start();
break;
}
}
},
),
)),
body: const TabBarView(
children: [
TaskDownloadingView(),
TaskDownloadedView(),
],
),
),
);
}
}
+57
View File
@@ -0,0 +1,57 @@
import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/views/task_view.dart';
import '../modules/create/bindings/create_binding.dart';
import '../modules/create/views/create_view.dart';
import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_view.dart';
import '../modules/root/bindings/root_binding.dart';
import '../modules/root/views/root_view.dart';
import '../modules/setting/bindings/setting_binding.dart';
import '../modules/setting/views/setting_view.dart';
import '../modules/task/bindings/task_binding.dart';
part 'app_routes.dart';
class AppPages {
AppPages._();
static final routes = [
GetPage(
name: _Paths.ROOT,
participatesInRootNavigator: true,
transition: Transition.topLevel,
// preventDuplicates: true,
page: () => const RootView(),
binding: RootBinding(),
children: [
GetPage(
name: _Paths.HOME,
// participatesInRootNavigator: true,
// transition: Transition.topLevel,
// preventDuplicates: true,
page: () => const HomeView(),
binding: HomeBinding(),
children: [
GetPage(
name: _Paths.TASK,
page: () => const TaskView(),
transition: Transition.noTransition,
binding: TaskBinding()),
GetPage(
name: _Paths.SETTING,
page: () => const SettingView(),
transition: Transition.noTransition,
binding: SettingBinding(),
),
]),
GetPage(
name: _Paths.CREATE,
transition: Transition.downToUp,
// preventDuplicates: true,
page: () => CreateView(),
binding: CreateBinding(),
),
]),
];
}
Loaded 100 of 139 files, more files were not shown because too many files have changed in this diff. Show more