mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-12 19:33:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
724276ac69 | ||
|
|
a3f559c8e5 | ||
|
|
edd189a8a8 | ||
|
|
c970378fd7 | ||
|
|
8b61343aef | ||
|
|
2cb93a03e2 | ||
|
|
81dfc073a6 | ||
|
|
427921ccab | ||
|
|
f4876c8a0e | ||
|
|
35a3625b11 | ||
|
|
dc9b501e35 | ||
|
|
85fe92a378 | ||
|
|
983d9ad09e | ||
|
|
d9f757aaf0 | ||
|
|
cd409fcfb9 | ||
|
|
366244fa5d | ||
|
|
7a2e9b4fd1 | ||
|
|
203385d3f8 | ||
|
|
aaf73dec24 | ||
|
|
2523282a89 | ||
|
|
3384f5197a | ||
|
|
cd77665f02 | ||
|
|
a22b431150 |
No files matched your search
@@ -13,7 +13,7 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.22"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -380,7 +380,7 @@ jobs:
|
|||||||
go install golang.org/x/mobile/cmd/gomobile@latest
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
go get golang.org/x/mobile/bind
|
go get golang.org/x/mobile/bind
|
||||||
gomobile init
|
gomobile init
|
||||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||||
flutter build apk
|
flutter build apk
|
||||||
@@ -441,6 +441,7 @@ 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 --web-renderer html
|
||||||
cd ../../
|
cd ../../
|
||||||
@@ -468,6 +469,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:
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ on:
|
|||||||
- "Dockerfile"
|
- "Dockerfile"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.22"
|
GO_VERSION: "1.23"
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
+96
-10
@@ -27,7 +27,7 @@ on:
|
|||||||
- "go.sum"
|
- "go.sum"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.22"
|
GO_VERSION: "1.23"
|
||||||
FLUTTER_VERSION: "3.16.8"
|
FLUTTER_VERSION: "3.16.8"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -58,11 +58,45 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
files: ./coverage.txt
|
files: ./coverage.txt
|
||||||
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
token: ${{ secrets.CODECOV_UPLOAD_TOKEN }}
|
||||||
build:
|
build-desktop:
|
||||||
name: Build check
|
name: Build desktop check
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
# needs: [lint, test]
|
strategy:
|
||||||
needs: [test]
|
matrix:
|
||||||
|
os: [ windows-latest, macos-latest, ubuntu-latest ]
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- if: runner.os == 'windows'
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build windows
|
||||||
|
- if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
build-mobile:
|
||||||
|
name: Build mobile check
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ macos-latest, ubuntu-latest ]
|
||||||
|
needs: [ test ]
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- uses: actions/setup-go@v4
|
- uses: actions/setup-go@v4
|
||||||
@@ -72,10 +106,62 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
- if: runner.os == 'macOS'
|
||||||
|
run: |
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build ipa --no-codesign
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
uses: actions/setup-java@v3
|
||||||
|
with:
|
||||||
|
distribution: "zulu"
|
||||||
|
java-version: "17"
|
||||||
|
- if: runner.os == 'Linux'
|
||||||
|
run: |
|
||||||
|
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
|
||||||
|
flutter build apk
|
||||||
|
|
||||||
|
build-web:
|
||||||
|
name: Build web check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build linux
|
flutter build web --web-renderer html
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
build-docker:
|
||||||
|
name: Build docker check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [ test ]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
- uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web --web-renderer html
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
docker build -t gopeed .
|
||||||
+3
-4
@@ -1,4 +1,4 @@
|
|||||||
FROM golang:1.22.6 AS go
|
FROM golang:1.23.3 AS go
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./go.mod ./go.sum ./
|
COPY ./go.mod ./go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
@@ -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"]
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|||||||
+1
-1
@@ -102,7 +102,7 @@ Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された
|
|||||||
|
|
||||||
### 環境
|
### 環境
|
||||||
|
|
||||||
1. Go 言語 1.22+
|
1. Go 言語 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### クローン
|
### クローン
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ Dự án này được chia thành hai phần, phần giao diện sử dụng `f
|
|||||||
|
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
1. Golang 1.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### Clone
|
### Clone
|
||||||
|
|||||||
+1
-1
@@ -114,7 +114,7 @@ Gopeed 还提供了浏览器扩展用于接管浏览器下载,支持 Chrome、
|
|||||||
|
|
||||||
### 环境要求
|
### 环境要求
|
||||||
|
|
||||||
1. Golang 1.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆项目
|
### 克隆项目
|
||||||
|
|||||||
+1
-1
@@ -110,7 +110,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
|
|
||||||
### 開發環境
|
### 開發環境
|
||||||
|
|
||||||
1. Golang 1.22+
|
1. Golang 1.23+
|
||||||
2. Flutter 3.16+
|
2. Flutter 3.16+
|
||||||
|
|
||||||
### 克隆項目
|
### 克隆項目
|
||||||
|
|||||||
+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:/root/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
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
module github.com/GopeedLab/gopeed
|
module github.com/GopeedLab/gopeed
|
||||||
|
|
||||||
go 1.22
|
go 1.23.0
|
||||||
|
|
||||||
|
toolchain go1.23.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.3
|
github.com/anacrolix/missinggo/v2 v2.8.0
|
||||||
github.com/anacrolix/torrent v1.56.1
|
github.com/anacrolix/torrent v1.57.2-0.20241017235801-4d8437a05621
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
|
github.com/dop251/goja v0.0.0-20240919115326-6c7d1df7ff05
|
||||||
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
github.com/dop251/goja_nodejs v0.0.0-20240728170619-29b559befffc
|
||||||
@@ -16,11 +18,10 @@ require (
|
|||||||
github.com/pkg/errors v0.9.1
|
github.com/pkg/errors v0.9.1
|
||||||
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
github.com/rs/dnscache v0.0.0-20230804202142-fc85eb664529
|
||||||
github.com/rs/zerolog v1.31.0
|
github.com/rs/zerolog v1.31.0
|
||||||
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
|
|
||||||
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
github.com/xiaoqidun/setft v0.0.0-20220310121541-be86327699ad
|
||||||
go.etcd.io/bbolt v1.3.10
|
go.etcd.io/bbolt v1.3.11
|
||||||
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
|
golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f
|
||||||
golang.org/x/sync v0.8.0
|
golang.org/x/sync v0.9.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -31,48 +32,48 @@ require (
|
|||||||
github.com/acomagu/bufpipe v1.0.4 // indirect
|
github.com/acomagu/bufpipe v1.0.4 // indirect
|
||||||
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
github.com/ajwerner/btree v0.0.0-20211221152037-f427b3e689c0 // indirect
|
||||||
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
github.com/alecthomas/atomic v0.1.0-alpha2 // indirect
|
||||||
github.com/anacrolix/chansync v0.5.1 // indirect
|
github.com/anacrolix/chansync v0.6.0 // indirect
|
||||||
github.com/anacrolix/dht/v2 v2.21.1 // indirect
|
github.com/anacrolix/dht/v2 v2.22.0 // indirect
|
||||||
github.com/anacrolix/envpprof v1.3.0 // indirect
|
github.com/anacrolix/envpprof v1.4.0 // indirect
|
||||||
github.com/anacrolix/generics v0.0.2 // indirect
|
github.com/anacrolix/generics v0.0.3-0.20240902042256-7fb2702ef0ca // indirect
|
||||||
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
github.com/anacrolix/go-libutp v1.3.1 // indirect
|
||||||
github.com/anacrolix/log v0.15.2 // indirect
|
github.com/anacrolix/log v0.16.0 // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.1.1 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.1-0.20221221005021-2d12701f83f7 // indirect
|
github.com/anacrolix/multiless v0.4.0 // indirect
|
||||||
github.com/anacrolix/stm v0.5.0 // indirect
|
github.com/anacrolix/stm v0.5.0 // indirect
|
||||||
github.com/anacrolix/sync v0.5.1 // indirect
|
github.com/anacrolix/sync v0.5.3 // indirect
|
||||||
github.com/anacrolix/upnp v0.1.4 // indirect
|
github.com/anacrolix/upnp v0.1.4 // indirect
|
||||||
github.com/anacrolix/utp v0.2.0 // indirect
|
github.com/anacrolix/utp v0.2.0 // indirect
|
||||||
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
github.com/bahlo/generic-list-go v0.2.0 // indirect
|
||||||
github.com/benbjohnson/immutable v0.4.3 // indirect
|
github.com/benbjohnson/immutable v0.4.3 // indirect
|
||||||
github.com/bits-and-blooms/bitset v1.13.0 // indirect
|
github.com/bits-and-blooms/bitset v1.15.0 // indirect
|
||||||
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
|
||||||
github.com/cespare/xxhash v1.1.0 // indirect
|
github.com/cespare/xxhash v1.1.0 // indirect
|
||||||
github.com/cloudflare/circl v1.3.3 // indirect
|
github.com/cloudflare/circl v1.3.3 // indirect
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/edsrzf/mmap-go v1.1.0 // indirect
|
github.com/edsrzf/mmap-go v1.2.0 // indirect
|
||||||
github.com/emirpasic/gods v1.18.1 // indirect
|
github.com/emirpasic/gods v1.18.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.1 // indirect
|
github.com/felixge/httpsnoop v1.0.1 // indirect
|
||||||
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect
|
||||||
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
github.com/go-git/go-billy/v5 v5.4.1 // indirect
|
||||||
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
github.com/go-llsqlite/adapter v0.1.0 // indirect
|
||||||
github.com/go-llsqlite/crawshaw v0.5.3 // indirect
|
github.com/go-llsqlite/crawshaw v0.5.5 // indirect
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect
|
||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
github.com/google/btree v1.1.2 // indirect
|
github.com/google/btree v1.1.3 // indirect
|
||||||
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
github.com/google/pprof v0.0.0-20240910150728-a0b0bb1d4134 // indirect
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/gorilla/websocket v1.5.3 // indirect
|
github.com/gorilla/websocket v1.5.3 // indirect
|
||||||
github.com/huandu/xstrings v1.5.0 // indirect
|
github.com/huandu/xstrings v1.5.0 // indirect
|
||||||
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect
|
||||||
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
github.com/kevinburke/ssh_config v1.2.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.9 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/minio/sha256-simd v1.0.1 // indirect
|
github.com/minio/sha256-simd v1.0.1 // indirect
|
||||||
@@ -81,48 +82,46 @@ require (
|
|||||||
github.com/multiformats/go-multihash v0.2.3 // indirect
|
github.com/multiformats/go-multihash v0.2.3 // indirect
|
||||||
github.com/multiformats/go-varint v0.0.7 // indirect
|
github.com/multiformats/go-varint v0.0.7 // indirect
|
||||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||||
github.com/pion/datachannel v1.5.8 // indirect
|
github.com/pion/datachannel v1.5.9 // indirect
|
||||||
github.com/pion/dtls/v2 v2.2.12 // indirect
|
github.com/pion/dtls/v3 v3.0.4 // indirect
|
||||||
github.com/pion/ice/v2 v2.3.31 // indirect
|
github.com/pion/ice/v4 v4.0.2 // indirect
|
||||||
github.com/pion/interceptor v0.1.29 // indirect
|
github.com/pion/interceptor v0.1.37 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/mdns v0.0.12 // indirect
|
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||||
github.com/pion/randutil v0.1.0 // indirect
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
github.com/pion/rtcp v1.2.14 // indirect
|
github.com/pion/rtcp v1.2.14 // indirect
|
||||||
github.com/pion/rtp v1.8.7 // indirect
|
github.com/pion/rtp v1.8.9 // indirect
|
||||||
github.com/pion/sctp v1.8.19 // indirect
|
github.com/pion/sctp v1.8.33 // indirect
|
||||||
github.com/pion/sdp/v3 v3.0.9 // indirect
|
github.com/pion/sdp/v3 v3.0.9 // indirect
|
||||||
github.com/pion/srtp/v2 v2.0.20 // indirect
|
github.com/pion/srtp/v3 v3.0.4 // indirect
|
||||||
github.com/pion/stun v0.6.1 // indirect
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.9 // indirect
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
github.com/pion/turn/v2 v2.1.6 // indirect
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
github.com/pion/webrtc/v3 v3.2.50 // indirect
|
github.com/pion/webrtc/v4 v4.0.2 // indirect
|
||||||
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
github.com/pjbgf/sha1cd v0.3.0 // indirect
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/protolambda/ctxlock v0.1.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/sergi/go-diff v1.1.0 // indirect
|
||||||
github.com/skeema/knownhosts v1.2.0 // indirect
|
github.com/skeema/knownhosts v1.2.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
github.com/spaolacci/murmur3 v1.1.0 // indirect
|
||||||
github.com/stretchr/testify v1.9.0 // indirect
|
|
||||||
github.com/tidwall/btree v1.7.0 // indirect
|
github.com/tidwall/btree v1.7.0 // indirect
|
||||||
github.com/wlynxg/anet v0.0.3 // indirect
|
github.com/wlynxg/anet v0.0.5 // indirect
|
||||||
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
github.com/xanzy/ssh-agent v0.3.3 // indirect
|
||||||
go.opentelemetry.io/otel v1.28.0 // indirect
|
go.opentelemetry.io/otel v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.28.0 // indirect
|
go.opentelemetry.io/otel/metric v1.32.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.28.0 // indirect
|
go.opentelemetry.io/otel/trace v1.32.0 // indirect
|
||||||
golang.org/x/crypto v0.25.0 // indirect
|
golang.org/x/crypto v0.29.0 // indirect
|
||||||
golang.org/x/mod v0.19.0 // indirect
|
golang.org/x/mod v0.22.0 // indirect
|
||||||
golang.org/x/net v0.27.0 // indirect
|
golang.org/x/net v0.31.0 // indirect
|
||||||
golang.org/x/sys v0.22.0 // indirect
|
golang.org/x/sys v0.27.0 // indirect
|
||||||
golang.org/x/text v0.18.0 // indirect
|
golang.org/x/text v0.20.0 // indirect
|
||||||
golang.org/x/time v0.5.0 // indirect
|
golang.org/x/time v0.8.0 // indirect
|
||||||
golang.org/x/tools v0.23.0 // indirect
|
golang.org/x/tools v0.27.0 // indirect
|
||||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
lukechampine.com/blake3 v1.3.0 // indirect
|
lukechampine.com/blake3 v1.3.0 // indirect
|
||||||
modernc.org/libc v1.55.6 // indirect
|
modernc.org/libc v1.61.0 // indirect
|
||||||
modernc.org/mathutil v1.6.0 // indirect
|
modernc.org/mathutil v1.6.0 // indirect
|
||||||
modernc.org/memory v1.8.0 // indirect
|
modernc.org/memory v1.8.0 // indirect
|
||||||
modernc.org/sqlite v1.31.1 // indirect
|
modernc.org/sqlite v1.33.1 // indirect
|
||||||
zombiezen.com/go/sqlite v1.3.0 // indirect
|
zombiezen.com/go/sqlite v1.4.0 // indirect
|
||||||
)
|
)
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
package bt
|
package bt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/anacrolix/torrent/storage"
|
"github.com/anacrolix/torrent/storage"
|
||||||
"io"
|
"io"
|
||||||
@@ -53,7 +54,7 @@ func (fs fileClientImpl) Close() error {
|
|||||||
return fs.opts.PieceCompletion.Close()
|
return fs.opts.PieceCompletion.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
func (fs fileClientImpl) OpenTorrent(ctx context.Context, info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||||
upvertedFiles := info.UpvertedFiles()
|
upvertedFiles := info.UpvertedFiles()
|
||||||
files := make([]file, 0, len(upvertedFiles))
|
files := make([]file, 0, len(upvertedFiles))
|
||||||
for _, fileInfo := range upvertedFiles {
|
for _, fileInfo := range upvertedFiles {
|
||||||
|
|||||||
@@ -3,10 +3,12 @@ package http
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"mime"
|
"mime"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -24,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
|
||||||
@@ -102,7 +107,7 @@ 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
|
||||||
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
|
||||||
if contentTotal != "" {
|
if contentTotal != "" {
|
||||||
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
parse, err := strconv.ParseInt(contentTotal, 10, 64)
|
||||||
@@ -145,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)
|
||||||
}
|
}
|
||||||
@@ -161,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
|
||||||
}
|
}
|
||||||
@@ -197,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()
|
||||||
}
|
}
|
||||||
@@ -246,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 {
|
||||||
@@ -285,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 (
|
||||||
@@ -329,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 {
|
||||||
@@ -347,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
|
||||||
@@ -448,7 +460,13 @@ 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{
|
||||||
|
InsecureSkipVerify: f.meta.Req.SkipVerifyCert,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
// Cookie handle
|
// Cookie handle
|
||||||
jar, _ := cookiejar.New(nil)
|
jar, _ := cookiejar.New(nil)
|
||||||
|
|||||||
@@ -132,6 +132,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)
|
||||||
|
|||||||
+14
-2
@@ -7,6 +7,7 @@ import (
|
|||||||
"golang.org/x/exp/slices"
|
"golang.org/x/exp/slices"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -18,6 +19,8 @@ type Request struct {
|
|||||||
Labels map[string]string `json:"labels"`
|
Labels map[string]string `json:"labels"`
|
||||||
// Proxy is special proxy config for request
|
// Proxy is special proxy config for request
|
||||||
Proxy *RequestProxy `json:"proxy"`
|
Proxy *RequestProxy `json:"proxy"`
|
||||||
|
// SkipVerifyCert is the flag that skip verify cert
|
||||||
|
SkipVerifyCert bool `json:"skipVerifyCert"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Request) Validate() error {
|
func (r *Request) Validate() error {
|
||||||
@@ -221,7 +224,7 @@ func (cfg *DownloaderProxyConfig) ToHandler() func(r *http.Request) (*url.URL, e
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cfg.System {
|
if cfg.System {
|
||||||
ieproxy.ReloadConf()
|
safeProxyReloadConf()
|
||||||
return ieproxy.GetProxyFunc()
|
return ieproxy.GetProxyFunc()
|
||||||
}
|
}
|
||||||
if cfg.Scheme == "" || cfg.Host == "" {
|
if cfg.Scheme == "" || cfg.Host == "" {
|
||||||
@@ -236,7 +239,7 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if cfg.System {
|
if cfg.System {
|
||||||
ieproxy.ReloadConf()
|
safeProxyReloadConf()
|
||||||
static := ieproxy.GetConf().Static
|
static := ieproxy.GetConf().Static
|
||||||
if static.Active && len(static.Protocols) > 0 {
|
if static.Active && len(static.Protocols) > 0 {
|
||||||
// If only one protocol, use it
|
// If only one protocol, use it
|
||||||
@@ -262,6 +265,15 @@ func (cfg *DownloaderProxyConfig) ToUrl() *url.URL {
|
|||||||
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
return util.BuildProxyUrl(cfg.Scheme, cfg.Host, cfg.Usr, cfg.Pwd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var prcLock sync.Mutex
|
||||||
|
|
||||||
|
func safeProxyReloadConf() {
|
||||||
|
prcLock.Lock()
|
||||||
|
defer prcLock.Unlock()
|
||||||
|
|
||||||
|
ieproxy.ReloadConf()
|
||||||
|
}
|
||||||
|
|
||||||
func parseUrlSafe(rawUrl string) *url.URL {
|
func parseUrlSafe(rawUrl string) *url.URL {
|
||||||
u, err := url.Parse(rawUrl)
|
u, err := url.Parse(rawUrl)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+30
-18
@@ -11,12 +11,12 @@ import (
|
|||||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"github.com/rs/zerolog/pkgerrors"
|
"github.com/rs/zerolog/pkgerrors"
|
||||||
"github.com/virtuald/go-paniclog"
|
|
||||||
"math"
|
"math"
|
||||||
gohttp "net/http"
|
gohttp "net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"runtime/debug"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -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)
|
||||||
@@ -1130,7 +1142,7 @@ func logPanic(logDir string) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
paniclog.RedirectStderr(f)
|
debug.SetCrashOutput(f, debug.CrashOptions{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
func (d *Downloader) buildFetcher(url string) (fetcher.Fetcher, error) {
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -8,12 +8,14 @@ class Request {
|
|||||||
Object? extra;
|
Object? extra;
|
||||||
Map<String, String>? labels = {};
|
Map<String, String>? labels = {};
|
||||||
RequestProxy? proxy;
|
RequestProxy? proxy;
|
||||||
|
bool skipVerifyCert = false;
|
||||||
|
|
||||||
Request({
|
Request({
|
||||||
required this.url,
|
required this.url,
|
||||||
this.extra,
|
this.extra,
|
||||||
this.labels,
|
this.labels,
|
||||||
this.proxy,
|
this.proxy,
|
||||||
|
this.skipVerifyCert = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
|||||||
proxy: json['proxy'] == null
|
proxy: json['proxy'] == null
|
||||||
? null
|
? null
|
||||||
: RequestProxy.fromJson(json['proxy'] as Map<String, dynamic>),
|
: RequestProxy.fromJson(json['proxy'] as Map<String, dynamic>),
|
||||||
|
skipVerifyCert: json['skipVerifyCert'] as bool? ?? false,
|
||||||
);
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||||
@@ -31,6 +32,7 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
|||||||
writeNotNull('extra', instance.extra);
|
writeNotNull('extra', instance.extra);
|
||||||
writeNotNull('labels', instance.labels);
|
writeNotNull('labels', instance.labels);
|
||||||
writeNotNull('proxy', instance.proxy?.toJson());
|
writeNotNull('proxy', instance.proxy?.toJson());
|
||||||
|
val['skipVerifyCert'] = instance.skipVerifyCert;
|
||||||
return val;
|
return val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
||||||
|
import 'package:checkable_treeview/checkable_treeview.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';
|
||||||
@@ -16,10 +17,11 @@ import '../../../../database/database.dart';
|
|||||||
import '../../../../util/input_formatter.dart';
|
import '../../../../util/input_formatter.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
|
import '../../../../util/extensions.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../../../views/compact_checkbox.dart';
|
import '../../../views/compact_checkbox.dart';
|
||||||
import '../../../views/directory_selector.dart';
|
import '../../../views/directory_selector.dart';
|
||||||
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';
|
||||||
@@ -40,9 +42,12 @@ class CreateView extends GetView<CreateController> {
|
|||||||
final _httpCookieController = TextEditingController();
|
final _httpCookieController = TextEditingController();
|
||||||
final _httpRefererController = TextEditingController();
|
final _httpRefererController = TextEditingController();
|
||||||
final _btTrackerController = TextEditingController();
|
final _btTrackerController = TextEditingController();
|
||||||
|
final _fileTreeViewKey = GlobalKey<TreeViewState<int>>();
|
||||||
|
|
||||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||||
|
|
||||||
|
final _skipVerifyCertController = false.obs;
|
||||||
|
|
||||||
CreateView({Key? key}) : super(key: key);
|
CreateView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -454,6 +459,22 @@ class CreateView extends GetView<CreateController> {
|
|||||||
decoration: const InputDecoration(
|
decoration: const InputDecoration(
|
||||||
labelText: 'Referer',
|
labelText: 'Referer',
|
||||||
)),
|
)),
|
||||||
|
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(
|
||||||
@@ -578,7 +599,11 @@ class CreateView extends GetView<CreateController> {
|
|||||||
await Future.wait(urls.map((url) {
|
await Future.wait(urls.map((url) {
|
||||||
return createTask(CreateTask(
|
return createTask(CreateTask(
|
||||||
req: Request(
|
req: Request(
|
||||||
url: url, extra: parseReqExtra(url), proxy: parseProxy()),
|
url: url,
|
||||||
|
extra: parseReqExtra(url),
|
||||||
|
proxy: parseProxy(),
|
||||||
|
skipVerifyCert: _skipVerifyCertController.value,
|
||||||
|
),
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: isMultiLine ? "" : _renameController.text,
|
name: isMultiLine ? "" : _renameController.text,
|
||||||
path: _pathController.text,
|
path: _pathController.text,
|
||||||
@@ -592,6 +617,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
url: submitUrl,
|
url: submitUrl,
|
||||||
extra: parseReqExtra(_urlController.text),
|
extra: parseReqExtra(_urlController.text),
|
||||||
proxy: parseProxy(),
|
proxy: parseProxy(),
|
||||||
|
skipVerifyCert: _skipVerifyCertController.value,
|
||||||
));
|
));
|
||||||
await _showResolveDialog(rr);
|
await _showResolveDialog(rr);
|
||||||
}
|
}
|
||||||
@@ -675,7 +701,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;
|
||||||
|
},
|
||||||
|
)),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -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> {
|
||||||
@@ -66,8 +65,8 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
|||||||
final fileName = basename(filePath);
|
final fileName = basename(filePath);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: file.isDirectory
|
leading: file.isDirectory
|
||||||
? const Icon(Icons.folder)
|
? const Icon(folderIcon)
|
||||||
: Icon(FaIcons.allIcons[findIcon(fileName)]),
|
: Icon(fileIcon(fileName)),
|
||||||
title: Text(fileName),
|
title: Text(fileName),
|
||||||
subtitle: file.isDirectory
|
subtitle: file.isDirectory
|
||||||
? Text('items'.trParams({
|
? Text('items'.trParams({
|
||||||
|
|||||||
@@ -4,14 +4,13 @@ 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/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;
|
||||||
@@ -185,8 +184,8 @@ class BuildTaskListView extends GetView {
|
|||||||
ListTile(
|
ListTile(
|
||||||
title: Text(task.name),
|
title: Text(task.name),
|
||||||
leading: isFolderTask()
|
leading: isFolderTask()
|
||||||
? const Icon(FaIcons.folder)
|
? const Icon(folderIcon)
|
||||||
: Icon(FaIcons.allIcons[findIcon(task.name)])),
|
: Icon(fileIcon(task.name))),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const folderIcon = Gopeed.folder;
|
||||||
|
const folderBtIcon = Gopeed.folder_bt;
|
||||||
|
|
||||||
|
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 ? folderBtIcon : folderIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
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,284 @@
|
|||||||
|
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 'sort_icon_button.dart';
|
||||||
|
|
||||||
|
const _toggleSwitchIcons = [
|
||||||
|
Gopeed.file_video,
|
||||||
|
Gopeed.file_audio,
|
||||||
|
Gopeed.file_image,
|
||||||
|
];
|
||||||
|
|
||||||
|
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 selectdFileSize = calcSelectedSize(null);
|
||||||
|
|
||||||
|
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),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Text('fileSelectedCount'.tr),
|
||||||
|
Text(
|
||||||
|
selectedFileCount.toString(),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Text('fileSelectedSize'.tr),
|
||||||
|
Text(
|
||||||
|
selectedFileCount > 0 && selectdFileSize == 0
|
||||||
|
? 'unknown'.tr
|
||||||
|
: Util.fmtByte(selectdFileSize),
|
||||||
|
style: Theme.of(context).textTheme.bodySmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
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();
|
||||||
|
},
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,5 +107,11 @@ const enUS = {
|
|||||||
'taskName': 'Task Name',
|
'taskName': 'Task Name',
|
||||||
'taskUrl': 'Task URL',
|
'taskUrl': 'Task URL',
|
||||||
'downloadPath': 'Download Path',
|
'downloadPath': 'Download Path',
|
||||||
|
'skipVerifyCert': 'Skip Certificate Verification',
|
||||||
|
'name': 'Name',
|
||||||
|
'size': 'Size',
|
||||||
|
'unknown': 'Unknown',
|
||||||
|
'fileSelectedCount': 'Files: ',
|
||||||
|
'fileSelectedSize': 'Size: ',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -103,5 +103,6 @@ const esES = {
|
|||||||
'taskName': 'Nombre de la Tarea',
|
'taskName': 'Nombre de la Tarea',
|
||||||
'taskUrl': 'URL de la Tarea',
|
'taskUrl': 'URL de la Tarea',
|
||||||
'downloadPath': 'Ruta de Descarga',
|
'downloadPath': 'Ruta de Descarga',
|
||||||
|
'skipVerifyCert': 'Omitir Verificación de Certificado',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -61,5 +61,6 @@ const faIR = {
|
|||||||
'thanksDesc':
|
'thanksDesc':
|
||||||
'از تمامی افرادی که در ساخت و توسعهی جامعهی Gopeed مشارکت کردهاند، سپاسگزاری میکنیم!',
|
'از تمامی افرادی که در ساخت و توسعهی جامعهی Gopeed مشارکت کردهاند، سپاسگزاری میکنیم!',
|
||||||
'browserExtension': 'فایل های دانلود شده را نگه دارد',
|
'browserExtension': 'فایل های دانلود شده را نگه دارد',
|
||||||
|
'skipVerifyCert': 'رد کردن تأیید گواهینامه',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -92,5 +92,6 @@ const frFR = {
|
|||||||
'thanksDesc': 'Merci à tous les contributeurs qui ont aidé à construire et développer la communauté Gopeed !',
|
'thanksDesc': 'Merci à tous les contributeurs qui ont aidé à construire et développer la communauté Gopeed !',
|
||||||
'browserExtension': 'Extension du navigateur',
|
'browserExtension': 'Extension du navigateur',
|
||||||
'launchAtStartup': 'Lancer au démarrage',
|
'launchAtStartup': 'Lancer au démarrage',
|
||||||
|
'skipVerifyCert': 'Bypass Verifikasi Sertifikat',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -107,5 +107,6 @@ const idID = {
|
|||||||
'taskName': 'Nama Tugas',
|
'taskName': 'Nama Tugas',
|
||||||
'taskUrl': 'URL Tugas',
|
'taskUrl': 'URL Tugas',
|
||||||
'downloadPath': 'Direktori Unduhan',
|
'downloadPath': 'Direktori Unduhan',
|
||||||
|
'skipVerifyCert': 'Lupakan verifikasi sertifikat',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -96,5 +96,6 @@ const itIT = {
|
|||||||
'Grazie a tutti i contributori che hanno contribuito a costruire e sviluppare la comunità Gopeed!',
|
'Grazie a tutti i contributori che hanno contribuito a costruire e sviluppare la comunità Gopeed!',
|
||||||
'browserExtension': 'Estensione del browser',
|
'browserExtension': 'Estensione del browser',
|
||||||
'launchAtStartup': "Lancia all'avvio",
|
'launchAtStartup': "Lancia all'avvio",
|
||||||
|
'skipVerifyCert': 'Salta la verifica del certificato',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -62,5 +62,6 @@ const jaJP = {
|
|||||||
'thanks': '感謝',
|
'thanks': '感謝',
|
||||||
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
|
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
|
||||||
'browserExtension': 'ブラウザ拡張機能',
|
'browserExtension': 'ブラウザ拡張機能',
|
||||||
|
'skipVerifyCert': '証明書の検証をスキップ',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -94,5 +94,6 @@ const plPL = {
|
|||||||
'Dziękuję wszystkim za pomoc przy powstawaniu aplikacji Gopeed!',
|
'Dziękuję wszystkim za pomoc przy powstawaniu aplikacji Gopeed!',
|
||||||
'browserExtension': 'Wtyczka dla przeglądarki',
|
'browserExtension': 'Wtyczka dla przeglądarki',
|
||||||
'launchAtStartup': 'Uruchom na starcie',
|
'launchAtStartup': 'Uruchom na starcie',
|
||||||
|
'skipVerifyCert': 'Pomiń weryfikację certyfikatu',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -77,5 +77,6 @@ const ruRU = {
|
|||||||
'thanksDesc':
|
'thanksDesc':
|
||||||
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
|
||||||
'browserExtension': 'Расширение браузера',
|
'browserExtension': 'Расширение браузера',
|
||||||
|
'skipVerifyCert': 'Пропустить проверку сертификата',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -95,5 +95,6 @@ const taTA = {
|
|||||||
'Gopeed சமூகத்தை உருவாக்கவும் மற்றும் மேம்படுத்த உதவிய அனைத்து பங்களிப்பாளர்களுக்கும் நன்றி!',
|
'Gopeed சமூகத்தை உருவாக்கவும் மற்றும் மேம்படுத்த உதவிய அனைத்து பங்களிப்பாளர்களுக்கும் நன்றி!',
|
||||||
'browserExtension': 'உலாவி நீட்டிப்பு',
|
'browserExtension': 'உலாவி நீட்டிப்பு',
|
||||||
'launchAtStartup': 'தொடக்கத்தின் போது இயங்கு',
|
'launchAtStartup': 'தொடக்கத்தின் போது இயங்கு',
|
||||||
|
'skipVerifyCert': 'திருத்தி சான்று சரிபார்க்கவும்',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -104,5 +104,6 @@ const trTR = {
|
|||||||
'taskName': 'Görev adı',
|
'taskName': 'Görev adı',
|
||||||
'taskUrl': 'Görev URL',
|
'taskUrl': 'Görev URL',
|
||||||
'downloadPath': 'İndirme yolu',
|
'downloadPath': 'İndirme yolu',
|
||||||
|
'skipVerifyCert': 'Sertifika doğrulamasını atla',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -89,5 +89,6 @@ const viVN = {
|
|||||||
'thanksDesc':
|
'thanksDesc':
|
||||||
'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!',
|
'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!',
|
||||||
'browserExtension': 'Tiện ích mở rộng trình duyệt',
|
'browserExtension': 'Tiện ích mở rộng trình duyệt',
|
||||||
|
'skipVerifyCert': 'Bỏ qua xác thực chứng chỉ',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -104,5 +104,11 @@ const zhCN = {
|
|||||||
'taskName': '任务名称',
|
'taskName': '任务名称',
|
||||||
'taskUrl': '任务链接',
|
'taskUrl': '任务链接',
|
||||||
'downloadPath': '下载路径',
|
'downloadPath': '下载路径',
|
||||||
|
'skipVerifyCert': '跳过证书验证',
|
||||||
|
'name': '名称',
|
||||||
|
'size': '大小',
|
||||||
|
'unknown': '未知',
|
||||||
|
'fileSelectedCount': '文件数:',
|
||||||
|
'fileSelectedSize': '大小:',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -104,5 +104,11 @@ const zhTW = {
|
|||||||
'taskName': '任務名稱',
|
'taskName': '任務名稱',
|
||||||
'taskUrl': '任務連結',
|
'taskUrl': '任務連結',
|
||||||
'downloadPath': '下載路徑',
|
'downloadPath': '下載路徑',
|
||||||
|
'skipVerifyCert': '跳過憑證驗證',
|
||||||
|
'name': '名稱',
|
||||||
|
'size': '大小',
|
||||||
|
'unknown': '未知',
|
||||||
|
'fileSelectedCount': '文件數:',
|
||||||
|
'fileSelectedSize': '大小:',
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
@@ -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
Loaded 50 of 53 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user