mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 10:54:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b0178fed9 | ||
|
|
83febe0a00 | ||
|
|
987580d400 | ||
|
|
0919936050 | ||
|
|
acc7ed8f6b | ||
|
|
ccb3267fba | ||
|
|
1249493a8a | ||
|
|
107141bbe7 | ||
|
|
b329fee107 | ||
|
|
3ce95f21fd | ||
|
|
f54ed5df5a | ||
|
|
727b7edb84 | ||
|
|
f5891ee03d | ||
|
|
724276ac69 | ||
|
|
a3f559c8e5 | ||
|
|
edd189a8a8 | ||
|
|
c970378fd7 | ||
|
|
8b61343aef | ||
|
|
2cb93a03e2 | ||
|
|
81dfc073a6 | ||
|
|
427921ccab | ||
|
|
f4876c8a0e | ||
|
|
35a3625b11 | ||
|
|
dc9b501e35 | ||
|
|
85fe92a378 | ||
|
|
983d9ad09e | ||
|
|
d9f757aaf0 |
No files matched your search
@@ -14,7 +14,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.24.3"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -97,11 +97,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 +113,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]
|
||||||
@@ -120,7 +122,7 @@ jobs:
|
|||||||
[Icons]
|
[Icons]
|
||||||
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
|
||||||
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
|
||||||
|
|
||||||
[Run]
|
[Run]
|
||||||
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
|
||||||
"@ > setup.iss
|
"@ > setup.iss
|
||||||
@@ -441,8 +443,9 @@ 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
|
||||||
@@ -468,6 +471,39 @@ jobs:
|
|||||||
|| true
|
|| true
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
|
sudo apt update -y
|
||||||
|
sudo apt install -y pv python2 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}
|
||||||
|
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:
|
||||||
@@ -499,7 +535,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
|
||||||
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
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.23"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.24.3"
|
FLUTTER_VERSION: "3.24.5"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
@@ -140,7 +140,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,7 +160,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
|
||||||
|
|||||||
+2
-3
@@ -8,14 +8,13 @@ RUN CGO_ENABLED=0 go build -tags nosqlite,web \
|
|||||||
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
-ldflags="-s -w -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION -X github.com/GopeedLab/gopeed/pkg/base.InDocker=true" \
|
||||||
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
-o dist/gopeed github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
|
||||||
FROM alpine:3.14.2
|
FROM alpine:3.18
|
||||||
LABEL maintainer="monkeyWie"
|
LABEL maintainer="monkeyWie"
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go /app/dist/gopeed ./
|
COPY --from=go /app/dist/gopeed ./
|
||||||
COPY entrypoint.sh ./entrypoint.sh
|
COPY entrypoint.sh ./entrypoint.sh
|
||||||
RUN apk update && \
|
RUN apk update && \
|
||||||
apk upgrade --no-cache && \
|
apk add --no-cache su-exec ; \
|
||||||
apk add --no-cache bash su-exec; \
|
|
||||||
chmod +x ./entrypoint.sh && \
|
chmod +x ./entrypoint.sh && \
|
||||||
rm -rf /var/cache/apk/*
|
rm -rf /var/cache/apk/*
|
||||||
VOLUME ["/app/storage"]
|
VOLUME ["/app/storage"]
|
||||||
|
|||||||
@@ -71,6 +71,11 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://
|
|||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Qnap</td>
|
||||||
|
<td><code>QPKG</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Link</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Web</td>
|
<td>Web</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -174,7 +179,7 @@ command:
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -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" -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
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-1
@@ -71,6 +71,11 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Link</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Qnap</td>
|
||||||
|
<td><code>QPKG</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Link</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Web</td>
|
<td>Web</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -162,7 +167,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -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" -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
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-1
@@ -71,6 +71,11 @@ Truy cập ✈ [Trang web chính thức](https://gopeed.com) | 📖 [Tài liệu
|
|||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">Liên kết</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Qnap</td>
|
||||||
|
<td><code>QPKG</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">Liên kết</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Web</td>
|
<td>Web</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -170,7 +175,7 @@ command:
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -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" -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
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-1
@@ -71,6 +71,11 @@ Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不
|
|||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Qnap</td>
|
||||||
|
<td><code>QPKG</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">前往</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Web</td>
|
<td>Web</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -174,7 +179,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -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" -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
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+6
-1
@@ -71,6 +71,11 @@ Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速
|
|||||||
<td><code>IPA</code></td>
|
<td><code>IPA</code></td>
|
||||||
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
<td><a href="https://gopeed.com/api/download?tpl=Gopeed-$version-ios.ipa">前往</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Qnap</td>
|
||||||
|
<td><code>QPKG</code></td>
|
||||||
|
<td><a href="https://gopeed.com/api/download?tpl=gopeed-qnap-$version-amd64.qpkg">前往</a></td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<td>Web</td>
|
<td>Web</td>
|
||||||
<td></td>
|
<td></td>
|
||||||
@@ -170,7 +175,7 @@ gomobile init
|
|||||||
- android
|
- android
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s" -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" -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
|
||||||
flutter build apk
|
flutter build apk
|
||||||
```
|
```
|
||||||
|
|||||||
+14
-11
@@ -1,12 +1,15 @@
|
|||||||
version: '3'
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
gopeed:
|
gopeed:
|
||||||
container_name: gopeed
|
container_name: gopeed
|
||||||
ports:
|
ports:
|
||||||
- "9999:9999" # HTTP port (host:container)
|
- 9999:9999 # HTTP port (host:container)
|
||||||
image: liwei2633/gopeed
|
environment:
|
||||||
volumes:
|
- PUID=0
|
||||||
- ~/gopeed/Downloads:/app/Downloads # mount download path
|
- PGID=0
|
||||||
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
- UMASK=022
|
||||||
restart: unless-stopped
|
volumes:
|
||||||
|
- ~/gopeed/Downloads:/app/Downloads # mount download path
|
||||||
|
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
|
||||||
|
restart: unless-stopped
|
||||||
|
image: liwei2633/gopeed
|
||||||
|
# command: -u Username -p Password # optional authentication
|
||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"io"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -359,17 +361,34 @@ func (f *Fetcher) addTorrent(req *base.Request, fromUpload bool) (err error) {
|
|||||||
schema := util.ParseSchema(req.URL)
|
schema := util.ParseSchema(req.URL)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(req.URL)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else if schema == "DATA" {
|
} else {
|
||||||
_, data := util.ParseDataUri(req.URL)
|
var reader io.Reader
|
||||||
buf := bytes.NewBuffer(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)
|
||||||
|
reader = bytes.NewBuffer(data)
|
||||||
|
} else {
|
||||||
|
reader, err = os.Open(req.URL)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer reader.(io.Closer).Close()
|
||||||
|
}
|
||||||
|
|
||||||
var metaInfo *metainfo.MetaInfo
|
var metaInfo *metainfo.MetaInfo
|
||||||
metaInfo, err = metainfo.Load(buf)
|
metaInfo, err = metainfo.Load(reader)
|
||||||
if err != nil {
|
// Hotfix for https://github.com/anacrolix/torrent/issues/992, ignore "expected EOF" error
|
||||||
|
// TODO remove this after the issue is fixed
|
||||||
|
if err != nil && !strings.Contains(err.Error(), "expected EOF") {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
f.torrent, err = client.AddTorrent(metaInfo)
|
f.torrent, err = client.AddTorrent(metaInfo)
|
||||||
} else {
|
|
||||||
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
gohttp "net/http"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -96,7 +97,7 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
t.Errorf("Resolve Single File Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -136,10 +137,29 @@ func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
|||||||
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
Hash: "ccbc92b0cd8deec16a2ef4be242a8c9243b1cedb",
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
|
||||||
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
t.Errorf("Resolve Multi Files Resolve() got = %v, want %v", fetcher.Meta().Res, want)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("Resolve Unclean Torrent", func(t *testing.T) {
|
||||||
|
err := fetcher.Resolve(&base.Request{
|
||||||
|
URL: "./testdata/test.unclean.torrent",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Resolve Unclean Torrent Resolve() got = %v, want nil", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
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) {
|
||||||
|
|||||||
Binary file not shown.
@@ -8,6 +8,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -25,6 +26,9 @@ import (
|
|||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const connectTimeout = 15 * time.Second
|
||||||
|
const readTimeout = 15 * time.Second
|
||||||
|
|
||||||
type RequestError struct {
|
type RequestError struct {
|
||||||
Code int
|
Code int
|
||||||
Msg string
|
Msg string
|
||||||
@@ -103,9 +107,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
|
||||||
// 解析资源大小: bytes 0-1000/1001 => 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
|
||||||
@@ -146,7 +150,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
file.Name = filename
|
file.Name = filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 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)
|
||||||
}
|
}
|
||||||
@@ -162,9 +166,6 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
func (f *Fetcher) Create(opts *base.Options) error {
|
func (f *Fetcher) Create(opts *base.Options) error {
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
|
|
||||||
if err := base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
if err := base.ParseOptsExtra[fhttp.OptsExtra](f.meta.Opts); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -198,6 +199,12 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Avoid request extra modified by extension
|
||||||
|
if err = base.ParseReqExtra[fhttp.ReqExtra](f.meta.Req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if f.chunks == nil {
|
if f.chunks == nil {
|
||||||
f.chunks = f.splitChunk()
|
f.chunks = f.splitChunk()
|
||||||
}
|
}
|
||||||
@@ -247,23 +254,42 @@ 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))
|
||||||
for i := 0; i < len(f.chunks); i++ {
|
for i := 0; i < len(f.chunks); i++ {
|
||||||
i := i
|
i := i
|
||||||
f.eg.Go(func() error {
|
f.eg.Go(func() error {
|
||||||
return f.fetchChunk(i, ctx)
|
err := f.fetchChunk(i, ctx)
|
||||||
|
// if canceled, fail fast
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
chunkErrs[i] = err
|
||||||
|
return nil
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
err := f.eg.Wait()
|
err := f.eg.Wait()
|
||||||
// check if canceled
|
// error returned only if canceled, just return
|
||||||
if errors.Is(err, context.Canceled) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
// check all fetch results, if any error, return
|
||||||
|
for _, chunkErr := range chunkErrs {
|
||||||
|
if chunkErr != nil {
|
||||||
|
err = chunkErr
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
f.file.Close()
|
f.file.Close()
|
||||||
// Update file last modified time
|
// Update file last modified time
|
||||||
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
if f.config.UseServerCtime && f.meta.Res.Files[0].Ctime != nil {
|
||||||
@@ -286,24 +312,11 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
for {
|
for {
|
||||||
// if chunk is completed, return
|
// if chunk is completed, return
|
||||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||||
return
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if chunk.retryTimes >= maxRetries {
|
if chunk.retryTimes >= maxRetries {
|
||||||
if !f.meta.Res.Range {
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
// check if all failed
|
|
||||||
allFailed := true
|
|
||||||
for _, c := range f.chunks {
|
|
||||||
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
|
||||||
allFailed = false
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if allFailed {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -330,10 +343,9 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
err = NewRequestError(resp.StatusCode, resp.Status)
|
err = NewRequestError(resp.StatusCode, resp.Status)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Http request success, reset retry times
|
reader := NewTimeoutReader(resp.Body, readTimeout)
|
||||||
chunk.retryTimes = 0
|
|
||||||
for {
|
for {
|
||||||
n, err := resp.Body.Read(buf)
|
n, err := reader.Read(buf)
|
||||||
if n > 0 {
|
if n > 0 {
|
||||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -348,7 +360,6 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// If canceled, do not retry
|
// If canceled, do not retry
|
||||||
@@ -416,6 +427,10 @@ func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq
|
|||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,6 +464,9 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
|
|||||||
|
|
||||||
func (f *Fetcher) buildClient() *http.Client {
|
func (f *Fetcher) buildClient() *http.Client {
|
||||||
transport := &http.Transport{
|
transport := &http.Transport{
|
||||||
|
DialContext: (&net.Dialer{
|
||||||
|
Timeout: connectTimeout,
|
||||||
|
}).DialContext,
|
||||||
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
Proxy: f.ctl.GetProxy(f.meta.Req.Proxy),
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
gohttp "net/http"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -59,6 +60,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()
|
||||||
@@ -132,6 +148,14 @@ func TestFetcher_DownloadLimit(t *testing.T) {
|
|||||||
downloadNormal(listener, 8, t)
|
downloadNormal(listener, 8, t)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFetcher_DownloadResponseBodyReadTimeout(t *testing.T) {
|
||||||
|
listener := test.StartTestLimitServer(16, readTimeout.Milliseconds()+5000)
|
||||||
|
defer listener.Close()
|
||||||
|
|
||||||
|
downloadError(listener, 1, t)
|
||||||
|
downloadError(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()
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TimeoutReader struct {
|
||||||
|
reader io.Reader
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewTimeoutReader(r io.Reader, timeout time.Duration) *TimeoutReader {
|
||||||
|
return &TimeoutReader{
|
||||||
|
reader: r,
|
||||||
|
timeout: timeout,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tr *TimeoutReader) Read(p []byte) (n int, err error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), tr.timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
done := make(chan struct{})
|
||||||
|
var readErr error
|
||||||
|
var bytesRead int
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
bytesRead, readErr = tr.reader.Read(p)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
return bytesRead, readErr
|
||||||
|
case <-ctx.Done():
|
||||||
|
return 0, ctx.Err()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package http
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTimeoutReader_Read(t *testing.T) {
|
||||||
|
data := []byte("Hello, World!")
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||||
|
|
||||||
|
buf := make([]byte, len(data))
|
||||||
|
n, err := timeoutReader.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected no error, got %v", err)
|
||||||
|
}
|
||||||
|
if n != len(data) {
|
||||||
|
t.Fatalf("expected to read %d bytes, read %d", len(data), n)
|
||||||
|
}
|
||||||
|
if !bytes.Equal(buf, data) {
|
||||||
|
t.Fatalf("expected %s, got %s", data, buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTimeoutReader_ReadTimeout(t *testing.T) {
|
||||||
|
reader := &slowReader{delay: 2 * time.Second}
|
||||||
|
timeoutReader := NewTimeoutReader(reader, 1*time.Second)
|
||||||
|
|
||||||
|
buf := make([]byte, 8192)
|
||||||
|
_, err := timeoutReader.Read(buf)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected timeout error, got nil")
|
||||||
|
}
|
||||||
|
if !errors.Is(err, context.DeadlineExceeded) {
|
||||||
|
t.Fatalf("expected %v, got %v", context.DeadlineExceeded, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type slowReader struct {
|
||||||
|
delay time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sr *slowReader) Read(p []byte) (n int, err error) {
|
||||||
|
time.Sleep(sr.delay)
|
||||||
|
return 0, io.EOF
|
||||||
|
}
|
||||||
+30
-19
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/armon/go-socks5"
|
"github.com/armon/go-socks5"
|
||||||
@@ -36,7 +37,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func StartTestFileServer() net.Listener {
|
func StartTestFileServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
return http.FileServer(http.Dir(Dir))
|
return http.FileServer(http.Dir(Dir))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -52,7 +53,7 @@ func (s *SlowFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
return &SlowFileServer{
|
return &SlowFileServer{
|
||||||
delay: delay,
|
delay: delay,
|
||||||
handler: http.FileServer(http.Dir(Dir)),
|
handler: http.FileServer(http.Dir(Dir)),
|
||||||
@@ -61,7 +62,7 @@ func StartTestSlowFileServer(delay time.Duration) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestCustomServer() net.Listener {
|
func StartTestCustomServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
@@ -88,7 +89,7 @@ func StartTestCustomServer() net.Listener {
|
|||||||
|
|
||||||
func StartTestRetryServer() net.Listener {
|
func StartTestRetryServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
@@ -108,7 +109,7 @@ func StartTestRetryServer() net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func StartTestPostServer() net.Listener {
|
func StartTestPostServer() net.Listener {
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
if request.Method == "POST" && request.Header.Get("Authorization") != "" {
|
||||||
@@ -132,7 +133,7 @@ func StartTestPostServer() net.Listener {
|
|||||||
|
|
||||||
func StartTestErrorServer() net.Listener {
|
func StartTestErrorServer() net.Listener {
|
||||||
counter := 0
|
counter := 0
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
counter++
|
counter++
|
||||||
@@ -149,7 +150,7 @@ func StartTestErrorServer() net.Listener {
|
|||||||
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
||||||
var connections atomic.Int32
|
var connections atomic.Int32
|
||||||
|
|
||||||
return startTestServer(func() http.Handler {
|
return startTestServer(func(sl *shutdownListener) http.Handler {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -165,12 +166,14 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
if r == "" {
|
if r == "" {
|
||||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||||
writer.WriteHeader(200)
|
writer.WriteHeader(200)
|
||||||
|
(writer.(http.Flusher)).Flush()
|
||||||
|
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
slowCopy(writer, file, delay)
|
slowCopy(sl, writer, file, delay)
|
||||||
} else {
|
} else {
|
||||||
// split range
|
// split range
|
||||||
s := strings.Split(r, "=")
|
s := strings.Split(r, "=")
|
||||||
@@ -204,6 +207,8 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||||
writer.Header().Set("Accept-Ranges", "bytes")
|
writer.Header().Set("Accept-Ranges", "bytes")
|
||||||
writer.WriteHeader(206)
|
writer.WriteHeader(206)
|
||||||
|
(writer.(http.Flusher)).Flush()
|
||||||
|
|
||||||
file, err := os.Open(BuildFile)
|
file, err := os.Open(BuildFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
writer.WriteHeader(500)
|
writer.WriteHeader(500)
|
||||||
@@ -211,7 +216,7 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
file.Seek(start, 0)
|
file.Seek(start, 0)
|
||||||
slowCopyN(writer, file, end-start+1, delay)
|
slowCopyN(sl, writer, file, end-start+1, delay)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
return mux
|
return mux
|
||||||
@@ -219,9 +224,12 @@ func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 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(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)
|
||||||
for {
|
for {
|
||||||
|
if sl.isShutdown {
|
||||||
|
return 0, errors.New("server shutdown")
|
||||||
|
}
|
||||||
nr, er := src.Read(buf)
|
nr, er := src.Read(buf)
|
||||||
if nr > 0 {
|
if nr > 0 {
|
||||||
nw, ew := dst.Write(buf[0:nr])
|
nw, ew := dst.Write(buf[0:nr])
|
||||||
@@ -250,8 +258,8 @@ func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err err
|
|||||||
return written, err
|
return written, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
func slowCopyN(sl *shutdownListener, dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
|
||||||
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
|
written, err = slowCopy(sl, dst, io.LimitReader(src, n), delay)
|
||||||
if written == n {
|
if written == n {
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -262,7 +270,7 @@ func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int6
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
func startTestServer(serverHandle func(sl *shutdownListener) http.Handler) net.Listener {
|
||||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -272,7 +280,7 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer file.Close()
|
defer file.Close()
|
||||||
// 随机生成一个文件
|
// Write random data
|
||||||
l := int64(8192)
|
l := int64(8192)
|
||||||
buf := make([]byte, l)
|
buf := make([]byte, l)
|
||||||
size := int64(0)
|
size := int64(0)
|
||||||
@@ -289,21 +297,24 @@ func startTestServer(serverHandle func() http.Handler) net.Listener {
|
|||||||
size += l
|
size += l
|
||||||
}
|
}
|
||||||
server := &http.Server{}
|
server := &http.Server{}
|
||||||
server.Handler = serverHandle()
|
sl := &shutdownListener{
|
||||||
go server.Serve(listener)
|
|
||||||
|
|
||||||
return &shutdownListener{
|
|
||||||
server: server,
|
server: server,
|
||||||
Listener: listener,
|
Listener: listener,
|
||||||
}
|
}
|
||||||
|
server.Handler = serverHandle(sl)
|
||||||
|
go server.Serve(listener)
|
||||||
|
|
||||||
|
return sl
|
||||||
}
|
}
|
||||||
|
|
||||||
type shutdownListener struct {
|
type shutdownListener struct {
|
||||||
server *http.Server
|
server *http.Server
|
||||||
|
isShutdown bool
|
||||||
net.Listener
|
net.Listener
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *shutdownListener) Close() error {
|
func (c *shutdownListener) Close() error {
|
||||||
|
c.isShutdown = true
|
||||||
closeErr := c.server.Shutdown(context.Background())
|
closeErr := c.server.Shutdown(context.Background())
|
||||||
if err := ifExistAndRemove(BuildFile); err != nil {
|
if err := ifExistAndRemove(BuildFile); err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
+28
-16
@@ -204,33 +204,29 @@ func (d *Downloader) Setup() error {
|
|||||||
|
|
||||||
current := task.fetcher.Progress().TotalDownloaded()
|
current := task.fetcher.Progress().TotalDownloaded()
|
||||||
tick := float64(d.cfg.RefreshInterval) / 1000
|
tick := float64(d.cfg.RefreshInterval) / 1000
|
||||||
|
downloadDataChanged := false
|
||||||
if task.Status == base.DownloadStatusRunning {
|
if task.Status == base.DownloadStatusRunning {
|
||||||
|
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.calcSpeed(task.speedArr, current-task.Progress.Downloaded, tick)
|
||||||
task.Progress.Downloaded = current
|
task.Progress.Downloaded = current
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uploadDataChanged := false
|
||||||
if task.Uploading {
|
if task.Uploading {
|
||||||
uploader := task.fetcher.(fetcher.Uploader)
|
uploader := task.fetcher.(fetcher.Uploader)
|
||||||
currentUploaded := uploader.UploadedBytes()
|
currentUploaded := uploader.UploadedBytes()
|
||||||
|
uploadDataChanged = currentUploaded != task.Progress.Uploaded
|
||||||
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
task.Progress.UploadSpeed = task.calcSpeed(task.uploadSpeedArr, currentUploaded-task.Progress.Uploaded, tick)
|
||||||
task.Progress.Uploaded = currentUploaded
|
task.Progress.Uploaded = currentUploaded
|
||||||
}
|
}
|
||||||
d.emit(EventKeyProgress, task)
|
d.emit(EventKeyProgress, task)
|
||||||
|
|
||||||
// store fetcher progress
|
// store fetcher progress when download/upload data changed
|
||||||
data, err := task.fetcherManager.Store(task.fetcher)
|
if !downloadDataChanged && !uploadDataChanged {
|
||||||
if err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := d.storage.Put(bucketSave, task.ID, data); err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err := d.storage.Put(bucketTask, task.ID, task); err != nil {
|
|
||||||
d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
d.saveTask(task)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -273,6 +269,23 @@ func (d *Downloader) setupFetcher(fm fetcher.FetcherManager, fetcher fetcher.Fet
|
|||||||
fetcher.Setup(ctl)
|
fetcher.Setup(ctl)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Downloader) saveTask(task *Task) error {
|
||||||
|
data, err := task.fetcherManager.Store(task.fetcher)
|
||||||
|
if err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("serialize fetcher failed: %s", task.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.storage.Put(bucketSave, task.ID, data); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("persist fetcher failed: %s", task.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := d.storage.Put(bucketTask, task.ID, task); err != nil {
|
||||||
|
d.Logger.Error().Stack().Err(err).Msgf("persist task failed: %s", task.ID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
||||||
rrId, err := gonanoid.New()
|
rrId, err := gonanoid.New()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -813,7 +826,7 @@ func (d *Downloader) watch(task *Task) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// When delete a not resolved task, need check if the task resource is nil
|
// When delete a not resolved task, need check if the task resource is nil
|
||||||
if task.Meta.Res == nil {
|
if task.Meta.Res == nil || d.GetTask(task.ID) == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1005,8 +1018,6 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
isCreate = task.Status == base.DownloadStatusReady
|
isCreate = task.Status == base.DownloadStatusReady
|
||||||
|
|
||||||
d.triggerOnStart(task)
|
|
||||||
task.updateStatus(base.DownloadStatusRunning)
|
task.updateStatus(base.DownloadStatusRunning)
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -1023,6 +1034,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
task.lock.Lock()
|
task.lock.Lock()
|
||||||
defer task.lock.Unlock()
|
defer task.lock.Unlock()
|
||||||
|
|
||||||
|
d.triggerOnStart(task)
|
||||||
if task.Meta.Res == nil {
|
if task.Meta.Res == nil {
|
||||||
err := task.fetcher.Resolve(task.Meta.Req)
|
err := task.fetcher.Resolve(task.Meta.Req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1064,7 +1076,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
|
|||||||
if err := task.fetcher.Start(); err != nil {
|
if err := task.fetcher.Start(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
if err := d.saveTask(task); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
d.emit(EventKeyStart, task)
|
d.emit(EventKeyStart, task)
|
||||||
|
|||||||
@@ -702,20 +702,22 @@ type OnDoneContext struct {
|
|||||||
Task *Task `json:"task"`
|
Task *Task `json:"task"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ExtensionTask is a wrapper of Task, it's used to interact with extension scripts.
|
||||||
|
// Avoid extension scripts modifying task directly, use ExtensionTask to encapsulate task,
|
||||||
|
// only some fields can be modified, such as request info.
|
||||||
type ExtensionTask struct {
|
type ExtensionTask struct {
|
||||||
*Task
|
|
||||||
|
|
||||||
download *Downloader
|
download *Downloader
|
||||||
|
|
||||||
|
*Task
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
|
||||||
|
// restricts extension scripts to only modify request info
|
||||||
newTask := task.clone()
|
newTask := task.clone()
|
||||||
// Assign the pointer of the properties that the extension supports modification
|
newTask.Meta.Req = task.Meta.Req
|
||||||
newTask.Meta = task.Meta
|
|
||||||
newTask.Status = task.Status
|
|
||||||
return &ExtensionTask{
|
return &ExtensionTask{
|
||||||
Task: newTask,
|
|
||||||
download: download,
|
download: download,
|
||||||
|
Task: newTask,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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 ""
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -129,12 +129,20 @@ Future<void> continueTask(String id) async {
|
|||||||
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
return _parse(() => _client.dio.put("/api/v1/tasks/$id/continue"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> pauseAllTasks() async {
|
Future<void> pauseAllTasks(List<String>? ids) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/pause"), null);
|
return _parse(
|
||||||
|
() => _client.dio.put("/api/v1/tasks/pause", queryParameters: {
|
||||||
|
"id": ids,
|
||||||
|
}),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> continueAllTasks() async {
|
Future<void> continueAllTasks(List<String>? ids) async {
|
||||||
return _parse(() => _client.dio.put("/api/v1/tasks/continue"), null);
|
return _parse(
|
||||||
|
() => _client.dio.put("/api/v1/tasks/continue", queryParameters: {
|
||||||
|
"id": ids,
|
||||||
|
}),
|
||||||
|
null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> deleteTask(String id, bool force) async {
|
Future<void> deleteTask(String id, bool force) async {
|
||||||
@@ -142,6 +150,15 @@ Future<void> deleteTask(String id, bool force) async {
|
|||||||
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
() => _client.dio.delete("/api/v1/tasks/$id?force=$force"), null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deleteTasks(List<String>? ids, bool force) async {
|
||||||
|
return _parse(
|
||||||
|
() => _client.dio.delete("/api/v1/tasks", queryParameters: {
|
||||||
|
"id": ids,
|
||||||
|
"force": force,
|
||||||
|
}),
|
||||||
|
null);
|
||||||
|
}
|
||||||
|
|
||||||
Future<DownloaderConfig> getConfig() async {
|
Future<DownloaderConfig> getConfig() async {
|
||||||
return _parse(() => _client.dio.get("/api/v1/config"),
|
return _parse(() => _client.dio.get("/api/v1/config"),
|
||||||
(data) => DownloaderConfig.fromJson(data));
|
(data) => DownloaderConfig.fromJson(data));
|
||||||
|
|||||||
@@ -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(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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,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>{
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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>{
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ 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';
|
||||||
@@ -27,6 +28,9 @@ import '../../../../util/log_util.dart';
|
|||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
|
import '../../create/controllers/create_controller.dart';
|
||||||
|
import '../../create/dto/create_router_params.dart';
|
||||||
|
import '../../redirect/views/redirect_view.dart';
|
||||||
|
|
||||||
const unixSocketPath = 'gopeed.sock';
|
const unixSocketPath = 'gopeed.sock';
|
||||||
|
|
||||||
@@ -160,8 +164,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,13 +172,13 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
|
|
||||||
// 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.getInitialLink();
|
final uri = await _appLinks.getInitialLink();
|
||||||
if (uri != null) {
|
if (uri != null) {
|
||||||
await _toCreate(uri);
|
await _handleDeepLink(uri);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,11 +221,11 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: "startAll".tr,
|
label: "startAll".tr,
|
||||||
onClick: (menuItem) async => {continueAllTasks()},
|
onClick: (menuItem) async => {continueAllTasks(null)},
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: "pauseAll".tr,
|
label: "pauseAll".tr,
|
||||||
onClick: (menuItem) async => {pauseAllTasks()},
|
onClick: (menuItem) async => {pauseAllTasks(null)},
|
||||||
),
|
),
|
||||||
MenuItem(
|
MenuItem(
|
||||||
label: 'setting'.tr,
|
label: 'setting'.tr,
|
||||||
@@ -304,13 +307,38 @@ 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 paramsJson = String.fromCharCodes(base64Decode(params!));
|
||||||
await Get.rootDelegate.offAndToNamed(Routes.CREATE, arguments: path);
|
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.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() {
|
||||||
|
|||||||
@@ -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,4 +1,6 @@
|
|||||||
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
import 'dart:convert';
|
||||||
|
|
||||||
|
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';
|
||||||
@@ -12,6 +14,7 @@ import '../../../../api/model/create_task.dart';
|
|||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
import '../../../../api/model/resolve_result.dart';
|
import '../../../../api/model/resolve_result.dart';
|
||||||
|
import '../../../../api/model/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';
|
||||||
@@ -19,10 +22,11 @@ import '../../../../util/util.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';
|
||||||
import '../../../views/file_list_view.dart';
|
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>();
|
||||||
@@ -36,9 +40,20 @@ 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 _availableSchemes = ["http:", "https:", "magnet:"];
|
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||||
@@ -60,32 +75,91 @@ 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 uppercaseUrl = url.toUpperCase();
|
||||||
// read clipboard
|
Protocol? protocol;
|
||||||
Clipboard.getData('text/plain').then((value) {
|
if (uppercaseUrl.startsWith("HTTP:") ||
|
||||||
if (value?.text?.isNotEmpty ?? false) {
|
uppercaseUrl.startsWith("HTTPS:")) {
|
||||||
if (_availableSchemes
|
protocol = Protocol.http;
|
||||||
.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!);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
if (uppercaseUrl.startsWith("MAGNET:") ||
|
||||||
|
uppercaseUrl.endsWith(".TORRENT")) {
|
||||||
|
protocol = Protocol.bt;
|
||||||
|
}
|
||||||
|
if (protocol != null) {
|
||||||
|
final extraHandlers = {
|
||||||
|
Protocol.http: () {
|
||||||
|
final reqExtra = ReqExtraHttp.fromJson(
|
||||||
|
jsonDecode(jsonEncode(routerParams.req!.extra)));
|
||||||
|
_httpHeaderControllers.clear();
|
||||||
|
reqExtra.header.forEach((key, value) {
|
||||||
|
_httpHeaderControllers.add(
|
||||||
|
(
|
||||||
|
name: TextEditingController(text: key),
|
||||||
|
value: TextEditingController(text: value),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
_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();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(
|
||||||
@@ -436,56 +510,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),
|
||||||
@@ -644,12 +767,10 @@ class CreateView extends GetView<CreateController> {
|
|||||||
if (controller.showAdvanced.value) {
|
if (controller.showAdvanced.value) {
|
||||||
switch (controller.advancedTabController.index) {
|
switch (controller.advancedTabController.index) {
|
||||||
case 0:
|
case 0:
|
||||||
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);
|
||||||
};
|
|
||||||
header.removeWhere((key, value) => value.trim().isEmpty);
|
|
||||||
if (header.isNotEmpty) {
|
if (header.isNotEmpty) {
|
||||||
reqExtra = ReqExtraHttp()..header = header;
|
reqExtra = ReqExtraHttp()..header = header;
|
||||||
}
|
}
|
||||||
@@ -698,7 +819,13 @@ class CreateView extends GetView<CreateController> {
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: createFormKey,
|
key: createFormKey,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
child: FileListView(files: rr.res.files)),
|
child: FileTreeView(
|
||||||
|
files: rr.res.files,
|
||||||
|
initialValues: rr.res.files.asMap().keys.toList(),
|
||||||
|
onSelectionChanged: (List<int> values) {
|
||||||
|
controller.selectedIndexes.value = values;
|
||||||
|
},
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@ 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/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 +112,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 +162,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 +230,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 +422,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(
|
||||||
@@ -947,6 +980,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,5 +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);
|
||||||
final copyUrlDone = false.obs;
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.disableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
super.onClose();
|
||||||
|
if (kIsWeb) {
|
||||||
|
BrowserContextMenu.enableContextMenu();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -9,5 +9,13 @@ class TaskDownloadingController extends TaskListController {
|
|||||||
Status.pause,
|
Status.pause,
|
||||||
Status.wait,
|
Status.wait,
|
||||||
Status.error
|
Status.error
|
||||||
], (a, b) => b.createdAt.compareTo(a.createdAt));
|
], (a, b) {
|
||||||
|
if (a.status == Status.running && b.status != Status.running) {
|
||||||
|
return -1;
|
||||||
|
} else if (a.status != Status.running && b.status == Status.running) {
|
||||||
|
return 1;
|
||||||
|
} else {
|
||||||
|
return b.updatedAt.compareTo(a.updatedAt);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@ abstract class TaskListController extends GetxController {
|
|||||||
TaskListController(this.statuses, this.compare);
|
TaskListController(this.statuses, this.compare);
|
||||||
|
|
||||||
final tasks = <Task>[].obs;
|
final tasks = <Task>[].obs;
|
||||||
|
final selectedTaskIds = <String>[].obs;
|
||||||
final isRunning = false.obs;
|
final isRunning = false.obs;
|
||||||
|
|
||||||
late final Timer _timer;
|
late final Timer _timer;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class TaskDownloadedView extends GetView<TaskDownloadedController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BuildTaskListView(tasks: controller.tasks);
|
return BuildTaskListView(
|
||||||
|
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ class TaskDownloadingView extends GetView<TaskDownloadingController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BuildTaskListView(tasks: controller.tasks);
|
return BuildTaskListView(
|
||||||
|
tasks: controller.tasks, selectedTaskIds: controller.selectedTaskIds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,10 +7,9 @@ import 'package:url_launcher/url_launcher.dart';
|
|||||||
|
|
||||||
import '../../../../api/api.dart' as api;
|
import '../../../../api/api.dart' as api;
|
||||||
import '../../../../util/browser_download/browser_download.dart';
|
import '../../../../util/browser_download/browser_download.dart';
|
||||||
import '../../../../util/file_icon.dart';
|
|
||||||
import '../../../../util/icons.dart';
|
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/breadcrumb_view.dart';
|
import '../../../views/breadcrumb_view.dart';
|
||||||
|
import '../../../views/file_icon.dart';
|
||||||
import '../controllers/task_files_controller.dart';
|
import '../controllers/task_files_controller.dart';
|
||||||
|
|
||||||
class TaskFilesView extends GetView<TaskFilesController> {
|
class TaskFilesView extends GetView<TaskFilesController> {
|
||||||
@@ -65,9 +64,8 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
|||||||
[meta.opts.path, meta.res!.name, fileRelativePath]);
|
[meta.opts.path, meta.res!.name, fileRelativePath]);
|
||||||
final fileName = basename(filePath);
|
final fileName = basename(filePath);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: file.isDirectory
|
leading:
|
||||||
? const Icon(Icons.folder)
|
Icon(fileIcon(fileName, isFolder: file.isDirectory)),
|
||||||
: Icon(FaIcons.allIcons[findIcon(fileName)]),
|
|
||||||
title: Text(fileName),
|
title: Text(fileName),
|
||||||
subtitle: file.isDirectory
|
subtitle: file.isDirectory
|
||||||
? Text('items'.trParams({
|
? Text('items'.trParams({
|
||||||
|
|||||||
@@ -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,24 +1,28 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
import '../../api/api.dart';
|
import '../../api/api.dart';
|
||||||
import '../../api/model/task.dart';
|
import '../../api/model/task.dart';
|
||||||
import '../../util/file_icon.dart';
|
|
||||||
import '../../util/icons.dart';
|
|
||||||
import '../../util/message.dart';
|
import '../../util/message.dart';
|
||||||
import '../../util/util.dart';
|
import '../../util/util.dart';
|
||||||
import '../modules/app/controllers/app_controller.dart';
|
import '../modules/app/controllers/app_controller.dart';
|
||||||
import '../modules/task/controllers/task_controller.dart';
|
import '../modules/task/controllers/task_controller.dart';
|
||||||
|
import '../modules/task/controllers/task_downloaded_controller.dart';
|
||||||
|
import '../modules/task/controllers/task_downloading_controller.dart';
|
||||||
import '../modules/task/views/task_view.dart';
|
import '../modules/task/views/task_view.dart';
|
||||||
import '../routes/app_pages.dart';
|
import '../routes/app_pages.dart';
|
||||||
|
import 'file_icon.dart';
|
||||||
|
|
||||||
class BuildTaskListView extends GetView {
|
class BuildTaskListView extends GetView {
|
||||||
final List<Task> tasks;
|
final List<Task> tasks;
|
||||||
|
final List<String> selectedTaskIds;
|
||||||
|
|
||||||
const BuildTaskListView({
|
const BuildTaskListView({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.tasks,
|
required this.tasks,
|
||||||
|
required this.selectedTaskIds,
|
||||||
}) : super(key: key);
|
}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -57,11 +61,15 @@ class BuildTaskListView extends GetView {
|
|||||||
return task.status == Status.running;
|
return task.status == Status.running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool isSelect() {
|
||||||
|
return selectedTaskIds.contains(task.id);
|
||||||
|
}
|
||||||
|
|
||||||
bool isFolderTask() {
|
bool isFolderTask() {
|
||||||
return task.isFolder;
|
return task.isFolder;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> showDeleteDialog(String id) {
|
Future<void> showDeleteDialog(List<String> ids) {
|
||||||
final appController = Get.find<AppController>();
|
final appController = Get.find<AppController>();
|
||||||
|
|
||||||
final context = Get.context!;
|
final context = Get.context!;
|
||||||
@@ -70,7 +78,8 @@ class BuildTaskListView extends GetView {
|
|||||||
context: context,
|
context: context,
|
||||||
barrierDismissible: false,
|
barrierDismissible: false,
|
||||||
builder: (_) => AlertDialog(
|
builder: (_) => AlertDialog(
|
||||||
title: Text('deleteTask'.tr),
|
title: Text(
|
||||||
|
'deleteTask'.trParams({'count': ids.length.toString()})),
|
||||||
content: Obx(() => CheckboxListTile(
|
content: Obx(() => CheckboxListTile(
|
||||||
value: appController
|
value: appController
|
||||||
.downloaderConfig.value.extra.lastDeleteTaskKeep,
|
.downloaderConfig.value.extra.lastDeleteTaskKeep,
|
||||||
@@ -96,7 +105,7 @@ class BuildTaskListView extends GetView {
|
|||||||
final force = !appController
|
final force = !appController
|
||||||
.downloaderConfig.value.extra.lastDeleteTaskKeep;
|
.downloaderConfig.value.extra.lastDeleteTaskKeep;
|
||||||
await appController.saveConfig();
|
await appController.saveConfig();
|
||||||
await deleteTask(id, force);
|
await deleteTasks(ids, force);
|
||||||
Get.back();
|
Get.back();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorMessage(e);
|
showErrorMessage(e);
|
||||||
@@ -144,12 +153,35 @@ class BuildTaskListView extends GetView {
|
|||||||
list.add(IconButton(
|
list.add(IconButton(
|
||||||
icon: const Icon(Icons.delete),
|
icon: const Icon(Icons.delete),
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
showDeleteDialog(task.id);
|
showDeleteDialog([task.id]);
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildContextItem(IconData icon, String label, Function() onTap,
|
||||||
|
{bool enabled = true}) {
|
||||||
|
return ListTile(
|
||||||
|
dense: true,
|
||||||
|
visualDensity: const VisualDensity(vertical: -1),
|
||||||
|
minLeadingWidth: 12,
|
||||||
|
leading: Icon(icon, size: 18),
|
||||||
|
title: Text(label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, // Make the text bold
|
||||||
|
)),
|
||||||
|
onTap: () async {
|
||||||
|
Get.back();
|
||||||
|
try {
|
||||||
|
await onTap();
|
||||||
|
} catch (e) {
|
||||||
|
showErrorMessage(e);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
enabled: enabled,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
double getProgress() {
|
double getProgress() {
|
||||||
final totalSize = task.meta.res?.size ?? 0;
|
final totalSize = task.meta.res?.size ?? 0;
|
||||||
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
|
return totalSize <= 0 ? 0 : task.progress.downloaded / totalSize;
|
||||||
@@ -168,53 +200,127 @@ class BuildTaskListView extends GetView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final taskController = Get.find<TaskController>();
|
final taskController = Get.find<TaskController>();
|
||||||
|
final taskListController = taskController.tabIndex.value == 0
|
||||||
|
? Get.find<TaskDownloadingController>()
|
||||||
|
: Get.find<TaskDownloadedController>();
|
||||||
|
|
||||||
return Card(
|
// Filter selected task ids that are still in the task list
|
||||||
elevation: 4.0,
|
filterSelectedTaskIds(Iterable<String> selectedTaskIds) => selectedTaskIds
|
||||||
child: InkWell(
|
.where((id) => tasks.any((task) => task.id == id))
|
||||||
onTap: () {
|
.toList();
|
||||||
taskController.scaffoldKey.currentState?.openEndDrawer();
|
|
||||||
taskController.selectTask.value = task;
|
return ContextMenuArea(
|
||||||
},
|
width: 140,
|
||||||
onDoubleTap: () {
|
builder: (context) => [
|
||||||
task.open();
|
buildContextItem(Icons.checklist, 'selectAll'.tr, () {
|
||||||
},
|
if (tasks.isEmpty) return;
|
||||||
child: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (selectedTaskIds.isNotEmpty) {
|
||||||
children: [
|
taskListController.selectedTaskIds([]);
|
||||||
ListTile(
|
} else {
|
||||||
title: Text(task.name),
|
taskListController.selectedTaskIds(tasks.map((e) => e.id).toList());
|
||||||
leading: isFolderTask()
|
}
|
||||||
? const Icon(FaIcons.folder)
|
}),
|
||||||
: Icon(FaIcons.allIcons[findIcon(task.name)])),
|
buildContextItem(Icons.check, 'select'.tr, () {
|
||||||
Row(
|
if (isSelect()) {
|
||||||
|
taskListController.selectedTaskIds(taskListController
|
||||||
|
.selectedTaskIds
|
||||||
|
.where((element) => element != task.id)
|
||||||
|
.toList());
|
||||||
|
} else {
|
||||||
|
taskListController.selectedTaskIds(
|
||||||
|
[...taskListController.selectedTaskIds, task.id]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
const Divider(
|
||||||
|
indent: 8,
|
||||||
|
endIndent: 8,
|
||||||
|
),
|
||||||
|
buildContextItem(Icons.play_arrow, 'continue'.tr, () async {
|
||||||
|
try {
|
||||||
|
await continueAllTasks(filterSelectedTaskIds(
|
||||||
|
{...taskListController.selectedTaskIds, task.id}));
|
||||||
|
} finally {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
}
|
||||||
|
}, enabled: !isDone() && !isRunning()),
|
||||||
|
buildContextItem(Icons.pause, 'pause'.tr, () async {
|
||||||
|
try {
|
||||||
|
await pauseAllTasks(filterSelectedTaskIds(
|
||||||
|
{...taskListController.selectedTaskIds, task.id}));
|
||||||
|
} finally {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
}
|
||||||
|
}, enabled: !isDone() && isRunning()),
|
||||||
|
buildContextItem(Icons.delete, 'delete'.tr, () async {
|
||||||
|
try {
|
||||||
|
await showDeleteDialog(filterSelectedTaskIds(
|
||||||
|
{...taskListController.selectedTaskIds, task.id}));
|
||||||
|
} finally {
|
||||||
|
taskListController.selectedTaskIds([]);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
child: Obx(
|
||||||
|
() => Card(
|
||||||
|
elevation: 4.0,
|
||||||
|
shape: isSelect()
|
||||||
|
? RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8.0),
|
||||||
|
side: BorderSide(
|
||||||
|
color: Theme.of(context).colorScheme.primary,
|
||||||
|
width: 2.0,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
taskController.scaffoldKey.currentState?.openEndDrawer();
|
||||||
|
taskController.selectTask.value = task;
|
||||||
|
},
|
||||||
|
onDoubleTap: () {
|
||||||
|
task.open();
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
ListTile(
|
||||||
flex: 1,
|
title: Text(task.name),
|
||||||
child: Text(
|
leading: Icon(
|
||||||
getProgressText(),
|
fileIcon(task.name,
|
||||||
style: Get.textTheme.bodyLarge
|
isFolder: isFolderTask(),
|
||||||
?.copyWith(color: Get.theme.disabledColor),
|
isBitTorrent: task.protocol == Protocol.bt),
|
||||||
).padding(left: 18)),
|
|
||||||
Expanded(
|
|
||||||
flex: 1,
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
|
||||||
children: [
|
|
||||||
Text("${Util.fmtByte(task.progress.speed)} / s",
|
|
||||||
style: Get.textTheme.titleSmall),
|
|
||||||
...buildActions()
|
|
||||||
],
|
|
||||||
)),
|
)),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Text(
|
||||||
|
getProgressText(),
|
||||||
|
style: Get.textTheme.bodyLarge
|
||||||
|
?.copyWith(color: Get.theme.disabledColor),
|
||||||
|
).padding(left: 18)),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text("${Util.fmtByte(task.progress.speed)} / s",
|
||||||
|
style: Get.textTheme.titleSmall),
|
||||||
|
...buildActions()
|
||||||
|
],
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
isDone()
|
||||||
|
? Container()
|
||||||
|
: LinearProgressIndicator(
|
||||||
|
value: getProgress(),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
isDone()
|
)).padding(horizontal: 14, top: 8),
|
||||||
? Container()
|
),
|
||||||
: LinearProgressIndicator(
|
);
|
||||||
value: getProgress(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)).padding(horizontal: 14, top: 8);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../icon/gopeed_icons.dart';
|
||||||
|
|
||||||
|
final Map<IconData, List<String>> iconConfigMap = {
|
||||||
|
Gopeed.install: ['exe', 'msi', 'dmg', 'deb', 'rpm'],
|
||||||
|
Gopeed.android: ['apk'],
|
||||||
|
Gopeed.app_store_ios: ['ipa'],
|
||||||
|
Gopeed.file_bt: ['torrent'],
|
||||||
|
Gopeed.cd: ['iso'],
|
||||||
|
Gopeed.html5: ['html', 'htm'],
|
||||||
|
Gopeed.file_alt: ['txt', 'md', 'log', 'csv', 'tsv', 'json', 'yaml', 'yml'],
|
||||||
|
Gopeed.file_pdf: ['pdf'],
|
||||||
|
Gopeed.file_word: ['doc', 'docx'],
|
||||||
|
Gopeed.file_excel: ['xls', 'xlsx'],
|
||||||
|
Gopeed.file_powerpoint: ['ppt', 'pptx'],
|
||||||
|
Gopeed.file_archive: ['zip', 'rar', '7z', 'tar', 'gz', 'bz2', 'xz'],
|
||||||
|
Gopeed.file_image: [
|
||||||
|
'jpg',
|
||||||
|
'jpeg',
|
||||||
|
'png',
|
||||||
|
'gif',
|
||||||
|
'bmp',
|
||||||
|
'tiff',
|
||||||
|
'svg',
|
||||||
|
'webp'
|
||||||
|
],
|
||||||
|
Gopeed.file_audio: ['mp3', 'wav', 'flac', 'aac', 'ogg', 'wma', 'm4a'],
|
||||||
|
Gopeed.file_video: ['mp4', 'avi', 'mkv', 'mov', 'wmv', 'flv', 'webm'],
|
||||||
|
Gopeed.file_code: [
|
||||||
|
'js',
|
||||||
|
'css',
|
||||||
|
'json',
|
||||||
|
'xml',
|
||||||
|
'java',
|
||||||
|
'cpp',
|
||||||
|
'dart',
|
||||||
|
'py',
|
||||||
|
'rb',
|
||||||
|
'php',
|
||||||
|
'ts',
|
||||||
|
'swift',
|
||||||
|
'go',
|
||||||
|
'rs'
|
||||||
|
],
|
||||||
|
Gopeed.file: [''],
|
||||||
|
};
|
||||||
|
|
||||||
|
final Map<String, IconData> _iconCache = Map.fromEntries(
|
||||||
|
iconConfigMap.entries.expand(
|
||||||
|
(entry) => entry.value.map((ext) => MapEntry(ext, entry.key)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
String fileExt(String? name) {
|
||||||
|
if (name == null) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
final ext = name.split('.').last;
|
||||||
|
if (ext.length > 8) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ext.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData fileIcon(String? name,
|
||||||
|
{bool isFolder = false, bool isBitTorrent = false}) {
|
||||||
|
if (isFolder) {
|
||||||
|
return isBitTorrent ? Gopeed.folder_bt : Gopeed.folder;
|
||||||
|
}
|
||||||
|
|
||||||
|
final ext = fileExt(name);
|
||||||
|
return _iconCache[ext] ?? Gopeed.file;
|
||||||
|
}
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
import 'dart:ui' as ui;
|
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart' as fluent;
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:get/get.dart';
|
|
||||||
import '../../api/model/resource.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
|
|
||||||
import '../../util/file_icon.dart';
|
|
||||||
import '../../util/icons.dart';
|
|
||||||
import '../../util/util.dart';
|
|
||||||
import '../modules/app/controllers/app_controller.dart';
|
|
||||||
import '../modules/create/controllers/create_controller.dart';
|
|
||||||
|
|
||||||
class FileListView extends GetView {
|
|
||||||
final List<FileInfo> files;
|
|
||||||
|
|
||||||
FileListView({
|
|
||||||
Key? key,
|
|
||||||
required this.files,
|
|
||||||
}) : super(key: key);
|
|
||||||
final parentController = Get.find<CreateController>();
|
|
||||||
final appController = Get.find<AppController>();
|
|
||||||
|
|
||||||
List findChildFileIdsR(List children) {
|
|
||||||
List res = [];
|
|
||||||
for (var k in children) {
|
|
||||||
if (parentController.fileInfos[k]['type'] == 'folder') {
|
|
||||||
res.addAll(
|
|
||||||
findChildFileIdsR(parentController.fileInfos[k]['children']));
|
|
||||||
} else {
|
|
||||||
res.add(parentController.fileInfos[k]['fileId']);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
void clearDescendantFolderSizeR(int id) {
|
|
||||||
if (parentController.fileInfos[id]['type'] == 'folder') {
|
|
||||||
parentController.fileInfos[id]['size'] = 0;
|
|
||||||
for (var childId in parentController.fileInfos[id]['children']) {
|
|
||||||
clearDescendantFolderSizeR(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void reduceAncestorFolderSizeR(int id, int size) {
|
|
||||||
int parentId = parentController.fileInfos[id]['parentId'];
|
|
||||||
|
|
||||||
if (parentId != -1) {
|
|
||||||
parentController.fileInfos[parentId]['size'] -= size;
|
|
||||||
reduceAncestorFolderSizeR(parentId, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int addDescendantFolderSizeR(int id) {
|
|
||||||
if (parentController.fileInfos[id]['type'] == 'folder') {
|
|
||||||
for (var childId in parentController.fileInfos[id]['children']) {
|
|
||||||
parentController.fileInfos[id]['size'] +=
|
|
||||||
addDescendantFolderSizeR(childId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return parentController.fileInfos[id]['size'];
|
|
||||||
}
|
|
||||||
|
|
||||||
void addAncestorFolderSizeR(int id, int size) {
|
|
||||||
int parentId = parentController.fileInfos[id]['parentId'];
|
|
||||||
if (parentId != -1) {
|
|
||||||
parentController.fileInfos[parentId]['size'] += size;
|
|
||||||
addAncestorFolderSizeR(parentId, size);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
List<fluent.TreeViewItem> buildTreeViewItemsR(int level, int parentId) {
|
|
||||||
List<fluent.TreeViewItem> res = [];
|
|
||||||
List children = parentController.fileInfos
|
|
||||||
.where((e) => e['level'] == level && e['parentId'] == parentId)
|
|
||||||
.toList();
|
|
||||||
for (int j = 0; j < children.length; j++) {
|
|
||||||
Map fileInfo = children[j];
|
|
||||||
|
|
||||||
if (fileInfo['type'] == 'folder') {
|
|
||||||
// folder
|
|
||||||
res.add(fluent.TreeViewItem(
|
|
||||||
// expanded: false, bug on init
|
|
||||||
value: fileInfo['id'],
|
|
||||||
onInvoked: (item, reason) async {
|
|
||||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
|
||||||
List childrenIds = findChildFileIdsR(fileInfo['children']);
|
|
||||||
if (item.selected == true) {
|
|
||||||
parentController.selectedIndexes.addAll(childrenIds.cast());
|
|
||||||
parentController.fileInfos[fileInfo['id']]['size'] = 0;
|
|
||||||
addDescendantFolderSizeR(item.value);
|
|
||||||
addAncestorFolderSizeR(item.value, fileInfo['size']);
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
} else {
|
|
||||||
parentController.selectedIndexes
|
|
||||||
.removeWhere((index) => childrenIds.contains(index));
|
|
||||||
reduceAncestorFolderSizeR(item.value, fileInfo['size']);
|
|
||||||
clearDescendantFolderSizeR(item.value);
|
|
||||||
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
leading: Obx(() {
|
|
||||||
return parentController.openedFolders.contains(fileInfo['id'])
|
|
||||||
? const Icon(FaIcons.folder_open,
|
|
||||||
color: Colors.lightBlueAccent)
|
|
||||||
: const Icon(FaIcons.folder, color: Colors.lightBlueAccent);
|
|
||||||
}),
|
|
||||||
onExpandToggle: (fluent.TreeViewItem item, bool getExpanded) async {
|
|
||||||
getExpanded
|
|
||||||
? parentController.openedFolders.add(item.value)
|
|
||||||
: parentController.openedFolders.remove(item.value);
|
|
||||||
},
|
|
||||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
fileInfo['name'],
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
// style: context.textTheme.titleSmall,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Obx(() {
|
|
||||||
return parentController.fileInfos[fileInfo['id']]['size'] != 0
|
|
||||||
? Text(
|
|
||||||
Util.fmtByte(
|
|
||||||
parentController.fileInfos[fileInfo['id']]['size']),
|
|
||||||
// style: context.textTheme.labelMedium,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink();
|
|
||||||
})
|
|
||||||
]),
|
|
||||||
children: buildTreeViewItemsR(
|
|
||||||
// parentController.fileInfos.where((e) => e['level'] > level).toList(),
|
|
||||||
level + 1,
|
|
||||||
fileInfo['id'])));
|
|
||||||
} else {
|
|
||||||
// file
|
|
||||||
res.add(fluent.TreeViewItem(
|
|
||||||
value: {'id': fileInfo['id'], 'fileId': fileInfo['fileId']},
|
|
||||||
selected: fileInfo['selected'],
|
|
||||||
onInvoked: (item, reason) async {
|
|
||||||
if (reason == fluent.TreeViewItemInvokeReason.selectionToggle) {
|
|
||||||
if (item.selected == true) {
|
|
||||||
parentController.selectedIndexes.add(item.value['fileId']);
|
|
||||||
addAncestorFolderSizeR(item.value['id'], fileInfo['size']);
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
} else {
|
|
||||||
parentController.selectedIndexes.remove(item.value['fileId']);
|
|
||||||
reduceAncestorFolderSizeR(item.value['id'], fileInfo['size']);
|
|
||||||
parentController.fileInfos.refresh();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
collapsable: false,
|
|
||||||
leading: fileInfo['name'].lastIndexOf('.') == -1
|
|
||||||
? Icon(
|
|
||||||
FaIcons.doc,
|
|
||||||
color: appController.downloaderConfig.value.extra.themeMode ==
|
|
||||||
'dark'
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black45,
|
|
||||||
)
|
|
||||||
: Icon(
|
|
||||||
FaIcons.allIcons[findIcon(fileInfo['name'])],
|
|
||||||
color: appController.downloaderConfig.value.extra.themeMode ==
|
|
||||||
'dark'
|
|
||||||
? Colors.white
|
|
||||||
: Colors.black45,
|
|
||||||
),
|
|
||||||
content: Row(mainAxisAlignment: MainAxisAlignment.end, children: [
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
fileInfo['name'],
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
// style: context.textTheme.titleSmall,
|
|
||||||
)),
|
|
||||||
fileInfo['size'] > 0
|
|
||||||
? Text(
|
|
||||||
Util.fmtByte(
|
|
||||||
fileInfo['size'],
|
|
||||||
),
|
|
||||||
// style: context.textTheme.labelMedium,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
]),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
List<fluent.TreeViewItem> get items {
|
|
||||||
List infos = [];
|
|
||||||
int idNext = 0;
|
|
||||||
List selectedFileIds = [];
|
|
||||||
List openedFolders = [];
|
|
||||||
//make fileInfos list
|
|
||||||
for (var i = 0; i < files.length; i++) {
|
|
||||||
//parentId -1 means path root
|
|
||||||
int parentId = -1;
|
|
||||||
List folders = path.split(files[i].path);
|
|
||||||
for (var folder in folders) {
|
|
||||||
int indexInInfos = infos.lastIndexWhere((fileInfo) =>
|
|
||||||
fileInfo['name'] == folder && fileInfo['parentId'] == parentId);
|
|
||||||
|
|
||||||
if (indexInInfos != -1) {
|
|
||||||
// folder exists
|
|
||||||
parentId = infos[indexInInfos]['id'];
|
|
||||||
// add size
|
|
||||||
infos[indexInInfos]['size'] += files[i].size;
|
|
||||||
} else {
|
|
||||||
// parent not exist
|
|
||||||
// create one and add index to parent's children
|
|
||||||
infos.add({
|
|
||||||
'level': folders.indexOf(folder),
|
|
||||||
'id': idNext,
|
|
||||||
'type': 'folder',
|
|
||||||
'name': folder,
|
|
||||||
'size': files[i].size,
|
|
||||||
'parentId': parentId,
|
|
||||||
'children': [],
|
|
||||||
});
|
|
||||||
openedFolders.add(idNext);
|
|
||||||
if (parentId != -1) {
|
|
||||||
infos[parentId]['children'].add(idNext);
|
|
||||||
}
|
|
||||||
parentId = idNext;
|
|
||||||
idNext++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
//add one file, add index to parent
|
|
||||||
infos.add({
|
|
||||||
'id': idNext,
|
|
||||||
'type': 'file',
|
|
||||||
'fileId': i,
|
|
||||||
'level': folders.length,
|
|
||||||
'name': files[i].name,
|
|
||||||
'size': files[i].size,
|
|
||||||
'parentId': parentId,
|
|
||||||
//TODO add unselected logic
|
|
||||||
'selected': true
|
|
||||||
});
|
|
||||||
selectedFileIds.add(i);
|
|
||||||
if (parentId != -1) {
|
|
||||||
infos[parentId]['children'].add(idNext);
|
|
||||||
}
|
|
||||||
idNext++;
|
|
||||||
}
|
|
||||||
parentController.fileInfos.value = infos;
|
|
||||||
parentController.selectedIndexes.value = selectedFileIds.cast();
|
|
||||||
parentController.openedFolders.value = openedFolders;
|
|
||||||
List<fluent.TreeViewItem> treeItems = buildTreeViewItemsR(0, -1);
|
|
||||||
return treeItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Widget build(BuildContext context) {
|
|
||||||
return fluent.FluentTheme(
|
|
||||||
data: appController.downloaderConfig.value.extra.themeMode == 'system'
|
|
||||||
? fluent.FluentThemeData(brightness: ui.window.platformBrightness)
|
|
||||||
: appController.downloaderConfig.value.extra.themeMode == 'light'
|
|
||||||
? fluent.FluentThemeData(brightness: Brightness.light)
|
|
||||||
: fluent.FluentThemeData(brightness: Brightness.dark),
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Padding(padding: EdgeInsets.only(top: 10)),
|
|
||||||
Text(
|
|
||||||
'selectFile'.tr,
|
|
||||||
// style: TextStyle(color: themeData.hintColor),
|
|
||||||
),
|
|
||||||
Expanded(
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.only(top: 10),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
border: Border.all(color: Colors.grey, width: 1),
|
|
||||||
borderRadius: BorderRadius.circular(5)),
|
|
||||||
child: fluent.TreeView(
|
|
||||||
selectionMode: fluent.TreeViewSelectionMode.multiple,
|
|
||||||
shrinkWrap: false,
|
|
||||||
// addRepaintBoundaries: false,
|
|
||||||
usePrototypeItem: true,
|
|
||||||
// cacheExtent: 20,
|
|
||||||
// onItemInvoked: (item, reason) async => {},
|
|
||||||
onSecondaryTap: (item, details) async {
|
|
||||||
debugPrint(
|
|
||||||
'onSecondaryTap $item at ${details.globalPosition}');
|
|
||||||
},
|
|
||||||
// onSelectionChanged: (selectedItems) async => {},
|
|
||||||
narrowSpacing: true,
|
|
||||||
items: items,
|
|
||||||
// scrollPrimary: true,
|
|
||||||
))),
|
|
||||||
],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,296 @@
|
|||||||
|
import 'package:checkable_treeview/checkable_treeview.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:toggle_switch/toggle_switch.dart';
|
||||||
|
|
||||||
|
import '../../api/model/resource.dart';
|
||||||
|
import '../../icon/gopeed_icons.dart';
|
||||||
|
import '../../util/util.dart';
|
||||||
|
import 'file_icon.dart';
|
||||||
|
import 'responsive_builder.dart';
|
||||||
|
import 'sort_icon_button.dart';
|
||||||
|
|
||||||
|
const _toggleSwitchIcons = [
|
||||||
|
Gopeed.file_video,
|
||||||
|
Gopeed.file_audio,
|
||||||
|
Gopeed.file_image,
|
||||||
|
];
|
||||||
|
const _sizeGapWidth = 72.0;
|
||||||
|
|
||||||
|
class FileTreeView extends StatefulWidget {
|
||||||
|
final List<FileInfo> files;
|
||||||
|
final List<int> initialValues;
|
||||||
|
final Function(List<int>) onSelectionChanged;
|
||||||
|
|
||||||
|
const FileTreeView(
|
||||||
|
{Key? key,
|
||||||
|
required this.files,
|
||||||
|
required this.initialValues,
|
||||||
|
required this.onSelectionChanged})
|
||||||
|
: super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FileTreeView> createState() => _FileTreeViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FileTreeViewState extends State<FileTreeView> {
|
||||||
|
late GlobalKey<TreeViewState<int>> key;
|
||||||
|
late int totalSize;
|
||||||
|
int? toggleSwitchIndex;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
key = GlobalKey<TreeViewState<int>>();
|
||||||
|
totalSize = widget.files
|
||||||
|
.fold(0, (previousValue, element) => previousValue + element.size);
|
||||||
|
widget.onSelectionChanged(widget.initialValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final selectedFileCount =
|
||||||
|
key.currentState?.getSelectedValues().where((e) => e != null).length ??
|
||||||
|
widget.files.length;
|
||||||
|
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(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: Theme.of(context).dividerColor),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
child: TreeView(
|
||||||
|
key: key,
|
||||||
|
nodes: buildTreeNodes(),
|
||||||
|
showExpandCollapseButton: true,
|
||||||
|
showSelectAll: true,
|
||||||
|
onSelectionChanged: (selectedValues) {
|
||||||
|
setState(() {});
|
||||||
|
widget.onSelectionChanged(selectedValues
|
||||||
|
.where((e) => e != null)
|
||||||
|
.map((e) => e!)
|
||||||
|
.toList());
|
||||||
|
},
|
||||||
|
selectAllTrailing: (context) {
|
||||||
|
return Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('name'.tr),
|
||||||
|
SortIconButton(
|
||||||
|
onStateChanged: (state) {
|
||||||
|
switch (state) {
|
||||||
|
case SortState.asc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return (p0.label as Text)
|
||||||
|
.data!
|
||||||
|
.compareTo((p1.label as Text).data!);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case SortState.desc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return (p1.label as Text)
|
||||||
|
.data!
|
||||||
|
.compareTo((p0.label as Text).data!);
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key.currentState?.sort(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text('size'.tr),
|
||||||
|
SortIconButton(
|
||||||
|
onStateChanged: (state) {
|
||||||
|
switch (state) {
|
||||||
|
case SortState.asc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return calcSelectedSize(p0)
|
||||||
|
.compareTo(calcSelectedSize(p1));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case SortState.desc:
|
||||||
|
key.currentState?.sort((p0, p1) {
|
||||||
|
return calcSelectedSize(p1)
|
||||||
|
.compareTo(calcSelectedSize(p0));
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
key.currentState?.sort(null);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
!ResponsiveBuilder.isNarrow(context)
|
||||||
|
? Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
filterRow,
|
||||||
|
countRow,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
filterRow,
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
countRow,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
int calcSelectedSize(TreeNode<int>? node) {
|
||||||
|
if (key.currentState == null) {
|
||||||
|
return widget.files
|
||||||
|
.fold(0, (previousValue, element) => previousValue + element.size);
|
||||||
|
}
|
||||||
|
|
||||||
|
final selectedFileIndexes = node == null
|
||||||
|
? key.currentState?.getSelectedValues()
|
||||||
|
: (node.value != null
|
||||||
|
? [node.value]
|
||||||
|
: key.currentState?.getChildSelectedValues(node));
|
||||||
|
|
||||||
|
if (selectedFileIndexes == null) return 0;
|
||||||
|
return selectedFileIndexes.where((e) => e != null).map((e) => e!).fold(0,
|
||||||
|
(previousValue, element) => previousValue + widget.files[element].size);
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TreeNode<int>> buildTreeNodes() {
|
||||||
|
final List<TreeNode<int>> rootNodes = [];
|
||||||
|
final Map<String, TreeNode<int>> dirNodes = {};
|
||||||
|
|
||||||
|
for (var i = 0; i < widget.files.length; i++) {
|
||||||
|
final file = widget.files[i];
|
||||||
|
final parts = file.path.split('/');
|
||||||
|
String currentPath = '';
|
||||||
|
TreeNode<int>? parentNode;
|
||||||
|
|
||||||
|
// Create or get directory nodes
|
||||||
|
for (final part in parts) {
|
||||||
|
if (part.isEmpty) continue;
|
||||||
|
|
||||||
|
currentPath += '/$part';
|
||||||
|
if (!dirNodes.containsKey(currentPath)) {
|
||||||
|
final node = TreeNode<int>(
|
||||||
|
label: Text(part),
|
||||||
|
icon: Icon(
|
||||||
|
fileIcon(part, isFolder: true),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
trailing: (context, node) {
|
||||||
|
final size = calcSelectedSize(node);
|
||||||
|
return size > 0
|
||||||
|
? Text(Util.fmtByte(calcSelectedSize(node)),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall)
|
||||||
|
: const SizedBox(width: _sizeGapWidth);
|
||||||
|
},
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
dirNodes[currentPath] = node;
|
||||||
|
|
||||||
|
if (parentNode == null) {
|
||||||
|
rootNodes.add(node);
|
||||||
|
} else {
|
||||||
|
parentNode.children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentNode = dirNodes[currentPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file node using file.name
|
||||||
|
final fileNode = TreeNode<int>(
|
||||||
|
label: Text(file.name),
|
||||||
|
value: i,
|
||||||
|
icon: Icon(fileIcon(file.name, isFolder: false), size: 18),
|
||||||
|
trailing: (context, node) {
|
||||||
|
return file.size > 0
|
||||||
|
? Text(Util.fmtByte(file.size),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall)
|
||||||
|
: const SizedBox(width: _sizeGapWidth);
|
||||||
|
},
|
||||||
|
isSelected: widget.initialValues.contains(i),
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add file node to parent or root
|
||||||
|
if (parentNode != null) {
|
||||||
|
parentNode.children.add(fileNode);
|
||||||
|
} else {
|
||||||
|
rootNodes.add(fileNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../icon/gopeed_icons.dart';
|
||||||
|
|
||||||
|
enum SortState { none, asc, desc }
|
||||||
|
|
||||||
|
class SortIconButton extends StatefulWidget {
|
||||||
|
final double size;
|
||||||
|
final Color? color;
|
||||||
|
final Color? activeColor;
|
||||||
|
final SortState initialState;
|
||||||
|
final Function(SortState) onStateChanged;
|
||||||
|
|
||||||
|
const SortIconButton({
|
||||||
|
Key? key,
|
||||||
|
this.size = 18.0,
|
||||||
|
this.color,
|
||||||
|
this.activeColor,
|
||||||
|
this.initialState = SortState.none,
|
||||||
|
required this.onStateChanged,
|
||||||
|
}) : super(key: key);
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<SortIconButton> createState() => _SortIconState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _SortIconState extends State<SortIconButton> {
|
||||||
|
late SortState _currentState;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_currentState = widget.initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _toggleState() {
|
||||||
|
setState(() {
|
||||||
|
switch (_currentState) {
|
||||||
|
case SortState.none:
|
||||||
|
_currentState = SortState.asc;
|
||||||
|
break;
|
||||||
|
case SortState.asc:
|
||||||
|
_currentState = SortState.desc;
|
||||||
|
break;
|
||||||
|
case SortState.desc:
|
||||||
|
_currentState = SortState.none;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
widget.onStateChanged(_currentState);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return InkWell(
|
||||||
|
customBorder: const CircleBorder(),
|
||||||
|
onTap: _toggleState,
|
||||||
|
child: SizedBox(
|
||||||
|
width: widget.size,
|
||||||
|
height: widget.size,
|
||||||
|
child: _currentState == SortState.none
|
||||||
|
? Icon(
|
||||||
|
Gopeed.sort,
|
||||||
|
size: widget.size,
|
||||||
|
color: widget.color,
|
||||||
|
)
|
||||||
|
: Column(
|
||||||
|
children: [
|
||||||
|
ClipRect(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.topCenter,
|
||||||
|
heightFactor: 0.5,
|
||||||
|
child: Icon(
|
||||||
|
Gopeed.sort,
|
||||||
|
size: widget.size,
|
||||||
|
color: _currentState == SortState.asc
|
||||||
|
? (widget.activeColor ??
|
||||||
|
Theme.of(context).colorScheme.primary)
|
||||||
|
: widget.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
ClipRect(
|
||||||
|
child: Align(
|
||||||
|
alignment: Alignment.bottomCenter,
|
||||||
|
heightFactor: 0.5,
|
||||||
|
child: Icon(
|
||||||
|
Gopeed.sort,
|
||||||
|
size: widget.size,
|
||||||
|
color: _currentState == SortState.desc
|
||||||
|
? (widget.activeColor ??
|
||||||
|
Theme.of(context).colorScheme.primary)
|
||||||
|
: widget.color,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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) =>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const enUS = {
|
|||||||
'on': 'On',
|
'on': 'On',
|
||||||
'off': 'Off',
|
'off': 'Off',
|
||||||
'selectAll': 'Select All',
|
'selectAll': 'Select All',
|
||||||
|
'select': 'Select',
|
||||||
'task': 'Tasks',
|
'task': 'Tasks',
|
||||||
'downloading': 'downloading',
|
'downloading': 'downloading',
|
||||||
'downloaded': 'downloaded',
|
'downloaded': 'downloaded',
|
||||||
@@ -20,8 +21,7 @@ const enUS = {
|
|||||||
'followSettings': 'Follow Settings',
|
'followSettings': 'Follow Settings',
|
||||||
'downloadLink': 'Download Link',
|
'downloadLink': 'Download Link',
|
||||||
'downloadLinkValid': 'Please enter the download link',
|
'downloadLinkValid': 'Please enter the download link',
|
||||||
'downloadLinkHit':
|
'downloadLinkHit': 'Please enter the download link, one per line@append',
|
||||||
'Please enter the download link, HTTP/HTTPS/MAGNET supported@append',
|
|
||||||
'downloadLinkHitDesktop': ', or drag the torrent file here directly',
|
'downloadLinkHitDesktop': ', or drag the torrent file here directly',
|
||||||
'download': 'Download',
|
'download': 'Download',
|
||||||
'noFileSelected': 'Please select at least one file to continue.',
|
'noFileSelected': 'Please select at least one file to continue.',
|
||||||
@@ -65,9 +65,11 @@ const enUS = {
|
|||||||
'developer': 'Developer',
|
'developer': 'Developer',
|
||||||
'logDirectory': 'Log Directory',
|
'logDirectory': 'Log Directory',
|
||||||
'show': 'Show',
|
'show': 'Show',
|
||||||
|
'continue': 'Continue',
|
||||||
|
'pause': 'Pause',
|
||||||
'startAll': 'Start All',
|
'startAll': 'Start All',
|
||||||
'pauseAll': 'Pause All',
|
'pauseAll': 'Pause All',
|
||||||
'deleteTask': 'Delete Task',
|
'deleteTask': 'Delete @count tasks',
|
||||||
'deleteTaskTip': 'Keep downloaded files',
|
'deleteTaskTip': 'Keep downloaded files',
|
||||||
'delete': 'Delete',
|
'delete': 'Delete',
|
||||||
'newVersionTitle': 'Discover new version @version',
|
'newVersionTitle': 'Discover new version @version',
|
||||||
@@ -103,10 +105,18 @@ 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',
|
||||||
'downloadPath': 'Download Path',
|
'downloadPath': 'Download Path',
|
||||||
'skipVerifyCert': 'Skip Certificate Verification',
|
'skipVerifyCert': 'Skip Certificate Verification',
|
||||||
|
'name': 'Name',
|
||||||
|
'size': 'Size',
|
||||||
|
'unknown': 'Unknown',
|
||||||
|
'fileSelectedCount': 'Files: ',
|
||||||
|
'fileSelectedSize': 'Size: ',
|
||||||
|
'httpHeaderName': 'Header Name',
|
||||||
|
'httpHeaderValue': 'Header Value',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -63,7 +63,7 @@ const esES = {
|
|||||||
'show': 'Mostrar',
|
'show': 'Mostrar',
|
||||||
'startAll': 'Iniciar Todo',
|
'startAll': 'Iniciar Todo',
|
||||||
'pauseAll': 'Pausar Todo',
|
'pauseAll': 'Pausar Todo',
|
||||||
'deleteTask': 'Eliminar Tarea',
|
'deleteTask': 'Eliminar @count tareas',
|
||||||
'deleteTaskTip': 'Mantener archivos descargados',
|
'deleteTaskTip': 'Mantener archivos descargados',
|
||||||
'delete': 'Eliminar',
|
'delete': 'Eliminar',
|
||||||
'newVersionTitle': 'Nueva versión @version disponible',
|
'newVersionTitle': 'Nueva versión @version disponible',
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ const faIR = {
|
|||||||
'effectAfterRestart': 'Effect after restart',
|
'effectAfterRestart': 'Effect after restart',
|
||||||
'startAll': 'شروع همه',
|
'startAll': 'شروع همه',
|
||||||
'pauseAll': 'توقف همه',
|
'pauseAll': 'توقف همه',
|
||||||
'deleteTask': 'پاک کردن کار',
|
'deleteTask': 'حذف @count کار',
|
||||||
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
|
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
|
||||||
'delete': 'پاک کردن',
|
'delete': 'پاک کردن',
|
||||||
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const frFR = {
|
|||||||
'show': 'Afficher',
|
'show': 'Afficher',
|
||||||
'startAll': 'Tout démarrer',
|
'startAll': 'Tout démarrer',
|
||||||
'pauseAll': 'Tout suspendre',
|
'pauseAll': 'Tout suspendre',
|
||||||
'deleteTask': 'Supprimer la tâche',
|
'deleteTask': 'Supprimer @count tâches',
|
||||||
'deleteTaskTip': 'Conserver les fichiers téléchargés',
|
'deleteTaskTip': 'Conserver les fichiers téléchargés',
|
||||||
'delete': 'Supprimer',
|
'delete': 'Supprimer',
|
||||||
'newVersionTitle': 'Nouvelle version @version disponible',
|
'newVersionTitle': 'Nouvelle version @version disponible',
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ const idID = {
|
|||||||
'show': 'Tampilkan',
|
'show': 'Tampilkan',
|
||||||
'startAll': 'Mulai Semua',
|
'startAll': 'Mulai Semua',
|
||||||
'pauseAll': 'Jeda Semua',
|
'pauseAll': 'Jeda Semua',
|
||||||
'deleteTask': 'Hapus Tugas',
|
'deleteTask': 'Hapus @count Tugas',
|
||||||
'deleteTaskTip': 'Simpan file yang terunduh',
|
'deleteTaskTip': 'Simpan file yang terunduh',
|
||||||
'delete': 'Hapus',
|
'delete': 'Hapus',
|
||||||
'newVersionTitle': 'Temukan versi batu @version',
|
'newVersionTitle': 'Temukan versi batu @version',
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const itIT = {
|
|||||||
'show': 'Mostra',
|
'show': 'Mostra',
|
||||||
'startAll': 'Avvia tutti',
|
'startAll': 'Avvia tutti',
|
||||||
'pauseAll': 'Mtti in pausa tutti',
|
'pauseAll': 'Mtti in pausa tutti',
|
||||||
'deleteTask': 'Elimina attività',
|
'deleteTask': 'Elimina @count attività',
|
||||||
'deleteTaskTip': 'Conserva i file scaricati',
|
'deleteTaskTip': 'Conserva i file scaricati',
|
||||||
'delete': 'Elimina',
|
'delete': 'Elimina',
|
||||||
'newVersionTitle': 'Scopri la nuova versione @version',
|
'newVersionTitle': 'Scopri la nuova versione @version',
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const jaJP = {
|
|||||||
'effectAfterRestart': '再起動後の効果',
|
'effectAfterRestart': '再起動後の効果',
|
||||||
'startAll': 'すべてを開始',
|
'startAll': 'すべてを開始',
|
||||||
'pauseAll': 'すべてを一時停止',
|
'pauseAll': 'すべてを一時停止',
|
||||||
'deleteTask': 'タスクを削除',
|
'deleteTask': '@count タスクを削除',
|
||||||
'deleteTaskTip': 'ダウンロードしたファイルを保持',
|
'deleteTaskTip': 'ダウンロードしたファイルを保持',
|
||||||
'delete': '削除',
|
'delete': '削除',
|
||||||
'newVersionTitle': '新しいバージョン @version を発見する',
|
'newVersionTitle': '新しいバージョン @version を発見する',
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const plPL = {
|
|||||||
'show': 'Pokaż',
|
'show': 'Pokaż',
|
||||||
'startAll': 'Zacznij wszystkie',
|
'startAll': 'Zacznij wszystkie',
|
||||||
'pauseAll': 'Zatrzymaj wszystkie',
|
'pauseAll': 'Zatrzymaj wszystkie',
|
||||||
'deleteTask': 'Usuń zadanie',
|
'deleteTask': 'Usuń @count zadania',
|
||||||
'deleteTaskTip': 'Zachowaj pobrane pliki',
|
'deleteTaskTip': 'Zachowaj pobrane pliki',
|
||||||
'delete': 'Usuń',
|
'delete': 'Usuń',
|
||||||
'newVersionTitle': 'Sprawdź aktualizację @version',
|
'newVersionTitle': 'Sprawdź aktualizację @version',
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ const ruRU = {
|
|||||||
'effectAfterRestart': 'Эффект после перезагрузки',
|
'effectAfterRestart': 'Эффект после перезагрузки',
|
||||||
'startAll': 'Запустить все',
|
'startAll': 'Запустить все',
|
||||||
'pauseAll': 'Приостановить все',
|
'pauseAll': 'Приостановить все',
|
||||||
'deleteTask': 'Удалить задачу',
|
'deleteTask': 'Удалить @count задач',
|
||||||
'deleteTaskTip': 'Сохранить загруженные файлы',
|
'deleteTaskTip': 'Сохранить загруженные файлы',
|
||||||
'delete': 'Удалить',
|
'delete': 'Удалить',
|
||||||
'newVersionTitle': 'Обнаружена новая версия @version',
|
'newVersionTitle': 'Обнаружена новая версия @version',
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ const taTA = {
|
|||||||
'show': 'காட்டு',
|
'show': 'காட்டு',
|
||||||
'startAll': 'அனைத்தையும் தொடங்கு',
|
'startAll': 'அனைத்தையும் தொடங்கு',
|
||||||
'pauseAll': 'அனைத்தையும் நிறுத்திவை',
|
'pauseAll': 'அனைத்தையும் நிறுத்திவை',
|
||||||
'deleteTask': 'பணியை நீக்கு',
|
'deleteTask': 'பணிகளை நீக்கு @count',
|
||||||
'deleteTaskTip': 'பதிவிறக்கம் செய்யப்பட்ட கோப்புகளை வைத்திரு',
|
'deleteTaskTip': 'பதிவிறக்கம் செய்யப்பட்ட கோப்புகளை வைத்திரு',
|
||||||
'delete': 'நீக்கு',
|
'delete': 'நீக்கு',
|
||||||
'newVersionTitle': 'புதிய பதிப்பைக் கண்டறியவும் @version',
|
'newVersionTitle': 'புதிய பதிப்பைக் கண்டறியவும் @version',
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const trTR = {
|
|||||||
'show': 'Göster',
|
'show': 'Göster',
|
||||||
'startAll': 'Hepsini başlat',
|
'startAll': 'Hepsini başlat',
|
||||||
'pauseAll': 'Hepsini durdur',
|
'pauseAll': 'Hepsini durdur',
|
||||||
'deleteTask': 'Görevleri 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ü keşfet @version',
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ const viVN = {
|
|||||||
'effectAfterRestart': 'Hiệu lực sau khi khởi động lại',
|
'effectAfterRestart': 'Hiệu lực sau khi khởi động lại',
|
||||||
'startAll': 'Bắt đầu tất cả',
|
'startAll': 'Bắt đầu tất cả',
|
||||||
'pauseAll': 'Tạm dừng tất cả',
|
'pauseAll': 'Tạm dừng tất cả',
|
||||||
'deleteTask': 'Xóa nhiệm vụ',
|
'deleteTask': 'Xóa @count nhiệm vụ',
|
||||||
'deleteTaskTip': 'Giữ các tệp đã tải về',
|
'deleteTaskTip': 'Giữ các tệp đã tải về',
|
||||||
'delete': 'Xóa',
|
'delete': 'Xóa',
|
||||||
'newVersionTitle': 'Khám phá phiên bản mới @version',
|
'newVersionTitle': 'Khám phá phiên bản mới @version',
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const zhCN = {
|
|||||||
'on': '开启',
|
'on': '开启',
|
||||||
'off': '关闭',
|
'off': '关闭',
|
||||||
'selectAll': '全选',
|
'selectAll': '全选',
|
||||||
|
'select': '选择',
|
||||||
'task': '任务',
|
'task': '任务',
|
||||||
'downloading': '下载中',
|
'downloading': '下载中',
|
||||||
'downloaded': '已完成',
|
'downloaded': '已完成',
|
||||||
@@ -20,7 +21,7 @@ const zhCN = {
|
|||||||
'followSettings': '跟随设置',
|
'followSettings': '跟随设置',
|
||||||
'downloadLink': '下载链接',
|
'downloadLink': '下载链接',
|
||||||
'downloadLinkValid': '请输入下载链接',
|
'downloadLinkValid': '请输入下载链接',
|
||||||
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
'downloadLinkHit': '请输入下载链接,支持批量下载,每行一个链接@append',
|
||||||
'downloadLinkHitDesktop': ',也可以直接拖拽种子文件到此处',
|
'downloadLinkHitDesktop': ',也可以直接拖拽种子文件到此处',
|
||||||
'download': '下载',
|
'download': '下载',
|
||||||
'noFileSelected': '请至少选择一个文件下载',
|
'noFileSelected': '请至少选择一个文件下载',
|
||||||
@@ -63,9 +64,11 @@ const zhCN = {
|
|||||||
'developer': '开发者',
|
'developer': '开发者',
|
||||||
'logDirectory': '日志目录',
|
'logDirectory': '日志目录',
|
||||||
'show': '显示',
|
'show': '显示',
|
||||||
|
'continue': '继续',
|
||||||
|
'pause': '暂停',
|
||||||
'startAll': '全部开始',
|
'startAll': '全部开始',
|
||||||
'pauseAll': '全部暂停',
|
'pauseAll': '全部暂停',
|
||||||
'deleteTask': '删除任务',
|
'deleteTask': '删除 @count 个任务',
|
||||||
'deleteTaskTip': '保留已下载的文件',
|
'deleteTaskTip': '保留已下载的文件',
|
||||||
'delete': '删除',
|
'delete': '删除',
|
||||||
'newVersionTitle': '发现新版本 @version',
|
'newVersionTitle': '发现新版本 @version',
|
||||||
@@ -100,10 +103,18 @@ const zhCN = {
|
|||||||
'seedKeep': '持续做种直到手动停止',
|
'seedKeep': '持续做种直到手动停止',
|
||||||
'seedRatio': '做种分享率',
|
'seedRatio': '做种分享率',
|
||||||
'seedTime': '做种时间(分钟)',
|
'seedTime': '做种时间(分钟)',
|
||||||
|
'setAsDefaultBtClient': '设为系统默认 BT 客户端',
|
||||||
'taskDetail': '任务详情',
|
'taskDetail': '任务详情',
|
||||||
'taskName': '任务名称',
|
'taskName': '任务名称',
|
||||||
'taskUrl': '任务链接',
|
'taskUrl': '任务链接',
|
||||||
'downloadPath': '下载路径',
|
'downloadPath': '下载路径',
|
||||||
'skipVerifyCert': '跳过证书验证',
|
'skipVerifyCert': '跳过证书验证',
|
||||||
|
'name': '名称',
|
||||||
|
'size': '大小',
|
||||||
|
'unknown': '未知',
|
||||||
|
'fileSelectedCount': '文件数:',
|
||||||
|
'fileSelectedSize': '大小:',
|
||||||
|
'httpHeaderName': '请求头名称',
|
||||||
|
'httpHeaderValue': '请求头值',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -8,6 +8,7 @@ const zhTW = {
|
|||||||
'on': '開啟',
|
'on': '開啟',
|
||||||
'off': '關閉',
|
'off': '關閉',
|
||||||
'selectAll': '全選',
|
'selectAll': '全選',
|
||||||
|
'select': '選擇',
|
||||||
'task': '任務',
|
'task': '任務',
|
||||||
'downloading': '下載中',
|
'downloading': '下載中',
|
||||||
'downloaded': '已下載',
|
'downloaded': '已下載',
|
||||||
@@ -20,7 +21,7 @@ const zhTW = {
|
|||||||
'followSettings': '跟隨設定',
|
'followSettings': '跟隨設定',
|
||||||
'downloadLink': '下載連結',
|
'downloadLink': '下載連結',
|
||||||
'downloadLinkValid': '請輸入下載連結',
|
'downloadLinkValid': '請輸入下載連結',
|
||||||
'downloadLinkHit': '請輸入下載連結,支援 HTTP/HTTPS/MAGNET@append',
|
'downloadLinkHit': '請輸入下載連結,支援批量下載,每行一個連結@append',
|
||||||
'downloadLinkHitDesktop': ',或直接拖曳種子檔到此處',
|
'downloadLinkHitDesktop': ',或直接拖曳種子檔到此處',
|
||||||
'download': '下載',
|
'download': '下載',
|
||||||
'noFileSelected': '請至少選擇一個檔案以繼續。',
|
'noFileSelected': '請至少選擇一個檔案以繼續。',
|
||||||
@@ -63,9 +64,11 @@ const zhTW = {
|
|||||||
'developer': '開發者',
|
'developer': '開發者',
|
||||||
'logDirectory': '日誌目錄',
|
'logDirectory': '日誌目錄',
|
||||||
'show': '顯示',
|
'show': '顯示',
|
||||||
|
'continue': '繼續',
|
||||||
|
'pause': '暫停',
|
||||||
'startAll': '全部開始',
|
'startAll': '全部開始',
|
||||||
'pauseAll': '全部暫停',
|
'pauseAll': '全部暫停',
|
||||||
'deleteTask': '刪除任務',
|
'deleteTask': '刪除 @count 個任務',
|
||||||
'deleteTaskTip': '保留已下載的檔案',
|
'deleteTaskTip': '保留已下載的檔案',
|
||||||
'delete': '刪除',
|
'delete': '刪除',
|
||||||
'newVersionTitle': '發現新版本 @version',
|
'newVersionTitle': '發現新版本 @version',
|
||||||
@@ -100,10 +103,18 @@ const zhTW = {
|
|||||||
'seedKeep': '持續做種直到手動停止',
|
'seedKeep': '持續做種直到手動停止',
|
||||||
'seedRatio': '做種分享率',
|
'seedRatio': '做種分享率',
|
||||||
'seedTime': '做種時間(分鐘)',
|
'seedTime': '做種時間(分鐘)',
|
||||||
|
'setAsDefaultBtClient': '設為系統預設 BT 客戶端',
|
||||||
'taskDetail': '任務詳情',
|
'taskDetail': '任務詳情',
|
||||||
'taskName': '任務名稱',
|
'taskName': '任務名稱',
|
||||||
'taskUrl': '任務連結',
|
'taskUrl': '任務連結',
|
||||||
'downloadPath': '下載路徑',
|
'downloadPath': '下載路徑',
|
||||||
'skipVerifyCert': '跳過憑證驗證',
|
'skipVerifyCert': '跳過憑證驗證',
|
||||||
|
'name': '名稱',
|
||||||
|
'size': '大小',
|
||||||
|
'unknown': '未知',
|
||||||
|
'fileSelectedCount': '文件數:',
|
||||||
|
'fileSelectedSize': '大小:',
|
||||||
|
'httpHeaderName': '標頭名稱',
|
||||||
|
'httpHeaderValue': '標頭值',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
/// Flutter icons Gopeed
|
||||||
|
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
|
||||||
|
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
|
||||||
|
///
|
||||||
|
/// To use this font, place it in your fonts/ directory and include the
|
||||||
|
/// following in your pubspec.yaml
|
||||||
|
///
|
||||||
|
/// flutter:
|
||||||
|
/// fonts:
|
||||||
|
/// - family: Gopeed
|
||||||
|
/// fonts:
|
||||||
|
/// - asset: fonts/Gopeed.ttf
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// * Entypo, Copyright (C) 2012 by Daniel Bruce
|
||||||
|
/// Author: Daniel Bruce
|
||||||
|
/// License: SIL (http://scripts.sil.org/OFL)
|
||||||
|
/// Homepage: http://www.entypo.com
|
||||||
|
/// * Zocial, Copyright (C) 2012 by Sam Collins
|
||||||
|
/// Author: Sam Collins
|
||||||
|
/// License: MIT (http://opensource.org/licenses/mit-license.php)
|
||||||
|
/// Homepage: http://zocial.smcllns.com/
|
||||||
|
/// * Font Awesome 5, Copyright (C) 2016 by Dave Gandy
|
||||||
|
/// Author: Dave Gandy
|
||||||
|
/// License: SIL (https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt)
|
||||||
|
/// Homepage: http://fortawesome.github.com/Font-Awesome/
|
||||||
|
///
|
||||||
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
|
class Gopeed {
|
||||||
|
Gopeed._();
|
||||||
|
|
||||||
|
static const _kFontFam = 'Gopeed';
|
||||||
|
static const String? _kFontPkg = null;
|
||||||
|
|
||||||
|
static const IconData install = IconData(0xe800, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData android = IconData(0xe801, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData cd = IconData(0xe802, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData folder_bt = IconData(0xe803, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_bt = IconData(0xe804, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData folder = IconData(0xf07b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData sort = IconData(0xf0dc, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData html5 = IconData(0xf13b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file = IconData(0xf15b, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_alt = IconData(0xf15c, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_pdf = IconData(0xf1c1, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_word = IconData(0xf1c2, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_excel = IconData(0xf1c3, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_powerpoint = IconData(0xf1c4, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_image = IconData(0xf1c5, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_archive = IconData(0xf1c6, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_audio = IconData(0xf1c7, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_video = IconData(0xf1c8, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData file_code = IconData(0xf1c9, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
static const IconData app_store_ios = IconData(0xf370, fontFamily: _kFontFam, fontPackage: _kFontPkg);
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import 'util/locale_manager.dart';
|
|||||||
import 'util/log_util.dart';
|
import 'util/log_util.dart';
|
||||||
import 'util/mac_secure_util.dart';
|
import 'util/mac_secure_util.dart';
|
||||||
import 'util/package_info.dart';
|
import 'util/package_info.dart';
|
||||||
|
import 'util/scheme_register/scheme_register.dart';
|
||||||
import 'util/util.dart';
|
import 'util/util.dart';
|
||||||
|
|
||||||
class Args {
|
class Args {
|
||||||
@@ -81,6 +82,15 @@ Future<void> init(Args args) async {
|
|||||||
logger.e("libgopeed init fail", e);
|
logger.e("libgopeed init fail", e);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
registerUrlScheme("gopeed");
|
||||||
|
if (controller.downloaderConfig.value.extra.defaultBtClient) {
|
||||||
|
registerDefaultTorrentClient();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
logger.e("register scheme fail", e);
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await controller.loadDownloaderConfig();
|
await controller.loadDownloaderConfig();
|
||||||
MacSecureUtil.loadBookmark();
|
MacSecureUtil.loadBookmark();
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import 'dart:core';
|
||||||
|
import 'package:checkable_treeview/checkable_treeview.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../api/model/resource.dart';
|
||||||
|
import '../app/views/file_icon.dart';
|
||||||
|
import 'util.dart';
|
||||||
|
|
||||||
|
extension ListFileInfoExtension on List<FileInfo> {
|
||||||
|
List<TreeNode<int>> toTreeNodes() {
|
||||||
|
final List<TreeNode<int>> rootNodes = [];
|
||||||
|
final Map<String, TreeNode<int>> dirNodes = {};
|
||||||
|
var nodeIndex = 0;
|
||||||
|
|
||||||
|
for (var i = 0; i < length; i++) {
|
||||||
|
final file = this[i];
|
||||||
|
final parts = file.path.split('/');
|
||||||
|
String currentPath = '';
|
||||||
|
TreeNode<int>? parentNode;
|
||||||
|
|
||||||
|
// Create or get directory nodes
|
||||||
|
for (final part in parts) {
|
||||||
|
if (part.isEmpty) continue;
|
||||||
|
|
||||||
|
currentPath += '/$part';
|
||||||
|
if (!dirNodes.containsKey(currentPath)) {
|
||||||
|
final node = TreeNode<int>(
|
||||||
|
value: nodeIndex++,
|
||||||
|
label: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(file.name),
|
||||||
|
Text(Util.fmtByte(file.size)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
icon: Icon(
|
||||||
|
fileIcon(part, isFolder: true),
|
||||||
|
size: 18,
|
||||||
|
),
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
dirNodes[currentPath] = node;
|
||||||
|
|
||||||
|
if (parentNode == null) {
|
||||||
|
rootNodes.add(node);
|
||||||
|
} else {
|
||||||
|
parentNode.children.add(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parentNode = dirNodes[currentPath];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create file node using file.name
|
||||||
|
final fileNode = TreeNode<int>(
|
||||||
|
value: nodeIndex++,
|
||||||
|
label: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Text(file.name),
|
||||||
|
Text(Util.fmtByte(file.size)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
icon: Icon(fileIcon(file.name, isFolder: false), size: 18),
|
||||||
|
children: [],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add file node to parent or root
|
||||||
|
if (parentNode != null) {
|
||||||
|
parentNode.children.add(fileNode);
|
||||||
|
} else {
|
||||||
|
rootNodes.add(fileNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rootNodes;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
findIcon(String filename) {
|
|
||||||
String res = 'file';
|
|
||||||
String ext = filename.substring(filename.lastIndexOf('.') + 1);
|
|
||||||
for (var iconMap in iconMaps) {
|
|
||||||
if (iconMap['extensions'].contains(ext)) {
|
|
||||||
res = iconMap['thumbnail'];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
const List iconMaps = [
|
|
||||||
//media
|
|
||||||
{
|
|
||||||
'extensions': [
|
|
||||||
'jpg',
|
|
||||||
'png',
|
|
||||||
'gif',
|
|
||||||
'bmp',
|
|
||||||
'jpeg',
|
|
||||||
'jpe',
|
|
||||||
'jif',
|
|
||||||
'jfif',
|
|
||||||
'jfi',
|
|
||||||
'webp',
|
|
||||||
'tiff',
|
|
||||||
'tif',
|
|
||||||
'ico',
|
|
||||||
'svg',
|
|
||||||
'webp'
|
|
||||||
],
|
|
||||||
'type': 'Image',
|
|
||||||
'thumbnail': 'file_image'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': [
|
|
||||||
'mp4',
|
|
||||||
'webm',
|
|
||||||
'mpg',
|
|
||||||
'mp2',
|
|
||||||
'mpeg',
|
|
||||||
'mpe',
|
|
||||||
'mpv',
|
|
||||||
'ocg',
|
|
||||||
'm4p',
|
|
||||||
'm4v',
|
|
||||||
'avi',
|
|
||||||
'wmv',
|
|
||||||
'mov',
|
|
||||||
'qt',
|
|
||||||
'flv',
|
|
||||||
'swf',
|
|
||||||
'mkv',
|
|
||||||
'rmvb',
|
|
||||||
'rm',
|
|
||||||
'vob',
|
|
||||||
'3gp',
|
|
||||||
'm4s',
|
|
||||||
'ts'
|
|
||||||
],
|
|
||||||
'type': 'Video',
|
|
||||||
'thumbnail': 'file_video'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': [
|
|
||||||
'mp3',
|
|
||||||
'ogg',
|
|
||||||
'ogm',
|
|
||||||
'wav',
|
|
||||||
'.aac',
|
|
||||||
'.ape',
|
|
||||||
'.flac',
|
|
||||||
'.flav',
|
|
||||||
'.m4a',
|
|
||||||
'.wma'
|
|
||||||
],
|
|
||||||
'type': 'Audio',
|
|
||||||
'thumbnail': 'file_audio'
|
|
||||||
},
|
|
||||||
//compressed
|
|
||||||
{
|
|
||||||
'extensions': [
|
|
||||||
'7z',
|
|
||||||
'brotli',
|
|
||||||
'bzip2',
|
|
||||||
'gz',
|
|
||||||
'gzip',
|
|
||||||
'rar',
|
|
||||||
'tgz',
|
|
||||||
'xz',
|
|
||||||
'zip',
|
|
||||||
'zstd',
|
|
||||||
'deb',
|
|
||||||
'msi',
|
|
||||||
'snap',
|
|
||||||
'iso',
|
|
||||||
'dmg',
|
|
||||||
'dockerfile',
|
|
||||||
'dockerignore'
|
|
||||||
],
|
|
||||||
'type': 'Archive',
|
|
||||||
'thumbnail': 'file_archive'
|
|
||||||
},
|
|
||||||
//office relative
|
|
||||||
{
|
|
||||||
'extensions': ['pdf'],
|
|
||||||
'type': 'Portable Document Format',
|
|
||||||
'thumbnail': 'file_pdf'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': ['txt', 'docb', 'rtf'],
|
|
||||||
'type': 'Document',
|
|
||||||
'thumbnail': 'doc_text'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': ['doc', 'docm', 'dot', 'dotm', 'docx'],
|
|
||||||
'type': 'Word Document',
|
|
||||||
'thumbnail': 'file_word'
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
'extensions': ['xlsx', 'xls', 'xlsb', 'xls', 'ods', 'fods', 'csv'],
|
|
||||||
'type': 'Excel Document',
|
|
||||||
'thumbnail': 'file_excel'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': [
|
|
||||||
'pot',
|
|
||||||
'potm',
|
|
||||||
'potx',
|
|
||||||
'ppam',
|
|
||||||
'pps',
|
|
||||||
'ppsm',
|
|
||||||
'ppsx',
|
|
||||||
'ppt',
|
|
||||||
'pptn',
|
|
||||||
'pptx'
|
|
||||||
],
|
|
||||||
'type': 'Powerpoint Document',
|
|
||||||
'thumbnail': 'file_powerpoint'
|
|
||||||
},
|
|
||||||
//windows
|
|
||||||
{
|
|
||||||
'extensions': ['exe'],
|
|
||||||
'type': 'Microsoft',
|
|
||||||
'thumbnail': 'windows'
|
|
||||||
},
|
|
||||||
//android
|
|
||||||
{
|
|
||||||
'extensions': ['apk'],
|
|
||||||
'type': 'Android',
|
|
||||||
'thumbnail': 'android'
|
|
||||||
},
|
|
||||||
//other
|
|
||||||
{
|
|
||||||
'extensions': ['lnk'],
|
|
||||||
'type': 'Shortcut',
|
|
||||||
'thumbnail': 'link_ext'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': [
|
|
||||||
'ini',
|
|
||||||
'dlc',
|
|
||||||
'config',
|
|
||||||
'conf',
|
|
||||||
'properties',
|
|
||||||
'prop',
|
|
||||||
'settings',
|
|
||||||
'option',
|
|
||||||
'props',
|
|
||||||
'toml',
|
|
||||||
'prefs',
|
|
||||||
'sln.dotsettings',
|
|
||||||
'sln.dotsettings.user',
|
|
||||||
'cfg'
|
|
||||||
],
|
|
||||||
'type': 'Settings',
|
|
||||||
'thumbnail': 'cog_alt'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'extensions': ['html', 'htm', 'xhtml', 'html_vm'],
|
|
||||||
'type': 'html',
|
|
||||||
'thumbnail': 'html5'
|
|
||||||
},
|
|
||||||
//code relative
|
|
||||||
// {
|
|
||||||
// 'extensions': ['sh', 'bat'],
|
|
||||||
// 'type': 'Batch',
|
|
||||||
// 'thumbnail': 'power_shell'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': [
|
|
||||||
// 'js',
|
|
||||||
// 'jsx',
|
|
||||||
// 'json',
|
|
||||||
// 'tsbuildinfo',
|
|
||||||
// 'json5',
|
|
||||||
// 'jsonl',
|
|
||||||
// 'ndjson'
|
|
||||||
// ],
|
|
||||||
// 'type': 'JavaScript',
|
|
||||||
// 'thumbnail': 'js'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': ['ts', 'tsx'],
|
|
||||||
// 'type': 'TypeScript',
|
|
||||||
// 'thumbnail': 'type_script_language'
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// 'extensions': ['asp', 'aspx', 'php', 'jsp'],
|
|
||||||
// 'type': 'HyperText Markup Language',
|
|
||||||
// 'thumbnail': 'file_a_s_p_x'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': [
|
|
||||||
// 'accdb',
|
|
||||||
// 'db',
|
|
||||||
// 'db3',
|
|
||||||
// 'mdb',
|
|
||||||
// 'pdb',
|
|
||||||
// 'pgsql',
|
|
||||||
// 'pkb',
|
|
||||||
// 'pks',
|
|
||||||
// 'postgres',
|
|
||||||
// 'psql',
|
|
||||||
// 'sql',
|
|
||||||
// 'sqlite',
|
|
||||||
// 'sqlite3'
|
|
||||||
// ],
|
|
||||||
// 'type': 'Database',
|
|
||||||
// 'thumbnail': 'database'
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// 'extensions': ['exe'],
|
|
||||||
// 'type': 'Executable',
|
|
||||||
// 'thumbnail': 'product'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': [
|
|
||||||
// 'c',
|
|
||||||
// 'h',
|
|
||||||
// 'cc',
|
|
||||||
// 'cpp',
|
|
||||||
// 'cxx',
|
|
||||||
// 'c++',
|
|
||||||
// 'cp',
|
|
||||||
// 'mm',
|
|
||||||
// 'mii',
|
|
||||||
// 'ii',
|
|
||||||
// 'dart',
|
|
||||||
// 'go'
|
|
||||||
// ],
|
|
||||||
// 'type': 'other Program language',
|
|
||||||
// 'thumbnail': 'file_code'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': ['py', 'py3', 'pyc', 'pylintrc', 'python-version'],
|
|
||||||
// 'type': 'Python Program',
|
|
||||||
// 'thumbnail': 'py'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': ['md', 'markdown', 'rst'],
|
|
||||||
// 'type': 'Markdown',
|
|
||||||
// 'thumbnail': 'mark_down_language'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': ['yml', 'yaml'],
|
|
||||||
// 'type': 'Yet Anoter Markup Language',
|
|
||||||
// 'thumbnail': 'file_y_m_l'
|
|
||||||
// },
|
|
||||||
//
|
|
||||||
// {
|
|
||||||
// 'type': 'Visual Studio',
|
|
||||||
// 'extensions': [
|
|
||||||
// 'csproj',
|
|
||||||
// 'ruleset',
|
|
||||||
// 'sln',
|
|
||||||
// 'suo',
|
|
||||||
// 'vb',
|
|
||||||
// 'vbs',
|
|
||||||
// 'vcxitems',
|
|
||||||
// 'vcxitems.filters',
|
|
||||||
// 'vcxproj',
|
|
||||||
// 'vcxproj.filters'
|
|
||||||
// ],
|
|
||||||
// 'thumbnail': 'visual_studio_for_windows'
|
|
||||||
// },
|
|
||||||
// {
|
|
||||||
// 'extensions': ['java'],
|
|
||||||
// 'type': 'Java Program',
|
|
||||||
// 'thumbnail': 'file_j_a_v_a'
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -8,9 +8,16 @@ void showErrorMessage(msg) {
|
|||||||
Get.snackbar(title, msg.msg!);
|
Get.snackbar(title, msg.msg!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (msg is Exception && (msg as dynamic).message is Result) {
|
if (msg is Exception) {
|
||||||
Get.snackbar(title, ((msg as dynamic).message as Result).msg!);
|
final message = (msg as dynamic).message;
|
||||||
return;
|
if (message is Result) {
|
||||||
|
Get.snackbar(title, ((msg as dynamic).message as Result).msg!);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message is String) {
|
||||||
|
Get.snackbar(title, message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Get.snackbar(title, msg.toString());
|
Get.snackbar(title, msg.toString());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:win32_registry/win32_registry.dart';
|
||||||
|
|
||||||
|
import '../../util.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add Windows registry key and value if not exists
|
||||||
|
_upsertRegistry(String keyPath, String valueName, String value) {
|
||||||
|
RegistryKey regKey;
|
||||||
|
try {
|
||||||
|
regKey = Registry.openPath(RegistryHive.currentUser,
|
||||||
|
path: keyPath, desiredAccessRights: AccessRights.allAccess);
|
||||||
|
} catch (e) {
|
||||||
|
regKey = Registry.currentUser.createKey(keyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regKey.getValueAsString(valueName) != value) {
|
||||||
|
regKey
|
||||||
|
.createValue(RegistryValue(valueName, RegistryValueType.string, value));
|
||||||
|
}
|
||||||
|
regKey.close();
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
@@ -11,4 +11,5 @@ Exec=gopeed
|
|||||||
Icon=com.gopeed.Gopeed
|
Icon=com.gopeed.Gopeed
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Utility;Internet
|
Categories=Utility;Internet
|
||||||
Keywords=Bittorrent;Downloader;
|
Keywords=Bittorrent;Downloader;
|
||||||
|
MimeType=x-scheme-handler/gopeed;x-scheme-handler/magnet;application/x-bittorrent;
|
||||||
@@ -17,6 +17,13 @@ G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION)
|
|||||||
// Implements GApplication::activate.
|
// Implements GApplication::activate.
|
||||||
static void my_application_activate(GApplication* application) {
|
static void my_application_activate(GApplication* application) {
|
||||||
MyApplication* self = MY_APPLICATION(application);
|
MyApplication* self = MY_APPLICATION(application);
|
||||||
|
|
||||||
|
GList* windows = gtk_application_get_windows(GTK_APPLICATION(application));
|
||||||
|
if (windows) {
|
||||||
|
gtk_window_present(GTK_WINDOW(windows->data));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
GtkWindow* window =
|
GtkWindow* window =
|
||||||
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application)));
|
||||||
|
|
||||||
@@ -48,8 +55,7 @@ static void my_application_activate(GApplication* application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
gtk_window_set_default_size(window, 1280, 720);
|
gtk_window_set_default_size(window, 1280, 720);
|
||||||
//gtk_widget_show(GTK_WIDGET(window));
|
gtk_widget_show(GTK_WIDGET(window));
|
||||||
gtk_widget_realize(GTK_WIDGET(window));
|
|
||||||
|
|
||||||
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
g_autoptr(FlDartProject) project = fl_dart_project_new();
|
||||||
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments);
|
||||||
@@ -59,6 +65,7 @@ static void my_application_activate(GApplication* application) {
|
|||||||
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view));
|
||||||
|
|
||||||
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
fl_register_plugins(FL_PLUGIN_REGISTRY(view));
|
||||||
|
gtk_widget_hide(GTK_WIDGET(window));
|
||||||
|
|
||||||
gtk_widget_grab_focus(GTK_WIDGET(view));
|
gtk_widget_grab_focus(GTK_WIDGET(view));
|
||||||
}
|
}
|
||||||
@@ -79,7 +86,7 @@ static gboolean my_application_local_command_line(GApplication* application, gch
|
|||||||
g_application_activate(application);
|
g_application_activate(application);
|
||||||
*exit_status = 0;
|
*exit_status = 0;
|
||||||
|
|
||||||
return TRUE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Implements GObject::dispose.
|
// Implements GObject::dispose.
|
||||||
@@ -100,6 +107,6 @@ static void my_application_init(MyApplication* self) {}
|
|||||||
MyApplication* my_application_new() {
|
MyApplication* my_application_new() {
|
||||||
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
return MY_APPLICATION(g_object_new(my_application_get_type(),
|
||||||
"application-id", APPLICATION_ID,
|
"application-id", APPLICATION_ID,
|
||||||
"flags", G_APPLICATION_NON_UNIQUE,
|
"flags", G_APPLICATION_HANDLES_COMMAND_LINE | G_APPLICATION_HANDLES_OPEN,
|
||||||
nullptr));
|
nullptr));
|
||||||
}
|
}
|
||||||
@@ -14,4 +14,9 @@ categories:
|
|||||||
- Network
|
- Network
|
||||||
- Utility
|
- Utility
|
||||||
|
|
||||||
|
supported_mime_type:
|
||||||
|
- x-scheme-handler/gopeed
|
||||||
|
- x-scheme-handler/magnet
|
||||||
|
- application/x-bittorrent
|
||||||
|
|
||||||
startup_notify: true
|
startup_notify: true
|
||||||
@@ -28,4 +28,9 @@ categories:
|
|||||||
- Network
|
- Network
|
||||||
- Utility
|
- Utility
|
||||||
|
|
||||||
|
supported_mime_type:
|
||||||
|
- x-scheme-handler/gopeed
|
||||||
|
- x-scheme-handler/magnet
|
||||||
|
- application/x-bittorrent
|
||||||
|
|
||||||
startup_notify: true
|
startup_notify: true
|
||||||
@@ -1,8 +1,22 @@
|
|||||||
import Cocoa
|
import Cocoa
|
||||||
import FlutterMacOS
|
import FlutterMacOS
|
||||||
|
import app_links
|
||||||
|
|
||||||
@main
|
@main
|
||||||
class AppDelegate: FlutterAppDelegate {
|
class AppDelegate: FlutterAppDelegate {
|
||||||
|
public override func application(_ application: NSApplication,
|
||||||
|
continue userActivity: NSUserActivity,
|
||||||
|
restorationHandler: @escaping ([any NSUserActivityRestoring]) -> Void) -> Bool {
|
||||||
|
|
||||||
|
guard let url = AppLinks.shared.getUniversalLink(userActivity) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
AppLinks.shared.handleLink(link: url.absoluteString)
|
||||||
|
|
||||||
|
return false // Returning true will stop the propagation to other packages
|
||||||
|
}
|
||||||
|
|
||||||
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -19,4 +33,18 @@ class AppDelegate: FlutterAppDelegate {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||||
|
let url = URL(fileURLWithPath: filename)
|
||||||
|
AppLinks.shared.handleLink(link: url.absoluteString)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
override func application(_ application: NSApplication, open urls: [URL]) {
|
||||||
|
// Only handle the first file
|
||||||
|
if(urls.isEmpty) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AppLinks.shared.handleLink(link: urls.first!.absoluteString)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,32 +1,96 @@
|
|||||||
<?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>
|
<dict>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIconFile</key>
|
<key>CFBundleIconFile</key>
|
||||||
<string></string>
|
<string></string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>$(PRODUCT_NAME)</string>
|
<string>$(PRODUCT_NAME)</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>$(FLUTTER_BUILD_NAME)</string>
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||||
<key>NSHumanReadableCopyright</key>
|
<key>NSHumanReadableCopyright</key>
|
||||||
<string>$(PRODUCT_COPYRIGHT)</string>
|
<string>$(PRODUCT_COPYRIGHT)</string>
|
||||||
<key>NSMainNibFile</key>
|
<key>NSMainNibFile</key>
|
||||||
<string>MainMenu</string>
|
<string>MainMenu</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>NSApplication</string>
|
<string>NSApplication</string>
|
||||||
</dict>
|
<key>CFBundleURLTypes</key>
|
||||||
</plist>
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>gopeed</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>gopeed</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleURLName</key>
|
||||||
|
<string>magnet</string>
|
||||||
|
<key>CFBundleURLSchemes</key>
|
||||||
|
<array>
|
||||||
|
<string>magnet</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleDocumentTypes</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleTypeExtensions</key>
|
||||||
|
<array>
|
||||||
|
<string>torrent</string>
|
||||||
|
</array>
|
||||||
|
<key>CFBundleTypeIconFile</key>
|
||||||
|
<string>icon</string>
|
||||||
|
<key>CFBundleTypeName</key>
|
||||||
|
<string>BitTorrent Document</string>
|
||||||
|
<key>CFBundleTypeRole</key>
|
||||||
|
<string>Viewer</string>
|
||||||
|
<key>LSTypeIsPackage</key>
|
||||||
|
<false />
|
||||||
|
<key>LSHandlerRank</key>
|
||||||
|
<string>Owner</string>
|
||||||
|
<key>LSItemContentTypes</key>
|
||||||
|
<array>
|
||||||
|
<string>org.bittorrent.torrent</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>UTExportedTypeDeclarations</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>UTTypeIdentifier</key>
|
||||||
|
<string>org.bittorrent.torrent</string>
|
||||||
|
<key>UTTypeDescription</key>
|
||||||
|
<string>Torrent File</string>
|
||||||
|
<key>UTTypeConformsTo</key>
|
||||||
|
<array>
|
||||||
|
<string>public.data</string>
|
||||||
|
</array>
|
||||||
|
<key>UTTypeTagSpecification</key>
|
||||||
|
<dict>
|
||||||
|
<key>public.filename-extension</key>
|
||||||
|
<array>
|
||||||
|
<string>torrent</string>
|
||||||
|
</array>
|
||||||
|
<key>public.mime-type</key>
|
||||||
|
<string>application/x-bittorrent</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
+63
-23
@@ -14,6 +14,14 @@ packages:
|
|||||||
description: dart
|
description: dart
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.3.2"
|
version: "0.3.2"
|
||||||
|
after_layout:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: after_layout
|
||||||
|
sha256: "95a1cb2ca1464f44f14769329fbf15987d20ab6c88f8fc5d359bd362be625f29"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.0"
|
||||||
analyzer:
|
analyzer:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -22,14 +30,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.7.0"
|
version: "6.7.0"
|
||||||
|
animations:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: animations
|
||||||
|
sha256: d3d6dcfb218225bbe68e87ccf6378bbb2e32a94900722c5f81611dad089911cb
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.11"
|
||||||
app_links:
|
app_links:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: app_links
|
name: app_links
|
||||||
sha256: ad1a6d598e7e39b46a34f746f9a8b011ee147e4c275d407fa457e7a62f84dd99
|
sha256: "433df2e61b10519407475d7f69e470789d23d593f28224c38ba1068597be7950"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.2"
|
version: "6.3.3"
|
||||||
app_links_linux:
|
app_links_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -78,14 +94,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.11.0"
|
version: "2.11.0"
|
||||||
autoscale_tabbarview:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: autoscale_tabbarview
|
|
||||||
sha256: "29449e8876185acc4763cc6cf26ffcc3f842f42f1502d5964c6d85f489ee1bc4"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.0.2"
|
|
||||||
badges:
|
badges:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -174,6 +182,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.0"
|
version: "1.3.0"
|
||||||
|
checkable_treeview:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: checkable_treeview
|
||||||
|
sha256: "3d7543e2c93a968864a57ca0666fc3b5827d89d13b22cb637a427596032f9272"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -214,6 +230,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.18.0"
|
version: "1.18.0"
|
||||||
|
contentsize_tabbarview:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: contentsize_tabbarview
|
||||||
|
sha256: "4a3cf5e3a17bb5ebe5ee56d55c518f4dde7db639334b79f874f03ecea4755b22"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.0.2"
|
||||||
context_menus:
|
context_menus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -222,6 +246,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.0"
|
version: "1.2.0"
|
||||||
|
contextmenu_plus:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: contextmenu_plus
|
||||||
|
sha256: c4c3a11633ba98b6e325e80942cd6ca29269b246e307b0a43514ae4f9c8bcc7f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
convert:
|
convert:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -302,6 +334,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.0"
|
version: "2.0.0"
|
||||||
|
expandable_page_view:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: expandable_page_view
|
||||||
|
sha256: "210dc6961cfc29f7ed42867824eb699c9a4b9b198a7c04b8bdc1c05844969dc6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.17"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -781,11 +821,10 @@ packages:
|
|||||||
permission_handler:
|
permission_handler:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
path: permission_handler
|
name: permission_handler
|
||||||
ref: "35fc72c30262b9b49e1965b48a7524b44ba9daa7"
|
sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb"
|
||||||
resolved-ref: "35fc72c30262b9b49e1965b48a7524b44ba9daa7"
|
url: "https://pub.dev"
|
||||||
url: "https://github.com/monkeyWie/flutter-permission-handler.git"
|
source: hosted
|
||||||
source: git
|
|
||||||
version: "11.3.1"
|
version: "11.3.1"
|
||||||
permission_handler_android:
|
permission_handler_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@@ -807,10 +846,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: permission_handler_html
|
name: permission_handler_html
|
||||||
sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851
|
sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.1.3+2"
|
version: "0.1.3+5"
|
||||||
permission_handler_platform_interface:
|
permission_handler_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -820,12 +859,13 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.3"
|
version: "4.2.3"
|
||||||
permission_handler_windows:
|
permission_handler_windows:
|
||||||
dependency: transitive
|
dependency: "direct overridden"
|
||||||
description:
|
description:
|
||||||
name: permission_handler_windows
|
path: permission_handler_windows
|
||||||
sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e"
|
ref: "35fc72c30262b9b49e1965b48a7524b44ba9daa7"
|
||||||
url: "https://pub.dev"
|
resolved-ref: "35fc72c30262b9b49e1965b48a7524b44ba9daa7"
|
||||||
source: hosted
|
url: "https://github.com/monkeyWie/flutter-permission-handler.git"
|
||||||
|
source: git
|
||||||
version: "0.2.1"
|
version: "0.2.1"
|
||||||
petitparser:
|
petitparser:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
@@ -1281,7 +1321,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "5.6.1"
|
version: "5.6.1"
|
||||||
win32_registry:
|
win32_registry:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: win32_registry
|
name: win32_registry
|
||||||
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852"
|
||||||
|
|||||||
+14
-10
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.6.2+1
|
version: 1.6.7+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.0.0 <4.0.0"
|
sdk: ">=3.0.0 <4.0.0"
|
||||||
@@ -35,8 +35,7 @@ dependencies:
|
|||||||
# The following adds the Cupertino Icons font to your application.
|
# The following adds the Cupertino Icons font to your application.
|
||||||
# Use with the CupertinoIcons class for iOS style icons.
|
# Use with the CupertinoIcons class for iOS style icons.
|
||||||
cupertino_icons: ^1.0.2
|
cupertino_icons: ^1.0.2
|
||||||
get: ^4.6.5
|
get: ^4.6.6
|
||||||
# lottie: ^1.4.2
|
|
||||||
styled_widget: ^0.4.0+3
|
styled_widget: ^0.4.0+3
|
||||||
context_menus: ^1.2.0
|
context_menus: ^1.2.0
|
||||||
json_annotation: ^4.8.1
|
json_annotation: ^4.8.1
|
||||||
@@ -52,10 +51,9 @@ dependencies:
|
|||||||
fluent_ui: ^4.9.2
|
fluent_ui: ^4.9.2
|
||||||
path: ^1.9.0
|
path: ^1.9.0
|
||||||
badges: ^3.0.3
|
badges: ^3.0.3
|
||||||
app_links: ^6.3.2
|
app_links: ^6.3.3
|
||||||
uri_to_file: ^1.0.0
|
uri_to_file: ^1.0.0
|
||||||
window_manager: ^0.4.2
|
window_manager: ^0.4.2
|
||||||
autoscale_tabbarview: ^1.0.2
|
|
||||||
share_plus: ^10.1.0
|
share_plus: ^10.1.0
|
||||||
flutter_form_builder: ^9.5.0
|
flutter_form_builder: ^9.5.0
|
||||||
form_builder_validators: ^11.0.0
|
form_builder_validators: ^11.0.0
|
||||||
@@ -67,12 +65,18 @@ dependencies:
|
|||||||
launch_at_startup: ^0.3.1
|
launch_at_startup: ^0.3.1
|
||||||
args: ^2.5.0
|
args: ^2.5.0
|
||||||
toggle_switch: ^2.3.0
|
toggle_switch: ^2.3.0
|
||||||
permission_handler:
|
permission_handler: ^11.3.1
|
||||||
|
device_info_plus: ^11.1.0
|
||||||
|
checkable_treeview: ^1.3.1
|
||||||
|
contextmenu_plus: ^1.0.1
|
||||||
|
contentsize_tabbarview: ^0.0.2
|
||||||
|
win32_registry: ^1.1.5
|
||||||
|
dependency_overrides:
|
||||||
|
permission_handler_windows:
|
||||||
git:
|
git:
|
||||||
url: https://github.com/monkeyWie/flutter-permission-handler.git
|
url: https://github.com/monkeyWie/flutter-permission-handler.git
|
||||||
ref: 35fc72c30262b9b49e1965b48a7524b44ba9daa7
|
ref: 35fc72c30262b9b49e1965b48a7524b44ba9daa7
|
||||||
path: permission_handler
|
path: permission_handler_windows
|
||||||
device_info_plus: ^11.1.0
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@@ -116,9 +120,9 @@ flutter:
|
|||||||
# list giving the asset and other descriptors for the font. For
|
# list giving the asset and other descriptors for the font. For
|
||||||
# example:
|
# example:
|
||||||
fonts:
|
fonts:
|
||||||
- family: FontAwesome
|
- family: Gopeed
|
||||||
fonts:
|
fonts:
|
||||||
- asset: assets/fonts/FontAwesome.ttf
|
- asset: assets/fonts/Gopeed.ttf
|
||||||
# - family: Trajan Pro
|
# - family: Trajan Pro
|
||||||
# fonts:
|
# fonts:
|
||||||
# - asset: fonts/TrajanPro.ttf
|
# - asset: fonts/TrajanPro.ttf
|
||||||
|
|||||||
@@ -18,41 +18,21 @@
|
|||||||
|
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
|
||||||
<meta name="description" content="A new Flutter project.">
|
<meta name="description" content="A modern download manager.">
|
||||||
|
|
||||||
<!-- iOS meta tags & icons -->
|
<!-- iOS meta tags & icons -->
|
||||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||||
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
<meta name="apple-mobile-web-app-status-bar-style" content="black">
|
||||||
<meta name="apple-mobile-web-app-title" content="gopeed">
|
<meta name="apple-mobile-web-app-title" content="Gopeed">
|
||||||
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
<link rel="apple-touch-icon" href="icons/Icon-192.png">
|
||||||
|
|
||||||
<!-- Favicon -->
|
<!-- Favicon -->
|
||||||
<link rel="icon" type="image/png" href="favicon.png"/>
|
<link rel="icon" type="image/png" href="favicon.png"/>
|
||||||
|
|
||||||
<title>gopeed</title>
|
<title>Gopeed</title>
|
||||||
<link rel="manifest" href="manifest.json">
|
<link rel="manifest" href="manifest.json">
|
||||||
|
|
||||||
<script>
|
|
||||||
// The value below is injected by flutter build, do not touch.
|
|
||||||
var serviceWorkerVersion = null;
|
|
||||||
</script>
|
|
||||||
<!-- This script adds the flutter initialization JS code -->
|
|
||||||
<script src="flutter.js" defer></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<script>
|
<script src="flutter_bootstrap.js" async></script>
|
||||||
window.addEventListener('load', function(ev) {
|
|
||||||
// Download main.dart.js
|
|
||||||
_flutter.loader.loadEntrypoint({
|
|
||||||
serviceWorker: {
|
|
||||||
serviceWorkerVersion: serviceWorkerVersion,
|
|
||||||
}
|
|
||||||
}).then(function(engineInitializer) {
|
|
||||||
return engineInitializer.initializeEngine();
|
|
||||||
}).then(function(appRunner) {
|
|
||||||
return appRunner.runApp();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -4,9 +4,49 @@
|
|||||||
|
|
||||||
#include "flutter_window.h"
|
#include "flutter_window.h"
|
||||||
#include "utils.h"
|
#include "utils.h"
|
||||||
|
#include "app_links/app_links_plugin_c_api.h"
|
||||||
|
|
||||||
|
bool SendAppLinkToInstance(const std::wstring& title) {
|
||||||
|
// Find our exact window
|
||||||
|
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", title.c_str());
|
||||||
|
|
||||||
|
if (hwnd) {
|
||||||
|
// Dispatch new link to current window
|
||||||
|
SendAppLink(hwnd);
|
||||||
|
|
||||||
|
// (Optional) Restore our window to front in same state
|
||||||
|
WINDOWPLACEMENT place = { sizeof(WINDOWPLACEMENT) };
|
||||||
|
GetWindowPlacement(hwnd, &place);
|
||||||
|
|
||||||
|
switch(place.showCmd) {
|
||||||
|
case SW_SHOWMAXIMIZED:
|
||||||
|
ShowWindow(hwnd, SW_SHOWMAXIMIZED);
|
||||||
|
break;
|
||||||
|
case SW_SHOWMINIMIZED:
|
||||||
|
ShowWindow(hwnd, SW_RESTORE);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ShowWindow(hwnd, SW_NORMAL);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetWindowPos(0, HWND_TOP, 0, 0, 0, 0, SWP_SHOWWINDOW | SWP_NOSIZE | SWP_NOMOVE);
|
||||||
|
SetForegroundWindow(hwnd);
|
||||||
|
// END (Optional) Restore
|
||||||
|
|
||||||
|
// Window has been found, don't create another one.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev,
|
||||||
_In_ wchar_t *command_line, _In_ int show_command) {
|
_In_ wchar_t *command_line, _In_ int show_command) {
|
||||||
|
if (SendAppLinkToInstance(L"gopeed")) {
|
||||||
|
return EXIT_SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"gopeed");
|
HWND hwnd = ::FindWindow(L"FLUTTER_RUNNER_WIN32_WINDOW", L"gopeed");
|
||||||
if (hwnd != NULL) {
|
if (hwnd != NULL) {
|
||||||
::ShowWindow(hwnd, SW_NORMAL);
|
::ShowWindow(hwnd, SW_NORMAL);
|
||||||
|
|||||||
Reference in new issue
Block a user