Compare commits

...
48 Commits
Author SHA1 Message Date
Levi 7705af6e68 release: v1.7.0 2025-04-08 22:22:09 +08:00
Levi 684e060ef3 chore: upgrade go to 1.24 (#984) 2025-04-08 22:18:18 +08:00
Levi 0c67792441 ci: build separate macos packages (#967) 2025-04-08 19:43:30 +08:00
Levi b0ef2c2a87 fix: bt seed cause crash (#985) 2025-04-08 18:38:17 +08:00
mak7im01 1977c2a0fc feat: update ru_ru.dart (#982) 2025-04-08 09:21:29 +08:00
Levi d289708f9e feat: use batch download api for create multitasks (#978) 2025-04-05 22:57:17 +08:00
Levi cd5fd45667 fix: resolve http non encode attachment filename (#975) 2025-04-05 22:37:19 +08:00
Levi 50bc9ab129 feat: improve http download strategy to prevent speed drops near completion (#969) 2025-04-05 22:20:35 +08:00
Levi bfad91dcab release: v1.6.11 2025-03-14 23:03:48 +08:00
Levi 61a959adda ci: apk build split abi (#962) 2025-03-14 22:58:28 +08:00
Levi f4b839fd11 fix: http download size exceeds file size (#959) 2025-03-14 20:07:34 +08:00
Levi 969aeb5e0a fix: : web auth priority when api token and basic auth coexist (#960) 2025-03-14 19:22:31 +08:00
Levi 68d42eb397 feat: web http file server caching and gzip support (#961) 2025-03-14 18:31:05 +08:00
Levi 08603f9510 feat: auto updater (#933) 2025-03-13 18:31:38 +08:00
Internetezoo b450bf67bd feat: Add Hungarian languages (#940) 2025-03-10 11:59:44 +08:00
Levi 03e26dbea8 feat: improve http download retry strategy (#951) 2025-03-07 12:01:52 +08:00
Levi 046a3ac9f9 fix: can not open url encode torrent file path (#945) 2025-02-27 00:00:25 +08:00
Levi 9c5aebaf36 fix: path url encode filename parse (#939) 2025-02-18 19:37:38 +08:00
Xiangxin a094715d69 fix: windows title bar theme switch (#936) 2025-02-17 19:32:09 +08:00
铺盖崽 69bb875172 ci: fix flathub build error (#937) 2025-02-17 09:15:42 +08:00
铺盖崽 589f10c7ef ci: add linux arm64 build (#935) 2025-02-16 10:54:25 +08:00
Levi 654e57e946 release: v1.6.10 2025-02-13 21:56:27 +08:00
Levi b94b99ae09 fix: gopeed scheme safe url params (#932) 2025-02-13 21:55:37 +08:00
Levi fba990a056 fix: http MIME encoded word filename parse (#931) 2025-02-13 21:54:13 +08:00
𝗦𝗵𝗟𝗲𝗿𝗣 cb07ce58bc feat: improve Turkish Locales (#917) 2025-02-07 08:05:38 +08:00
Levi 50beef7b08 docs: improve download links (#913) 2025-02-06 12:19:27 +08:00
Levi bd39e4110f feat: add ukraine language (#909) 2025-02-05 10:51:58 +08:00
Levi 86efcb98c5 fix: web white screen (#908) 2025-02-05 10:51:33 +08:00
Levi f3d9873395 release: v1.6.9 2025-02-03 18:50:25 +08:00
Levi ef228c1722 feat: update android target sdk to 34 (#902) 2025-02-03 16:15:13 +08:00
Levi 6863317e9d feat: support tcp protocol on android (#903) 2025-02-03 13:49:46 +08:00
Levi 2b83a735cb fix: advanced options used by default when creating task (#901) 2025-02-03 13:42:50 +08:00
Levi 447f599783 fix: firefox native host integration (#895) 2025-02-01 18:14:59 +08:00
liwei f986f6253d ci: fix download-artifact version 2025-01-31 20:00:45 +08:00
liwei 2e1b621250 ci: fix 2025-01-31 20:00:14 +08:00
Levi d50947b2e6 release: v1.6.8 2025-01-31 19:15:01 +08:00
Levi 520a72415c feat: browser extensions native messaging host (#890) 2025-01-31 17:57:34 +08:00
Levi a372b67edb ci: fix linux package (#882) 2025-01-23 12:45:44 +08:00
Levi 2d86b5c132 fix: handle share links (#883) 2025-01-23 12:17:25 +08:00
Levi 63b3144633 ci: qnap separate build (#877) 2025-01-17 23:03:55 +08:00
Levi 1b0178fed9 release: v1.6.7 2025-01-17 08:25:24 +08:00
Levi 83febe0a00 feat: support creating tasks using gopeed scheme (#876) 2025-01-16 18:49:32 +08:00
Levi 987580d400 fix: windows uninstall icon (#875) 2025-01-16 18:48:46 +08:00
Levi 0919936050 feat: support custom http request headers (#868) 2025-01-16 00:19:50 +08:00
Levi acc7ed8f6b feat: torrent scheme deep link and file associations (#866) 2025-01-16 00:02:00 +08:00
Levi ccb3267fba feat: update flutter to v3.24 (#865) 2025-01-06 18:34:50 +08:00
Levi 1249493a8a feat: improve file_tree_view display on mobile (#862) 2024-12-30 20:35:39 +08:00
Levi 107141bbe7 fix: not disabling the context menu on the browser (#861) 2024-12-30 20:17:33 +08:00
119 changed files with 5740 additions and 1537 deletions

No files matched your search

+163 -28
View File
@@ -1,46 +1,181 @@
name-template: 'v$NEXT_PATCH_VERSION 🌈' name-template: "v$NEXT_PATCH_VERSION 🌈"
tag-template: 'v$NEXT_PATCH_VERSION' tag-template: "v$NEXT_PATCH_VERSION"
categories: categories:
- title: '🆕 Features' - title: "🆕 Features"
labels: labels:
- 'feat' - "feat"
- 'feature' - "feature"
- 'enhancement' - "enhancement"
- title: '🐛 Bug Fixes' - title: "🐛 Bug Fixes"
labels: labels:
- 'fix' - "fix"
- 'bugfix' - "bugfix"
- 'bug' - "bug"
- title: '🔧 Performance Improvements' - title: "🔧 Performance Improvements"
labels: labels:
- 'perf' - "perf"
- title: '🧪 Tests' - title: "🧪 Tests"
label: 'test' label: "test"
- title: '🧰 Maintenance' - title: "🧰 Maintenance"
label: 'chore' label: "chore"
- title: '📖 Document' - title: "📖 Document"
label: 'docs' label: "docs"
- title: '🚀 CI/CD' - title: "🚀 CI/CD"
label: 'ci' label: "ci"
- title: '🌎 Internationalization' - title: "🌎 Internationalization"
label: 'translation' label: "translation"
change-template: '- $TITLE @$AUTHOR (#$NUMBER)' change-template: "- $TITLE @$AUTHOR (#$NUMBER)"
version-resolver: version-resolver:
major: major:
labels: labels:
- 'major' - "major"
minor: minor:
labels: labels:
- 'minor' - "minor"
patch: patch:
labels: labels:
- 'patch' - "patch"
default: patch default: patch
template: | template: |
<table>
<tbody>
<tr>
<td rowspan="2">🪟 Windows</td>
<td><code>EXE</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td><code>Portable</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-windows-amd64-portable.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3">🍎 MacOS</td>
<td rowspan="3"><code>DMG</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos.dmg">📥</a></td>
</tr>
<tr>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-amd64.dmg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-macos-arm64.dmg">📥</a></td>
</tr>
<tr>
<td rowspan="6">🐧 Linux</td>
<td><code>Flathub</code></td>
<td>amd64</td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
</tr>
<tr>
<td><code>SNAP</code></td>
<td>amd64</td>
<td><a href="https://snapcraft.io/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>DEB</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.deb">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.deb">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>AppImage</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-amd64.AppImage">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-linux-arm64.AppImage">📥</a></td>
</tr>
<tr>
<td rowspan="4">🤖 Android</td>
<td rowspan="4"><code>APK</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-v$NEXT_PATCH_VERSION-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-v$NEXT_PATCH_VERSION-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-v$NEXT_PATCH_VERSION-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table>
# Release notes # Release notes
$CHANGES $CHANGES
# 更新日志 # 更新日志
$CHANGES $CHANGES
+238 -99
View File
@@ -4,17 +4,17 @@ on:
workflow_dispatch: workflow_dispatch:
inputs: inputs:
platform: platform:
description: 'Build platform' description: "Build platform"
required: true required: true
default: 'all' default: "all"
test: test:
description: 'Test mode' description: "Test mode"
required: true required: true
default: "false" default: "false"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.16.8" FLUTTER_VERSION: "3.24.5"
permissions: permissions:
contents: write contents: write
@@ -35,7 +35,7 @@ jobs:
build-windows: build-windows:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
runs-on: windows-latest runs-on: windows-latest
needs: [ get-release ] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -52,9 +52,11 @@ jobs:
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\" mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$env:VERSION" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host.exe github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter cd ui/flutter
$TAG = "v$env:VERSION" $TAG = "v$env:VERSION"
flutter build windows flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
$mingw = "C:\Program Files\Git\mingw64\bin" $mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32" $system = "C:\Windows\System32"
$release = "build\windows\x64\runner\Release\" $release = "build\windows\x64\runner\Release\"
@@ -68,6 +70,7 @@ jobs:
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-$TAG-windows-amd64-portable.zip" Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-amd64-portable.zip"
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller"
cd build/windows cd build/windows
echo @" echo @"
; Script generated by the Inno Setup Script Wizard. ; Script generated by the Inno Setup Script Wizard.
@@ -97,11 +100,13 @@ jobs:
PrivilegesRequired=lowest PrivilegesRequired=lowest
OutputBaseFilename=gopeed OutputBaseFilename=gopeed
SetupIconFile=..\..\assets\icon\icon.ico SetupIconFile=..\..\assets\icon\icon.ico
UninstallDisplayIcon={app}\{#MyAppExeName}
Compression=lzma Compression=lzma
SolidCompression=yes SolidCompression=yes
WizardStyle=modern WizardStyle=modern
LanguageDetectionMethod=uilanguage LanguageDetectionMethod=uilanguage
ShowLanguageDialog=yes ShowLanguageDialog=yes
CloseApplications=force
[Languages] [Languages]
Name: "english"; MessagesFile: "compiler:Default.isl" Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -111,7 +116,7 @@ jobs:
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files] [Files]
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs Source: ".\x64\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] [UninstallDelete]
@@ -134,10 +139,10 @@ jobs:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/windows/Output/* asset_path: ui/flutter/build/windows/Output/*
overwrite: true overwrite: true
build-macos-arm64-lib: build-macos-amd64-lib:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-latest runs-on: macos-13
needs: [ get-release ] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -148,14 +153,22 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
- uses: actions/upload-artifact@v3 go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" github.com/GopeedLab/gopeed/cmd/updater
- uses: actions/upload-artifact@v4
with: with:
name: macos-arm64-lib name: macos-amd64-lib
path: libgopeed.dylib path: |
libgopeed.dylib
host
updater
build-macos: build-macos:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-13 runs-on: macos-latest
needs: [ get-release, build-macos-arm64-lib ] strategy:
matrix:
channel: [ arm64, amd64, universal ]
needs: [get-release, build-macos-amd64-lib]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -171,27 +184,49 @@ jobs:
run: | run: |
python3 -m pip install setuptools --break-system-packages python3 -m pip install setuptools --break-system-packages
npm install -g appdmg npm install -g appdmg
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v4
with: with:
name: macos-arm64-lib name: macos-amd64-lib
path: ui/flutter/lib-arm64 path: ui/flutter/lib-amd64
- if: ${{ matrix.channel == 'arm64' }}
name: Build Arm64 Libraries
run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
- if: ${{ matrix.channel == 'amd64' }}
name: Build Amd64 Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/macos/Frameworks/libgopeed.dylib
cp ui/flutter/lib-amd64/host ui/flutter/assets/exec/host
cp ui/flutter/lib-amd64/updater ui/flutter/assets/exec/updater
- if: ${{ matrix.channel == 'universal' }}
name: Build Universal Libraries
run: |
mkdir -p ui/flutter/macos/Frameworks
# arm64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/.temp/libgopeed.dylib.arm64 github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/.temp/host.arm64 github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/.temp/updater.arm64 github.com/GopeedLab/gopeed/cmd/updater
# amd64 lib
cp ui/flutter/lib-amd64/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.amd64
cp ui/flutter/lib-amd64/host ui/flutter/.temp/host.amd64
cp ui/flutter/lib-amd64/updater ui/flutter/.temp/updater.amd64
# universal lib
lipo -create -output ui/flutter/macos/Frameworks/libgopeed.dylib ui/flutter/.temp/libgopeed.dylib.arm64 ui/flutter/.temp/libgopeed.dylib.amd64
lipo -create -output ui/flutter/assets/exec/host ui/flutter/.temp/host.arm64 ui/flutter/.temp/host.amd64
lipo -create -output ui/flutter/assets/exec/updater ui/flutter/.temp/updater.arm64 ui/flutter/.temp/updater.amd64
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
PROJECT_DIR=$(pwd) cd ui/flutter
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
# amd64 lib
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -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
cd build/macos/Build/Products/Release cd build/macos/Build/Products/Release
cat>appdmg.json<<EOF cat>appdmg.json<<EOF
{ {
@@ -205,6 +240,13 @@ jobs:
EOF EOF
mkdir dist mkdir dist
appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg appdmg appdmg.json dist/Gopeed-v$VERSION-macos.dmg
if [[ "${{ matrix.channel }}" == "arm64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-arm64.dmg
fi
if [[ "${{ matrix.channel }}" == "amd64" ]]; then
mv dist/Gopeed-v$VERSION-macos.dmg dist/Gopeed-v$VERSION-macos-amd64.dmg
fi
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -213,23 +255,34 @@ jobs:
overwrite: true overwrite: true
build-linux: build-linux:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
needs: [ get-release ] strategy:
matrix:
os: [ubuntu-22.04, ubuntu-22.04-arm]
needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2 - if: matrix.os == 'ubuntu-22.04'
uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- if: matrix.os == 'ubuntu-22.04-arm'
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: master
- run: | - run: |
sudo apt update -y sudo apt update -y
# flutter build dependencies # flutter build dependencies
sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev sudo apt install -y ninja-build libgtk-3-dev libayatana-appindicator3-1 libayatana-appindicator3-dev
# appimage build dependencies # appimage build dependencies
sudo apt install -y libfuse2 locate sudo apt install -y libfuse2 locate
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage" arch=$(uname -m)
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
wget -O appimagetool "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-${arch}.AppImage"
chmod +x appimagetool chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/ sudo mv appimagetool /usr/local/bin/
- name: Build - name: Build
@@ -237,12 +290,42 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter cd ui/flutter
cat << EOF > distribute_options.yaml
output: dist/
releases:
- name: linux
jobs:
- name: appimage
package:
platform: linux
target: appimage
build_args:
dart-define:
UPDATE_CHANNEL: linuxAppImage
- name: deb
package:
platform: linux
target: deb
build_args:
dart-define:
UPDATE_CHANNEL: linuxDeb
EOF
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
flutter_distributor package --platform linux --targets appimage,deb flutter_distributor release --name linux
cd dist/* cd dist/*
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-x86_64.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-amd64.deb # Get current running architecture, x86_64 -> amd64 and aarch64 -> arm64
ARCH="amd64"
if [[ "${{ matrix.os }}" == *-arm ]]; then
ARCH="arm64"
fi
mv gopeed-*-linux.AppImage Gopeed-v${VERSION}-linux-${ARCH}.AppImage
mv gopeed-*-linux.deb Gopeed-v${VERSION}-linux-${ARCH}.deb
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -252,7 +335,7 @@ jobs:
build-snap: build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -267,41 +350,45 @@ jobs:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
go build -ldflags="-w -s" -o ui/flutter/assets/exec/host github.com/GopeedLab/gopeed/cmd/host
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater github.com/GopeedLab/gopeed/cmd/updater
cd ui/flutter cd ui/flutter
sudo snap install snapcraft --classic sudo snap install snapcraft --classic
mkdir -p snap/gui mkdir -p snap/gui
cp assets/icon/icon.svg snap/gui/gopeed.svg cp assets/icon/icon.svg snap/gui/gopeed.svg
cat>snap/snapcraft.yaml<<EOF cat>snap/snapcraft.yaml<<EOF
name: gopeed name: gopeed
version: $VERSION version: $VERSION
summary: A modern download manager summary: A modern download manager
description: High speed downloader that supports all platforms. description: High speed downloader that supports all platforms.
confinement: strict confinement: strict
base: core22 base: core24
grade: stable grade: stable
architectures: platforms:
- build-on: amd64 amd64:
build-on: amd64
build-for: amd64 build-for: amd64
slots: slots:
dbus-gopeed: dbus-gopeed:
interface: dbus interface: dbus
bus: session bus: session
name: com.gopeed.gopeed name: com.gopeed.gopeed
apps: apps:
gopeed: gopeed:
command-chain:
- bin/gpu-2404-wrapper
command: bin/gopeed command: bin/gopeed
extensions: [gnome] extensions: [gnome]
common-id: com.gopeed.Gopeed
plugs: plugs:
- network - network
- home - home
slots:
- dbus-gopeed
parts: parts:
flutter-git: flutter-git:
source: https://github.com/flutter/flutter.git source: https://github.com/flutter/flutter.git
@@ -341,14 +428,37 @@ jobs:
set +e set +e
dart pub get dart pub get
set -eux set -eux
flutter build linux --release -v flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
mkdir -p \$CRAFT_PART_INSTALL/bin/ mkdir -p \$CRAFT_PART_INSTALL/bin/
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/ cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
gpu-2404:
after: [gopeed]
source: https://github.com/canonical/gpu-snap.git
plugin: dump
override-prime: |
craftctl default
\${CRAFT_PART_SRC}/bin/gpu-2404-cleanup mesa-2404
prime:
- bin/gpu-2404-wrapper
plugs:
gpu-2404:
interface: content
target: \$SNAP/gpu-2404
default-provider: mesa-2404
layout:
/usr/share/libdrm:
bind: \$SNAP/gpu-2404/libdrm
/usr/share/drirc.d:
symlink: \$SNAP/gpu-2404/drirc.d
/usr/share/X11/XErrorDB:
symlink: \$SNAP/gpu-2404/X11/XErrorDB
EOF EOF
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
snapcraft --use-lxd snapcraft --use-lxd
# Snapcraft login # Snapcraft login
@@ -357,7 +467,7 @@ jobs:
build-android: build-android:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -366,7 +476,7 @@ jobs:
- uses: actions/setup-java@v3 - uses: actions/setup-java@v3
with: with:
distribution: "zulu" distribution: "zulu"
java-version: "11" java-version: "17"
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
@@ -383,9 +493,13 @@ jobs:
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -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 echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
flutter build apk flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
mkdir dist mkdir dist
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/Gopeed-v$VERSION-android-armeabi-v7a.apk
cp build/app/outputs/flutter-apk/app-arm64-v8a-release.apk dist/Gopeed-v$VERSION-android-arm64-v8a.apk
cp build/app/outputs/flutter-apk/app-x86_64-release.apk dist/Gopeed-v$VERSION-android-x86_64.apk
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
@@ -395,7 +509,7 @@ jobs:
build-ios: build-ios:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
runs-on: macos-latest runs-on: macos-latest
needs: [ get-release ] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -413,7 +527,7 @@ jobs:
gomobile init gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter cd ui/flutter
flutter build ios --no-codesign flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
mkdir Payload mkdir Payload
cp -r build/ios/iphoneos/Runner.app Payload cp -r build/ios/iphoneos/Runner.app Payload
zip -r -y Payload.zip Payload/Runner.app zip -r -y Payload.zip Payload/Runner.app
@@ -426,9 +540,9 @@ jobs:
asset_path: ui/flutter/dist/* asset_path: ui/flutter/dist/*
overwrite: true overwrite: true
build-web: build-web:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' || github.event.inputs.platform == 'qnap' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [get-release]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- uses: actions/setup-go@v4 - uses: actions/setup-go@v4
@@ -441,13 +555,12 @@ jobs:
env: env:
VERSION: ${{ needs.get-release.outputs.version }} VERSION: ${{ needs.get-release.outputs.version }}
run: | run: |
VERSION=${VERSION:-$(date +%y%m%d%H%M)} # Use current time as version if not specified
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
cd ../../ cd ../../
cp -r ui/flutter/build/web cmd/web/dist cp -r ui/flutter/build/web cmd/web/dist
mkdir -p dist/zip mkdir -p dist/zip
goos_arr=(windows darwin linux) goos_arr=(windows darwin linux)
goarch_arr=(386 amd64 arm64) goarch_arr=(386 amd64 arm64)
export CGO_ENABLED=0 export CGO_ENABLED=0
@@ -469,49 +582,75 @@ jobs:
|| true || true
done done
done done
sudo apt update -y - uses: actions/upload-artifact@v4
sudo apt install -y pv python2 bsdmainutils with:
wget -O qdk2_0.32.bionic_amd64.deb "https://github.com/qnap-dev/qdk2/releases/download/v0.32/qdk2_0.32.bionic_amd64.deb" name: web-dist
dpkg -X qdk2_0.32.bionic_amd64.deb qdk2 # Direct installs will fail due to missing dependencies! path: dist/zip/
[[ -d qdk2 ]] || exit 1
export PATH=$(pwd)/qdk2/usr/bin:$(pwd)/qdk2/usr/share/qdk2/QDK/bin:${PATH}
wget -O Gopeed.template.tar.gz "https://github.com/GopeedLab/QpkgBuild/raw/refs/heads/master/template/Gopeed.template.tar.gz"
tar -zxf Gopeed.template.tar.gz
[[ -d Gopeed ]] || exit 1
goos=linux
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
name=gopeed-web-v${VERSION}-${goos}-${goarch}
cp dist/${name}/${name}/* Gopeed/${qarch}/
done
cd Gopeed
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
qbuild || exit 1
for f in build/*; do echo ">>> build=[${f}]"; done
goos=qnap
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
sname=Gopeed_${VERSION}_${qarch}.qpkg
dname=gopeed-${goos}-v${VERSION}-${goarch}.qpkg
echo "sname=${sname}, dname=${dname}"
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/zip/${dname}
done
for f in ../dist/zip/*; do echo ">>> dist=[${f}]"; done
cd ..
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: dist/zip/* asset_path: dist/zip/*
overwrite: true overwrite: true
build-qnap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'qnap' }}
runs-on: ubuntu-latest
needs: [get-release, build-web]
steps:
- uses: actions/setup-python@v5
with:
python-version: "3.8.18"
- uses: actions/download-artifact@v4
with:
name: web-dist
path: dist/zip/
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.version }}
run: |
sudo apt update -y
sudo apt install -y pv bsdmainutils
wget -O qdk2_0.32.bionic_amd64.deb "https://github.com/qnap-dev/qdk2/releases/download/v0.32/qdk2_0.32.bionic_amd64.deb"
dpkg -X qdk2_0.32.bionic_amd64.deb qdk2 # Direct installs will fail due to missing dependencies!
[[ -d qdk2 ]] || exit 1
export PATH=$(pwd)/qdk2/usr/bin:$(pwd)/qdk2/usr/share/qdk2/QDK/bin:${PATH}
wget -O Gopeed.template.tar.gz "https://github.com/GopeedLab/QpkgBuild/raw/refs/heads/master/template/Gopeed.template.tar.gz"
tar -zxf Gopeed.template.tar.gz
[[ -d Gopeed ]] || exit 1
goos=linux
goarch_arr=(amd64 arm64)
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
name=gopeed-web-v${VERSION}-${goos}-${goarch}
unzip dist/zip/${name}.zip -d dist/${name}
cp dist/${name}/${name}/* Gopeed/${qarch}/
done
cd Gopeed
sed -i -e 's/__QPKG_VER__/${VERSION}/g' qpkg.cfg
qbuild || exit 1
mkdir -p ../dist/qnap
goos=qnap
for goarch in "${goarch_arr[@]}"; do
qarch=x86_64
[[ "${goarch}" == "arm64" ]] && qarch=arm_64
sname=Gopeed_${VERSION}_${qarch}.qpkg
dname=gopeed-v${VERSION}-${goos}-${goarch}.qpkg
[[ -f build/${sname} ]] && cp -ra build/${sname} ../dist/qnap/${dname}
done
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: dist/qnap/*
overwrite: true
build-docker: build-docker:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [get-release]
steps: steps:
- name: Remove unnecessary files - name: Remove unnecessary files
run: | run: |
@@ -533,7 +672,7 @@ jobs:
- name: Build flutter web - name: Build flutter web
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
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
+1 -1
View File
@@ -17,7 +17,7 @@ on:
- "Dockerfile" - "Dockerfile"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
permissions: permissions:
contents: write contents: write
+9 -8
View File
@@ -27,8 +27,8 @@ on:
- "go.sum" - "go.sum"
env: env:
GO_VERSION: "1.23" GO_VERSION: "1.24"
FLUTTER_VERSION: "3.16.8" FLUTTER_VERSION: "3.24.5"
jobs: jobs:
# lint: # lint:
@@ -53,7 +53,7 @@ jobs:
with: with:
go-version: ${{ env.GO_VERSION }} go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests. - name: Run Unit tests.
run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/ ) -v -covermode=count -coverprofile=coverage.txt run: go test $(go list ./... | grep -v /cmd/ | grep -v /bind/) -v -covermode=count -coverprofile=coverage.txt
- uses: codecov/codecov-action@v4 - uses: codecov/codecov-action@v4
with: with:
files: ./coverage.txt files: ./coverage.txt
@@ -63,7 +63,7 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [ windows-latest, macos-latest, ubuntu-latest ] os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
needs: [ test ] needs: [ test ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -73,7 +73,8 @@ jobs:
- uses: subosito/flutter-action@v2 - uses: subosito/flutter-action@v2
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- if: runner.os == 'windows' channel: master
- if: runner.os == 'Windows'
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/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
@@ -140,7 +141,7 @@ jobs:
- name: Build - name: Build
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
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
@@ -160,8 +161,8 @@ jobs:
- name: Build - name: Build
run: | run: |
cd ui/flutter cd ui/flutter
flutter build web --web-renderer html flutter build web --no-web-resources-cdn
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
docker build -t gopeed . docker build -t gopeed .
+148 -76
View File
@@ -13,80 +13,152 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## Introduction ## 🚀 Introduction
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html). Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com) Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
## Download ## ⬇️ Download
<table> <table>
<thead> <tbody>
<tr> <tr>
<th>Platform</th> <td rowspan="2">🪟 Windows</td>
<th>Package Type</th> <td><code>EXE</code></td>
<th>Download Link</th> <td>amd64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</thead> </tr>
<tbody> <tr>
<tr> <td><code>Portable</code></td>
<td rowspan=2>Windows</td> <td>amd64</td>
<td><code>EXE Installer</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="3">🍎 MacOS</td>
<td><code>Portable ZIP</code></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td> <td>universal</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<tr> </tr>
<td>MacOS</td> <tr>
<td><code>DMG Installer</code></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr> </tr>
<tr> <tr>
<td rowspan=4>Linux</td> <td>arm64</td>
<td><code>Flathub</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="6">🐧 Linux</td>
<td><code>SNAP</code></td> <td><code>Flathub</code></td>
<td><a href="https://snapcraft.io/gopeed">Link</a></td> <td>amd64</td>
</tr> <td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
<tr> </tr>
<td><code>DEB</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td> <td><code>SNAP</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://snapcraft.io/gopeed">📥</a></td>
<td><code>AppImage</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td> <tr>
</tr> <td rowspan="2"><code>DEB</code></td>
<tr> <td>amd64</td>
<td>Android</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
<td><code>APK</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td> <tr>
</tr> <td>arm64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
<td>iOS</td> </tr>
<td><code>IPA</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <td rowspan="2"><code>AppImage</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
<td>Web</td> </tr>
<td></td> <tr>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td> <td>arm64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
<tr> </tr>
<td>Docker</td> <tr>
<td></td> <td rowspan="4">🤖 Android</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td> <td rowspan="4"><code>APK</code></td>
</tr> <td>universal</td>
</tbody> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table> </table>
More about installation, please refer to [Installation](https://docs.gopeed.com/install.html) More about installation, please refer to [Installation](https://docs.gopeed.com/install.html)
### Command tool ### 🛠️ Command tool
use `go install`: use `go install`:
@@ -94,40 +166,40 @@ use `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## Browser Extension ## 🔌 Browser Extension
Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension) Gopeed also provides a browser extension to take over browser downloads, supporting browsers such as Chrome, Edge, Firefox, etc., please refer to: [https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## Donate ## 💝 Donate
If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you! If you like this project, please consider [donating](https://docs.gopeed.com/donate.html) to support the development of this project, thank you!
## Showcase ## 🖼️ Showcase
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## Development ## 👨‍💻 Development
This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used. This project is divided into two parts, the front end uses `flutter`, the back end uses `Golang`, and the two sides communicate through the `http` protocol. On the unix system, `unix socket` is used, and on the windows system, `tcp` protocol is used.
> The front code is located in the `ui/flutter` directory. > The front code is located in the `ui/flutter` directory.
### Environment ### 🌍 Environment
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### Clone ### 📋 Clone
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### Contributing ### 🤝 Contributing
Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
### Build ### 🏗️ Build
#### Desktop #### Desktop
@@ -200,18 +272,18 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## Credits ## ❤️ Credits
### Contributors ### 👥 Contributors
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors"> <a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" /> <img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a> </a>
### JetBrains ### 🏢 JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed) [![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## License ## 📄 License
[GPLv3](LICENSE) [GPLv3](LICENSE)
+145 -72
View File
@@ -13,109 +13,182 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## はじめに ## 🚀 はじめに
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。 Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com) 見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
## インストール ## ⬇️ インストール
<table> <table>
<thead> <tbody>
<tr> <tr>
<th>Platform</th> <td rowspan="2">🪟 Windows</td>
<th>Package Type</th> <td><code>EXE</code></td>
<th>Download Link</th> <td>amd64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</thead> </tr>
<tbody> <tr>
<tr> <td><code>Portable</code></td>
<td rowspan=2>Windows</td> <td>amd64</td>
<td><code>EXE Installer</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Link</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="3">🍎 MacOS</td>
<td><code>Portable ZIP</code></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Link</a></td> <td>universal</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<tr> </tr>
<td>MacOS</td> <tr>
<td><code>DMG Installer</code></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Link</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr> </tr>
<tr> <tr>
<td rowspan=4>Linux</td> <td>arm64</td>
<td><code>Flathub</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Link</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="6">🐧 Linux</td>
<td><code>SNAP</code></td> <td><code>Flathub</code></td>
<td><a href="https://snapcraft.io/gopeed">Link</a></td> <td>amd64</td>
</tr> <td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
<tr> </tr>
<td><code>DEB</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Link</a></td> <td><code>SNAP</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://snapcraft.io/gopeed">📥</a></td>
<td><code>AppImage</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Link</a></td> <tr>
</tr> <td rowspan="2"><code>DEB</code></td>
<tr> <td>amd64</td>
<td>Android</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
<td><code>APK</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Link</a></td> <tr>
</tr> <td>arm64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
<td>iOS</td> </tr>
<td><code>IPA</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td> <td rowspan="2"><code>AppImage</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
<td>Web</td> </tr>
<td></td> <tr>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Link</a></td> <td>arm64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
<tr> </tr>
<td>Docker</td> <tr>
<td></td> <td rowspan="4">🤖 Android</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Link</a></td> <td rowspan="4"><code>APK</code></td>
</tr> <td>universal</td>
</tbody> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table> </table>
インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。 インストールについての詳細は、[インストール](https://docs.gopeed.com/install.html)を参照してください。
## 寄付 ### 🛠️ コマンドツール
## 💝 寄付
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください! もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](https://docs.gopeed.com/donate.html)をご検討ください!
## ショーケース ## 🖼️ ショーケース
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## 開発 ## 👨‍💻 開発
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。 このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
> フロントコードは `ui/flutter` ディレクトリにあります。 > フロントコードは `ui/flutter` ディレクトリにあります。
### 環境 ### 🌍 環境
1. Go 言語 1.23+ 1. Go 言語 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### クローン ### 📋 クローン
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### コントリビュート ### 🤝 コントリビュート
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください [CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
### ビルド ### 🏗️ ビルド
#### デスクトップ #### デスクトップ
@@ -188,7 +261,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## 感謝 ## ❤️ 感謝
### コントリビューター ### コントリビューター
+144 -73
View File
@@ -13,80 +13,151 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## Giới thiệu ## 🚀 Giới thiệu
Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html). Gopeed (tên đầy đủ Go Speed), một công cụ tải xuống tốc độ cao được phát triển bởi `Golang` + `Flutter`, hỗ trợ giao thức (HTTP, BitTorrent, Magnet) và hỗ trợ tất cả các nền tảng. Ngoài các chức năng tải xuống cơ bản, Gopeed còn là một công cụ tải xuống có thể tùy chỉnh cao cho phép triển khai thêm tính năng thông qua việc tích hợp với [APIs](https://docs.gopeed.com/dev-api.html) hoặc cài đặt và phát triển các [tiện ích mở rộng](https://docs.gopeed.com/dev-extension.html).
Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com) Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu chính thức](https://docs.gopeed.com)
## Tải về ## ⬇️ Tải về
<table> <table>
<thead> <tbody>
<tr> <tr>
<th>Nền tảng</th> <td rowspan="2">🪟 Windows</td>
<th>Loại gói</th> <td><code>EXE</code></td>
<th>Liên kết tải xuống</th> <td>amd64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</thead> </tr>
<tbody> <tr>
<tr> <td><code>Portable</code></td>
<td rowspan=2>Windows</td> <td>amd64</td>
<td><code>Bộ cài đặt EXE</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">Liên kết</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="3">🍎 MacOS</td>
<td><code>ZIP Portable</code></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">Liên kết</a></td> <td>universal</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<tr> </tr>
<td>MacOS</td> <tr>
<td><code>Bộ cài đặt DMG</code></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">Liên kết</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr> </tr>
<tr> <tr>
<td rowspan=4>Linux</td> <td>arm64</td>
<td><code>Flathub</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">Liên kết</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="6">🐧 Linux</td>
<td><code>SNAP</code></td> <td><code>Flathub</code></td>
<td><a href="https://snapcraft.io/gopeed">Liên kết</a></td> <td>amd64</td>
</tr> <td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
<tr> </tr>
<td><code>DEB</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">Liên kết</a></td> <td><code>SNAP</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://snapcraft.io/gopeed">📥</a></td>
<td><code>AppImage</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">Liên kết</a></td> <tr>
</tr> <td rowspan="2"><code>DEB</code></td>
<tr> <td>amd64</td>
<td>Android</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
<td><code>APK</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">Liên kết</a></td> <tr>
</tr> <td>arm64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
<td>iOS</td> </tr>
<td><code>IPA</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td> <td rowspan="2"><code>AppImage</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
<td>Web</td> </tr>
<td></td> <tr>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">Liên kết</a></td> <td>arm64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
<tr> </tr>
<td>Docker</td> <tr>
<td></td> <td rowspan="4">🤖 Android</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">Liên kết</a></td> <td rowspan="4"><code>APK</code></td>
</tr> <td>universal</td>
</tbody> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table> </table>
Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html) Thêm thông tin về cài đặt, vui lòng tham khảo [Cài đặt](https://docs.gopeed.com/install.html)
### Command tool ### 🛠️ Công cụ lệnh
Sử dụng `go install`: Sử dụng `go install`:
@@ -94,36 +165,36 @@ Sử dụng `go install`:
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## Quyên góp ## 💝 Quyên góp
Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn! Nếu bạn thích dự án này, xin vui lòng xem xét [quyên góp](https://docs.gopeed.com/donate.html) để hỗ trợ phát triển dự án này, cảm ơn bạn!
## Trưng bày ## 🖼️ Trưng bày
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## Development ## 👨‍💻 Development
Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`. Dự án này được chia thành hai phần, phần giao diện sử dụng `flutter`, phần backend sử dụng `Golang`, và hai phía giao tiếp thông qua giao thức `http`. Trên hệ thống unix, sử dụng `unix socket`, và trên hệ thống windows, sử dụng giao thức `tcp`.
> Mã giao diện nằm trong thư mục `ui/flutter`. > Mã giao diện nằm trong thư mục `ui/flutter`.
### Environment ### 🌍 Environment
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### Clone ### 📋 Clone
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### Đóng góp ### 🤝 Đóng góp
Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md) Vui lòng tham khảo [CONTRIBUTING_vi-VN.md](/CONTRIBUTING_vi-VN.md)
### Xây dựng ### 🏗️ Xây dựng
#### Desktop #### Desktop
@@ -196,7 +267,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## Tín dụng ## ❤️ Tín dụng
### Người đóng góp ### Người đóng góp
+145 -74
View File
@@ -13,80 +13,151 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 介绍 ## 🚀 介绍
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。 Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/) 访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## 下载 ## ⬇️ 下载
<table> <table>
<thead> <tbody>
<tr> <tr>
<th>运行平台</th> <td rowspan="2">🪟 Windows</td>
<th>安装包类型</th> <td><code>EXE</code></td>
<th>下载链接</th> <td>amd64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</thead> </tr>
<tbody> <tr>
<tr> <td><code>Portable</code></td>
<td rowspan=2>Windows</td> <td>amd64</td>
<td><code>EXE Installer</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="3">🍎 MacOS</td>
<td><code>Portable ZIP</code></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td> <td>universal</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<tr> </tr>
<td>MacOS</td> <tr>
<td><code>DMG Installer</code></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr> </tr>
<tr> <tr>
<td rowspan=4>Linux</td> <td>arm64</td>
<td><code>Flathub</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="6">🐧 Linux</td>
<td><code>SNAP</code></td> <td><code>Flathub</code></td>
<td><a href="https://snapcraft.io/gopeed">前往</a></td> <td>amd64</td>
</tr> <td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
<tr> </tr>
<td><code>DEB</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td> <td><code>SNAP</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://snapcraft.io/gopeed">📥</a></td>
<td><code>AppImage</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td> <tr>
</tr> <td rowspan="2"><code>DEB</code></td>
<tr> <td>amd64</td>
<td>Android</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
<td><code>APK</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td> <tr>
</tr> <td>arm64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
<td>iOS</td> </tr>
<td><code>IPA</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td> <td rowspan="2"><code>AppImage</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
<td>Web</td> </tr>
<td></td> <tr>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td> <td>arm64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
<tr> </tr>
<td>Docker</td> <tr>
<td></td> <td rowspan="4">🤖 Android</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td> <td rowspan="4"><code>APK</code></td>
</tr> <td>universal</td>
</tbody> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table> </table>
更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html) 更多关于安装的内容请参考[安装文档](https://docs.gopeed.com/zh/install.html)
### 命令行工具 ### 🛠️ 命令行工具
使用`go install`安装: 使用`go install`安装:
@@ -94,40 +165,40 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## 浏览器扩展 ## 🔌 浏览器扩展
Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension) Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、Edge、Firefox 等浏览器,具体请参考:[https://github.com/GopeedLab/browser-extension](https://github.com/GopeedLab/browser-extension)
## 赞助 ## 💝 赞助
如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢! 如果觉得项目对你有帮助,请考虑[赞助](https://docs.gopeed.com/zh/donate)以支持这个项目的发展,非常感谢!
## 界面展示 ## 🖼️ 界面展示
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## 开发 ## 👨‍💻 开发
本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。 本项目分为前端和后端两个部分,前端使用`flutter`,后端使用`Golang`,两边通过`http`协议进行通讯,在 unix 系统下,使用的是`unix socket`,在 windows 系统下,使用的是`tcp`协议。
> 前端代码位于`ui/flutter`目录下。 > 前端代码位于`ui/flutter`目录下。
### 环境要求 ### 🌍 环境要求
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### 克隆项目 ### 📋 克隆项目
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### 贡献代码 ### 🤝 贡献代码
请参考[贡献指南](CONTRIBUTING_zh-CN.md) 请参考[贡献指南](CONTRIBUTING_zh-CN.md)
### 编译 ### 🏗️ 编译
#### 桌面端 #### 桌面端
@@ -200,7 +271,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## 感谢 ## ❤️ 感谢
### 贡献者 ### 贡献者
+144 -72
View File
@@ -13,80 +13,152 @@
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md) | [Tiếng Việt](/README_vi-VN.md)
## 簡介 ## 🚀 簡介
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。 Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/) 前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
## 下載 ## ⬇️ 下載
<table> <table>
<thead> <tbody>
<tr> <tr>
<th>運行平台</th> <td rowspan="2">🪟 Windows</td>
<th>安裝包類型</th> <td><code>EXE</code></td>
<th>下載鏈接</th> <td>amd64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">📥</a></td>
</thead> </tr>
<tbody> <tr>
<tr> <td><code>Portable</code></td>
<td rowspan=2>Windows</td> <td>amd64</td>
<td><code>EXE Installer</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">📥</a></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64.zip">前往</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="3">🍎 MacOS</td>
<td><code>Portable ZIP</code></td> <td rowspan="3"><code>DMG</code></td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-windows-amd64-portable.zip">前往</a></td> <td>universal</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">📥</a></td>
<tr> </tr>
<td>MacOS</td> <tr>
<td><code>DMG Installer</code></td> <td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos.dmg">前往</a></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-amd64.dmg">📥</a></td>
</tr> </tr>
<tr> <tr>
<td rowspan=4>Linux</td> <td>arm64</td>
<td><code>Flathub</code></td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-macos-arm64.dmg">📥</a></td>
<td><a href="https://flathub.org/apps/com.gopeed.Gopeed">前往</a></td> </tr>
</tr> <tr>
<tr> <td rowspan="6">🐧 Linux</td>
<td><code>SNAP</code></td> <td><code>Flathub</code></td>
<td><a href="https://snapcraft.io/gopeed">前往</a></td> <td>amd64</td>
</tr> <td><a href="https://flathub.org/apps/com.gopeed.Gopeed">📥</a></td>
<tr> </tr>
<td><code>DEB</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">前往</a></td> <td><code>SNAP</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://snapcraft.io/gopeed">📥</a></td>
<td><code>AppImage</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-x86_64.AppImage">前往</a></td> <tr>
</tr> <td rowspan="2"><code>DEB</code></td>
<tr> <td>amd64</td>
<td>Android</td> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.deb">📥</a></td>
<td><code>APK</code></td> </tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">前往</a></td> <tr>
</tr> <td>arm64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.deb">📥</a></td>
<td>iOS</td> </tr>
<td><code>IPA</code></td> <tr>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td> <td rowspan="2"><code>AppImage</code></td>
</tr> <td>amd64</td>
<tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-amd64.AppImage">📥</a></td>
<td>Web</td> </tr>
<td></td> <tr>
<td><a href="https://github.com/GopeedLab/gopeed/releases/latest">前往</a></td> <td>arm64</td>
</tr> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-linux-arm64.AppImage">📥</a></td>
<tr> </tr>
<td>Docker</td> <tr>
<td></td> <td rowspan="4">🤖 Android</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">前往</a></td> <td rowspan="4"><code>APK</code></td>
</tr> <td>universal</td>
</tbody> <td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android.apk">📥</a></td>
</tr>
<tr>
<td>armeabi-v7a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-armeabi-v7a.apk">📥</a></td>
</tr>
<tr>
<td>arm64-v8a</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-arm64-v8a.apk">📥</a></td>
</tr>
<tr>
<td>x86_64</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-android-x86_64.apk">📥</a></td>
</tr>
<tr>
<td>📱 iOS</td>
<td><code>IPA</code></td>
<td>universal</td>
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">📥</a></td>
</tr>
<tr>
<td>🐳 Docker</td>
<td>-</td>
<td>universal</td>
<td><a href="https://hub.docker.com/r/liwei2633/gopeed">📥</a></td>
</tr>
<tr>
<td rowspan="2">💾 Qnap</td>
<td rowspan="2"><code>QPKG</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-amd64.qpkg">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-$version-qnap-arm64.qpkg">📥</a></td>
</tr>
<tr>
<td rowspan="8">🌐 Web</td>
<td rowspan="3"><code>Windows</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-windows-386.zip">📥</a></td>
</tr>
<tr>
<td rowspan="2"><code>MacOS</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-macos-arm64.zip">📥</a></td>
</tr>
<tr>
<td rowspan="3"><code>Linux</code></td>
<td>amd64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-amd64.zip">📥</a></td>
</tr>
<tr>
<td>arm64</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-arm64.zip">📥</a></td>
</tr>
<tr>
<td>386</td>
<td><a href="https://gopeed.com/api/download?tpl=gopeed-web-$version-linux-386.zip">📥</a></td>
</tr>
</tbody>
</table> </table>
更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html) 更多關於安裝的內容請參考[安裝文檔](https://docs.gopeed.com/zh/install.html)
### 使用CLI安裝 ### 🛠️ 使用 CLI 安裝
使用`go install`安裝: 使用`go install`安裝:
@@ -94,36 +166,36 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
``` ```
## 贊助 ## 💝 贊助
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝! 如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
## 軟體介面 ## 🖼️ 軟體介面
![](_docs/img/ui-demo.png) ![](_docs/img/ui-demo.png)
## 開發 ## 👨‍💻 開發
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。 該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
> 前端代碼位於`ui/flutter`目錄內。 > 前端代碼位於`ui/flutter`目錄內。
### 開發環境 ### 🌍 開發環境
1. Golang 1.23+ 1. Golang 1.23+
2. Flutter 3.16+ 2. Flutter 3.24+
### 克隆項目 ### 📋 克隆項目
```bash ```bash
git clone [email protected]:GopeedLab/gopeed.git git clone [email protected]:GopeedLab/gopeed.git
``` ```
### 協助開發 ### 🤝 協助開發
請參考[協助指南](CONTRIBUTING_zh-TW.md) 請參考[協助指南](CONTRIBUTING_zh-TW.md)
### 編譯 ### 🏗️ 編譯
#### 桌面端 #### 桌面端
@@ -196,7 +268,7 @@ cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
``` ```
## 感謝 ## ❤️ 感謝
### 貢獻者 ### 貢獻者
+125
View File
@@ -0,0 +1,125 @@
package main
import (
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strings"
"github.com/pkg/browser"
"github.com/shirou/gopsutil/v4/process"
)
const identifier = "gopeed"
type Message struct {
Method string `json:"method"`
Params json.RawMessage `json:"params"`
}
type Response struct {
Code int `json:"code"`
Data any `json:"data,omitempty"`
Message string `json:"message,omitempty"`
}
var apiMap = map[string]func(params json.RawMessage) (data any, err error){
"ping": func(params json.RawMessage) (data any, err error) {
processes, err := process.Processes()
if err != nil {
return false, err
}
for _, p := range processes {
name, err := p.Name()
if err != nil {
continue
}
if strings.Contains(strings.ToLower(name), strings.ToLower(identifier)) {
return true, nil
}
}
return false, nil
},
"create": func(params json.RawMessage) (data any, err error) {
var strParams string
if err = json.Unmarshal(params, &strParams); err != nil {
return
}
err = browser.OpenURL(fmt.Sprintf("%s:///create?params=%s", identifier, strParams))
return
},
}
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/host
func main() {
for {
// Read message length (first 4 bytes)
var length uint32
if err := binary.Read(os.Stdin, binary.NativeEndian, &length); err != nil {
if err == io.EOF {
// Connection closed by Chrome
return
}
sendError("Failed to read message length: " + err.Error())
return
}
// Read the message
input := make([]byte, length)
if _, err := io.ReadFull(os.Stdin, input); err != nil {
sendError("Failed to read message: " + err.Error())
return
}
// Parse message
var message Message
if err := json.Unmarshal(input, &message); err != nil {
sendError("Failed to parse message: " + err.Error())
return
}
// Handle request
var data any
var err error
if handler, ok := apiMap[message.Method]; ok {
data, err = handler(message.Params)
} else {
err = errors.New("Unknown method: " + message.Method)
}
if err != nil {
sendError(err.Error())
continue
}
sendResponse(0, data, "")
}
}
func sendResponse(code int, data interface{}, message string) {
response := Response{
Code: code,
Data: data,
Message: message,
}
// Encode response
responseBytes, err := json.Marshal(response)
if err != nil {
sendError("Failed to encode response: " + err.Error())
return
}
// Write message length
binary.Write(os.Stdout, binary.NativeEndian, uint32(len(responseBytes)))
// Write message
os.Stdout.Write(responseBytes)
}
func sendError(msg string) {
sendResponse(1, nil, msg)
}
+116
View File
@@ -0,0 +1,116 @@
package main
import (
"flag"
"fmt"
"log"
"os"
"path/filepath"
"syscall"
"time"
"github.com/pkg/browser"
"github.com/pkg/errors"
)
// go build -ldflags="-s -w" -o ui/flutter/assets/exec/ github.com/GopeedLab/gopeed/cmd/updater
func main() {
pid := flag.Int("pid", 0, "PID of the process to update")
updateChannel := flag.String("channel", "", "Update channel")
packagePath := flag.String("asset", "", "Path to the package asset")
logPath := flag.String("log", "", "Log file path")
flag.Parse()
if *pid == 0 {
log.Println("Invalid PID")
os.Exit(1)
}
if *updateChannel == "" {
log.Println("Invalid update channel")
os.Exit(1)
}
if *logPath != "" {
logFile, err := os.OpenFile(*logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0666)
if err == nil {
defer logFile.Close()
log.SetOutput(logFile)
}
}
var (
restart bool
err error
)
if restart, err = update(*pid, *updateChannel, *packagePath); err != nil {
log.Printf("Update failed: %v\n", err)
os.Exit(1)
}
// Restart the application
if restart {
browser.OpenURL("gopeed:///")
}
// Delete package asset
if *packagePath != "" {
os.Remove(*packagePath)
}
os.Exit(0)
}
func update(pid int, updateChannel, packagePath string) (restart bool, err error) {
killSignalChan := make(chan any, 1)
go func() {
<-killSignalChan
if err = killProcess(pid); err != nil {
log.Printf("Failed to kill process: %v\n", err)
}
if err = waitForProcessExit(pid); err != nil {
log.Printf("Failed to wait for process exit: %v\n", err)
}
}()
appDir := filepath.Dir(os.Args[0])
log.Printf("Updating process updateChannel=%s packagePath=%s appDir=%s\n", updateChannel, packagePath, appDir)
if restart, err = install(killSignalChan, updateChannel, packagePath, appDir); err != nil {
return false, errors.Wrap(err, "failed to install package")
}
return
}
func waitForProcessExit(pid int) error {
deadline := time.Now().Add(10 * time.Second)
for time.Now().Before(deadline) {
process, err := os.FindProcess(pid)
if err != nil {
// On some systems, error is returned if process doesn't exist
return nil
}
// Send null signal to test if process exists
err = process.Signal(syscall.Signal(0))
if err != nil {
// If error occurs, the process no longer exists
return nil
}
time.Sleep(100 * time.Millisecond)
}
return fmt.Errorf("process %d still running after timeout", pid)
}
func killProcess(pid int) error {
process, err := os.FindProcess(pid)
if err != nil {
return err
}
return process.Kill()
}
+68
View File
@@ -0,0 +1,68 @@
//go:build darwin
package main
import (
"fmt"
"os/exec"
"path/filepath"
"strings"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
return true, installByDmg(killSignalChan, packagePath, destDir)
}
// installByDmg handles macOS dmg package installation
func installByDmg(killSignalChan chan<- any, packagePath, destDir string) error {
// /Applications/Gopeed.app/Contents/MacOS -> /Applications
appPath := getParentDir(getParentDir(getParentDir(destDir)))
output, err := exec.Command("hdiutil", "attach", packagePath, "-nobrowse").Output()
if err != nil {
return err
}
mountPoint := ""
for _, line := range strings.Split(string(output), "\n") {
if strings.Contains(line, "/Volumes/") {
fields := strings.Fields(line)
if len(fields) > 2 {
mountPoint = fields[len(fields)-1]
break
}
}
}
if mountPoint == "" {
return fmt.Errorf("failed to get mount point")
}
// Detach the mounted DMG
defer exec.Command("hdiutil", "detach", mountPoint, "-quiet").Run()
matches, err := filepath.Glob(filepath.Join(mountPoint, "*.app"))
if err != nil {
return err
}
if len(matches) == 0 {
return fmt.Errorf("no .app found in dmg")
}
killSignalChan <- nil
// Copy the new app to the destination
// cp -Rf /Volumes/GoPeed/GoPeed.app /Applications
if err := exec.Command("cp", "-Rf", matches[0], appPath).Run(); err != nil {
return err
}
return nil
}
// Get parent directory safely, handling trailing separators
func getParentDir(path string) string {
// Remove trailing separators if they exist
path = strings.TrimRight(path, string(filepath.Separator))
// Now get the parent directory
return filepath.Dir(path)
}
+72
View File
@@ -0,0 +1,72 @@
//go:build linux
package main
import (
"fmt"
"os/exec"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
killSignalChan <- nil
switch updateChannel {
case "linuxDeb":
return true, installByDeb(packagePath)
case "linuxFlathub":
return true, installByFlathub()
case "linuxSnap":
return true, installBySnap()
default:
return false, fmt.Errorf("unsupported update channel for Linux: %s", updateChannel)
}
}
// executeInTerminal tries to execute a command in one of several common terminal emulators
func executeInTerminal(command string) error {
terminals := []string{
"gnome-terminal", // GNOME
"konsole", // KDE
"xfce4-terminal", // XFCE
"xterm", // X11
}
command = fmt.Sprintf(`echo "Starting update..." && echo "[CMD] %s" && %s`, command, command)
for _, term := range terminals {
if _, err := exec.LookPath(term); err == nil {
var cmd *exec.Cmd
switch term {
case "gnome-terminal", "xfce4-terminal":
cmd = exec.Command(term, "--", "bash", "-c", command)
case "konsole":
cmd = exec.Command(term, "-e", "bash", "-c", command)
case "xterm":
cmd = exec.Command(term, "-e", command)
}
if err := cmd.Start(); err == nil {
return nil
}
}
}
return fmt.Errorf("no suitable terminal emulator found. Please install gnome-terminal, konsole, xfce4-terminal or xterm")
}
// installByDeb installs the .deb package
func installByDeb(packagePath string) error {
command := fmt.Sprintf(`sudo dpkg -i "%s"`, packagePath)
return executeInTerminal(command)
}
// installByFlathub updates the application via Flathub
func installByFlathub() error {
command := "flatpak update com.gopeed.Gopeed -y"
return executeInTerminal(command)
}
// installBySnap updates the application via Snap
func installBySnap() error {
command := "sudo snap refresh gopeed"
return executeInTerminal(command)
}
+134
View File
@@ -0,0 +1,134 @@
//go:build windows
package main
import (
"archive/zip"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
)
func install(killSignalChan chan<- any, updateChannel, packagePath, destDir string) (bool, error) {
switch updateChannel {
case "windowsInstaller":
return false, installByInstaller(killSignalChan, packagePath, destDir)
default:
return true, installByPortable(killSignalChan, packagePath, destDir)
}
}
// installByInstaller extracts the installer from the zip file and runs it
func installByInstaller(killSignalChan chan<- any, packagePath, destDir string) error {
// Create a temp directory for extraction
tempDir, err := os.MkdirTemp("", "gopeed_update")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
// Extract the zip file
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
// Find the installer file
var installerPath string
for _, file := range reader.File {
if file.FileInfo().IsDir() {
continue
}
// Extract file
path := filepath.Join(tempDir, file.Name)
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}
_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
// If this is likely an installer (.exe, .msi), save its path
ext := strings.ToLower(filepath.Ext(file.Name))
if ext == ".exe" || ext == ".msi" {
installerPath = path
}
}
if installerPath == "" {
return fmt.Errorf("no installer found in the update package")
}
// Run the installer
cmd := exec.Command(installerPath)
if err := cmd.Start(); err != nil {
return err
}
killSignalChan <- nil
return nil
}
// installByPortable extracts the portable version to the destination directory
func installByPortable(killSignalChan chan<- any, packagePath, destDir string) error {
killSignalChan <- nil
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
for _, file := range reader.File {
path := filepath.Join(destDir, file.Name)
if file.FileInfo().IsDir() {
os.MkdirAll(path, file.Mode())
continue
}
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
dstFile, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode())
if err != nil {
return err
}
srcFile, err := file.Open()
if err != nil {
dstFile.Close()
return err
}
_, err = io.Copy(dstFile, srcFile)
srcFile.Close()
dstFile.Close()
if err != nil {
return err
}
}
return nil
}
+13 -1
View File
@@ -24,6 +24,16 @@ require (
golang.org/x/sync v0.9.0 golang.org/x/sync v0.9.0
) )
require (
github.com/ebitengine/purego v0.8.1 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c // indirect
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
)
require ( require (
dario.cat/mergo v1.0.0 // indirect dario.cat/mergo v1.0.0 // indirect
github.com/Microsoft/go-winio v0.6.1 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect
@@ -99,9 +109,11 @@ require (
github.com/pion/turn/v4 v4.0.0 // indirect github.com/pion/turn/v4 v4.0.0 // indirect
github.com/pion/webrtc/v4 v4.0.2 // indirect github.com/pion/webrtc/v4 v4.0.2 // indirect
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c
github.com/protolambda/ctxlock v0.1.0 // indirect github.com/protolambda/ctxlock v0.1.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/shirou/gopsutil/v4 v4.24.12
github.com/skeema/knownhosts v1.2.0 // indirect github.com/skeema/knownhosts v1.2.0 // indirect
github.com/spaolacci/murmur3 v1.1.0 // indirect github.com/spaolacci/murmur3 v1.1.0 // indirect
github.com/tidwall/btree v1.7.0 // indirect github.com/tidwall/btree v1.7.0 // indirect
@@ -113,7 +125,7 @@ require (
golang.org/x/crypto v0.29.0 // indirect golang.org/x/crypto v0.29.0 // indirect
golang.org/x/mod v0.22.0 // indirect golang.org/x/mod v0.22.0 // indirect
golang.org/x/net v0.31.0 // indirect golang.org/x/net v0.31.0 // indirect
golang.org/x/sys v0.27.0 // indirect golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.20.0 // indirect golang.org/x/text v0.20.0 // indirect
golang.org/x/time v0.8.0 // indirect golang.org/x/time v0.8.0 // indirect
golang.org/x/tools v0.27.0 // indirect golang.org/x/tools v0.27.0 // indirect
+25
View File
@@ -143,6 +143,8 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I=
github.com/ebitengine/purego v0.8.1 h1:sdRKd6plj7KYW33EH5As6YKfe8m9zbN9JMrOjNVF/BE=
github.com/ebitengine/purego v0.8.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84= github.com/edsrzf/mmap-go v1.2.0 h1:hXLYlkbaPzt1SaQk+anYwKSRNhufIDCchSPkUD6dD84=
github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= github.com/edsrzf/mmap-go v1.2.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
@@ -187,6 +189,8 @@ github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI= github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow= github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q=
@@ -288,6 +292,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c h1:VtwQ41oftZwlMnOEbMWQtSEUgU64U4s+GHk7hZK+jtY=
github.com/lufia/plan9stats v0.0.0-20220913051719-115f729f3c8c/go.mod h1:JKx41uQRwqlTZabZc+kILPrO/3jlKnQ2Z8b7YiVw5cE=
github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U= github.com/matoous/go-nanoid v1.5.0/go.mod h1:zyD2a71IubI24efhpvkJz+ZwfwagzgSO6UNiFsZKN7U=
github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0= github.com/matoous/go-nanoid/v2 v2.0.0 h1:d19kur2QuLeHmJBkvYkFdhFBzLoo1XVm2GgTpL+9Tj0=
github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g= github.com/matoous/go-nanoid/v2 v2.0.0/go.mod h1:FtS4aGPVfEkxKxhdWPAspZpZSh1cOjtM7Ej/So3hR0g=
@@ -363,6 +369,8 @@ github.com/pion/webrtc/v4 v4.0.2 h1:fBwm5/hqSUybrCWl0DDBSTDrpbkcgkqpeLmXw9CsBQA=
github.com/pion/webrtc/v4 v4.0.2/go.mod h1:moylBT2A4dNoEaYBCdV1nThM3TLwRHzWszIG+eSPaqQ= github.com/pion/webrtc/v4 v4.0.2/go.mod h1:moylBT2A4dNoEaYBCdV1nThM3TLwRHzWszIG+eSPaqQ=
github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4=
github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -370,6 +378,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c h1:NRoLoZvkBTKvR5gQLgA3e0hqjkY9u1wm+iOL45VN/qI=
github.com/power-devops/perfstat v0.0.0-20220216144756-c35f1ee13d7c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
@@ -412,6 +422,8 @@ github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46 h1:GHRpF1pTW19a
github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8= github.com/ryszard/goskiplist v0.0.0-20150312221310-2dfbae5fcf46/go.mod h1:uAQ5PCi+MFsC7HjREoAz1BU+Mq60+05gifQSsHSDG/8=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shirou/gopsutil/v4 v4.24.12 h1:qvePBOk20e0IKA1QXrIIU+jmk+zEiYVVx06WjBRlZo4=
github.com/shirou/gopsutil/v4 v4.24.12/go.mod h1:DCtMPAad2XceTeIAbGyVfycbYQNBGk2P8cvDi7/VN9o=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
@@ -447,6 +459,10 @@ github.com/tidwall/btree v1.7.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EU
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= github.com/tinylib/msgp v1.1.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4= github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
@@ -458,6 +474,8 @@ github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad/go.mod h1:Jj8p9bgK
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0=
go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
@@ -543,6 +561,7 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -552,6 +571,7 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
@@ -562,13 +582,18 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
+4 -2
View File
@@ -24,8 +24,10 @@ type DefaultFileController struct {
func NewController() *Controller { func NewController() *Controller {
return &Controller{ return &Controller{
GetConfig: func(v any) {}, GetConfig: func(v any) {},
GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) { return nil }, GetProxy: func(requestProxy *base.RequestProxy) func(*http.Request) (*url.URL, error) {
return requestProxy.ToHandler()
},
FileController: &DefaultFileController{}, FileController: &DefaultFileController{},
} }
} }
+11 -4
View File
@@ -100,9 +100,6 @@ func (f *Fetcher) initClient() (err error) {
} }
func (f *Fetcher) Resolve(req *base.Request) error { func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return err
}
f.meta.Req = req f.meta.Req = req
if err := f.addTorrent(req, false); err != nil { if err := f.addTorrent(req, false); err != nil {
return err return err
@@ -355,6 +352,9 @@ func (f *Fetcher) WaitUpload() (err error) {
} }
func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) { func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
if err = base.ParseReqExtra[bt.ReqExtra](req); err != nil {
return
}
if err = f.initClient(); err != nil { if err = f.initClient(); err != nil {
return return
} }
@@ -363,7 +363,14 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
f.torrent, err = client.AddMagnet(req.URL) f.torrent, err = client.AddMagnet(req.URL)
} else { } else {
var reader io.Reader var reader io.Reader
if schema == "DATA" { if schema == "FILE" {
fileUrl, _ := url.Parse(req.URL)
filePath := fileUrl.Path[1:]
reader, err = os.Open(filePath)
if err != nil {
return
}
} else if schema == "DATA" {
_, data := util.ParseDataUri(req.URL) _, data := util.ParseDataUri(req.URL)
reader = bytes.NewBuffer(data) reader = bytes.NewBuffer(data)
} else { } else {
+11
View File
@@ -11,6 +11,7 @@ import (
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
"os" "os"
"path/filepath"
"reflect" "reflect"
"testing" "testing"
) )
@@ -149,6 +150,16 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
} }
}) })
t.Run("Resolve file scheme Torrent", func(t *testing.T) {
file, _ := filepath.Abs("./testdata/test.unclean.torrent")
uri := "file:///" + file
err := fetcher.Resolve(&base.Request{
URL: uri,
})
if err != nil {
t.Errorf("Resolve file scheme Torrent Resolve() got = %v, want nil", err)
}
})
} }
func TestFetcherManager_ParseName(t *testing.T) { func TestFetcherManager_ParseName(t *testing.T) {
+260 -112
View File
@@ -16,6 +16,7 @@ import (
"path" "path"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/GopeedLab/gopeed/internal/controller" "github.com/GopeedLab/gopeed/internal/controller"
@@ -26,8 +27,11 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
const connectTimeout = 15 * time.Second const (
const readTimeout = 15 * time.Second connectTimeout = 15 * time.Second
readTimeout = 15 * time.Second
helpMinSize = 1 * 1024 * 1024
)
type RequestError struct { type RequestError struct {
Code int Code int
@@ -46,10 +50,22 @@ type chunk struct {
Begin int64 Begin int64
End int64 End int64
Downloaded int64 Downloaded int64
}
type connection struct {
Chunk *chunk
Downloaded int64
Completed bool
failed bool
retryTimes int retryTimes int
} }
// get remain to download bytes
func (c *chunk) remain() int64 {
return c.End - c.Begin + 1 - c.Downloaded
}
func newChunk(begin int64, end int64) *chunk { func newChunk(begin int64, end int64) *chunk {
return &chunk{ return &chunk{
Begin: begin, Begin: begin,
@@ -62,8 +78,11 @@ type Fetcher struct {
config *config config *config
doneCh chan error doneCh chan error
meta *fetcher.FetcherMeta meta *fetcher.FetcherMeta
chunks []*chunk connections []*connection
helpLock sync.Mutex
redirectURL string
redirectLock sync.Mutex
file *os.File file *os.File
cancel context.CancelFunc cancel context.CancelFunc
@@ -107,9 +126,9 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) { if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// response 206 status code, support breakpoint continuation // response 206 status code, support breakpoint continuation
res.Range = true res.Range = true
// parse content length from Content-Range header, eg: bytes 0-1000/1001 // parse content length from Content-Range header, eg: bytes 0-1000/1001 or bytes 0-0/*
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
if contentTotal != "" { if contentTotal != "" && contentTotal != "*" {
parse, err := strconv.ParseInt(contentTotal, 10, 64) parse, err := strconv.ParseInt(contentTotal, 10, 64)
if err != nil { if err != nil {
return err return err
@@ -147,12 +166,29 @@ func (f *Fetcher) Resolve(req *base.Request) error {
_, params, _ := mime.ParseMediaType(contentDisposition) _, params, _ := mime.ParseMediaType(contentDisposition)
filename := params["filename"] filename := params["filename"]
if filename != "" { if filename != "" {
file.Name = filename // Check if the filename is MIME encoded-word
if strings.HasPrefix(filename, "=?") {
decoder := new(mime.WordDecoder)
filename = strings.Replace(filename, "UTF8", "UTF-8", 1)
file.Name, _ = decoder.Decode(filename)
} else {
file.Name = filename
}
} else {
substr := "attachment; filename="
index := strings.Index(contentDisposition, substr)
if index != -1 {
file.Name = contentDisposition[index+len(substr):]
}
} }
} }
// get file filePath by URL // get file filePath by URL
if file.Name == "" { if file.Name == "" {
file.Name = path.Base(httpReq.URL.Path) file.Name = path.Base(httpReq.URL.Path)
// Url decode
if file.Name != "" {
file.Name, _ = url.QueryUnescape(file.Name)
}
} }
// unknown file filePath // unknown file filePath
if file.Name == "" || file.Name == "/" || file.Name == "." { if file.Name == "" || file.Name == "/" || file.Name == "." {
@@ -205,9 +241,10 @@ func (f *Fetcher) Start() (err error) {
return err return err
} }
if f.chunks == nil { if f.connections == nil {
f.chunks = f.splitChunk() f.connections = f.splitConnection()
} }
f.redirectURL = ""
f.fetch() f.fetch()
return return
} }
@@ -234,16 +271,26 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
} }
func (f *Fetcher) Stats() any { func (f *Fetcher) Stats() any {
// todo http download health statsConnections := make([]*fhttp.StatsConnection, 0)
return &fhttp.Stats{} for _, connection := range f.connections {
statsConnections = append(statsConnections, &fhttp.StatsConnection{
Downloaded: connection.Downloaded,
Completed: connection.Completed,
Failed: connection.failed,
RetryTimes: connection.retryTimes,
})
}
return &fhttp.Stats{
Connections: statsConnections,
}
} }
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.connections) > 0 {
total := int64(0) total := int64(0)
for _, chunk := range f.chunks { for _, connection := range f.connections {
total += chunk.Downloaded total += connection.Downloaded
} }
p = append(p, total) p = append(p, total)
} }
@@ -254,24 +301,20 @@ func (f *Fetcher) Wait() (err error) {
return <-f.doneCh return <-f.doneCh
} }
type fetchResult struct {
err error
}
func (f *Fetcher) fetch() { func (f *Fetcher) fetch() {
var ctx context.Context var ctx context.Context
ctx, f.cancel = context.WithCancel(context.Background()) ctx, f.cancel = context.WithCancel(context.Background())
f.eg, _ = errgroup.WithContext(ctx) f.eg, _ = errgroup.WithContext(ctx)
chunkErrs := make([]error, len(f.chunks)) connectionErrs := make([]error, len(f.connections))
for i := 0; i < len(f.chunks); i++ { for i := 0; i < len(f.connections); i++ {
i := i i := i
f.eg.Go(func() error { f.eg.Go(func() error {
err := f.fetchChunk(i, ctx) err := f.run(i, ctx)
// if canceled, fail fast // if canceled, fail fast
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return err return err
} }
chunkErrs[i] = err connectionErrs[i] = err
return nil return nil
}) })
} }
@@ -283,7 +326,7 @@ func (f *Fetcher) fetch() {
return return
} }
// check all fetch results, if any error, return // check all fetch results, if any error, return
for _, chunkErr := range chunkErrs { for _, chunkErr := range connectionErrs {
if chunkErr != nil { if chunkErr != nil {
err = chunkErr err = chunkErr
break break
@@ -299,102 +342,192 @@ func (f *Fetcher) fetch() {
}() }()
} }
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) { func (f *Fetcher) run(index int, ctx context.Context) (err error) {
chunk := f.chunks[index] connection := f.connections[index]
chunk.retryTimes = 0 connection.failed = false
connection.retryTimes = 0
var ( var (
client = f.buildClient() client = f.buildClient()
buf = make([]byte, 8192) buf = make([]byte, 8192)
maxRetries = 3
) )
// retry until all remain chunks failed
for {
// if chunk is completed, return
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
return nil
}
if chunk.retryTimes >= maxRetries { downloadChunk := func(chunk *chunk) (err error) {
return // retry until all remain chunks failed
} for {
// if chunk is completed, return
var ( if f.meta.Res.Range && chunk.remain() <= 0 {
httpReq *http.Request return nil
resp *http.Response
)
httpReq, err = f.buildRequest(ctx, f.meta.Req)
if err != nil {
return
}
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else {
chunk.Downloaded = 0
}
err = func() error {
resp, err = client.Do(httpReq)
if err != nil {
return err
} }
defer resp.Body.Close() // if all chunks failed, return
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent { if connection.failed {
err = NewRequestError(resp.StatusCode, resp.Status) allFailed := true
return err for _, c := range f.connections {
} if c.Completed {
reader := NewTimeoutReader(resp.Body, readTimeout) continue
for { }
n, err := reader.Read(buf) if !c.failed {
if n > 0 { allFailed = false
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded) break
if err != nil {
return err
} }
chunk.Downloaded += int64(n)
} }
if err != nil { if allFailed {
if err == io.EOF { if connection.retryTimes >= 3 {
return nil return
} else {
connection.retryTimes++
} }
}
}
err = func() error {
var (
httpReq *http.Request
resp *http.Response
)
f.redirectLock.Lock()
if f.redirectURL != "" {
f.redirectLock.Unlock()
}
err = func() (err error) {
defer func() {
if f.redirectURL == "" {
if err == nil {
f.redirectURL = resp.Request.URL.String()
}
f.redirectLock.Unlock()
}
}()
httpReq, err = f.buildRequest(ctx, f.meta.Req)
if err != nil {
return
}
if f.meta.Res.Range {
httpReq.Header.Set(base.HttpHeaderRange,
fmt.Sprintf(base.HttpHeaderRangeFormat, chunk.Begin+chunk.Downloaded, chunk.End))
} else {
chunk.Downloaded = 0
}
resp, err = client.Do(httpReq)
if err != nil {
return
}
return
}()
if err != nil {
return err return err
} }
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status)
return err
}
connection.failed = false
reader := NewTimeoutReader(resp.Body, readTimeout)
for {
n, err := reader.Read(buf)
if n > 0 {
finished := false
if f.meta.Res.Range {
remain := chunk.remain()
// If downloaded bytes exceed the remain bytes, only write remain bytes
if remain < int64(n) {
n = int(remain)
finished = true
}
}
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
if err != nil {
return err
}
chunk.Downloaded += int64(n)
connection.Downloaded += int64(n)
if finished {
return nil
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second
connection.failed = true
time.Sleep(time.Second)
continue
} }
}() break
if err != nil { }
// If canceled, do not retry return
if errors.Is(err, context.Canceled) { }
return
} for {
// retry request after 1 second if err = downloadChunk(connection.Chunk); err != nil {
chunk.retryTimes = chunk.retryTimes + 1 return
time.Sleep(time.Second) }
// check this connection is completed
if !f.meta.Res.Range || !f.helpOtherConnection(connection) {
connection.Completed = true
return
}
}
}
func (f *Fetcher) helpOtherConnection(helper *connection) bool {
f.helpLock.Lock()
defer f.helpLock.Unlock()
// find the slowest connection
var maxRemainConnection *connection
var maxRemain int64
for _, r := range f.connections {
if r == helper || r.Completed {
continue continue
} }
// if chunk is completed, reset other not complete chunks retry times
if err == nil { remain := r.Chunk.remain()
for _, c := range f.chunks { if remain > maxRemain && remain > helpMinSize {
if c != chunk && c.retryTimes > 0 { maxRemainConnection = r
c.retryTimes = 0 maxRemain = remain
}
}
} }
break
} }
return
if maxRemainConnection == nil {
return false
}
// re-calculate the chunk range
helper.Chunk.Begin = maxRemainConnection.Chunk.End - maxRemainConnection.Chunk.remain()/2
helper.Chunk.End = maxRemainConnection.Chunk.End
helper.Chunk.Downloaded = 0
maxRemainConnection.Chunk.End = helper.Chunk.Begin - 1
return true
} }
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) { func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
url, err := url.Parse(req.URL) var reqUrl string
if err != nil { if f.redirectURL != "" {
return reqUrl = f.redirectURL
} else {
reqUrl = req.URL
} }
var ( var (
method string method string
body io.Reader body io.Reader
) )
headers := http.Header{} headers := http.Header{}
if req.Extra == nil { if req.Extra == nil {
method = http.MethodGet method = http.MethodGet
@@ -419,41 +552,48 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
} }
if ctx != nil { if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body) httpReq, err = http.NewRequestWithContext(ctx, method, reqUrl, body)
} else { } else {
httpReq, err = http.NewRequest(method, url.String(), body) httpReq, err = http.NewRequest(method, reqUrl, body)
} }
if err != nil { if err != nil {
return return
} }
httpReq.Header = headers httpReq.Header = headers
// Override Host header
if host := headers.Get(base.HttpHeaderHost); host != "" {
httpReq.Host = host
}
return httpReq, nil return httpReq, nil
} }
func (f *Fetcher) splitChunk() (chunks []*chunk) { func (f *Fetcher) splitConnection() (connections []*connection) {
if f.meta.Res.Range { if f.meta.Res.Range {
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections optConnections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
// 每个连接平均需要下载的分块大小 // 每个连接平均需要下载的分块大小
chunkSize := f.meta.Res.Size / int64(connections) chunkSize := f.meta.Res.Size / int64(optConnections)
chunks = make([]*chunk, connections) connections = make([]*connection, optConnections)
for i := 0; i < connections; i++ { for i := 0; i < optConnections; i++ {
var ( var (
begin = chunkSize * int64(i) begin = chunkSize * int64(i)
end int64 end int64
) )
if i == connections-1 { if i == optConnections-1 {
// 最后一个分块需要保证把文件下载完 // 最后一个分块需要保证把文件下载完
end = f.meta.Res.Size - 1 end = f.meta.Res.Size - 1
} else { } else {
end = begin + chunkSize - 1 end = begin + chunkSize - 1
} }
chunk := newChunk(begin, end) connections[i] = &connection{
chunks[i] = chunk Chunk: newChunk(begin, end),
}
} }
} else { } else {
// 只支持单连接下载 // 只支持单连接下载
chunks = make([]*chunk, 1) connections = make([]*connection, 1)
chunks[0] = newChunk(0, 0) connections[0] = &connection{
Chunk: newChunk(0, 0),
}
} }
return return
} }
@@ -476,8 +616,16 @@ func (f *Fetcher) buildClient() *http.Client {
} }
} }
func decodeMangledString(mangled string) string {
rawBytes := make([]byte, 0, len(mangled)*3)
for _, r := range mangled {
rawBytes = append(rawBytes, byte(r))
}
return string(rawBytes)
}
type fetcherData struct { type fetcherData struct {
Chunks []*chunk Connections []*connection
} }
type FetcherManager struct { type FetcherManager struct {
@@ -533,7 +681,7 @@ func (fm *FetcherManager) DefaultConfig() any {
func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) { func (fm *FetcherManager) Store(f fetcher.Fetcher) (data any, err error) {
_f := f.(*Fetcher) _f := f.(*Fetcher)
return &fetcherData{ return &fetcherData{
Chunks: _f.chunks, Connections: _f.connections,
}, nil }, nil
} }
@@ -545,8 +693,8 @@ func (fm *FetcherManager) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
fetcher.meta = meta fetcher.meta = meta
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req) base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts) base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Chunks) > 0 { if len(fd.Connections) > 0 {
fetcher.chunks = fd.Chunks fetcher.connections = fd.Connections
} }
return fetcher return fetcher
} }
+81
View File
@@ -11,6 +11,7 @@ import (
"net" "net"
gohttp "net/http" gohttp "net/http"
"net/url" "net/url"
"strings"
"testing" "testing"
"time" "time"
) )
@@ -36,6 +37,36 @@ func TestFetcher_Resolve(t *testing.T) {
}, },
}, },
}, t) }, t)
testResolve(test.StartTestCustomServer, "encoded-word", &base.Resource{
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, "no-encode", &base.Resource{
Size: test.BuildSize,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: test.BuildSize,
},
},
}, t)
testResolve(test.StartTestCustomServer, "%E6%B5%8B%E8%AF%95.zip", &base.Resource{
Size: 0,
Range: false,
Files: []*base.FileInfo{
{
Name: "测试.zip",
Size: 0,
},
},
}, t)
testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{ testResolve(test.StartTestCustomServer, test.BuildName, &base.Resource{
Size: 0, Size: 0,
Range: false, Range: false,
@@ -59,6 +90,21 @@ func TestFetcher_Resolve(t *testing.T) {
} }
} }
func TestFetcher_ResolveWithHostHeader(t *testing.T) {
fetcher := buildFetcher()
err := fetcher.Resolve(&base.Request{
URL: "https://bing.com",
Extra: &http.ReqExtra{
Header: map[string]string{
"Host": "test",
},
},
})
if err == nil || !strings.Contains(err.Error(), "400") {
t.Errorf("Resolve() got = %v, want %v", err, "400")
}
}
func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) { func testResolve(startTestServer func() net.Listener, path string, want *base.Resource, t *testing.T) {
listener := startTestServer() listener := startTestServer()
defer listener.Close() defer listener.Close()
@@ -140,6 +186,14 @@ func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
downloadError(listener, 4, t) downloadError(listener, 4, t)
} }
func TestFetcher_DownloadOnBugFileServer(t *testing.T) {
listener := test.StartTestRangeBugServer()
defer listener.Close()
downloadNormal(listener, 1, t)
downloadNormal(listener, 4, t)
}
func TestFetcher_DownloadResume(t *testing.T) { func TestFetcher_DownloadResume(t *testing.T) {
listener := test.StartTestFileServer() listener := test.StartTestFileServer()
defer listener.Close() defer listener.Close()
@@ -202,6 +256,33 @@ func TestFetcher_ConfigUseServerCtime(t *testing.T) {
} }
} }
func TestFetcher_Stats(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
fetcher := doDownloadReady(buildConfigFetcher(config{
Connections: 16,
}), listener, 0, t)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
stats := fetcher.Stats().(*http.Stats)
if len(stats.Connections) != 16 {
t.Errorf("Stats() got = %v, want %v", len(stats.Connections), 16)
}
totalDownloaded := int64(0)
for _, conn := range stats.Connections {
totalDownloaded += conn.Downloaded
}
if totalDownloaded != test.BuildSize {
t.Errorf("Stats() got = %v, want %v", totalDownloaded, test.BuildSize)
}
}
func TestFetcherManager_ParseName(t *testing.T) { func TestFetcherManager_ParseName(t *testing.T) {
type args struct { type args struct {
u string u string
+117 -57
View File
@@ -83,6 +83,36 @@ func StartTestCustomServer() net.Listener {
defer file.Close() defer file.Close()
io.Copy(writer, file) io.Copy(writer, file)
}) })
mux.HandleFunc("/encoded-word", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=\"=?UTF8?B?5rWL6K+VLnppcA==?=\"")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/%E6%B5%8B%E8%AF%95.zip", func(writer http.ResponseWriter, request *http.Request) {
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
mux.HandleFunc("/no-encode", func(writer http.ResponseWriter, request *http.Request) {
writer.Header().Set("Content-Disposition", "attachment; filename=测试.zip")
writer.Header().Set("Content-Type", "application/octet-stream")
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
file, err := os.Open(BuildFile)
if err != nil {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
})
return mux return mux
}) })
} }
@@ -161,68 +191,98 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
writer.WriteHeader(403) writer.WriteHeader(403)
return return
} }
rangeFileHandle(
r := request.Header.Get("Range") writer,
if r == "" { request,
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize)) nil,
writer.WriteHeader(200) func(file *os.File, n int64) {
(writer.(http.Flusher)).Flush() slowCopyN(sl, writer, file, n, delay)
},
file, err := os.Open(BuildFile) )
if err != nil {
panic(err)
}
defer file.Close()
slowCopy(sl, writer, file, delay)
} else {
// split range
s := strings.Split(r, "=")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
s = strings.Split(s[1], "-")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
start, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
end, err := strconv.ParseInt(s[1], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
if start < 0 || end < 0 || start > end {
writer.WriteHeader(400)
return
}
if end >= BuildSize {
end = BuildSize - 1
}
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
writer.Header().Set("Accept-Ranges", "bytes")
writer.WriteHeader(206)
(writer.(http.Flusher)).Flush()
file, err := os.Open(BuildFile)
if err != nil {
writer.WriteHeader(500)
return
}
defer file.Close()
file.Seek(start, 0)
slowCopyN(sl, writer, file, end-start+1, delay)
}
}) })
return mux return mux
}) })
} }
// StartTestRangeBugServer simulate bug server:
// Don't follow Range request rules, always return more data than range, e.g. Range: bytes=0-100, return 150 bytes
func StartTestRangeBugServer() net.Listener {
return startTestServer(func(sl *shutdownListener) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
rangeFileHandle(
writer,
request,
func(end int64) int64 {
var bugEnd = end
if end != 0 {
bugEnd = end + 50
if bugEnd >= BuildSize {
bugEnd = end
}
}
return bugEnd
},
func(file *os.File, n int64) {
io.CopyN(writer, file, n)
},
)
})
return mux
})
}
func rangeFileHandle(writer http.ResponseWriter, request *http.Request, modifyEnd func(end int64) int64, iocpN func(file *os.File, n int64)) {
r := request.Header.Get("Range")
// split range
s := strings.Split(r, "=")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
s = strings.Split(s[1], "-")
if len(s) != 2 {
writer.WriteHeader(400)
return
}
start, err := strconv.ParseInt(s[0], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
end, err := strconv.ParseInt(s[1], 10, 64)
if err != nil {
writer.WriteHeader(400)
return
}
if start < 0 || end < 0 || start > end {
writer.WriteHeader(400)
return
}
if end >= BuildSize {
end = BuildSize - 1
}
if modifyEnd != nil {
end = modifyEnd(end)
}
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
writer.Header().Set("Accept-Ranges", "bytes")
writer.WriteHeader(206)
(writer.(http.Flusher)).Flush()
file, err := os.Open(BuildFile)
if err != nil {
writer.WriteHeader(500)
return
}
defer file.Close()
file.Seek(start, 0)
iocpN(file, end-start+1)
}
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second // slowCopyN copies n bytes from src to dst, speed limit is bytes per second
func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) { func slowCopy(sl *shutdownListener, dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
buf := make([]byte, 32*1024) buf := make([]byte, 32*1024)
+1
View File
@@ -15,6 +15,7 @@ const (
HttpCodeOK = 200 HttpCodeOK = 200
HttpCodePartialContent = 206 HttpCodePartialContent = 206
HttpHeaderHost = "Host"
HttpHeaderRange = "Range" HttpHeaderRange = "Range"
HttpHeaderAcceptRanges = "Accept-Ranges" HttpHeaderAcceptRanges = "Accept-Ranges"
HttpHeaderContentLength = "Content-Length" HttpHeaderContentLength = "Content-Length"
+10
View File
@@ -163,6 +163,16 @@ func ParseOptsExtra[E any](opts *Options) error {
return nil return nil
} }
type CreateTaskBatch struct {
Reqs []*CreateTaskBatchItem `json:"reqs"`
Opts *Options `json:"opts"`
}
type CreateTaskBatchItem struct {
Req *Request `json:"req"`
Opts *Options `json:"opts"`
}
// DownloaderStoreConfig is the config that can restore the downloader. // DownloaderStoreConfig is the config that can restore the downloader.
type DownloaderStoreConfig struct { type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
+25 -18
View File
@@ -148,6 +148,7 @@ func (d *Downloader) Setup() error {
tasks = append(tasks[:i], tasks[i+1:]...) tasks = append(tasks[:i], tasks[i+1:]...)
continue continue
} }
d.assignFetcherManager(task)
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
@@ -208,7 +209,7 @@ func (d *Downloader) Setup() error {
if task.Status == base.DownloadStatusRunning { if task.Status == base.DownloadStatusRunning {
downloadDataChanged = current != task.Progress.Downloaded downloadDataChanged = current != task.Progress.Downloaded
task.Progress.Used = task.timer.Used() task.Progress.Used = task.timer.Used()
task.Progress.Speed = task.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick) task.Progress.Speed = task.updateSpeed(current-task.Progress.Downloaded, tick)
task.Progress.Downloaded = current task.Progress.Downloaded = current
} }
@@ -217,7 +218,7 @@ func (d *Downloader) Setup() error {
uploader := task.fetcher.(fetcher.Uploader) uploader := task.fetcher.(fetcher.Uploader)
currentUploaded := uploader.UploadedBytes() currentUploaded := uploader.UploadedBytes()
uploadDataChanged = currentUploaded != task.Progress.Uploaded uploadDataChanged = currentUploaded != task.Progress.Uploaded
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick) task.Progress.UploadSpeed = task.updateUploadSpeed(currentUploaded-task.Progress.Uploaded, tick)
task.Progress.Uploaded = currentUploaded task.Progress.Uploaded = currentUploaded
} }
d.emit(EventKeyProgress, task) d.emit(EventKeyProgress, task)
@@ -358,10 +359,14 @@ func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId
return d.doCreate(fetcher, opts) return d.doCreate(fetcher, opts)
} }
func (d *Downloader) CreateDirectBatch(reqs []*base.Request, opts *base.Options) (taskId []string, err error) { func (d *Downloader) CreateDirectBatch(req *base.CreateTaskBatch) (taskId []string, err error) {
taskIds := make([]string, 0) taskIds := make([]string, 0)
for _, req := range reqs { for _, ir := range req.Reqs {
taskId, err := d.CreateDirect(req, opts.Clone()) opts := ir.Opts
if opts == nil {
opts = req.Opts
}
taskId, err := d.CreateDirect(ir.Req, opts.Clone())
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -573,15 +578,19 @@ func (d *Downloader) Delete(filter *TaskFilter, force bool) (err error) {
} }
func (d *Downloader) deleteAll() (err error) { func (d *Downloader) deleteAll() (err error) {
var deleteTasksTemp []*Task
func() { func() {
d.lock.Lock() d.lock.Lock()
defer d.lock.Unlock() defer d.lock.Unlock()
for _, task := range d.tasks {
deleteTasksTemp = append(deleteTasksTemp, task)
}
d.tasks = make([]*Task, 0) d.tasks = make([]*Task, 0)
d.waitTasks = make([]*Task, 0) d.waitTasks = make([]*Task, 0)
}() }()
for _, task := range d.tasks { for _, task := range deleteTasksTemp {
if err = d.doDelete(task, true); err != nil { if err = d.doDelete(task, true); err != nil {
return return
} }
@@ -892,18 +901,7 @@ func (d *Downloader) restoreTask(task *Task) error {
} }
func (d *Downloader) restoreFetcher(task *Task) error { func (d *Downloader) restoreFetcher(task *Task) error {
var fm fetcher.FetcherManager v, f := task.fetcherManager.Restore()
for _, f := range d.cfg.FetchManagers {
if f.Name() == task.Protocol {
fm = f
break
}
}
if fm == nil {
return ErrUnSupportedProtocol
}
task.fetcherManager = fm
v, f := fm.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 {
@@ -1145,6 +1143,15 @@ func logPanic(logDir string) {
debug.SetCrashOutput(f, debug.CrashOptions{}) debug.SetCrashOutput(f, debug.CrashOptions{})
} }
func (d *Downloader) assignFetcherManager(task *Task) error {
fm, err := d.parseFm(task.Meta.Req.URL)
if err != nil {
return err
}
task.fetcherManager = fm
return nil
}
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) { func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
fm, err := d.parseFm(url) fm, err := d.parseFm(url)
if err != nil { if err != nil {
+24 -14
View File
@@ -137,13 +137,15 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
downloader.Clear() downloader.Clear()
}() }()
reqs := make([]*base.Request, 0) reqs := make([]*base.CreateTaskBatchItem, 0)
fileNames := make([]string, 0) fileNames := make([]string, 0)
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
reqs = append(reqs, req) reqs = append(reqs, &base.CreateTaskBatchItem{
Req: req,
})
if i == 0 { if i == 0 {
fileNames = append(fileNames, test.DownloadName) fileNames = append(fileNames, test.DownloadName)
} else { } else {
@@ -160,11 +162,14 @@ func TestDownloader_CreateDirectBatch(t *testing.T) {
} }
}) })
_, err := downloader.CreateDirectBatch(reqs, &base.Options{ _, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
Path: test.Dir, Reqs: reqs,
Name: test.DownloadName, Opts: &base.Options{
Extra: http.OptsExtra{ Path: test.Dir,
Connections: 4, Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
}, },
}) })
if err != nil { if err != nil {
@@ -499,13 +504,15 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
downloader.Clear() downloader.Clear()
}() }()
reqs := make([]*base.Request, 0) reqs := make([]*base.CreateTaskBatchItem, 0)
fileNames := make([]string, 0) fileNames := make([]string, 0)
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
req := &base.Request{ req := &base.Request{
URL: "http://" + listener.Addr().String() + "/" + test.BuildName, URL: "http://" + listener.Addr().String() + "/" + test.BuildName,
} }
reqs = append(reqs, req) reqs = append(reqs, &base.CreateTaskBatchItem{
Req: req,
})
if i == 0 { if i == 0 {
fileNames = append(fileNames, test.DownloadName) fileNames = append(fileNames, test.DownloadName)
} else { } else {
@@ -522,11 +529,14 @@ func TestDownloader_GetTasksByFilter(t *testing.T) {
} }
}) })
taskIds, err := downloader.CreateDirectBatch(reqs, &base.Options{ taskIds, err := downloader.CreateDirectBatch(&base.CreateTaskBatch{
Path: test.Dir, Reqs: reqs,
Name: test.DownloadName, Opts: &base.Options{
Extra: http.OptsExtra{ Path: test.Dir,
Connections: 4, Name: test.DownloadName,
Extra: http.OptsExtra{
Connections: 4,
},
}, },
}) })
if err != nil { if err != nil {
+17 -6
View File
@@ -100,16 +100,27 @@ func (t *Task) clone() *Task {
return util.DeepClone(t) return util.DeepClone(t)
} }
func (t *Task) calcSpeed(speedArr []int64, downloaded int64, usedTime float64) int64 { func (t *Task) updateSpeed(downloaded int64, usedTime float64) int64 {
speedArr = append(speedArr, downloaded) return calcSpeed(&t.speedArr, downloaded, usedTime)
if len(speedArr) > 6 { }
speedArr = speedArr[1:]
func (t *Task) updateUploadSpeed(downloaded int64, usedTime float64) int64 {
return calcSpeed(&t.uploadSpeedArr, downloaded, usedTime)
}
func calcSpeed(speedArr *[]int64, downloaded int64, usedTime float64) int64 {
*speedArr = append(*speedArr, downloaded)
// Record last 5 seconds of download speed to calculate the average speed
if len(*speedArr) > int(5.0/usedTime) {
*speedArr = (*speedArr)[1:]
} }
var total int64 var total int64
for _, v := range speedArr { for _, v := range *speedArr {
total += v total += v
} }
return int64(float64(total) / float64(len(speedArr)) / usedTime)
return int64(float64(total) / float64(len(*speedArr)) / usedTime)
} }
type TaskFilter struct { type TaskFilter struct {
+8 -2
View File
@@ -14,6 +14,12 @@ type OptsExtra struct {
// Stats for download // Stats for download
type Stats struct { type Stats struct {
// http stats Connections []*StatsConnection `json:"connections"`
// health indicators of http }
type StatsConnection struct {
Downloaded int64 `json:"downloaded"`
Completed bool `json:"completed"`
Failed bool `json:"failed"`
RetryTimes int `json:"retryTimes"`
} }
+2 -2
View File
@@ -58,13 +58,13 @@ func CreateTask(w http.ResponseWriter, r *http.Request) {
} }
func CreateTaskBatch(w http.ResponseWriter, r *http.Request) { func CreateTaskBatch(w http.ResponseWriter, r *http.Request) {
var req model.CreateTaskBatch var req base.CreateTaskBatch
if ReadJson(r, w, &req) { if ReadJson(r, w, &req) {
if len(req.Reqs) == 0 { if len(req.Reqs) == 0 {
WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam)) WriteJson(w, model.NewErrorResult("param invalid: reqs", model.CodeInvalidParam))
return return
} }
taskIds, err := Downloader.CreateDirectBatch(req.Reqs, req.Opt) taskIds, err := Downloader.CreateDirectBatch(&req)
if err != nil { if err != nil {
WriteJson(w, model.NewErrorResult(err.Error())) WriteJson(w, model.NewErrorResult(err.Error()))
return return
+34
View File
@@ -0,0 +1,34 @@
package rest
import (
"compress/gzip"
"io"
"net/http"
"strings"
)
type gzipResponseWriter struct {
io.Writer
http.ResponseWriter
}
func (g gzipResponseWriter) Write(b []byte) (int, error) {
return g.Writer.Write(b)
}
func gzipMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
next.ServeHTTP(w, r)
return
}
w.Header().Set("Content-Encoding", "gzip")
w.Header().Add("Vary", "Accept-Encoding")
gz := gzip.NewWriter(w)
defer gz.Close()
next.ServeHTTP(gzipResponseWriter{Writer: gz, ResponseWriter: w}, r)
})
}
-5
View File
@@ -7,8 +7,3 @@ type CreateTask struct {
Req *base.Request `json:"req"` Req *base.Request `json:"req"`
Opt *base.Options `json:"opt"` Opt *base.Options `json:"opt"`
} }
type CreateTaskBatch struct {
Reqs []*base.Request `json:"reqs"`
Opt *base.Options `json:"opt"`
}
+81 -5
View File
@@ -10,12 +10,14 @@ import (
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors" "github.com/pkg/errors"
"io/fs"
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"time"
) )
var ( var (
@@ -126,16 +128,28 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
if startCfg.WebEnable { if startCfg.WebEnable {
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem))) r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem))) r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS))) r.PathPrefix("/").Handler(gzipMiddleware(http.FileServer(newEmbedCacheFileSystem(http.FS(startCfg.WebFS)))))
} }
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) { enableApiToken := startCfg.ApiToken != ""
enableBasicAuth := startCfg.WebEnable && startCfg.WebBasicAuth != nil
if enableApiToken || enableBasicAuth {
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 startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken { if enableApiToken {
h.ServeHTTP(w, r) apiTokenHeader := r.Header["X-Api-Token"]
return // If api token header is set, only check api token ignore basic auth
if len(apiTokenHeader) > 0 {
if apiTokenHeader[0] == startCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
return
}
} }
if startCfg.WebEnable && startCfg.WebBasicAuth != nil { if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() { if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
@@ -222,6 +236,68 @@ func (e *extensionFileSystem) Open(name string) (http.File, error) {
return os.Open(filepath.Join(extensionPath, path)) return os.Open(filepath.Join(extensionPath, path))
} }
type embedCacheFileSystem struct {
fs http.FileSystem
lastModTime time.Time
}
func newEmbedCacheFileSystem(fs http.FileSystem) *embedCacheFileSystem {
efs := &embedCacheFileSystem{
fs: fs,
lastModTime: time.Now(),
}
exe, err := os.Executable()
if err != nil {
return efs
}
fi, err := os.Stat(exe)
if err != nil {
return efs
}
efs.lastModTime = fi.ModTime()
return efs
}
func (e *embedCacheFileSystem) Open(name string) (http.File, error) {
file, err := e.fs.Open(name)
if err != nil {
return nil, err
}
return &embedFile{
File: file,
lastModTime: e.lastModTime,
}, nil
}
type embedFile struct {
http.File
lastModTime time.Time
}
type embedFileInfo struct {
fs.FileInfo
lastModTime time.Time
}
func (e *embedFileInfo) ModTime() time.Time {
return e.lastModTime
}
func (e *embedFile) Stat() (fs.FileInfo, error) {
fi, err := e.File.Stat()
if err != nil {
return nil, err
}
return &embedFileInfo{
FileInfo: fi,
lastModTime: e.lastModTime,
}, nil
}
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool { func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
if err := json.NewDecoder(r.Body).Decode(v); err != nil { if err := json.NewDecoder(r.Body).Decode(v); err != nil {
WriteJson(w, model.NewErrorResult(err.Error())) WriteJson(w, model.NewErrorResult(err.Error()))
+142 -4
View File
@@ -14,7 +14,9 @@ import (
"net" "net"
"net/http" "net/http"
"os" "os"
"path/filepath"
"reflect" "reflect"
"strings"
"sync" "sync"
"testing" "testing"
"time" "time"
@@ -139,11 +141,13 @@ func TestCreateDirectTask(t *testing.T) {
func TestCreateDirectTaskBatch(t *testing.T) { func TestCreateDirectTaskBatch(t *testing.T) {
doTest(func() { doTest(func() {
reqs := make([]*base.Request, 0) reqs := make([]*base.CreateTaskBatchItem, 0)
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
reqs = append(reqs, createReq.Req) reqs = append(reqs, &base.CreateTaskBatchItem{
Req: createReq.Req,
})
} }
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &model.CreateTaskBatch{ taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
Reqs: reqs, Reqs: reqs,
}) })
if len(taskIds) != len(reqs) { if len(taskIds) != len(reqs) {
@@ -152,6 +156,45 @@ func TestCreateDirectTaskBatch(t *testing.T) {
}) })
} }
func TestCreateDirectTaskBatchWithOpt(t *testing.T) {
doTest(func() {
reqs := make([]*base.CreateTaskBatchItem, 0)
for i := 0; i < 5; i++ {
item := &base.CreateTaskBatchItem{
Req: createReq.Req,
}
if i == 0 {
item.Opts = &base.Options{
Name: "spe_opt.data",
}
}
reqs = append(reqs, item)
}
taskIds := httpRequestCheckOk[[]string](http.MethodPost, "/api/v1/tasks/batch", &base.CreateTaskBatch{
Reqs: reqs,
Opts: &base.Options{
Name: "default_opt.data",
},
})
if len(taskIds) != len(reqs) {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", len(taskIds), len(reqs))
}
for i, taskId := range taskIds {
task := httpRequestCheckOk[*download.Task](http.MethodGet, "/api/v1/tasks/"+taskId, nil)
if i == 0 {
if !strings.Contains(task.Name(), "spe_opt") {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "spe_opt.data")
}
} else {
if !strings.Contains(task.Name(), "default_opt") {
t.Errorf("CreateDirectTaskBatch() got = %v, want %v", task.Name(), "default_opt.data")
}
}
}
})
}
func TestCreateDirectTaskWithResoled(t *testing.T) { func TestCreateDirectTaskWithResoled(t *testing.T) {
doTest(func() { doTest(func() {
var wg sync.WaitGroup var wg sync.WaitGroup
@@ -478,6 +521,66 @@ func TestFsExtensionFail(t *testing.T) {
}) })
} }
func TestWebFsEnhance(t *testing.T) {
indexHtml := `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>index</h1>
</body>
</html>
`
webDistPath := "dist"
os.MkdirAll("dist", os.ModePerm)
if err := os.WriteFile(filepath.Join(webDistPath, "index.html"), []byte(indexHtml), os.ModePerm); err != nil {
panic(err)
}
defer os.RemoveAll(webDistPath)
doTest0(func(cfg *model.StartConfig) {
cfg.WebFS = os.DirFS(webDistPath)
}, func() {
// First request no cache
code, header, _ := doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
"Accept-Encoding": "gzip",
}, nil)
if code != http.StatusOK {
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
}
// Check header last-modified
if _, ok := header["Last-Modified"]; !ok {
t.Errorf("TestWebFsEnhance() missing key = %v", "Last-Modified")
}
// Check gzip compress
if _, ok := header["Content-Encoding"]; !ok || header["Content-Encoding"] != "gzip" {
t.Errorf("TestWebFsEnhance() no gzip compress")
}
// Request with If-Modified-Since
ifModifiedSince := header["Last-Modified"]
code, _, _ = doHttpRequest1(http.MethodGet, "/index.html", map[string]string{
"If-Modified-Since": ifModifiedSince,
}, nil)
if code != http.StatusNotModified {
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusNotModified)
}
// Request with un gzip
code, header, _ = doHttpRequest1(http.MethodGet, "/index.html?t=123", nil, nil)
if code != http.StatusOK {
t.Errorf("TestWebFsEnhance() got = %v, want %v", code, http.StatusOK)
}
// Check no gzip compress
if _, ok := header["Content-Encoding"]; ok && header["Content-Encoding"] == "gzip" {
t.Errorf("TestWebFsEnhance() has gzip compress")
}
})
}
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{
@@ -564,15 +667,38 @@ func TestAuthorization(t *testing.T) {
if status != http.StatusOK { if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK) t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
} }
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
"X-Api-Token": cfg.ApiToken,
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
"X-Api-Token": "",
}, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
} }
func doTest(handler func()) { func doTest(handler func()) {
doTest0(nil, handler)
}
func doTest0(onStart func(cfg *model.StartConfig), handler func()) {
testFunc := func(storage model.Storage) { testFunc := func(storage model.Storage) {
var cfg = &model.StartConfig{} var cfg = &model.StartConfig{}
cfg.Init() cfg.Init()
cfg.Storage = storage cfg.Storage = storage
cfg.StorageDir = ".test_storage" cfg.StorageDir = ".test_storage"
cfg.WebEnable = true cfg.WebEnable = true
if onStart != nil {
onStart(cfg)
}
fileListener := doStart(cfg) fileListener := doStart(cfg)
defer func() { defer func() {
if err := fileListener.Close(); err != nil { if err := fileListener.Close(); err != nil {
@@ -582,6 +708,7 @@ func doTest(handler func()) {
Downloader.Clear() Downloader.Clear()
}() }()
defer func() { defer func() {
time.Sleep(500 * time.Millisecond)
Downloader.Pause(nil) Downloader.Pause(nil)
Downloader.Delete(nil, true) Downloader.Delete(nil, true)
os.RemoveAll(cfg.StorageDir) os.RemoveAll(cfg.StorageDir)
@@ -603,6 +730,11 @@ func doStart(cfg *model.StartConfig) net.Listener {
} }
func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) { func doHttpRequest0(method string, path string, headers map[string]string, body any) (int, []byte) {
r1, _, r3 := doHttpRequest1(method, path, headers, body)
return r1, r3
}
func doHttpRequest1(method string, path string, headers map[string]string, body any) (int, map[string]string, []byte) {
var reader io.Reader var reader io.Reader
if body != nil { if body != nil {
buf, _ := json.Marshal(body) buf, _ := json.Marshal(body)
@@ -623,12 +755,18 @@ func doHttpRequest0(method string, path string, headers map[string]string, body
panic(err) panic(err)
} }
defer response.Body.Close() defer response.Body.Close()
respHeader := make(map[string]string)
for k, vv := range response.Header {
respHeader[k] = vv[0]
}
respBody, err := io.ReadAll(response.Body) respBody, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return response.StatusCode, respBody return response.StatusCode, respHeader, 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]) {
-1
View File
@@ -10,7 +10,6 @@ import (
func ParseSchema(url string) string { func ParseSchema(url string) string {
index := strings.Index(url, ":") index := strings.Index(url, ":")
// if no schema or windows path like C:\a.txt, return FILE
if index == -1 || index == 1 { if index == -1 || index == 1 {
return "" return ""
} }
+6 -1
View File
@@ -67,4 +67,9 @@ windows/flutter/generated_plugins.cmake
# Hive database files # Hive database files
database.hive database.hive
database.lock database.lock
assets/exec/host
assets/exec/host.exe
assets/exec/updater
assets/exec/updater.exe
+2 -2
View File
@@ -44,11 +44,11 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.gopeed" applicationId "com.gopeed.gopeed"
// 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 21 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 34
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
} }
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed"> package="com.gopeed.gopeed">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
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.
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed"> package="com.gopeed.gopeed">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" /> <uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
@@ -7,6 +7,8 @@
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" /> <uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="32" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<application <application
@@ -91,7 +93,8 @@
<!-- Add android:stopWithTask option only when necessary. --> <!-- Add android:stopWithTask option only when necessary. -->
<service <service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService" android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:stopWithTask="true" /> android:foregroundServiceType="dataSync"
android:exported="false" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
@@ -1,4 +1,4 @@
package com.gopeed package com.gopeed.gopeed
import androidx.annotation.NonNull import androidx.annotation.NonNull
import com.gopeed.libgopeed.Libgopeed import com.gopeed.libgopeed.Libgopeed
@@ -1,5 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.gopeed"> package="com.gopeed.gopeed">
<!-- The INTERNET permission is required for development. Specifically, <!-- The INTERNET permission is required for development. Specifically,
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.
View File
Whitespace-only changes.
+7
View File
@@ -32,6 +32,13 @@ target 'Runner' do
use_modular_headers! use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# share_handler addition start
target 'ShareExtension' do
inherit! :search_paths
pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models"
end
# share_handler addition end
end end
post_install do |installer| post_install do |installer|
+244 -4
View File
@@ -7,6 +7,9 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0C585CBA2D41E28900FF2EC0 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */; };
0C585CBD2D41E28900FF2EC0 /* Base in Resources */ = {isa = PBXBuildFile; fileRef = 0C585CBC2D41E28900FF2EC0 /* Base */; };
0C585CC12D41E28900FF2EC0 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = 0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
@@ -20,7 +23,28 @@
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */; }; DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
0C585CBF2D41E28900FF2EC0 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = 0C585CB62D41E28900FF2EC0;
remoteInfo = ShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */ /* Begin PBXCopyFilesBuildPhase section */
0C585CC22D41E28900FF2EC0 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
0C585CC12D41E28900FF2EC0 /* ShareExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
9705A1C41CF9048500538489 /* Embed Frameworks */ = { 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase; isa = PBXCopyFilesBuildPhase;
buildActionMask = 8; buildActionMask = 8;
@@ -35,7 +59,13 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; }; 029EF293EAA92EB3D18BBF19 /* libresolv.tbd */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = "<group>"; };
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
0C585CBC2D41E28900FF2EC0 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
0C585CBE2D41E28900FF2EC0 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
0C585CC72D41E36800FF2EC0 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
0C585CC82D41E38D00FF2EC0 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; }; 25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
@@ -58,6 +88,13 @@
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
0C585CB42D41E28900FF2EC0 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -72,6 +109,17 @@
/* End PBXFrameworksBuildPhase section */ /* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */ /* Begin PBXGroup section */
0C585CB82D41E28900FF2EC0 /* ShareExtension */ = {
isa = PBXGroup;
children = (
0C585CC82D41E38D00FF2EC0 /* ShareExtension.entitlements */,
0C585CB92D41E28900FF2EC0 /* ShareViewController.swift */,
0C585CBB2D41E28900FF2EC0 /* MainInterface.storyboard */,
0C585CBE2D41E28900FF2EC0 /* Info.plist */,
);
path = ShareExtension;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = { 9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@@ -88,6 +136,7 @@
children = ( children = (
9740EEB11CF90186004384FC /* Flutter */, 9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
0C585CB82D41E28900FF2EC0 /* ShareExtension */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
ADF7F433B4BC23D6C18AB78B /* Pods */, ADF7F433B4BC23D6C18AB78B /* Pods */,
B4A1CBAC5AA50208793CFF34 /* Frameworks */, B4A1CBAC5AA50208793CFF34 /* Frameworks */,
@@ -98,6 +147,7 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
97C146EE1CF9000F007C117D /* Runner.app */, 97C146EE1CF9000F007C117D /* Runner.app */,
0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -105,6 +155,7 @@
97C146F01CF9000F007C117D /* Runner */ = { 97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
0C585CC72D41E36800FF2EC0 /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */, 97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FD1CF9000F007C117D /* Assets.xcassets */,
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -141,6 +192,23 @@
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
0C585CB62D41E28900FF2EC0 /* ShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = 0C585CC62D41E28900FF2EC0 /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = (
0C585CB32D41E28900FF2EC0 /* Sources */,
0C585CB42D41E28900FF2EC0 /* Frameworks */,
0C585CB52D41E28900FF2EC0 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ShareExtension;
productName = ShareExtension;
productReference = 0C585CB72D41E28900FF2EC0 /* ShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
97C146ED1CF9000F007C117D /* Runner */ = { 97C146ED1CF9000F007C117D /* Runner */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
@@ -150,6 +218,7 @@
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
0C585CC22D41E28900FF2EC0 /* Embed Foundation Extensions */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */, DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */,
@@ -157,6 +226,7 @@
buildRules = ( buildRules = (
); );
dependencies = ( dependencies = (
0C585CC02D41E28900FF2EC0 /* PBXTargetDependency */,
); );
name = Runner; name = Runner;
productName = Runner; productName = Runner;
@@ -169,9 +239,13 @@
97C146E61CF9000F007C117D /* Project object */ = { 97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject; isa = PBXProject;
attributes = { attributes = {
LastSwiftUpdateCheck = 1540;
LastUpgradeCheck = 1430; LastUpgradeCheck = 1430;
ORGANIZATIONNAME = ""; ORGANIZATIONNAME = "";
TargetAttributes = { TargetAttributes = {
0C585CB62D41E28900FF2EC0 = {
CreatedOnToolsVersion = 15.4;
};
97C146ED1CF9000F007C117D = { 97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1; CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100; LastSwiftMigration = 1100;
@@ -192,11 +266,20 @@
projectRoot = ""; projectRoot = "";
targets = ( targets = (
97C146ED1CF9000F007C117D /* Runner */, 97C146ED1CF9000F007C117D /* Runner */,
0C585CB62D41E28900FF2EC0 /* ShareExtension */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */ /* Begin PBXResourcesBuildPhase section */
0C585CB52D41E28900FF2EC0 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0C585CBD2D41E28900FF2EC0 /* Base in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EC1CF9000F007C117D /* Resources */ = { 97C146EC1CF9000F007C117D /* Resources */ = {
isa = PBXResourcesBuildPhase; isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -284,6 +367,14 @@
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
0C585CB32D41E28900FF2EC0 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
0C585CBA2D41E28900FF2EC0 /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EA1CF9000F007C117D /* Sources */ = { 97C146EA1CF9000F007C117D /* Sources */ = {
isa = PBXSourcesBuildPhase; isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
@@ -295,7 +386,23 @@
}; };
/* End PBXSourcesBuildPhase section */ /* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
0C585CC02D41E28900FF2EC0 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 0C585CB62D41E28900FF2EC0 /* ShareExtension */;
targetProxy = 0C585CBF2D41E28900FF2EC0 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */ /* Begin PBXVariantGroup section */
0C585CBB2D41E28900FF2EC0 /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
0C585CBC2D41E28900FF2EC0 /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
97C146FA1CF9000F007C117D /* Main.storyboard */ = { 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup; isa = PBXVariantGroup;
children = ( children = (
@@ -315,6 +422,123 @@
/* End PBXVariantGroup section */ /* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
0C585CC32D41E28900FF2EC0 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
0C585CC42D41E28900FF2EC0 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
0C585CC52D41E28900FF2EC0 /* Profile */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 17.5;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
249021D3217E4FDB00AE95B9 /* Profile */ = { 249021D3217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
@@ -369,8 +593,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K; DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -379,7 +605,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed; PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -498,8 +724,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K; DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -508,7 +736,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed; PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -521,8 +749,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = { buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K; DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
@@ -531,7 +761,7 @@
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
PRODUCT_BUNDLE_IDENTIFIER = com.gopeed; PRODUCT_BUNDLE_IDENTIFIER = com.gopeed.gopeed;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -542,6 +772,16 @@
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
0C585CC62D41E28900FF2EC0 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
0C585CC32D41E28900FF2EC0 /* Debug */,
0C585CC42D41E28900FF2EC0 /* Release */,
0C585CC52D41E28900FF2EC0 /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
isa = XCConfigurationList; isa = XCConfigurationList;
buildConfigurations = ( buildConfigurations = (
+101 -49
View File
@@ -1,54 +1,106 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gopeed</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true/> <true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Gopeed</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false />
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<!-- Add for share_handler start -->
<!-- The 'NSUserActivityTypes' key is only needed if you plan to use the recordSentMessage API
allowing for conversations to show up as direct share suggestions -->
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<!-- Uncomment below lines if you want to use a custom group id rather than the default. Set it
in Build Settings -> User-Defined -->
<!-- <key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string> -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>NSPhotoLibraryUsageDescription</key>
<string>Photos can be shared to and used in this app</string>
<!-- Optional: Add/Customize for AirDrop support -->
<key>LSSupportsOpeningDocumentsInPlace</key>
<string>No</string>
<key>CFBundleDocumentTypes</key>
<array>
<dict>
<key>CFBundleTypeName</key>
<string>ShareHandler</string>
<key>LSHandlerRank</key>
<string>Alternate</string>
<key>LSItemContentTypes</key>
<array>
<string>public.file-url</string>
<string>public.image</string>
<string>public.text</string>
<string>public.movie</string>
<string>public.url</string>
<string>public.data</string>
</array>
</dict>
</array>
<!-- Add for share_handler end -->
</dict> </dict>
<key>UILaunchStoryboardName</key> </plist>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
+10
View File
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.gopeed.gopeed</string>
</array>
</dict>
</plist>
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>
+49
View File
@@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<!-- Uncomment below lines if you want to use a custom group id rather than the default. Set it
in Build Settings -> User-Defined -->
<!-- <key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string> -->
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<!-- Add supported message intent if you support sharing to a specific conversation - start -->
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
</array>
<!-- Add supported message intent if you support sharing to a specific conversation
(registered via the recordSentMessage api call) - end -->
<key>NSExtensionActivationRule</key>
<!-- Comment or delete the TRUEPREDICATE NSExtensionActivationRule that only works in
development mode -->
<!-- <string>TRUEPREDICATE</string> -->
<!-- Add a new NSExtensionActivationRule. The rule below will allow sharing one or more file
of any type, url, or text content, You can modify these rules to your liking for which types
of share content, as well as how many your app can handle -->
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
$attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count > 0 ).@count
> 0 </string>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.com.gopeed.gopeed</string>
</array>
</dict>
</plist>
@@ -0,0 +1,3 @@
import share_handler_ios_models
class ShareViewController: ShareHandlerIosViewController {}
+11 -4
View File
@@ -3,19 +3,20 @@ import 'dart:io';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:dio/io.dart'; import 'package:dio/io.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:gopeed/api/model/extension.dart';
import 'package:gopeed/api/model/install_extension.dart';
import 'package:gopeed/api/model/switch_extension.dart';
import '../util/util.dart'; import '../util/util.dart';
import 'model/create_task.dart'; import 'model/create_task.dart';
import 'model/create_task_batch.dart';
import 'model/downloader_config.dart'; import 'model/downloader_config.dart';
import 'model/extension.dart';
import 'model/install_extension.dart';
import 'model/request.dart'; import 'model/request.dart';
import 'model/resolve_result.dart'; import 'model/resolve_result.dart';
import 'model/result.dart'; import 'model/result.dart';
import 'model/switch_extension.dart';
import 'model/task.dart'; import 'model/task.dart';
import 'model/update_extension_settings.dart';
import 'model/update_check_extension_resp.dart'; import 'model/update_check_extension_resp.dart';
import 'model/update_extension_settings.dart';
class _Client { class _Client {
static _Client? _instance; static _Client? _instance;
@@ -114,6 +115,12 @@ Future<String> createTask(CreateTask createTask) async {
(data) => data as String); (data) => data as String);
} }
Future<List<String>> createTaskBatch(CreateTaskBatch createTaskBatch) async {
return _parse<List<String>>(
() => _client.dio.post("/api/v1/tasks/batch", data: createTaskBatch),
(data) => (data as List).map((e) => e as String).toList());
}
Future<List<Task>> getTasks(List<Status> statuses) async { Future<List<Task>> getTasks(List<Status> statuses) async {
return _parse<List<Task>>( return _parse<List<Task>>(
() => _client.dio.get( () => _client.dio.get(
@@ -0,0 +1,39 @@
import 'package:json_annotation/json_annotation.dart';
import 'options.dart';
import 'request.dart';
part 'create_task_batch.g.dart';
@JsonSerializable(explicitToJson: true)
class CreateTaskBatch {
List<CreateTaskBatchItem>? reqs;
Options? opt;
CreateTaskBatch({
this.reqs,
this.opt,
});
factory CreateTaskBatch.fromJson(
Map<String, dynamic> json,
) =>
_$CreateTaskBatchFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskBatchToJson(this);
}
@JsonSerializable()
class CreateTaskBatchItem {
Request? req;
Options? opts;
CreateTaskBatchItem({
this.req,
this.opts,
});
factory CreateTaskBatchItem.fromJson(Map<String, dynamic> json) =>
_$CreateTaskBatchItemFromJson(json);
Map<String, dynamic> toJson() => _$CreateTaskBatchItemToJson(this);
}
@@ -0,0 +1,55 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'create_task_batch.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreateTaskBatch _$CreateTaskBatchFromJson(Map<String, dynamic> json) =>
CreateTaskBatch(
reqs: (json['reqs'] as List<dynamic>?)
?.map((e) => CreateTaskBatchItem.fromJson(e as Map<String, dynamic>))
.toList(),
opt: json['opt'] == null
? null
: Options.fromJson(json['opt'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateTaskBatchToJson(CreateTaskBatch instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('reqs', instance.reqs?.map((e) => e.toJson()).toList());
writeNotNull('opt', instance.opt?.toJson());
return val;
}
CreateTaskBatchItem _$CreateTaskBatchItemFromJson(Map<String, dynamic> json) =>
CreateTaskBatchItem(
req: json['req'] == null
? null
: Request.fromJson(json['req'] as Map<String, dynamic>),
opts: json['opts'] == null
? null
: Options.fromJson(json['opts'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateTaskBatchItemToJson(CreateTaskBatchItem instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('req', instance.req);
writeNotNull('opts', instance.opts);
return val;
}
@@ -80,6 +80,7 @@ class ExtraConfig {
String locale; String locale;
bool lastDeleteTaskKeep; bool lastDeleteTaskKeep;
bool defaultDirectDownload; bool defaultDirectDownload;
bool defaultBtClient;
ExtraConfigBt bt = ExtraConfigBt(); ExtraConfigBt bt = ExtraConfigBt();
@@ -88,6 +89,7 @@ class ExtraConfig {
this.locale = '', this.locale = '',
this.lastDeleteTaskKeep = false, this.lastDeleteTaskKeep = false,
this.defaultDirectDownload = false, this.defaultDirectDownload = false,
this.defaultBtClient = true,
}); });
factory ExtraConfig.fromJson(Map<String, dynamic>? json) => factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
@@ -73,6 +73,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
locale: json['locale'] as String? ?? '', locale: json['locale'] as String? ?? '',
lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false, lastDeleteTaskKeep: json['lastDeleteTaskKeep'] as bool? ?? false,
defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false, defaultDirectDownload: json['defaultDirectDownload'] as bool? ?? false,
defaultBtClient: json['defaultBtClient'] as bool? ?? true,
)..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>); )..bt = ExtraConfigBt.fromJson(json['bt'] as Map<String, dynamic>);
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
@@ -81,6 +82,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'locale': instance.locale, 'locale': instance.locale,
'lastDeleteTaskKeep': instance.lastDeleteTaskKeep, 'lastDeleteTaskKeep': instance.lastDeleteTaskKeep,
'defaultDirectDownload': instance.defaultDirectDownload, 'defaultDirectDownload': instance.defaultDirectDownload,
'defaultBtClient': instance.defaultBtClient,
'bt': instance.bt.toJson(), 'bt': instance.bt.toJson(),
}; };
+9 -5
View File
@@ -10,8 +10,8 @@ class Options {
Object? extra; Object? extra;
Options({ Options({
required this.name, this.name = '',
required this.path, this.path = '',
this.selectFiles = const [], this.selectFiles = const [],
this.extra, this.extra,
}); });
@@ -24,12 +24,16 @@ class Options {
@JsonSerializable() @JsonSerializable()
class OptsExtraHttp { class OptsExtraHttp {
int connections = 0; int connections;
bool autoTorrent = false; bool autoTorrent;
OptsExtraHttp(); OptsExtraHttp({
this.connections = 0,
this.autoTorrent = false,
});
factory OptsExtraHttp.fromJson(Map<String, dynamic> json) => factory OptsExtraHttp.fromJson(Map<String, dynamic> json) =>
_$OptsExtraHttpFromJson(json); _$OptsExtraHttpFromJson(json);
Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this); Map<String, dynamic> toJson() => _$OptsExtraHttpToJson(this);
} }
+7 -6
View File
@@ -7,10 +7,10 @@ part of 'options.dart';
// ************************************************************************** // **************************************************************************
Options _$OptionsFromJson(Map<String, dynamic> json) => Options( Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
name: json['name'] as String, name: json['name'] as String? ?? '',
path: json['path'] as String, path: json['path'] as String? ?? '',
selectFiles: (json['selectFiles'] as List<dynamic>?) selectFiles: (json['selectFiles'] as List<dynamic>?)
?.map((e) => e as int) ?.map((e) => (e as num).toInt())
.toList() ?? .toList() ??
const [], const [],
extra: json['extra'], extra: json['extra'],
@@ -34,9 +34,10 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
} }
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) => OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp() OptsExtraHttp(
..connections = json['connections'] as int connections: (json['connections'] as num?)?.toInt() ?? 0,
..autoTorrent = json['autoTorrent'] as bool; autoTorrent: json['autoTorrent'] as bool? ?? false,
);
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) => Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{ <String, dynamic>{
+27 -14
View File
@@ -6,9 +6,9 @@ part 'request.g.dart';
class Request { class Request {
String url; String url;
Object? extra; Object? extra;
Map<String, String>? labels = {}; Map<String, String>? labels;
RequestProxy? proxy; RequestProxy? proxy;
bool skipVerifyCert = false; bool skipVerifyCert;
Request({ Request({
required this.url, required this.url,
@@ -26,11 +26,15 @@ class Request {
@JsonSerializable() @JsonSerializable()
class ReqExtraHttp { class ReqExtraHttp {
String method = 'GET'; String method;
Map<String, String> header = {}; Map<String, String> header;
String body = ''; String body;
ReqExtraHttp(); ReqExtraHttp({
this.method = 'GET',
this.header = const {},
this.body = '',
});
factory ReqExtraHttp.fromJson(Map<String, dynamic> json) => factory ReqExtraHttp.fromJson(Map<String, dynamic> json) =>
_$ReqExtraHttpFromJson(json); _$ReqExtraHttpFromJson(json);
@@ -40,9 +44,11 @@ class ReqExtraHttp {
@JsonSerializable() @JsonSerializable()
class ReqExtraBt { class ReqExtraBt {
List<String> trackers = []; List<String> trackers;
ReqExtraBt(); ReqExtraBt({
this.trackers = const [],
});
factory ReqExtraBt.fromJson(Map<String, dynamic> json) => factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
_$ReqExtraBtFromJson(json); _$ReqExtraBtFromJson(json);
@@ -58,15 +64,22 @@ enum RequestProxyMode {
@JsonSerializable() @JsonSerializable()
class RequestProxy { class RequestProxy {
RequestProxyMode mode = RequestProxyMode.follow; RequestProxyMode mode;
String scheme = 'http'; String scheme;
String host = ''; String host;
String usr = ''; String usr;
String pwd = ''; String pwd;
RequestProxy(); RequestProxy({
this.mode = RequestProxyMode.follow,
this.scheme = 'http',
this.host = '',
this.usr = '',
this.pwd = '',
});
factory RequestProxy.fromJson(Map<String, dynamic> json) => factory RequestProxy.fromJson(Map<String, dynamic> json) =>
_$RequestProxyFromJson(json); _$RequestProxyFromJson(json);
Map<String, dynamic> toJson() => _$RequestProxyToJson(this); Map<String, dynamic> toJson() => _$RequestProxyToJson(this);
} }
+22 -13
View File
@@ -36,10 +36,14 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
return val; return val;
} }
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp() ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp(
..method = json['method'] as String method: json['method'] as String? ?? 'GET',
..header = Map<String, String>.from(json['header'] as Map) header: (json['header'] as Map<String, dynamic>?)?.map(
..body = json['body'] as String; (k, e) => MapEntry(k, e as String),
) ??
const {},
body: json['body'] as String? ?? '',
);
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) => Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
<String, dynamic>{ <String, dynamic>{
@@ -48,21 +52,26 @@ Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
'body': instance.body, 'body': instance.body,
}; };
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt() ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt(
..trackers = trackers: (json['trackers'] as List<dynamic>?)
(json['trackers'] as List<dynamic>).map((e) => e as String).toList(); ?.map((e) => e as String)
.toList() ??
const [],
);
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) => Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
<String, dynamic>{ <String, dynamic>{
'trackers': instance.trackers, 'trackers': instance.trackers,
}; };
RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy() RequestProxy _$RequestProxyFromJson(Map<String, dynamic> json) => RequestProxy(
..mode = $enumDecode(_$RequestProxyModeEnumMap, json['mode']) mode: $enumDecodeNullable(_$RequestProxyModeEnumMap, json['mode']) ??
..scheme = json['scheme'] as String RequestProxyMode.follow,
..host = json['host'] as String scheme: json['scheme'] as String? ?? 'http',
..usr = json['usr'] as String host: json['host'] as String? ?? '',
..pwd = json['pwd'] as String; usr: json['usr'] as String? ?? '',
pwd: json['pwd'] as String? ?? '',
);
Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) => Map<String, dynamic> _$RequestProxyToJson(RequestProxy instance) =>
<String, dynamic>{ <String, dynamic>{
+1 -1
View File
@@ -11,7 +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, code: (json['code'] as num).toInt(),
msg: json['msg'] as String?, msg: json['msg'] as String?,
data: _$nullableGenericFromJson(json['data'], fromJsonT), data: _$nullableGenericFromJson(json['data'], fromJsonT),
); );
@@ -9,6 +9,7 @@ import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:share_handler/share_handler.dart';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
import 'package:uri_to_file/uri_to_file.dart'; import 'package:uri_to_file/uri_to_file.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
@@ -16,62 +17,42 @@ import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart'; import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart'; import '../../../../api/model/downloader_config.dart';
import '../../../../api/model/request.dart';
import '../../../../core/common/start_config.dart'; import '../../../../core/common/start_config.dart';
import '../../../../core/libgopeed_boot.dart'; import '../../../../core/libgopeed_boot.dart';
import '../../../../database/database.dart'; import '../../../../database/database.dart';
import '../../../../database/entity.dart'; import '../../../../database/entity.dart';
import '../../../../i18n/message.dart'; import '../../../../i18n/message.dart';
import '../../../../main.dart'; import '../../../../main.dart';
import '../../../../util/github_mirror.dart';
import '../../../../util/locale_manager.dart'; import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/updater.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../../create/dto/create_router_params.dart';
import '../../redirect/views/redirect_view.dart';
const unixSocketPath = 'gopeed.sock'; const unixSocketPath = 'gopeed.sock';
const allTrackerSubscribeUrls = [ const allTrackerSubscribeUrls = [
'https://github.com/ngosang/trackerslist/raw/master/trackers_all.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_http.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_http.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_https.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_https.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ip.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ip.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_udp.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_udp.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_all_ws.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all_ws.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_best.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best.txt',
'https://github.com/ngosang/trackerslist/raw/master/trackers_best_ip.txt', 'https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_best_ip.txt',
'https://github.com/XIU2/TrackersListCollection/raw/master/all.txt', 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/all.txt',
'https://github.com/XIU2/TrackersListCollection/raw/master/best.txt', 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/best.txt',
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt', 'https://raw.githubusercontent.com/XIU2/TrackersListCollection/master/http.txt',
]; ];
const allTrackerCdns = [ final allTrackerSubscribeUrlCdns = {
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt for (var v in allTrackerSubscribeUrls)
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"], v: githubMirrorUrls(v, MirrorType.githubSource)
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt };
[
"https://ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
],
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
[
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
],
];
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
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 with WindowListener, TrayListener { class AppController extends GetxController with WindowListener, TrayListener {
static StartConfig? _defaultStartConfig; static StartConfig? _defaultStartConfig;
@@ -105,6 +86,9 @@ class AppController extends GetxController with WindowListener, TrayListener {
_initLaunchAtStartup().onError((error, stackTrace) => _initLaunchAtStartup().onError((error, stackTrace) =>
logger.w("initLaunchAtStartup error", error, stackTrace)); logger.w("initLaunchAtStartup error", error, stackTrace));
_initCheckUpdate().onError((error, stackTrace) =>
logger.w("initCheckUpdate error", error, stackTrace));
} }
@override @override
@@ -160,22 +144,46 @@ class AppController extends GetxController with WindowListener, TrayListener {
} }
Future<void> _initDeepLinks() async { Future<void> _initDeepLinks() async {
// currently only support android if (Util.isWeb()) {
if (!Util.isAndroid()) {
return; return;
} }
_appLinks = AppLinks(); // Handle deep link
() async {
_appLinks = AppLinks();
// Handle link when app is in warm state (front or background) // Handle link when app is in warm state (front or background)
_linkSubscription = _appLinks.uriLinkStream.listen((uri) async { _linkSubscription = _appLinks.uriLinkStream.listen((uri) async {
await _toCreate(uri); await _handleDeepLink(uri);
}); });
// Check initial link if app was in cold state (terminated) // Check initial link if app was in cold state (terminated)
final uri = await _appLinks.getInitialAppLink(); final uri = await _appLinks.getInitialLink();
if (uri != null) { if (uri != null) {
await _toCreate(uri); await _handleDeepLink(uri);
}
}();
// Handle shared media, e.g. shared link from browser
if (Util.isMobile()) {
() async {
final handler = ShareHandlerPlatform.instance;
handler.sharedMediaStream.listen((SharedMedia media) {
if (media.content?.isNotEmpty == true) {
final uri = Uri.parse(media.content!);
// content uri will be handled by the app_links plugin
if (uri.scheme != "content") {
_handleDeepLink(uri);
}
}
});
final media = await handler.getInitialSharedMedia();
if (media?.content?.isNotEmpty == true) {
_handleDeepLink(Uri.parse(media!.content!));
}
}();
} }
} }
@@ -304,13 +312,41 @@ class AppController extends GetxController with WindowListener, TrayListener {
} }
} }
Future<void> _toCreate(Uri uri) async { Future<void> _handleDeepLink(Uri uri) async {
final path = (uri.scheme == "magnet" || if (uri.scheme == "gopeed") {
uri.scheme == "http" || if (uri.path == "/create") {
uri.scheme == "https") final params = uri.queryParameters["params"];
? uri.toString() if (params?.isNotEmpty == true) {
: (await toFile(uri.toString())).path; final safeParams = params!.replaceAll(" ", "+");
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path); final paramsJson =
String.fromCharCodes(base64Decode(base64.normalize(safeParams)));
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE,
arguments:
CreateRouterParams.fromJson(jsonDecode(paramsJson))));
return;
}
Get.rootDelegate.offAndToNamed(Routes.CREATE);
return;
}
Get.rootDelegate.offAndToNamed(Routes.HOME);
return;
}
String path;
if (uri.scheme == "magnet" ||
uri.scheme == "http" ||
uri.scheme == "https") {
path = uri.toString();
} else if (uri.scheme == "file") {
path =
Util.isWindows() ? Uri.decodeFull(uri.path.substring(1)) : uri.path;
} else {
path = (await toFile(uri.toString())).path;
}
Get.rootDelegate.offAndToNamed(Routes.REDIRECT,
arguments: RedirectArgs(Routes.CREATE,
arguments: CreateRouterParams(req: Request(url: path))));
} }
String runningAddress() { String runningAddress() {
@@ -469,6 +505,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
autoStartup.value = await launchAtStartup.isEnabled(); autoStartup.value = await launchAtStartup.isEnabled();
} }
Future<void> _initCheckUpdate() async {
final versionInfo = await checkUpdate();
if (versionInfo != null) {
await showUpdateDialog(Get.context!, versionInfo);
}
}
Future<void> saveConfig() async { Future<void> saveConfig() async {
Database.instance.saveStartConfig(StartConfigEntity( Database.instance.saveStartConfig(StartConfigEntity(
network: startConfig.value.network, network: startConfig.value.network,
@@ -2,10 +2,12 @@ import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; // Import the required packages
import '../../../../i18n/message.dart'; import '../../../../i18n/message.dart';
import '../../../../theme/theme.dart'; import '../../../../theme/theme.dart';
import '../../../../util/locale_manager.dart'; import '../../../../util/locale_manager.dart';
import '../../../../util/util.dart'; // Import the required packages
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../controllers/app_controller.dart'; import '../controllers/app_controller.dart';
@@ -32,6 +34,19 @@ class AppView extends GetView<AppController> {
], ],
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(), supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
getPages: AppPages.routes, getPages: AppPages.routes,
// Add listening to theme changes, set the title bar color according to the current system theme.
builder: (context, child) {
// if platform is desktop
if (Util.isDesktop()){
// actual brightness of the UI
Brightness brightness = Theme.of(context).brightness;
// Set the title bar to use the actual brightness of the UI
windowManager.setBrightness(brightness);
}
return child!;
},
), ),
); );
} }
@@ -9,7 +9,6 @@ import '../../app/controllers/app_controller.dart';
class CreateController extends GetxController class CreateController extends GetxController
with GetSingleTickerProviderStateMixin { with GetSingleTickerProviderStateMixin {
// final files = [].obs;
final RxList fileInfos = [].obs; final RxList fileInfos = [].obs;
final RxList openedFolders = [].obs; final RxList openedFolders = [].obs;
final selectedIndexes = <int>[].obs; final selectedIndexes = <int>[].obs;
@@ -0,0 +1,24 @@
import 'package:json_annotation/json_annotation.dart';
import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart';
part 'create_router_params.g.dart';
@JsonSerializable(explicitToJson: true)
class CreateRouterParams {
Request? req;
Options? opt;
CreateRouterParams({
this.req,
this.opt,
});
factory CreateRouterParams.fromJson(
Map<String, dynamic> json,
) =>
_$CreateRouterParamsFromJson(json);
Map<String, dynamic> toJson() => _$CreateRouterParamsToJson(this);
}
@@ -0,0 +1,31 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'create_router_params.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
CreateRouterParams _$CreateRouterParamsFromJson(Map<String, dynamic> json) =>
CreateRouterParams(
req: json['req'] == null
? null
: Request.fromJson(json['req'] as Map<String, dynamic>),
opt: json['opt'] == null
? null
: Options.fromJson(json['opt'] as Map<String, dynamic>),
);
Map<String, dynamic> _$CreateRouterParamsToJson(CreateRouterParams instance) {
final val = <String, dynamic>{};
void writeNotNull(String key, dynamic value) {
if (value != null) {
val[key] = value;
}
}
writeNotNull('req', instance.req?.toJson());
writeNotNull('opt', instance.opt?.toJson());
return val;
}
@@ -1,5 +1,6 @@
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart'; import 'dart:convert';
import 'package:checkable_treeview/checkable_treeview.dart';
import 'package:contentsize_tabbarview/contentsize_tabbarview.dart';
import 'package:desktop_drop/desktop_drop.dart'; import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@@ -10,14 +11,15 @@ import 'package:rounded_loading_button_plus/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/create_task_batch.dart';
import '../../../../api/model/options.dart'; import '../../../../api/model/options.dart';
import '../../../../api/model/request.dart'; import '../../../../api/model/request.dart';
import '../../../../api/model/resolve_result.dart'; import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/task.dart';
import '../../../../database/database.dart'; import '../../../../database/database.dart';
import '../../../../util/input_formatter.dart'; import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../../util/extensions.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
import '../../../views/compact_checkbox.dart'; import '../../../views/compact_checkbox.dart';
import '../../../views/directory_selector.dart'; import '../../../views/directory_selector.dart';
@@ -25,6 +27,7 @@ import '../../../views/file_tree_view.dart';
import '../../app/controllers/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../../history/views/history_view.dart'; import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart'; import '../controllers/create_controller.dart';
import '../dto/create_router_params.dart';
class CreateView extends GetView<CreateController> { class CreateView extends GetView<CreateController> {
final _confirmFormKey = GlobalKey<FormState>(); final _confirmFormKey = GlobalKey<FormState>();
@@ -38,11 +41,21 @@ class CreateView extends GetView<CreateController> {
final _proxyPortController = TextEditingController(); final _proxyPortController = TextEditingController();
final _proxyUsrController = TextEditingController(); final _proxyUsrController = TextEditingController();
final _proxyPwdController = TextEditingController(); final _proxyPwdController = TextEditingController();
final _httpUaController = TextEditingController(); final _httpHeaderControllers = [
final _httpCookieController = TextEditingController(); (
final _httpRefererController = TextEditingController(); name: TextEditingController(text: "User-Agent"),
value: TextEditingController()
),
(
name: TextEditingController(text: "Cookie"),
value: TextEditingController()
),
(
name: TextEditingController(text: "Referer"),
value: TextEditingController()
),
];
final _btTrackerController = TextEditingController(); final _btTrackerController = TextEditingController();
final _fileTreeViewKey = GlobalKey<TreeViewState<int>>();
final _availableSchemes = ["http:", "https:", "magnet:"]; final _availableSchemes = ["http:", "https:", "magnet:"];
@@ -63,32 +76,82 @@ class CreateView extends GetView<CreateController> {
_pathController.text = appController.downloaderConfig.value.downloadDir; _pathController.text = appController.downloaderConfig.value.downloadDir;
} }
final String? filePath = Get.rootDelegate.arguments(); final CreateRouterParams? routerParams = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty) { if (routerParams?.req?.url.isNotEmpty ?? false) {
if (filePath?.isNotEmpty ?? false) { // get url from route arguments
// get file path from route arguments final url = routerParams!.req!.url;
_urlController.text = filePath!; _urlController.text = url;
_urlController.selection = TextSelection.fromPosition( _urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length)); TextPosition(offset: _urlController.text.length));
} else { final protocol = parseProtocol(url);
// read clipboard if (protocol != null) {
Clipboard.getData('text/plain').then((value) { final extraHandlers = {
if (value?.text?.isNotEmpty ?? false) { Protocol.http: () {
if (_availableSchemes final reqExtra = ReqExtraHttp.fromJson(
.where((e) => jsonDecode(jsonEncode(routerParams.req!.extra)));
value!.text!.startsWith(e) || _httpHeaderControllers.clear();
value.text!.startsWith(e.toUpperCase())) reqExtra.header.forEach((key, value) {
.isNotEmpty) { _httpHeaderControllers.add(
_urlController.text = value!.text!; (
_urlController.selection = TextSelection.fromPosition( name: TextEditingController(text: key),
TextPosition(offset: _urlController.text.length)); value: TextEditingController(text: value),
return; ),
} );
});
_skipVerifyCertController.value = routerParams.req!.skipVerifyCert;
},
Protocol.bt: () {
final reqExtra = ReqExtraBt.fromJson(
jsonDecode(jsonEncode(routerParams.req!.extra)));
_btTrackerController.text = reqExtra.trackers.join("\n");
},
};
if (routerParams.req?.extra != null) {
extraHandlers[protocol]?.call();
}
recognizeMagnetUri(value!.text!); // handle options
if (routerParams.opt != null) {
_renameController.text = routerParams.opt!.name;
_pathController.text = routerParams.opt!.path;
final optionsHandlers = {
Protocol.http: () {
final opt = routerParams.opt!;
_renameController.text = opt.name;
_pathController.text = opt.path;
if (opt.extra != null) {
final optsExtraHttp =
OptsExtraHttp.fromJson(jsonDecode(jsonEncode(opt.extra)));
_connectionsController.text =
optsExtraHttp.connections.toString();
}
},
Protocol.bt: null,
};
if (routerParams.opt?.extra != null) {
optionsHandlers[protocol]?.call();
} }
}); }
} }
} else if (_urlController.text.isEmpty) {
// read clipboard
Clipboard.getData('text/plain').then((value) {
if (value?.text?.isNotEmpty ?? false) {
if (_availableSchemes
.where((e) =>
value!.text!.startsWith(e) ||
value.text!.startsWith(e.toUpperCase()))
.isNotEmpty) {
_urlController.text = value!.text!;
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
return;
}
recognizeMagnetUri(value!.text!);
}
});
} }
return Scaffold( return Scaffold(
@@ -439,56 +502,105 @@ class CreateView extends GetView<CreateController> {
) )
], ],
), ),
AutoScaleTabBarView( DefaultTabController(
controller: controller.advancedTabController, length: 2,
children: [ child: ContentSizeTabBarView(
Column( controller:
children: [ controller.advancedTabController,
TextFormField( children: [
controller: _httpUaController, Column(
decoration: const InputDecoration( children: [
labelText: 'User-Agent', ..._httpHeaderControllers.map((e) {
)), return Row(
TextFormField( children: [
controller: _httpCookieController, Flexible(
decoration: const InputDecoration( child: TextFormField(
labelText: 'Cookie', controller: e.name,
)), decoration: InputDecoration(
TextFormField( hintText:
controller: _httpRefererController, 'httpHeaderName'.tr,
decoration: const InputDecoration( ),
labelText: 'Referer', ),
)), ),
Padding( const Padding(
padding: padding: EdgeInsets.only(
const EdgeInsets.only(top: 10), left: 10)),
child: CompactCheckbox( Flexible(
label: 'skipVerifyCert'.tr, child: TextFormField(
value: controller: e.value,
_skipVerifyCertController.value, decoration: InputDecoration(
onChanged: (bool? value) { hintText:
_skipVerifyCertController.value = 'httpHeaderValue'.tr,
value ?? false; ),
}, ),
textStyle: const TextStyle( ),
color: Colors.grey, const Padding(
padding: EdgeInsets.only(
left: 10)),
IconButton(
icon: const Icon(Icons.add),
onPressed: () {
_httpHeaderControllers.add(
(
name:
TextEditingController(),
value:
TextEditingController(),
),
);
controller.showAdvanced
.update((val) => val);
},
),
IconButton(
icon:
const Icon(Icons.remove),
onPressed: () {
if (_httpHeaderControllers
.length <=
1) {
return;
}
_httpHeaderControllers
.remove(e);
controller.showAdvanced
.update((val) => val);
},
),
],
);
}),
Padding(
padding:
const EdgeInsets.only(top: 10),
child: CompactCheckbox(
label: 'skipVerifyCert'.tr,
value: _skipVerifyCertController
.value,
onChanged: (bool? value) {
_skipVerifyCertController
.value = value ?? false;
},
textStyle: const TextStyle(
color: Colors.grey,
),
), ),
), ),
), ],
], ),
), Column(
Column( children: [
children: [ TextFormField(
TextFormField( controller: _btTrackerController,
controller: _btTrackerController, maxLines: 5,
maxLines: 5, decoration: InputDecoration(
decoration: InputDecoration( labelText: 'Trackers',
labelText: 'Trackers', hintText: 'addTrackerHit'.tr,
hintText: 'addTrackerHit'.tr, )),
)), ],
], )
) ],
], ),
) )
], ],
).paddingOnly(top: 16), ).paddingOnly(top: 16),
@@ -552,6 +664,20 @@ class CreateView extends GetView<CreateController> {
); );
} }
// parse protocol from url
parseProtocol(String url) {
final uppercaseUrl = url.toUpperCase();
Protocol? protocol;
if (uppercaseUrl.startsWith("HTTP:") || uppercaseUrl.startsWith("HTTPS:")) {
protocol = Protocol.http;
}
if (uppercaseUrl.startsWith("MAGNET:") ||
uppercaseUrl.endsWith(".TORRENT")) {
protocol = Protocol.bt;
}
return protocol;
}
// recognize magnet uri, if length == 40, auto add magnet prefix // recognize magnet uri, if length == 40, auto add magnet prefix
recognizeMagnetUri(String text) { recognizeMagnetUri(String text) {
if (text.length != 40) { if (text.length != 40) {
@@ -644,26 +770,23 @@ class CreateView extends GetView<CreateController> {
Object? parseReqExtra(String url) { Object? parseReqExtra(String url) {
Object? reqExtra; Object? reqExtra;
if (controller.showAdvanced.value) { final protocol = parseProtocol(url);
switch (controller.advancedTabController.index) { switch (protocol) {
case 0: case Protocol.http:
final header = { final header = Map<String, String>.fromEntries(_httpHeaderControllers
"User-Agent": _httpUaController.text, .map((e) => MapEntry(e.name.text, e.value.text)));
"Cookie": _httpCookieController.text, header.removeWhere(
"Referer": _httpRefererController.text, (key, value) => key.trim().isEmpty || value.trim().isEmpty);
}; if (header.isNotEmpty) {
header.removeWhere((key, value) => value.trim().isEmpty); reqExtra = ReqExtraHttp()..header = header;
if (header.isNotEmpty) { }
reqExtra = ReqExtraHttp()..header = header; break;
} case Protocol.bt:
break; if (_btTrackerController.text.trim().isNotEmpty) {
case 1: reqExtra = ReqExtraBt()
if (_btTrackerController.text.trim().isNotEmpty) { ..trackers = Util.textToLines(_btTrackerController.text);
reqExtra = ReqExtraBt() }
..trackers = Util.textToLines(_btTrackerController.text); break;
}
break;
}
} }
return reqExtra; return reqExtra;
} }
@@ -747,18 +870,20 @@ class CreateView extends GetView<CreateController> {
if (createFormKey.currentState!.validate()) { if (createFormKey.currentState!.validate()) {
if (rr.id.isEmpty) { if (rr.id.isEmpty) {
// from extension resolve result // from extension resolve result
await Future.wait( final reqs =
controller.selectedIndexes.map((index) { controller.selectedIndexes.map((index) {
final file = rr.res.files[index]; final file = rr.res.files[index];
return createTask(CreateTask( return CreateTaskBatchItem(
req: file.req!..proxy = parseProxy(), req: file.req!..proxy = parseProxy(),
opt: Options( opts: Options(
name: file.name, name: file.name,
path: path.join(_pathController.text, path: path.join(_pathController.text,
rr.res.name, file.path), rr.res.name, file.path),
selectFiles: [], selectFiles: [],
extra: optExtra))); extra: optExtra));
})); }).toList();
await createTaskBatch(
CreateTaskBatch(reqs: reqs));
} else { } else {
await createTask(CreateTask( await createTask(CreateTask(
rid: rr.id, rid: rr.id,
@@ -16,7 +16,6 @@ import '../../../../api/model/extension.dart';
import '../../../../api/model/install_extension.dart'; import '../../../../api/model/install_extension.dart';
import '../../../../api/model/switch_extension.dart'; import '../../../../api/model/switch_extension.dart';
import '../../../../api/model/update_extension_settings.dart'; import '../../../../api/model/update_extension_settings.dart';
import '../../../../util/mac_secure_util.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../views/icon_button_loading.dart'; import '../../../views/icon_button_loading.dart';
@@ -96,7 +95,6 @@ class ExtensionView extends GetView<ExtensionController> {
var dir = var dir =
await FilePicker.platform.getDirectoryPath(); await FilePicker.platform.getDirectoryPath();
if (dir != null) { if (dir != null) {
MacSecureUtil.saveBookmark(dir);
try { try {
await installExtension( await installExtension(
InstallExtension(devMode: true, url: dir)); InstallExtension(devMode: true, url: dir));
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/redirect_controller.dart';
class RedirectBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RedirectController>(
() => RedirectController(),
);
}
}
@@ -0,0 +1,3 @@
import 'package:get/get.dart';
class RedirectController extends GetxController {}
@@ -0,0 +1,27 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../controllers/redirect_controller.dart';
class RedirectArgs {
final String page;
final dynamic arguments;
RedirectArgs(this.page, {this.arguments});
}
class RedirectView extends GetView<RedirectController> {
const RedirectView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final redirectArgs = Get.rootDelegate.arguments() as RedirectArgs;
// Waiting for previous page controller to delete, avoid deleting controller that route page after redirect
WidgetsBinding.instance.addPostFrameCallback((_) async {
await Future.delayed(const Duration(milliseconds: 350));
Get.rootDelegate
.offAndToNamed(redirectArgs.page, arguments: redirectArgs.arguments);
});
return const SizedBox();
}
}
@@ -1,11 +1,10 @@
import 'dart:convert';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/api/api.dart';
import '../../../../util/updater.dart';
class SettingController extends GetxController { class SettingController extends GetxController {
final tapStatues = <String, bool>{}.obs; final tapStatues = <String, bool>{}.obs;
final latestVersion = "".obs; final latestVersion = Rxn<VersionInfo>();
@override @override
void onInit() { void onInit() {
@@ -26,23 +25,6 @@ class SettingController extends GetxController {
// fetch latest version // fetch latest version
void fetchLatestVersion() async { void fetchLatestVersion() async {
String? releaseDataStr; latestVersion.value = await checkUpdate();
try {
releaseDataStr = (await proxyRequest(
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
.data;
} catch (e) {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
}
if (releaseDataStr == null) {
return;
}
final releaseData = jsonDecode(releaseDataStr);
final tagName = releaseData["tag_name"];
if (tagName == null) {
return;
}
latestVersion.value = releaseData["tag_name"].substring(1);
} }
} }
@@ -17,6 +17,8 @@ import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/scheme_register/scheme_register.dart';
import '../../../../util/updater.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../views/check_list_view.dart'; import '../../../views/check_list_view.dart';
import '../../../views/directory_selector.dart'; import '../../../views/directory_selector.dart';
@@ -111,7 +113,7 @@ class SettingView extends GetView<SettingController> {
}); });
final buildDefaultDirectDownload = final buildDefaultDirectDownload =
_buildConfigItem('defaultDirectDownload'.tr, () { _buildConfigItem('defaultDirectDownload', () {
return appController.downloaderConfig.value.extra.defaultDirectDownload return appController.downloaderConfig.value.extra.defaultDirectDownload
? 'on'.tr ? 'on'.tr
: 'off'.tr; : 'off'.tr;
@@ -161,7 +163,7 @@ class SettingView extends GetView<SettingController> {
// Currently auto startup only support Windows and Linux // Currently auto startup only support Windows and Linux
final buildAutoStartup = !Util.isWindows() && !Util.isLinux() final buildAutoStartup = !Util.isWindows() && !Util.isLinux()
? () => null ? () => null
: _buildConfigItem('launchAtStartup'.tr, () { : _buildConfigItem('launchAtStartup', () {
return appController.autoStartup.value ? 'on'.tr : 'off'.tr; return appController.autoStartup.value ? 'on'.tr : 'off'.tr;
}, (Key key) { }, (Key key) {
return Container( return Container(
@@ -229,8 +231,9 @@ class SettingView extends GetView<SettingController> {
], ],
); );
}); });
final buildHttpUseServerCtime = _buildConfigItem('useServerCtime'.tr, final buildHttpUseServerCtime = _buildConfigItem(
() => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr, (Key key) { 'useServerCtime', () => httpConfig.useServerCtime ? 'on'.tr : 'off'.tr,
(Key key) {
return Container( return Container(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Switch( child: Switch(
@@ -420,6 +423,37 @@ class SettingView extends GetView<SettingController> {
].where((e) => e != null).map((e) => e!).toList(), ].where((e) => e != null).map((e) => e!).toList(),
); );
}); });
final buildBtDefaultClientConfig = !Util.isWindows()
? () => null
: _buildConfigItem('setAsDefaultBtClient', () {
return appController.downloaderConfig.value.extra.defaultBtClient
? 'on'.tr
: 'off'.tr;
}, (Key key) {
return Container(
alignment: Alignment.centerLeft,
child: Switch(
value:
appController.downloaderConfig.value.extra.defaultBtClient,
onChanged: (bool value) async {
try {
if (value) {
registerDefaultTorrentClient();
} else {
unregisterDefaultTorrentClient();
}
appController.downloaderConfig.update((val) {
val!.extra.defaultBtClient = value;
});
await debounceSave();
} catch (e) {
showErrorMessage(e);
logger.e('register default torrent client fail', e);
}
},
),
);
});
// ui config items start // ui config items start
final buildTheme = _buildConfigItem( final buildTheme = _buildConfigItem(
@@ -481,29 +515,7 @@ class SettingView extends GetView<SettingController> {
} }
buildVersion() { buildVersion() {
bool isNewVersion(String current, String latest) { var hasNewVersion = controller.latestVersion.value != null;
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( return ListTile(
title: hasNewVersion title: hasNewVersion
? badges.Badge( ? badges.Badge(
@@ -513,27 +525,7 @@ class SettingView extends GetView<SettingController> {
subtitle: Text(packageInfo.version), subtitle: Text(packageInfo.version),
onTap: () { onTap: () {
if (hasNewVersion) { if (hasNewVersion) {
showDialog( showUpdateDialog(context, controller.latestVersion.value!);
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),
),
],
));
} }
}, },
); );
@@ -729,7 +721,7 @@ class SettingView extends GetView<SettingController> {
width: 80, width: 80,
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: startCfg.value.network, value: startCfg.value.network,
onChanged: Util.isDesktop() onChanged: Util.isDesktop() || Util.isAndroid()
? (value) async { ? (value) async {
startCfg.update((val) { startCfg.update((val) {
val!.network = value!; val!.network = value!;
@@ -753,7 +745,8 @@ class SettingView extends GetView<SettingController> {
), ),
) )
]; ];
if (Util.isDesktop() && startCfg.value.network == 'tcp') { if ((Util.isDesktop() || Util.isAndroid()) &&
startCfg.value.network == 'tcp') {
final arr = startCfg.value.address.split(':'); final arr = startCfg.value.address.split(':');
var ip = '127.0.0.1'; var ip = '127.0.0.1';
var port = '0'; var port = '0';
@@ -947,6 +940,7 @@ class SettingView extends GetView<SettingController> {
buildBtTrackerSubscribeUrls(), buildBtTrackerSubscribeUrls(),
buildBtTrackers(), buildBtTrackers(),
buildBtSeedConfig(), buildBtSeedConfig(),
buildBtDefaultClientConfig(),
]), ]),
)), )),
Text('ui'.tr), Text('ui'.tr),
@@ -1,4 +1,6 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/api/model/task.dart'; import 'package:gopeed/api/model/task.dart';
@@ -6,4 +8,20 @@ class TaskController extends GetxController {
final tabIndex = 0.obs; final tabIndex = 0.obs;
final scaffoldKey = GlobalKey<ScaffoldState>(); final scaffoldKey = GlobalKey<ScaffoldState>();
final selectTask = Rx<Task?>(null); final selectTask = Rx<Task?>(null);
@override
void onInit() {
super.onInit();
if (kIsWeb) {
BrowserContextMenu.disableContextMenu();
}
}
@override
void onClose() {
super.onClose();
if (kIsWeb) {
BrowserContextMenu.enableContextMenu();
}
}
} }
+9 -2
View File
@@ -1,6 +1,4 @@
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/views/task_files_view.dart';
import 'package:gopeed/app/modules/task/views/task_view.dart';
import '../modules/create/bindings/create_binding.dart'; import '../modules/create/bindings/create_binding.dart';
import '../modules/create/views/create_view.dart'; import '../modules/create/views/create_view.dart';
@@ -8,12 +6,16 @@ import '../modules/extension/bindings/extension_binding.dart';
import '../modules/extension/views/extension_view.dart'; import '../modules/extension/views/extension_view.dart';
import '../modules/home/bindings/home_binding.dart'; import '../modules/home/bindings/home_binding.dart';
import '../modules/home/views/home_view.dart'; import '../modules/home/views/home_view.dart';
import '../modules/redirect/bindings/redirect_binding.dart';
import '../modules/redirect/views/redirect_view.dart';
import '../modules/root/bindings/root_binding.dart'; import '../modules/root/bindings/root_binding.dart';
import '../modules/root/views/root_view.dart'; import '../modules/root/views/root_view.dart';
import '../modules/setting/bindings/setting_binding.dart'; import '../modules/setting/bindings/setting_binding.dart';
import '../modules/setting/views/setting_view.dart'; import '../modules/setting/views/setting_view.dart';
import '../modules/task/bindings/task_binding.dart'; import '../modules/task/bindings/task_binding.dart';
import '../modules/task/bindings/task_files_binding.dart'; import '../modules/task/bindings/task_files_binding.dart';
import '../modules/task/views/task_files_view.dart';
import '../modules/task/views/task_view.dart';
part 'app_routes.dart'; part 'app_routes.dart';
@@ -68,6 +70,11 @@ class AppPages {
page: () => CreateView(), page: () => CreateView(),
binding: CreateBinding(), binding: CreateBinding(),
), ),
GetPage(
name: _Paths.REDIRECT,
page: () => const RedirectView(),
binding: RedirectBinding(),
),
]), ]),
]; ];
} }
@@ -3,6 +3,7 @@ part of 'app_pages.dart';
abstract class Routes { abstract class Routes {
Routes._(); Routes._();
static const ROOT = _Paths.ROOT; static const ROOT = _Paths.ROOT;
static const HOME = _Paths.HOME; static const HOME = _Paths.HOME;
static const CREATE = _Paths.CREATE; static const CREATE = _Paths.CREATE;
@@ -10,10 +11,12 @@ abstract class Routes {
static const TASK_FILES = TASK + _Paths.TASK_FILES; static const TASK_FILES = TASK + _Paths.TASK_FILES;
static const EXTENSION = _Paths.HOME + _Paths.EXTENSION; static const EXTENSION = _Paths.HOME + _Paths.EXTENSION;
static const SETTING = _Paths.HOME + _Paths.SETTING; static const SETTING = _Paths.HOME + _Paths.SETTING;
static const REDIRECT = _Paths.REDIRECT;
} }
abstract class _Paths { abstract class _Paths {
_Paths._(); _Paths._();
static const ROOT = '/'; static const ROOT = '/';
static const HOME = '/home'; static const HOME = '/home';
static const CREATE = '/create'; static const CREATE = '/create';
@@ -21,4 +24,5 @@ abstract class _Paths {
static const TASK_FILES = '/files'; static const TASK_FILES = '/files';
static const EXTENSION = '/extension'; static const EXTENSION = '/extension';
static const SETTING = '/setting'; static const SETTING = '/setting';
static const REDIRECT = '/redirect';
} }
@@ -1,4 +1,4 @@
import 'package:contextmenu/contextmenu.dart'; import 'package:contextmenu_plus/contextmenu_plus.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
@@ -9,7 +9,6 @@ import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:toggle_switch/toggle_switch.dart'; import 'package:toggle_switch/toggle_switch.dart';
import '../../util/mac_secure_util.dart';
import '../../util/message.dart'; import '../../util/message.dart';
import '../../util/util.dart'; import '../../util/util.dart';
@@ -42,7 +41,6 @@ class _DirectorySelectorState extends State<DirectorySelector> {
var dir = await FilePicker.platform.getDirectoryPath(); var dir = await FilePicker.platform.getDirectoryPath();
if (dir != null) { if (dir != null) {
widget.controller.text = dir; widget.controller.text = dir;
MacSecureUtil.saveBookmark(dir);
} }
}); });
} }
+61 -57
View File
@@ -7,6 +7,7 @@ import '../../api/model/resource.dart';
import '../../icon/gopeed_icons.dart'; import '../../icon/gopeed_icons.dart';
import '../../util/util.dart'; import '../../util/util.dart';
import 'file_icon.dart'; import 'file_icon.dart';
import 'responsive_builder.dart';
import 'sort_icon_button.dart'; import 'sort_icon_button.dart';
const _toggleSwitchIcons = [ const _toggleSwitchIcons = [
@@ -53,6 +54,52 @@ class _FileTreeViewState extends State<FileTreeView> {
widget.files.length; widget.files.length;
final selectedFileSize = calcSelectedSize(null); final selectedFileSize = calcSelectedSize(null);
final filterRow = InkWell(
onTap: () {},
child: ToggleSwitch(
minHeight: 32,
cornerRadius: 8,
doubleTapDisable: true,
inactiveBgColor: Theme.of(context).dividerColor,
activeBgColor: [Theme.of(context).colorScheme.primary],
initialLabelIndex: toggleSwitchIndex,
icons: _toggleSwitchIcons,
onToggle: (index) {
toggleSwitchIndex = index;
if (index == null) {
key.currentState?.setSelectedValues(List.empty());
return;
}
final iconFileExtArr = iconConfigMap[_toggleSwitchIcons[index]] ?? [];
final selectedFileIndexes = widget.files
.asMap()
.entries
.where((e) => iconFileExtArr.contains(fileExt(e.value.name)))
.map((e) => e.key)
.toList();
key.currentState?.setSelectedValues(selectedFileIndexes);
},
),
);
final countRow = Row(
children: [
Text('fileSelectedCount'.tr),
Text(
selectedFileCount.toString(),
style: Theme.of(context).textTheme.bodySmall,
),
const SizedBox(width: 12),
Text('fileSelectedSize'.tr),
Text(
selectedFileCount > 0 && selectedFileSize == 0
? 'unknown'.tr
: Util.fmtByte(selectedFileSize),
style: Theme.of(context).textTheme.bodySmall,
),
],
);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@@ -141,66 +188,23 @@ class _FileTreeViewState extends State<FileTreeView> {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
Row( !ResponsiveBuilder.isNarrow(context)
mainAxisAlignment: MainAxisAlignment.spaceBetween, ? Row(
crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ crossAxisAlignment: CrossAxisAlignment.center,
Flexible(
flex: 4,
child: InkWell(
onTap: () {},
child: ToggleSwitch(
minHeight: 32,
cornerRadius: 8,
doubleTapDisable: true,
inactiveBgColor: Theme.of(context).dividerColor,
activeBgColor: [Theme.of(context).colorScheme.primary],
initialLabelIndex: toggleSwitchIndex,
icons: _toggleSwitchIcons,
onToggle: (index) {
toggleSwitchIndex = index;
if (index == null) {
key.currentState?.setSelectedValues(List.empty());
return;
}
final iconFileExtArr =
iconConfigMap[_toggleSwitchIcons[index]] ?? [];
final selectedFileIndexes = widget.files
.asMap()
.entries
.where((e) =>
iconFileExtArr.contains(fileExt(e.value.name)))
.map((e) => e.key)
.toList();
key.currentState?.setSelectedValues(selectedFileIndexes);
},
),
),
),
Flexible(
flex: 6,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
Text('fileSelectedCount'.tr), filterRow,
Text( countRow,
selectedFileCount.toString(), ],
style: Theme.of(context).textTheme.bodySmall, )
), : Column(
const SizedBox(width: 12), crossAxisAlignment: CrossAxisAlignment.start,
Text('fileSelectedSize'.tr), children: [
Text( filterRow,
selectedFileCount > 0 && selectedFileSize == 0 const SizedBox(height: 8),
? 'unknown'.tr countRow,
: Util.fmtByte(selectedFileSize),
style: Theme.of(context).textTheme.bodySmall,
),
], ],
), ),
),
],
),
], ],
); );
} }
@@ -11,7 +11,7 @@ StartConfig _$StartConfigFromJson(Map<String, dynamic> json) => StartConfig()
..address = json['address'] as String ..address = json['address'] as String
..storage = json['storage'] as String ..storage = json['storage'] as String
..storageDir = json['storageDir'] as String ..storageDir = json['storageDir'] as String
..refreshInterval = json['refreshInterval'] as int ..refreshInterval = (json['refreshInterval'] as num).toInt()
..apiToken = json['apiToken'] as String; ..apiToken = json['apiToken'] as String;
Map<String, dynamic> _$StartConfigToJson(StartConfig instance) => Map<String, dynamic> _$StartConfigToJson(StartConfig instance) =>
+3
View File
@@ -105,6 +105,7 @@ const enUS = {
'seedKeep': 'Keep seeding until manually stopped', 'seedKeep': 'Keep seeding until manually stopped',
'seedRatio': 'Seed ratio', 'seedRatio': 'Seed ratio',
'seedTime': 'Seed time (minutes)', 'seedTime': 'Seed time (minutes)',
'setAsDefaultBtClient': 'Set as the default BT client',
'taskDetail': 'Task Detail', 'taskDetail': 'Task Detail',
'taskName': 'Task Name', 'taskName': 'Task Name',
'taskUrl': 'Task URL', 'taskUrl': 'Task URL',
@@ -115,5 +116,7 @@ const enUS = {
'unknown': 'Unknown', 'unknown': 'Unknown',
'fileSelectedCount': 'Files: ', 'fileSelectedCount': 'Files: ',
'fileSelectedSize': 'Size: ', 'fileSelectedSize': 'Size: ',
'httpHeaderName': 'Header Name',
'httpHeaderValue': 'Header Value',
}, },
}; };
+122
View File
@@ -0,0 +1,122 @@
const huHU = {
'hu_HU': {
'label': 'Magyar',
'error': 'Hiba',
'tip': 'Tipp',
'confirm': 'Megerősít',
'cancel': 'Mégsem',
'on': 'Bekapcsolva',
'off': 'Kikapcsolva',
'selectAll': 'Összes kiválasztása',
'select': 'Kiválaszt',
'task': 'Feladatok',
'downloading': 'letöltés alatt',
'downloaded': 'letöltve',
'setting': 'Beállítások',
'donate': 'Támogatás',
'exit': 'Kilépés',
'create': 'Feladat létrehozása',
'directDownload': 'Közvetlen letöltés',
'advancedOptions': 'Haladó beállítások',
'followSettings': 'Beállítások követése',
'downloadLink': 'Letöltési hivatkozás',
'downloadLinkValid': 'Kérjük, adja meg a letöltési hivatkozást',
'downloadLinkHit': 'Kérjük, adja meg a letöltési hivatkozást, soronként egyet@append',
'downloadLinkHitDesktop': ', vagy húzza ide a torrent fájlt közvetlenül',
'download': 'Letöltés',
'noFileSelected': 'Kérjük, válasszon ki legalább egy fájlt a folytatáshoz.',
'noStoragePermission': 'Tárolási engedély szükséges',
'selectFile': 'Fájl kiválasztása',
'rename': 'Átnevezés',
'basic': 'Alap',
'advanced': 'Haladó',
'general': 'Általános',
'downloadDir': 'Letöltési könyvtár',
'downloadDirValid': 'Kérjük, válassza ki a letöltési könyvtárat',
'connections': 'Kapcsolatok',
'useServerCtime': 'Használja a szerveridőt a fájl létrehozásához',
'maxRunning': 'Maximális futó feladatok',
'defaultDirectDownload': 'Alapértelmezettként ellenőrizze a közvetlen letöltést',
'items': '@count elem',
'subscribeTracker': 'Nyomkövető előfizetése',
'subscribeFail':
'Az előfizetés nem sikerült, kérjük, ellenőrizze a hálózatot, vagy próbálja újra később',
'update': 'Frissítés',
'updateDaily': 'Napi frissítés',
'lastUpdate': 'Utolsó frissítés: @time',
'addTracker': 'Nyomkövető hozzáadása',
'addTrackerHit': 'Kérjük, adja meg a nyomkövető szerver URL-jét, soronként egyet',
'ui': 'Felhasználói felület',
'theme': 'Téma',
'themeSystem': 'Rendszer',
'themeLight': 'Világos',
'themeDark': 'Sötét',
'locale': 'Nyelv',
'about': 'Névjegy',
'homepage': 'Főoldal',
'version': 'Verzió',
'protocol': 'Protokoll',
'port': 'Port',
'apiToken': 'API token',
'notSet': 'NS',
'set': 'BEÁLLÍT',
'portInUse': 'A [@port] port használatban van, kérjük, változtassa meg a portot',
'effectAfterRestart': 'Kihatás indítás után',
'developer': 'Fejlesztő',
'logDirectory': 'Napló könyvtár',
'show': 'Megjelenítés',
'continue': 'Folyatás',
'pause': 'Szünet',
'startAll': 'Összes indítása',
'pauseAll': 'Összes szüneteltetése',
'deleteTask': '@count feladat törlése',
'deleteTaskTip': 'Letöltött fájlok megőrzése',
'delete': 'Törlés',
'newVersionTitle': 'Új verzió felfedezése @version',
'newVersionUpdate': 'Frissítés most',
'newVersionLater': 'Később',
'extensions': 'Bővítmények',
'extensionInstallUrl': 'Telepítési URL',
'extensionInstallSuccess': 'Sikeresen telepítve',
'extensionUpdateSuccess': 'Sikeresen frissítve',
'extensionDelete': 'Bővítmény törlése',
'extensionAlreadyLatest': 'Ez már a legfrissebb verzió',
'extensionFind': 'Bővítmények keresése',
'extensionDevelop': 'Bővítmények fejlesztése',
'history': 'Előzmények',
'clearHistory': 'Előzmények törlése',
'noHistoryFound': 'Nincs előzménye',
'serviceTitle': 'Letöltési szolgáltatás',
'serviceText': 'Futtatás',
'network': 'Hálózat',
'proxy': 'Proxy',
'noProxy': 'Nincs proxy',
'systemProxy': 'Rendszer proxy',
'customProxy': 'Egyéni proxy',
'server': 'Szerver',
'username': 'Felhasználónév',
'password': 'Jelszó',
'thanks': 'Köszönet',
'thanksDesc':
'Köszönet minden hozzájárulónak, aki segített a Gopeed közösség építésében és fejlesztésében!',
'browserExtension': 'Böngésző bővítmény',
'launchAtStartup': 'Indítás indításkor',
'seedConfig': 'Seed beállítás',
'seedKeep': 'Tartsa a seedet amíg manuálisan le nem állítják',
'seedRatio': 'Seed arány',
'seedTime': 'Seed idő (perc)',
'setAsDefaultBtClient': 'Alapértelmezett BT klienseként beállít',
'taskDetail': 'Feladat részlete',
'taskName': 'Feladat neve',
'taskUrl': 'Feladat URL',
'downloadPath': 'Letöltési útvonal',
'skipVerifyCert': 'Tanúsítvány ellenőrzés átugrása',
'name': 'Név',
'size': 'Méret',
'unknown': 'Ismeretlen',
'fileSelectedCount': 'Fájlok: ',
'fileSelectedSize': 'Méret: ',
'httpHeaderName': 'Fejléc neve',
'httpHeaderValue': 'Fejléc értéke',
},
};
+44 -3
View File
@@ -1,11 +1,14 @@
const ruRU = { const ruRU = {
'ru_RU': { 'ru_RU': {
'label': 'Русский(Россия)', 'label': 'Русский',
'error': 'Ошибка', 'error': 'Ошибка',
'tip': 'Подсказка', 'tip': 'Подсказка',
'confirm': 'Подтвердить', 'confirm': 'Подтвердить',
'cancel': 'Отмена', 'cancel': 'Отмена',
'on': 'Вкл.',
'off': 'Откл.',
'selectAll': 'Выбрать все', 'selectAll': 'Выбрать все',
'select': 'Выбрать',
'task': 'Задачи', 'task': 'Задачи',
'downloading': 'Загрузка', 'downloading': 'Загрузка',
'downloaded': 'Загружено', 'downloaded': 'Загружено',
@@ -13,7 +16,9 @@ const ruRU = {
'donate': 'Пожертвовать', 'donate': 'Пожертвовать',
'exit': 'Выход', 'exit': 'Выход',
'create': 'Создать задачу', 'create': 'Создать задачу',
'directDownload': 'Прямая загрузка',
'advancedOptions': 'Расширенные опции', 'advancedOptions': 'Расширенные опции',
'followSettings': 'По умолчанию',
'downloadLink': 'Ссылка на скачивание', 'downloadLink': 'Ссылка на скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания', 'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
'downloadLinkHit': 'downloadLinkHit':
@@ -30,7 +35,9 @@ const ruRU = {
'downloadDir': 'Папка загрузки', 'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки', 'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений', 'connections': 'Соединений',
'useServerCtime': 'Использовать время сервера для создания файла',
'maxRunning': 'Максимальное количество активных задач', 'maxRunning': 'Максимальное количество активных задач',
'defaultDirectDownload': 'Прямая загрузка по умолчанию',
'items': '@подсчет элементов', 'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера', 'subscribeTracker': 'Подпись трекера',
'subscribeFail': 'subscribeFail':
@@ -40,11 +47,11 @@ const ruRU = {
'lastUpdate': 'Последнее обновление: @время', 'lastUpdate': 'Последнее обновление: @время',
'addTracker': 'Добавить трекер', 'addTracker': 'Добавить трекер',
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке', 'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
'ui': 'UI', 'ui': 'Интерфейс',
'theme': 'Тема', 'theme': 'Тема',
'themeSystem': 'Системная', 'themeSystem': 'Системная',
'themeLight': 'Светлая', 'themeLight': 'Светлая',
'themeDark': 'Темная', 'themeDark': 'Тёмная',
'locale': 'Язык', 'locale': 'Язык',
'about': 'О приложении', 'about': 'О приложении',
'homepage': 'Домашняя страница', 'homepage': 'Домашняя страница',
@@ -54,7 +61,13 @@ const ruRU = {
'apiToken': 'API Токен', 'apiToken': 'API Токен',
'notSet': 'Не установлено', 'notSet': 'Не установлено',
'set': 'Установлено', 'set': 'Установлено',
'portInUse': 'Порт [@PORT] уже используется, пожалуйста, измените порт',
'effectAfterRestart': 'Эффект после перезагрузки', 'effectAfterRestart': 'Эффект после перезагрузки',
'developer': 'Разработчик',
'logDirectory': 'Каталог журналов',
'show': 'Показать',
'continue': 'Продолжить',
'pause': 'Пауза',
'startAll': 'Запустить все', 'startAll': 'Запустить все',
'pauseAll': 'Приостановить все', 'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить @count задач', 'deleteTask': 'Удалить @count задач',
@@ -64,6 +77,7 @@ const ruRU = {
'newVersionUpdate': 'Обновить', 'newVersionUpdate': 'Обновить',
'newVersionLater': 'позже', 'newVersionLater': 'позже',
'extensions': 'Расширения', 'extensions': 'Расширения',
'extensionInstallUrl': 'URL для установки',
'extensionInstallSuccess': 'Установка завершена', 'extensionInstallSuccess': 'Установка завершена',
'extensionUpdateSuccess': 'Обновление завершено', 'extensionUpdateSuccess': 'Обновление завершено',
'extensionDelete': 'Удалить расширение', 'extensionDelete': 'Удалить расширение',
@@ -73,10 +87,37 @@ const ruRU = {
'history': 'История', 'history': 'История',
'clearHistory': 'Очистить историю', 'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена', 'noHistoryFound': 'История не найдена',
'serviceTitle': 'Служба загрузки',
'serviceText': 'Работает',
'network': 'Сеть',
'proxy': 'Прокси',
'noProxy': 'Без прокси',
'systemProxy': 'Системный прокси',
'customProxy': 'Пользовательский прокси',
'server': 'Сервер',
'username': 'Имя пользователя',
'password': 'Пароль',
'thanks': 'Благодарности', 'thanks': 'Благодарности',
'thanksDesc': 'thanksDesc':
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!', 'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
'browserExtension': 'Расширение браузера', 'browserExtension': 'Расширение браузера',
'launchAtStartup': 'Запускать при загрузке системы',
'seedConfig': 'Настройка раздачи',
'seedKeep': 'Раздавать пока не остановлю вручную',
'seedRatio': 'Коэффициент раздачи',
'seedTime': 'Время раздачи (минуты)',
'setAsDefaultBtClient': 'Установить как клиент BT по умолчанию',
'taskDetail': 'Детали задания',
'taskName': 'Название задачи',
'taskUrl': 'URL задачи',
'downloadPath': 'Путь загрузки',
'skipVerifyCert': 'Пропустить проверку сертификата', 'skipVerifyCert': 'Пропустить проверку сертификата',
'name': 'Название',
'size': 'Размер',
'unknown': 'Неизвестно',
'fileSelectedCount': 'Файлов: ',
'fileSelectedSize': 'Размер: ',
'httpHeaderName': 'Название заголовка HTTP',
'httpHeaderValue': 'Значение заголовка HTTP',
} }
}; };
+77 -65
View File
@@ -7,43 +7,44 @@ const trTR = {
'cancel': 'İptal', 'cancel': 'İptal',
'on': 'Açık', 'on': 'Açık',
'off': 'Kapalı', 'off': 'Kapalı',
'selectAll': 'Hepsini seç', 'selectAll': 'Tümünü Seç',
'select': 'Seç',
'task': 'Görevler', 'task': 'Görevler',
'downloading': 'indiriliyor', 'downloading': 'İndiriliyor',
'downloaded': 'indirildi', 'downloaded': 'İndirildi',
'setting': 'Ayarlar', 'setting': 'Ayarlar',
'donate': 'Bağış', 'donate': 'Bağış Yap',
'exit': 'Çık', 'exit': 'Çıkış',
'create': 'Görev oluştur', 'create': 'Görev Oluştur',
'directDownload': 'Doğrudan indir', 'directDownload': 'Doğrudan İndirme',
'advancedOptions': 'Gelişmiş ayarlar', 'advancedOptions': 'Gelişmiş Seçenekler',
'followSettings': 'Ayarları takip et', 'followSettings': 'Ayarları Takip Et',
'downloadLink': 'İndirme bağlantısı', 'downloadLink': 'İndirme Bağlantısı',
'downloadLinkValid': 'Lütfen indirme bağlantısını girin', 'downloadLinkValid': 'Lütfen indirme bağlantısını girin',
'downloadLinkHit': 'Lütfen indirme bağlantısını girin, HTTP/HTTPS/MAGNET desteklenir@append', 'downloadLinkHit': 'Lütfen her satıra bir indirme bağlantısı girin@append',
'downloadLinkHitDesktop': ', veya torrent dosyasını doğrudan buraya sürükleyin', 'downloadLinkHitDesktop': ', veya doğrudan torrent dosyasını buraya sürükleyin',
'download': 'İndir', 'download': 'İndir',
'noFileSelected': 'Devam etmek için lütfen en az bir dosya seçin.', 'noFileSelected': 'Devam etmek için en az bir dosya seçin.',
'noStoragePermission': 'Depolama izni gerekli', 'noStoragePermission': 'Depolama izni gerekli',
'selectFile': 'Dosya seç', 'selectFile': 'Dosya Seç',
'rename': 'Yeniden adlandır', 'rename': 'Yeniden Adlandır',
'basic': 'Temel', 'basic': 'Temel',
'advanced': 'Gelişmiş', 'advanced': 'Gelişmiş',
'general': 'Genel', 'general': 'Genel',
'downloadDir': 'İndirme dizini', 'downloadDir': 'İndirme Dizini',
'downloadDirValid': 'Lütfen indirme dizinini seçin', 'downloadDirValid': 'Lütfen bir indirme dizini seçin',
'connections': 'Bağlantılar', 'connections': 'Bağlantılar',
'useServerCtime': 'Dosya oluşturma için sunucu saatini kullan', 'useServerCtime': 'Dosya oluşturma için sunucu zamanını kullan',
'maxRunning': 'Maksimum çalışan görev sayısı', 'maxRunning': 'Maksimum Çalışan Görev',
'defaultDirectDownload': 'Doğrudan indir seçili olsun', 'defaultDirectDownload': 'Varsayılan olarak doğrudan indirmeyi seç',
'items': '@count tane', 'items': '@count öğe',
'subscribeTracker': 'İzleyiciye abone ol', 'subscribeTracker': 'Tracker Aboneliği',
'subscribeFail': 'Abone olunamadı, lütfen ağı kontrol edin veya daha sonra tekrar deneyin', 'subscribeFail': 'Abonelik başarısız oldu, lütfen ağı kontrol edin veya daha sonra tekrar deneyin',
'update': 'Güncelle', 'update': 'Güncelle',
'updateDaily': 'Günlük olarak güncelle', 'updateDaily': 'Günlük güncelle',
'lastUpdate': 'Son güncellenme tarihi: @time', 'lastUpdate': 'Son güncelleme: @time',
'addTracker': 'İzleyici ekle', 'addTracker': 'Tracker Ekle',
'addTrackerHit': 'Lütfen her satıra bir tane olmak üzere izleyici sunucusu bağlantısını girin', 'addTrackerHit': 'Lütfen tracker sunucu URL’sini girin, her satıra bir tane',
'ui': 'Arayüz', 'ui': 'Arayüz',
'theme': 'Tema', 'theme': 'Tema',
'themeSystem': 'Sistem', 'themeSystem': 'Sistem',
@@ -51,59 +52,70 @@ const trTR = {
'themeDark': 'Koyu', 'themeDark': 'Koyu',
'locale': 'Dil', 'locale': 'Dil',
'about': 'Hakkında', 'about': 'Hakkında',
'homepage': 'Anasayfa', 'homepage': 'Ana Sayfa',
'version': 'Sürüm', 'version': 'Sürüm',
'protocol': 'Protokol', 'protocol': 'Protokol',
'port': 'Bağlantı noktası', 'port': 'Bağlantı Noktası',
'apiToken': 'API anahtarı', 'apiToken': 'API Anahtarı',
'notSet': 'AYARLI DEĞİL', 'notSet': 'AYARLANMADI',
'set': 'AYARLI', 'set': 'AYARLA',
'portInUse': 'Bağlantı noktası [@port] kullanımda, lütfen bağlantı noktasını değiştirin', 'portInUse': 'Bağlantı noktası [@port] kullanımda, lütfen değiştirin',
'effectAfterRestart': 'Yeniden başlatma sonrası etki edecektir', 'effectAfterRestart': 'Yeniden başlatmadan sonra etkili olacak',
'developer': 'Geliştirici', 'developer': 'Geliştirici',
'logDirectory': 'Kayıt dizini', 'logDirectory': 'Günlük Dizini',
'show': 'Göster', 'show': 'Göster',
'startAll': 'Hepsini başlat', 'continue': 'Devam Et',
'pauseAll': 'Hepsini durdur', 'pause': 'Duraklat',
'startAll': 'Tümünü Başlat',
'pauseAll': 'Tümünü Duraklat',
'deleteTask': '@count görevi sil', 'deleteTask': '@count görevi sil',
'deleteTaskTip': 'İndirilen dosyaları sakla', 'deleteTaskTip': 'İndirilen dosyaları sakla',
'delete': 'Sil', 'delete': 'Sil',
'newVersionTitle': 'Yeni sürümü keşfet @version', 'newVersionTitle': 'Yeni sürüm bulundu @version',
'newVersionUpdate': 'Şimdi güncelle', 'newVersionUpdate': 'Şimdi Güncelle',
'newVersionLater': 'Sonra', 'newVersionLater': 'Daha Sonra',
'extensions': 'Uzantılar', 'extensions': 'Eklentiler',
'extensionInstallUrl': 'İndirme bağlantısı', 'extensionInstallUrl': 'Kurulum URL’si',
'extensionInstallSuccess': 'Başarıyla indirildi', 'extensionInstallSuccess': 'Başarıyla yüklendi',
'extensionUpdateSuccess': 'Başarıyla güncellendi', 'extensionUpdateSuccess': 'Başarıyla güncellendi',
'extensionDelete': 'Uzantıyı sil', 'extensionDelete': 'Eklentiyi Sil',
'extensionAlreadyLatest': 'Bu zaten en son sürüm', 'extensionAlreadyLatest': 'Zaten en son sürüm',
'extensionFind': 'Uzantı ara', 'extensionFind': 'Eklentileri Bul',
'extensionDevelop': 'Uzantı geliştir', 'extensionDevelop': 'Eklenti Geliştir',
'history': 'Geçmiş', 'history': 'Geçmiş',
'clearHistory': 'Geçmişi temizle', 'clearHistory': 'Geçmişi Temizle',
'noHistoryFound': 'Geçmiş bulunamadı', 'noHistoryFound': 'Geçmiş Bulunamadı',
'serviceTitle': 'Hizmet indir', 'serviceTitle': 'İndirme Servisi',
'serviceText': 'Çalışıyor', 'serviceText': 'Çalışıyor',
'network': 'Ağ', 'network': 'Ağ',
'proxy': 'Vekil', 'proxy': 'Vekil Sunucu',
'noProxy': 'Vekil yok', 'noProxy': 'Vekil Sunucu Yok',
'systemProxy': 'Sistem vekili', 'systemProxy': 'Sistem Vekil Sunucusu',
'customProxy': 'Özel vekil', 'customProxy': 'Özel Vekil Sunucu',
'server': 'Sunucu', 'server': 'Sunucu',
'username': 'Kullanıcı adı', 'username': 'Kullanıcı Adı',
'password': 'Şifre', 'password': 'Şifre',
'thanks': 'Teşekkürler', 'thanks': 'Teşekkürler',
'thanksDesc': 'Gopeed topluluğunun oluşmasına ve gelişmesine yardımcı olan tüm katılımcılara teşekkürler!', 'thanksDesc':
'browserExtension': 'Tarayıcı uzantısı', 'Gopeed topluluğunun inşasında ve geliştirilmesinde katkıda bulunan herkese teşekkürler!',
'launchAtStartup': 'Başlangıçta başlat', 'browserExtension': 'Tarayıcı Eklentisi',
'seedConfig': 'Seed ayarları', 'launchAtStartup': 'Başlangıçta Çalıştır',
'seedKeep': 'Manuel durdurulana kadar seed etmeye devam et', 'seedConfig': 'Seed Ayarları',
'seedKeep': 'Manuel olarak durdurulana kadar seed etmeye devam et',
'seedRatio': 'Seed oranı', 'seedRatio': 'Seed oranı',
'seedTime': 'Seed süresi (dakika)', 'seedTime': 'Seed süresi (dakika)',
'taskDetail': 'Görev detayı', 'setAsDefaultBtClient': 'Varsayılan BT istemcisi olarak ayarla',
'taskName': 'Görev adı', 'taskDetail': 'Görev Detayı',
'taskUrl': 'Görev URL', 'taskName': 'Görev Adı',
'downloadPath': 'İndirme yolu', 'taskUrl': 'Görev URL’si',
'skipVerifyCert': 'Sertifika doğrulamasını atla', 'downloadPath': 'İndirme Yolu',
'skipVerifyCert': 'Sertifika Doğrulamayı Atla',
'name': 'Ad',
'size': 'Boyut',
'unknown': 'Bilinmeyen',
'fileSelectedCount': 'Dosya: ',
'fileSelectedSize': 'Boyut: ',
'httpHeaderName': 'Başlık Adı',
'httpHeaderValue': 'Başlık Değeri',
}, },
}; };
+123
View File
@@ -0,0 +1,123 @@
const ukUA = {
'uk_UA': {
'label': 'Мова',
'error': 'Помилка',
'tip': 'Підказка',
'confirm': 'Підтвердити',
'cancel': 'Скасувати',
'on': 'Увімк.',
'off': 'Вимк.',
'selectAll': 'Вибрати все',
'select': 'Вибрати',
'task': 'Завдання',
'downloading': 'завантажується',
'downloaded': 'завантажено',
'setting': 'Налаштування',
'donate': 'Підтримати',
'exit': 'Вихід',
'create': 'Створити завдання',
'directDownload': 'Пряме завантаження',
'advancedOptions': 'Додаткові параметри',
'followSettings': 'За замовчуванням',
'downloadLink': 'Посилання на завантаження',
'downloadLinkValid': 'Будь ласка, введіть посилання на завантаження',
'downloadLinkHit':
'Будь ласка, введіть посилання на завантаження, одне на рядок@Append',
'downloadLinkHitDesktop': ', або перетягніть сюди файл торенту',
'download': 'Завантажити',
'noFileSelected': 'Будь ласка, виберіть хоча б один файл для продовження.',
'noStoragePermission': 'Потрібен дозвіл на зберігання',
'selectFile': 'Вибрати файл',
'rename': 'Перейменувати',
'basic': 'Основні',
'advanced': 'Розширені',
'general': 'Загальні',
'downloadDir': 'Каталог завантажень',
'downloadDirValid': 'Будь ласка, виберіть каталог завантажень',
'connections': 'З’єднання',
'useServerCtime': 'Використовувати час сервера для створення файлу',
'maxRunning': 'Макс. завдань у черзі',
'defaultDirectDownload': 'Пряме завантаження за замовчуванням',
'items': '@count елементів',
'subscribeTracker': 'Підписатися на трекер',
'subscribeFail':
'Не вдалося підписатися, перевірте мережу або спробуйте пізніше',
'update': 'Оновити',
'updateDaily': 'Оновлювати щодня',
'lastUpdate': 'Останнє оновлення: @time',
'addTracker': 'Додати трекер',
'addTrackerHit': 'Будь ласка, введіть URL-адресу трекера, одну на рядок',
'ui': 'Інтерфейс',
'theme': 'Тема',
'themeSystem': 'Системна',
'themeLight': 'Світла',
'themeDark': 'Темна',
'locale': 'Мова',
'about': 'Про програму',
'homepage': 'Домашня сторінка',
'version': 'Версія',
'protocol': 'Протокол',
'port': 'Порт',
'apiToken': 'API Токен',
'notSet': 'Не задано',
'set': 'Задати',
'portInUse': 'Порт [@PORT] вже використовується, будь ласка, змініть порт',
'effectAfterRestart': 'Ефект після перезапуску',
'developer': 'Розробник',
'logDirectory': 'Каталог журналів',
'show': 'Показати',
'continue': 'Продовжити',
'pause': 'Пауза',
'startAll': 'Запустити все',
'pauseAll': 'Зупинити все',
'deleteTask': 'Видалити @count завдань',
'deleteTaskTip': 'Зберегти завантажені файли',
'delete': 'Видалити',
'newVersionTitle': 'Доступна нова версія @Version',
'newVersionUpdate': 'Оновити зараз',
'newVersionLater': 'Пізніше',
'extensions': 'Розширення',
'extensionInstallUrl': 'URL для встановлення',
'extensionInstallSuccess': 'Встановлено успішно',
'extensionUpdateSuccess': 'Оновлено успішно',
'extensionDelete': 'Видалити розширення',
'extensionAlreadyLatest': 'Вже остання версія',
'extensionFind': 'Знайти розширення',
'extensionDevelop': 'Розробка розширень',
'history': 'Історія',
'clearHistory': 'Очистити історію',
'noHistoryFound': 'Історію не знайдено',
'serviceTitle': 'Служба завантаження',
'serviceText': 'Працює',
'network': 'Мережа',
'proxy': 'Проксі',
'noProxy': 'Без проксі',
'systemProxy': 'Системний проксі',
'customProxy': 'Користувацький проксі',
'server': 'Сервер',
'username': 'Ім’я користувача',
'password': 'Пароль',
'thanks': 'Подяки',
'thanksDesc':
'Дякуємо всім учасникам, які допомогли створити та розвивати спільноту Gopeed!',
'browserExtension': 'Розширення для браузера',
'launchAtStartup': 'Запускати під час завантаження системи',
'seedConfig': 'Налаштування роздачі',
'seedKeep': 'Роздавати доки не зупиню вручну',
'seedRatio': 'Коефіцієнт роздачі',
'seedTime': 'Час роздачі (хвилини)',
'setAsDefaultBtClient': 'Встановити як клієнт BT за замовчуванням',
'taskDetail': 'Деталі завдання',
'taskName': 'Назва завдання',
'taskUrl': 'URL завдання',
'downloadPath': 'Шлях завантаження',
'skipVerifyCert': 'Пропустити перевірку сертифіката',
'name': 'Назва',
'size': 'Розмір',
'unknown': 'Невідомо',
'fileSelectedCount': 'Файлів: ',
'fileSelectedSize': 'Розмір: ',
'httpHeaderName': 'Назва заголовка HTTP',
'httpHeaderValue': 'Значення заголовка HTTP',
},
};
+3
View File
@@ -103,6 +103,7 @@ const zhCN = {
'seedKeep': '持续做种直到手动停止', 'seedKeep': '持续做种直到手动停止',
'seedRatio': '做种分享率', 'seedRatio': '做种分享率',
'seedTime': '做种时间(分钟)', 'seedTime': '做种时间(分钟)',
'setAsDefaultBtClient': '设为系统默认 BT 客户端',
'taskDetail': '任务详情', 'taskDetail': '任务详情',
'taskName': '任务名称', 'taskName': '任务名称',
'taskUrl': '任务链接', 'taskUrl': '任务链接',
@@ -113,5 +114,7 @@ const zhCN = {
'unknown': '未知', 'unknown': '未知',
'fileSelectedCount': '文件数:', 'fileSelectedCount': '文件数:',
'fileSelectedSize': '大小:', 'fileSelectedSize': '大小:',
'httpHeaderName': '请求头名称',
'httpHeaderValue': '请求头值',
} }
}; };
+3
View File
@@ -103,6 +103,7 @@ const zhTW = {
'seedKeep': '持續做種直到手動停止', 'seedKeep': '持續做種直到手動停止',
'seedRatio': '做種分享率', 'seedRatio': '做種分享率',
'seedTime': '做種時間(分鐘)', 'seedTime': '做種時間(分鐘)',
'setAsDefaultBtClient': '設為系統預設 BT 客戶端',
'taskDetail': '任務詳情', 'taskDetail': '任務詳情',
'taskName': '任務名稱', 'taskName': '任務名稱',
'taskUrl': '任務連結', 'taskUrl': '任務連結',
@@ -113,5 +114,7 @@ const zhTW = {
'unknown': '未知', 'unknown': '未知',
'fileSelectedCount': '文件數:', 'fileSelectedCount': '文件數:',
'fileSelectedSize': '大小:', 'fileSelectedSize': '大小:',
'httpHeaderName': '標頭名稱',
'httpHeaderValue': '標頭值',
} }
}; };
+4
View File
@@ -14,6 +14,8 @@ import 'langs/vi_vn.dart';
import 'langs/zh_cn.dart'; import 'langs/zh_cn.dart';
import 'langs/zh_tw.dart'; import 'langs/zh_tw.dart';
import 'langs/es_es.dart'; import 'langs/es_es.dart';
import 'langs/uk_ua.dart';
import 'langs/hu_hu.dart';
final messages = _Messages(); final messages = _Messages();
@@ -35,5 +37,7 @@ class _Messages extends Translations {
...idID, ...idID,
...frFR, ...frFR,
...esES, ...esES,
...ukUA,
...huHU,
}; };
} }
+40 -2
View File
@@ -1,6 +1,7 @@
import 'package:args/args.dart'; import 'package:args/args.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:window_manager/window_manager.dart'; import 'package:window_manager/window_manager.dart';
@@ -10,10 +11,12 @@ import 'app/modules/app/views/app_view.dart';
import 'core/libgopeed_boot.dart'; import 'core/libgopeed_boot.dart';
import 'database/database.dart'; import 'database/database.dart';
import 'i18n/message.dart'; import 'i18n/message.dart';
import 'util/browser_extension_host/browser_extension_host.dart';
import 'util/locale_manager.dart'; import 'util/locale_manager.dart';
import 'util/log_util.dart'; import 'util/log_util.dart';
import 'util/mac_secure_util.dart';
import 'util/package_info.dart'; import 'util/package_info.dart';
import 'util/scheme_register/scheme_register.dart';
import 'util/updater.dart';
import 'util/util.dart'; import 'util/util.dart';
class Args { class Args {
@@ -39,6 +42,9 @@ void main(List<String> arguments) async {
Future<void> init(Args args) async { Future<void> init(Args args) async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (Util.isMobile()) {
FlutterForegroundTask.initCommunicationPort();
}
await Util.initStorageDir(); await Util.initStorageDir();
await Database.instance.init(); await Database.instance.init();
if (Util.isDesktop()) { if (Util.isDesktop()) {
@@ -83,10 +89,42 @@ Future<void> init(Args args) async {
try { try {
await controller.loadDownloaderConfig(); await controller.loadDownloaderConfig();
MacSecureUtil.loadBookmark();
} catch (e) { } catch (e) {
logger.e("load config fail", e); logger.e("load config fail", e);
} }
() async {
if (Util.isDesktop()) {
try {
registerUrlScheme("gopeed");
if (controller.downloaderConfig.value.extra.defaultBtClient) {
registerDefaultTorrentClient();
}
} catch (e) {
logger.e("register scheme fail", e);
}
try {
await installHost();
} catch (e) {
logger.e("browser extension host binary install fail", e);
}
for (final browser in Browser.values) {
try {
await installManifest(browser);
} catch (e) {
logger.e(
"browser [${browser.name}] extension host integration fail", e);
}
}
try {
await installUpdater();
} catch (e) {
logger.e("updater install fail", e);
}
}
}();
} }
Future<void> onStart() async { Future<void> onStart() async {
+15
View File
@@ -0,0 +1,15 @@
import 'arch_stub.dart'
if (dart.library.io) 'entry/arch_native.dart'
if (dart.library.html) 'entry/arch_web.dart';
// Copy from pkg/sky_engine/lib/ffi/abi.dart
enum Architecture {
arm,
arm64,
ia32,
x64,
riscv32,
riscv64,
}
Architecture getArch() => doGetArch();
+3
View File
@@ -0,0 +1,3 @@
import 'arch.dart';
Architecture doGetArch() => throw UnimplementedError();
@@ -0,0 +1,13 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:ffi';
import '../arch.dart';
Architecture doGetArch() {
final currentAbi = Abi.current().toString();
final archName = currentAbi.split("_")[1];
final arch = Architecture.values.firstWhere(
(element) => element.name == archName,
orElse: () => Architecture.x64);
return arch;
}
@@ -0,0 +1,5 @@
import '../arch.dart';
Architecture doGetArch() {
return Architecture.x64;
}
@@ -0,0 +1,18 @@
import 'browser_extension_host_stub.dart'
if (dart.library.io) 'entry/browser_extension_host_native.dart';
enum Browser { chrome, edge, firefox }
/// Install host binary for browser extension
Future<void> installHost() => doInstallHost();
/// Check if specified browser is installed
Future<bool> checkBrowserInstalled(Browser browser) =>
doCheckBrowserInstalled(browser);
/// Check if browser extension manifest is properly installed
Future<bool> checkManifestInstalled(Browser browser) =>
doCheckManifestInstalled(browser);
/// Install browser extension manifest
Future<void> installManifest(Browser browser) => doInstallManifest(browser);
@@ -0,0 +1,8 @@
import 'browser_extension_host.dart';
Future<void> doInstallHost() => throw UnimplementedError();
Future<bool> doCheckBrowserInstalled(Browser browser) =>
throw UnimplementedError();
Future<bool> doCheckManifestInstalled(Browser browser) =>
throw UnimplementedError();
Future<void> doInstallManifest(Browser browser) => throw UnimplementedError();
@@ -0,0 +1,288 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
import 'package:win32_registry/win32_registry.dart';
import '../../util.dart';
import '../../win32.dart';
import '../browser_extension_host.dart';
final _hostExecName = 'host${Platform.isWindows ? '.exe' : ''}';
const _hostName = 'com.gopeed.gopeed';
const _chromeExtensionId = 'mijpgljlfcapndmchhjffkpckknofcnd';
const _edgeExtensionId = 'dkajnckekendchdleoaenoophcobooce';
const _firefoxExtensionId = '{c5d69a8f-2ed0-46a7-afa4-b3a00dc58088}';
const _debugExtensionIds = [
'gjddllnejledbfaeondocjpejpamclkk',
'goaohdfiokcjapgonhofgljfccoccief'
];
// Windows NativeMessagingHosts registry constants
const _chromeNativeHostsKey = r'Software\Google\Chrome\NativeMessagingHosts';
const _edgeNativeHostsKey = r'Software\Microsoft\Edge\NativeMessagingHosts';
const _firefoxNativeHostsKey = r'Software\Mozilla\NativeMessagingHosts';
/// Install host binary for browser extension
Future<void> doInstallHost() async {
final hostPath = Util.homePathJoin(_hostExecName);
await Util.installAsset('assets/exec/$_hostExecName', hostPath,
executable: true);
}
/// Check if specified browser is installed
Future<bool> doCheckBrowserInstalled(Browser browser) async {
if (Platform.isWindows) {
switch (browser) {
case Browser.chrome:
return await _checkWindowsRegistry(
r'SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe') ||
await _checkWindowsRegistry(
r'SOFTWARE\WOW6432Node\Google\Chrome') ||
await _checkWindowsExecutable(browser);
case Browser.edge:
return await _checkWindowsRegistry(
r'SOFTWARE\Microsoft\Edge\BLBeacon') ||
await _checkWindowsRegistry(
r'SOFTWARE\WOW6432Node\Microsoft\Edge\BLBeacon') ||
await _checkWindowsExecutable(browser);
case Browser.firefox:
return await _checkWindowsRegistry(r'SOFTWARE\Mozilla\Firefox') ||
await _checkWindowsRegistry(
r'SOFTWARE\WOW6432Node\Mozilla\Firefox') ||
await _checkWindowsExecutable(browser);
}
} else {
return await _checkUnixExecutable(browser);
}
}
/// Check if browser extension manifest is properly installed
Future<bool> doCheckManifestInstalled(Browser browser) async {
if (await checkBrowserInstalled(browser) == false) return false;
final manifestPath = _getManifestPath(browser);
if (manifestPath == null) return false;
if (Platform.isWindows) {
final regKey = _getWindowsRegistryKey(browser);
if (!checkRegistry('$regKey\\$_hostName', '', manifestPath)) {
return false;
}
}
if (!await File(manifestPath).exists()) {
return false;
}
final existingContent = await File(manifestPath).readAsString();
final expectedContent = await _getManifestContent(browser);
return existingContent == expectedContent;
}
/// Install browser extension manifest
Future<void> doInstallManifest(Browser browser) async {
if (await checkBrowserInstalled(browser) == false) return;
if (await checkManifestInstalled(browser)) return;
final manifestPath = _getManifestPath(browser)!;
final manifestContent = await _getManifestContent(browser);
final manifestDir = path.dirname(manifestPath);
await Directory(manifestDir).create(recursive: true);
await File(manifestPath).writeAsString(manifestContent);
if (Platform.isWindows) {
final regKey = _getWindowsRegistryKey(browser);
upsertRegistry('$regKey\\$_hostName', '', manifestPath);
}
}
Future<bool> _checkWindowsExecutable(Browser browser) async {
final paths = _getWindowsExecutablePaths(browser);
for (var execPath in paths) {
if (await File(execPath).exists()) {
return true;
}
}
return false;
}
List<String> _getWindowsExecutablePaths(Browser browser) {
final programFiles = Platform.environment['PROGRAMFILES'];
final programFilesX86 = Platform.environment['PROGRAMFILES(X86)'];
final localAppData = Platform.environment['LOCALAPPDATA'];
switch (browser) {
case Browser.chrome:
return [
if (programFiles != null)
path.join(
programFiles, 'Google', 'Chrome', 'Application', 'chrome.exe'),
if (programFilesX86 != null)
path.join(
programFilesX86, 'Google', 'Chrome', 'Application', 'chrome.exe'),
if (localAppData != null)
path.join(
localAppData, 'Google', 'Chrome', 'Application', 'chrome.exe'),
];
case Browser.edge:
return [
if (programFiles != null)
path.join(
programFiles, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
if (programFilesX86 != null)
path.join(programFilesX86, 'Microsoft', 'Edge', 'Application',
'msedge.exe'),
if (localAppData != null)
path.join(
localAppData, 'Microsoft', 'Edge', 'Application', 'msedge.exe'),
];
case Browser.firefox:
return [
if (programFiles != null)
path.join(programFiles, 'Mozilla Firefox', 'firefox.exe'),
if (programFilesX86 != null)
path.join(programFilesX86, 'Mozilla Firefox', 'firefox.exe'),
];
}
}
Future<bool> _checkUnixExecutable(Browser browser) async {
final paths = _getUnixExecutablePaths(browser);
for (var execPath in paths) {
execPath = execPath.replaceAll('~', Platform.environment['HOME'] ?? '');
if (await Directory(execPath).exists() || await File(execPath).exists()) {
return true;
}
}
return false;
}
List<String> _getUnixExecutablePaths(Browser browser) {
if (Platform.isMacOS) {
switch (browser) {
case Browser.chrome:
return [
'/Applications/Google Chrome.app',
'~/Applications/Google Chrome.app',
'/Users/${Platform.environment['USER']}/Applications/Google Chrome.app'
];
case Browser.edge:
return [
'/Applications/Microsoft Edge.app',
'~/Applications/Microsoft Edge.app',
'/Users/${Platform.environment['USER']}/Applications/Microsoft Edge.app'
];
case Browser.firefox:
return [
'/Applications/Firefox.app',
'~/Applications/Firefox.app',
'/Users/${Platform.environment['USER']}/Applications/Firefox.app'
];
}
} else {
switch (browser) {
case Browser.chrome:
return [
'/usr/bin/google-chrome',
'/usr/bin/google-chrome-stable',
'/usr/bin/chrome',
'/snap/bin/google-chrome',
'/opt/google/chrome/google-chrome'
];
case Browser.edge:
return [
'/usr/bin/microsoft-edge',
'/usr/bin/microsoft-edge-stable',
'/snap/bin/microsoft-edge',
'/opt/microsoft/msedge/msedge'
];
case Browser.firefox:
return [
'/usr/bin/firefox',
'/snap/bin/firefox',
'/usr/lib/firefox/firefox',
'/opt/firefox/firefox'
];
}
}
}
String? _getManifestPath(Browser browser) {
final manifestName =
browser == Browser.firefox ? '$_hostName.moz.json' : '$_hostName.json';
if (Platform.isWindows) {
return Util.homePathJoin(manifestName);
}
final home =
Platform.environment['HOME'] ?? Platform.environment['USERPROFILE'];
if (home == null) return null;
if (Platform.isMacOS) {
switch (browser) {
case Browser.chrome:
return path.join(home, 'Library', 'Application Support', 'Google',
'Chrome', 'NativeMessagingHosts', manifestName);
case Browser.edge:
return path.join(home, 'Library', 'Application Support',
'Microsoft Edge', 'NativeMessagingHosts', manifestName);
case Browser.firefox:
return path.join(home, 'Library', 'Application Support', 'Mozilla',
'NativeMessagingHosts', manifestName);
}
} else if (Platform.isLinux) {
switch (browser) {
case Browser.chrome:
return path.join(home, '.config', 'google-chrome',
'NativeMessagingHosts', manifestName);
case Browser.edge:
return path.join(home, '.config', 'microsoft-edge',
'NativeMessagingHosts', manifestName);
case Browser.firefox:
return path.join(
home, '.mozilla', 'native-messaging-hosts', manifestName);
}
}
return null;
}
Future<bool> _checkWindowsRegistry(String keyPath) async {
try {
final key = Registry.openPath(RegistryHive.localMachine, path: keyPath);
key.close();
return true;
} catch (e) {
return false;
}
}
Future<String> _getManifestContent(Browser browser) async {
final hostPath = Util.homePathJoin(_hostExecName);
final manifest = {
'name': _hostName,
'description': 'Gopeed browser extension host',
'path': hostPath,
'type': 'stdio',
if (browser != Browser.firefox)
'allowed_origins': [
'chrome-extension://$_chromeExtensionId/',
'chrome-extension://$_edgeExtensionId/',
..._debugExtensionIds.map((id) => 'chrome-extension://$id/'),
],
if (browser == Browser.firefox) 'allowed_extensions': [_firefoxExtensionId],
};
return const JsonEncoder.withIndent(' ').convert(manifest);
}
String _getWindowsRegistryKey(Browser browser) {
switch (browser) {
case Browser.chrome:
return _chromeNativeHostsKey;
case Browser.edge:
return _edgeNativeHostsKey;
case Browser.firefox:
return _firefoxNativeHostsKey;
}
}
+79
View File
@@ -0,0 +1,79 @@
import 'package:dio/dio.dart';
// List of mirrors for GitHub
const _sourceMirror = [
// ghfast.top: https://ghfast.top/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://ghfast.top/", r"(.*)"],
// github.moeyy.xyz: https://github.moeyy.xyz/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
["https://github.moeyy.xyz/", r"(.*)"],
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
[
"https://fastly.jsdelivr.net/gh",
r".*raw.githubusercontent.com(/.*)(/.*)/(?:master|main)(/.*)"
],
];
// List of mirrors for GitHub release
const _assertMirror = [
// ghfast.top: https://ghfast.top/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://ghfast.top/", r"(.*)"],
// github.moeyy.xyz: https://github.moeyy.xyz/https://github.com/GopeedLab/gopeed/releases/download/v1.6.10/Gopeed-v1.6.10-windows-amd64.zip
["https://github.moeyy.xyz/", r"(.*)"],
];
enum MirrorType {
githubSource,
githubRelease,
}
/// Auto detect the best mirror for the given [rawUrl] and [type]
Future<String> githubAutoMirror(String rawUrl, MirrorType type) async {
final mirrorUrls = githubMirrorUrls(rawUrl, type);
// Ping all mirrors and get the fastest one
final pingResult = await Future.wait(mirrorUrls.map((e) async {
final client = Dio()
..options.sendTimeout = const Duration(seconds: 3)
..options.connectTimeout = const Duration(seconds: 3);
var time = DateTime.now().millisecondsSinceEpoch;
try {
final response = await client.head(e);
if (response.statusCode == 200) {
time = DateTime.now().millisecondsSinceEpoch - time;
return (e, time);
}
} catch (e) {
// ignore
} finally {
client.close();
}
return (e, -1);
}));
var list = pingResult.where((e) => e.$2 != -1).toList()
..sort((a, b) => a.$2.compareTo(b.$2));
if (list.isNotEmpty) {
return list.first.$1;
}
return rawUrl;
}
List<String> githubMirrorUrls(String rawUrl, MirrorType type) {
final mirrors = switch (type) {
MirrorType.githubSource => _sourceMirror,
MirrorType.githubRelease => _assertMirror,
};
final ret = <String>[];
for (final cdn in mirrors) {
final reg = RegExp(cdn[1]);
final match = reg.firstMatch(rawUrl.toString());
var matchStr = "";
for (var i = 1; i <= match!.groupCount; i++) {
matchStr += match.group(i)!;
}
ret.add("${cdn[0]}$matchStr");
}
return ret;
}
-30
View File
@@ -1,30 +0,0 @@
import 'dart:io';
import 'package:macos_secure_bookmarks/macos_secure_bookmarks.dart';
import '../database/database.dart';
import 'util.dart';
class MacSecureUtil {
static Future<void> saveBookmark(String dir) async {
if (!Util.isMacos()) {
return;
}
final secureBookmarks = SecureBookmarks();
final bookmark = await secureBookmarks.bookmark(Directory(dir));
Database.instance.saveBookmark(MapEntry(dir, bookmark));
}
static Future<void> loadBookmark() async {
if (!Util.isMacos()) {
return;
}
final secureBookmarks = SecureBookmarks();
final bookmark = Database.instance.getBookmark() ?? {};
bookmark.forEach((_, v) async {
final resolvedFile =
await secureBookmarks.resolveBookmark(v, isDirectory: true);
await secureBookmarks.startAccessingSecurityScopedResource(resolvedFile);
});
}
}
@@ -0,0 +1,77 @@
import 'dart:io';
import 'package:win32_registry/win32_registry.dart';
import '../../util.dart';
import '../../win32.dart';
doRegisterUrlScheme(String scheme) {
if (Util.isWindows()) {
final schemeKey = 'Software\\Classes\\$scheme';
final appPath = Platform.resolvedExecutable;
upsertRegistry(
schemeKey,
'URL Protocol',
'',
);
upsertRegistry(
'$schemeKey\\shell\\open\\command',
'',
'"$appPath" "%1"',
);
}
}
doUnregisterUrlScheme(String scheme) {
if (Util.isWindows()) {
Registry.currentUser
.deleteKey('Software\\Classes\\$scheme', recursive: true);
}
}
const _torrentRegKey = 'Software\\Classes\\.torrent';
const _torrentRegValue = 'Gopeed_torrent';
const _torrentAppRegKey = 'Software\\Classes\\$_torrentRegValue';
/// Register as the system's default torrent client
/// 1. Register the scheme "magnet"
/// 2. Register the file type ".torrent"
doRegisterDefaultTorrentClient() {
if (Util.isWindows()) {
doRegisterUrlScheme("magnet");
final appPath = Platform.resolvedExecutable;
final iconPath =
'${File(appPath).parent.path}\\data\\flutter_assets\\assets\\tray_icon\\icon.ico';
upsertRegistry(
_torrentRegKey,
'',
_torrentRegValue,
);
upsertRegistry(
_torrentAppRegKey,
'',
'Torrent file',
);
upsertRegistry(
'$_torrentAppRegKey\\DefaultIcon',
'',
iconPath,
);
upsertRegistry(
'$_torrentAppRegKey\\shell\\open\\command',
'',
'"$appPath" "file:///%1"',
);
}
}
doUnregisterDefaultTorrentClient() {
if (Util.isWindows()) {
doUnregisterUrlScheme("magnet");
Registry.currentUser.deleteKey(_torrentRegKey, recursive: true);
Registry.currentUser.deleteKey(_torrentAppRegKey, recursive: true);
}
}
@@ -0,0 +1,11 @@
import 'scheme_register_stub.dart'
if (dart.library.io) 'entry/scheme_register_native.dart';
registerUrlScheme(String scheme) => doRegisterUrlScheme(scheme);
unregisterUrlScheme(String scheme) => doUnregisterUrlScheme(scheme);
registerDefaultTorrentClient() => doRegisterDefaultTorrentClient();
unregisterDefaultTorrentClient() => doUnregisterDefaultTorrentClient();
@@ -0,0 +1,7 @@
doRegisterUrlScheme(String scheme) => throw UnimplementedError();
doUnregisterUrlScheme(String scheme) => throw UnimplementedError();
doRegisterDefaultTorrentClient() => throw UnimplementedError();
doUnregisterDefaultTorrentClient() => throw UnimplementedError();
+432
View File
@@ -0,0 +1,432 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:install_plugin/install_plugin.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:url_launcher/url_launcher.dart';
import '../api/api.dart';
import '../app/views/outlined_button_loading.dart';
import 'arch/arch.dart';
import 'github_mirror.dart';
import 'log_util.dart';
import 'message.dart';
import 'package_info.dart';
import 'util.dart';
enum Channel {
windowsInstaller,
windowsPortable,
macosDmg,
linuxFlathub,
linuxSnap,
linuxDeb,
linuxAppImage,
androidApk,
iosIpa,
docker,
}
const _channelEnv = String.fromEnvironment("UPDATE_CHANNEL");
final _channel =
Channel.values.where((e) => e.name == _channelEnv).firstOrNull ??
() {
if (Util.isWindows()) {
return Channel.windowsPortable;
} else if (Util.isMacos()) {
return Channel.macosDmg;
} else if (Util.isAndroid()) {
return Channel.androidApk;
} else if (Util.isIOS()) {
return Channel.iosIpa;
} else {
return null;
}
}();
final _updaterBin = "updater${Util.isWindows() ? ".exe" : ""}";
Future<void> installUpdater() async {
await Util.installAsset(
'assets/exec/$_updaterBin', Util.homePathJoin(_updaterBin),
executable: true);
}
class VersionInfo {
final String version;
final String changeLog;
VersionInfo(this.version, this.changeLog);
}
Future<VersionInfo?> checkUpdate() async {
String? releaseDataStr;
try {
releaseDataStr = (await proxyRequest(
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
.data;
} catch (e) {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
}
if (releaseDataStr == null) {
return null;
}
final releaseData = jsonDecode(releaseDataStr);
final tagName = releaseData["tag_name"];
if (tagName == null) {
return null;
}
final latestVersion = releaseData["tag_name"].substring(1);
// compare version x.y.z to x.y.z
final currentVersion = packageInfo.version;
var isNewVersion = false;
if (latestVersion != currentVersion) {
final currentVersionList = currentVersion.split(".");
final latestVersionList = latestVersion.split(".");
for (var i = 0; i < currentVersionList.length; i++) {
if (int.parse(latestVersionList[i]) > int.parse(currentVersionList[i])) {
isNewVersion = true;
break;
}
}
}
if (!isNewVersion) {
return null;
}
return VersionInfo(latestVersion, releaseData["body"]);
}
Future<void> showUpdateDialog(
BuildContext context, VersionInfo versionInfo) async {
final fullChangeLog = versionInfo.changeLog;
final isZh = Get.locale?.languageCode == "zh";
final changeLogRegex = isZh
? RegExp(r"(#\s+更新日志.*)", multiLine: true, dotAll: true)
: RegExp(r"(# Release notes.*)#\s+更新日志", multiLine: true, dotAll: true);
final changeLog = changeLogRegex.firstMatch(fullChangeLog)?.group(1) ?? "";
await showDialog(
context: Get.context!,
barrierDismissible: false,
builder: (context) {
bool isDownloading = false;
double progress = 0;
int total = 0;
final buttonController = OutlinedButtonLoadingController();
return StatefulBuilder(
builder: (context, setState) {
final screenSize = MediaQuery.of(context).size;
final dialogWidth =
screenSize.width < 500 ? screenSize.width * 0.9 : 500.0;
final dialogHeight =
screenSize.height < 600 ? screenSize.height * 0.8 : 400.0;
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
child: SizedBox(
width: dialogWidth,
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'newVersionTitle'
.trParams({'version': versionInfo.version}),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
Container(
height: dialogHeight * 0.5,
decoration: BoxDecoration(
border:
Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8),
),
child: ScrollConfiguration(
behavior: ScrollConfiguration.of(context).copyWith(
scrollbars: true,
),
child: Builder(
builder: (context) {
final controller = ScrollController();
return Scrollbar(
controller: controller,
thumbVisibility: true,
child: SingleChildScrollView(
controller: controller,
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children:
_parseMarkdown(changeLog, context),
),
),
),
);
},
),
),
),
if (isDownloading) ...[
const SizedBox(height: 16),
LinearProgressIndicator(
value: progress,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).colorScheme.primary,
),
),
const SizedBox(height: 4),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'${(progress * 100).toStringAsFixed(1)}%',
style: const TextStyle(fontSize: 12),
),
Text(
total == 0
? ''
: '${Util.fmtByte((total * progress).toInt())} / ${Util.fmtByte(total)}',
style: const TextStyle(fontSize: 12),
),
],
)
],
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: isDownloading ? null : () => Get.back(),
child: Text(
'newVersionLater'.tr,
style: TextStyle(
color: isDownloading
? Theme.of(context).disabledColor
: Theme.of(context).colorScheme.error),
),
),
const SizedBox(width: 8),
OutlinedButtonLoading(
controller: buttonController,
onPressed: () async {
setState(() {
isDownloading = true;
});
buttonController.start();
try {
await _update(versionInfo.version,
(received, fileTotal) {
setState(() {
total = fileTotal;
progress = received / fileTotal;
});
});
} catch (e) {
showErrorMessage(e);
} finally {
setState(() {
isDownloading = false;
});
buttonController.stop();
}
},
child: Text('newVersionUpdate'.tr),
),
],
),
],
),
),
),
);
},
);
},
);
}
List<Widget> _parseMarkdown(String markdown, BuildContext context) {
final List<Widget> widgets = [];
final lines = markdown.split('\n');
for (final line in lines) {
if (line.trim().isEmpty) continue;
if (line.startsWith('# ')) {
// H1 header
widgets.add(Text(
line.substring(2),
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
));
} else if (line.startsWith('## ')) {
// H2 header
widgets.add(Text(
line.substring(3),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
));
} else if (line.trim().startsWith('- ')) {
// List item
widgets.add(Padding(
padding: const EdgeInsets.only(left: 8),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('• ', style: TextStyle(fontSize: 14)),
Expanded(
child: Text(
line.substring(line.indexOf('-') + 1).trim().replaceFirst(
RegExp(r'@[^\s]*\s\(#\d+\)'),
''), // Remove contributor and pr number
style: const TextStyle(fontSize: 14),
),
),
],
),
));
} else {
// Normal text
widgets.add(Text(
line,
style: const TextStyle(fontSize: 14),
));
}
// Add spacing between elements
widgets.add(const SizedBox(height: 8));
}
return widgets;
}
Future<void> _update(String version, Function(int, int) onProgress) async {
var newVersionAssetPath = "";
final newVersionAssetName = _getAssetName(version);
// Need to download the asset
if (newVersionAssetName.isNotEmpty) {
final downloadUrl =
'https://github.com/GopeedLab/gopeed/releases/download/v$version/$newVersionAssetName';
newVersionAssetPath = await _getAssetPath(version);
if (downloadUrl.isNotEmpty) {
final fastDownloadUrl =
await githubAutoMirror(downloadUrl, MirrorType.githubRelease);
final downloadClient = Dio();
await downloadClient.download(fastDownloadUrl, newVersionAssetPath,
onReceiveProgress: onProgress);
}
}
switch (_channel) {
case Channel.windowsInstaller:
case Channel.windowsPortable:
case Channel.macosDmg:
case Channel.linuxFlathub:
case Channel.linuxSnap:
case Channel.linuxDeb:
final updaterPath = Util.homePathJoin(_updaterBin);
// Check the updater binary is exists
if (!await File(updaterPath).exists()) {
await launchUrl(
Uri.parse(
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
mode: LaunchMode.externalApplication);
break;
}
/**
*Usage of updater command:
-pid int
PID of the process to update
-channel string
Update channel
-asset string
Path to the package asset
-log string
Log file path
*/
await Process.run(updaterPath, [
"-pid",
pid.toString(),
"-channel",
_channel!.name,
"-asset",
newVersionAssetPath,
"-log",
path.join(logsDir(), "updater.log")
]);
break;
case Channel.androidApk:
await InstallPlugin.installApk(newVersionAssetPath);
break;
default:
await launchUrl(
Uri.parse(
'https://github.com/GopeedLab/gopeed/releases/tag/v$version'),
mode: LaunchMode.externalApplication);
break;
}
}
String _getAssetName(String version) {
final arch = getArch();
String commonArchName() {
return switch (arch) {
Architecture.ia32 => "386",
Architecture.x64 => "amd64",
_ => arch.name
};
}
switch (_channel) {
case Channel.windowsInstaller:
return 'Gopeed-v$version-windows-${commonArchName()}.zip';
case Channel.windowsPortable:
return 'Gopeed-v$version-windows-${commonArchName()}-portable.zip';
case Channel.macosDmg:
return 'Gopeed-v$version-macos-${commonArchName()}.dmg';
case Channel.linuxDeb:
return 'Gopeed-v$version-linux-${commonArchName()}.deb';
case Channel.androidApk:
final apkArchName = switch (arch) {
Architecture.arm => "armeabi-v7a",
Architecture.arm64 => "arm64-v8a",
Architecture.x64 => "x86_64",
_ => null
};
var apkNamePrefix = "Gopeed-v$version-android";
if (apkArchName != null) {
apkNamePrefix += "-$apkArchName";
}
return "$apkNamePrefix.apk";
default:
return "";
}
}
Future<String> _getAssetPath(String version) async {
return path.join(
(await getTemporaryDirectory()).path, _getAssetName(version));
}
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more