Compare commits

...
17 Commits
Author SHA1 Message Date
Levi ef97528114 ci: fix window 2023-11-29 21:12:06 +08:00
Levi b58669bb46 ci: fix windows build 2023-11-29 20:35:20 +08:00
Levi 51d28c5e53 release: v1.5.0 2023-11-29 20:18:08 +08:00
Levi db15650664 feat: support browse files on web (#283) 2023-11-29 18:24:01 +08:00
Levi ee7da179c1 chore: upgrade flutter to v3.16.1 (#282) 2023-11-29 13:32:07 +08:00
Levi 6bb32372ab feat: support more configurations (#281) 2023-11-29 12:52:51 +08:00
Levi 7d6428c967 fix: task can not completed (#280) 2023-11-28 18:27:36 +08:00
Levi bba5107dd5 fix: crash on android arm-32 (#279) 2023-11-27 11:10:50 +08:00
liwei ddfe76b635 fix: script engine event loop stop 2023-11-25 00:43:40 +08:00
Levi aad7395dde feat: enhanced extension script engine (#277)
1. update goja version
2. add node vm polyfill
3. non-stop event loop handle
4 improve logger print
2023-11-24 22:41:16 +08:00
Levi 6f541715df fix: extension download path (#274) 2023-11-24 18:16:03 +08:00
neovali 848797b2fd fix: .deb build (#269) 2023-11-15 21:01:28 +08:00
Levi 0a05bf41e7 fix: incorrect storage path in linux (#270) 2023-11-15 16:26:29 +08:00
liwei 485656d1d4 ci: fix linux desktop entry 2023-11-13 16:23:26 +08:00
liwei c074e0c2bc chore: generate locales 2023-11-13 15:55:02 +08:00
yuloury 0db0a2c987 ci: fix deb desktop entry (#268) 2023-11-11 09:23:17 +08:00
Elliot b06a79b5c7 feat: improve zh_TW translate 2023-11-10 09:11:50 +08:00
58 changed files with 986 additions and 365 deletions

No files matched your search

+81 -71
View File
@@ -7,10 +7,14 @@ on:
description: 'Build platform'
required: true
default: 'all'
channel:
description: 'Snap channel'
required: true
default: 'stable'
env:
GO_VERSION: "1.19"
FLUTTER_VERSION: "3.10.6"
FLUTTER_VERSION: "3.16.1"
permissions:
contents: write
@@ -52,7 +56,7 @@ jobs:
flutter build windows
$mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32"
$release = "build\windows\runner\Release\"
$release = "build\windows\x64\runner\Release\"
cp $mingw\libstdc++-6.dll $release
cp $mingw\libgcc_s_seh-1.dll $release
cp $mingw\libwinpthread-1.dll $release
@@ -106,7 +110,7 @@ jobs:
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[UninstallDelete]
@@ -208,7 +212,7 @@ jobs:
overwrite: true
build-linux:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
runs-on: ubuntu-latest
runs-on: ubuntu-20.04
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
@@ -221,10 +225,6 @@ jobs:
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
- name: Setup LXD
uses: canonical/[email protected]
with:
channel: latest/stable
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -235,46 +235,45 @@ jobs:
mkdir -p build/dist
# Build portable
tar -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz build/linux/x64/release/bundle
tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./
# Build deb package
mkdir -p debian/gui
cp assets/icon/icon_1024.png debian/gui/gopeed.png
cat>debian/debian.yaml<<EOF
flutter_app:
command: gopeed
arch: x64
parent: /usr/local/bin
control:
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
mkdir -p debian/gopeed/opt/gopeed
mkdir -p debian/gopeed/usr/share/applications
mkdir -p debian/gopeed/usr/share/icons/hicolor/scalable/apps
mkdir -p debian/gopeed/DEBIAN
cat>debian/gui/gopeed.desktop<<EOF
[Desktop Entry]
Version=${VERSION:1}
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
Exec=gopeed
Icon=gopeed.png
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;Downloader;
EOF
dart pub global activate flutter_to_debian
flutter_to_debian
mv debian/packages/gopeed*.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
cp -r build/linux/x64/release/bundle/* debian/gopeed/opt/gopeed/
cp assets/icon/icon.svg debian/gopeed/usr/share/icons/hicolor/scalable/apps/com.gopeed.Gopeed.svg
cat>debian/gopeed/DEBIAN/postinst<<EOF
#!/bin/bash
ln -sf /opt/gopeed/gopeed /usr/bin/gopeed
EOF
cat>debian/gopeed/DEBIAN/postrm<<EOF
#!/bin/bash
rm -rf /usr/bin/gopeed
EOF
chmod 0755 debian/gopeed/DEBIAN/postinst
chmod 0755 debian/gopeed/DEBIAN/postrm
cat>debian/gopeed/DEBIAN/control<<EOF
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cp linux/assets/com.gopeed.Gopeed.desktop debian/gopeed/usr/share/applications/gopeed.desktop
dpkg-deb --build debian/gopeed
mv debian/gopeed.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
# Build AppImage
sudo apt-get -y install libfuse2
@@ -282,8 +281,8 @@ jobs:
chmod +x appimage-builder
mkdir AppDir
cp -r build/linux/x64/release/bundle/* AppDir
mkdir -p AppDir/usr/share/icons
cp assets/icon/icon_1024.png AppDir/usr/share/icons/gopeed.png
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
cp assets/icon/icon.svg AppDir/usr/share/icons/hicolor/scalable/apps/gopeed.svg
cat>AppImageBuilder.yml<<EOF
version: 1
AppDir:
@@ -291,7 +290,7 @@ jobs:
app_info:
id: com.gopeed.gopeed
name: Gopeed
icon: gopeed.png
icon: gopeed
version: ${VERSION:1}
exec: gopeed
exec_args: \$@
@@ -338,12 +337,39 @@ jobs:
EOF
./appimage-builder --skip-tests --recipe AppImageBuilder.yml
mv Gopeed-*-x86_64.AppImage build/dist/Gopeed-$VERSION-linux-amd64.AppImage
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/dist/*
overwrite: true
build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
runs-on: ubuntu-latest
needs: [ get-release ]
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: Setup LXD
uses: canonical/[email protected]
with:
channel: latest/stable
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
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
# Build snap
sudo snap install snapcraft --classic
rm -rf build/linux
mkdir -p snap/gui
cp assets/icon/icon_1024.png snap/gui/gopeed.png
cp assets/icon/icon.svg snap/gui/gopeed.svg
cat>snap/snapcraft.yaml<<EOF
name: gopeed
@@ -378,35 +404,19 @@ jobs:
source: .
plugin: flutter
stage-packages:
- libgtk-3-dev
- libayatana-appindicator3-dev
flutter-target: lib/main.dart
EOF
cat>snap/gui/gopeed.desktop<<EOF
[Desktop Entry]
Version=${VERSION:1}
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
Exec=gopeed
Icon=\${SNAP}/meta/gui/gopeed.png
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;Downloader;
EOF
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
snapcraft --use-lxd
# Snapcraft login
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
snapcraft upload --release=stable gopeed_${VERSION:1}_amd64.snap
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/dist/*
overwrite: true
snapcraft upload --release=${{ github.event.inputs.channel }} gopeed_${VERSION:1}_amd64.snap
build-android:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
runs-on: ubuntu-latest
+1 -1
View File
@@ -28,7 +28,7 @@ on:
env:
GO_VERSION: "1.19"
FLUTTER_VERSION: "3.10.6"
FLUTTER_VERSION: "3.16.1"
jobs:
# lint:
+46
View File
@@ -0,0 +1,46 @@
# Gopeed 協助指南
首先感謝您願意幫助我們改進並優化該項目,這份指南將會幫助您更好的參與 Gopeed 的開發。
## 分支說明
本項目只有一個分支,即 `main` 分支,如果您想要參與 Gopeed 的開發,請先 fork 該項目,再在您自己的 fork 中進行開發,開發完成後再開啟PR,以合併至 `main` 分支。
## 離線開發
建議使用 web 端進行開發與調試,首先啟動服務,使用指令 `go run cmd/api/main.go` 啟動 ,該服務默認連接埠為 `9999`,接著以 `debug` 模式啟動前端 flutter 項目即可。
## 翻譯
Gopeed 的翻譯文件位於 `ui/flutter/assets/locales` 目錄中,只需要修改或新建翻譯文件即可。
修改locales後請執行一下指令:
```
get generate locales
```
請以 `en_US.json` 作為參照。
## flutter開發
每次提交PR前請務必執行 `dart format ./ui/flutter`
如果需要編輯 api/models,請打開build_runner watcher:
```
flutter pub run build_runner watch
```
適用於 get-cli 的指令:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+1 -1
View File
@@ -1,4 +1,4 @@
FROM instrumentisto/flutter:3.10.6 AS flutter
FROM instrumentisto/flutter:3.16.1 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
+1 -1
View File
@@ -9,7 +9,7 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## Introduction
+1 -1
View File
@@ -9,7 +9,7 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## はじめに
+1 -1
View File
@@ -9,7 +9,7 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## 介绍
+179
View File
@@ -0,0 +1,179 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## 簡介
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
## 安裝
**已支援的平台**
- [x] Windows
- [x] MacOS
- [x] Linux
- [x] Android
- [x] iOS
- [x] Web
- [x] Docker
[下載](https://github.com/GopeedLab/gopeed/releases/latest)
> 註:MacOS 版運行時若提示損壞,請在終端中執行 `xattr -d com.apple.quarantine /Applications/Gopeed.app`
### 使用CLI安裝
使用`go install`安裝:
```bash
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接執行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### 使用 Docker Compose
```bash
docker-compose up -d
```
#### 訪問服務
當 docker 容器運作時,可以通過 `http://localhost:9999` 訪問 web 頁面。
## 贊助
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
## 軟體介面
![](_docs/img/ui-demo.png)
## 開發
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
> 前端代碼位於`ui/flutter`目錄內。
### 開發環境
1. Golang 1.19+
2. Flutter 3.10+
### 克隆項目
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 協助開發
請參考[協助指南](CONTRIBUTING_zh-TW.md)
### 編譯
#### 桌面端
首先需要按照[flutter desktop 官方文檔](https://docs.flutter.dev/development/platform-integration/desktop)配置開發環境,並準備好`cgo`環境,具體方法可以自行搜索。
組建指令:
- windows
```bash
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
```
- macos
```bash
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
```
- linux
```bash
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
```
#### 移動設備
需要`cgo`環境,並安裝`gomobile`:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
組建指令:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
- ios
```bash
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 ios --no-codesign
```
#### 網頁端
組建指令:
```bash
cd ui/flutter
flutter build web
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感謝
### 貢獻者
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 軟體許可
該軟體遵循 [GPLv3](LICENSE) 。
+4 -3
View File
@@ -8,9 +8,10 @@ import (
// only for local development
func main() {
cfg := &model.StartConfig{
Network: "tcp",
Address: "127.0.0.1:9999",
Storage: model.StorageBolt,
Network: "tcp",
Address: "127.0.0.1:9999",
Storage: model.StorageBolt,
WebEnable: true,
}
cmd.Start(cfg)
}
+11 -11
View File
@@ -5,14 +5,16 @@ go 1.19
require (
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.5
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
github.com/go-git/go-git/v5 v5.8.1
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0
github.com/rs/zerolog v1.31.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6
go.etcd.io/bbolt v1.3.8
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/sync v0.3.0
)
@@ -45,7 +47,7 @@ require (
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect
github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect
@@ -57,7 +59,7 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect
@@ -88,19 +90,17 @@ require (
github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect
github.com/tidwall/btree v1.6.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opentelemetry.io/otel v1.8.0 // indirect
go.opentelemetry.io/otel/trace v1.8.0 // indirect
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
golang.org/x/crypto v0.14.0 // indirect
golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/sys v0.13.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.6.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
+19 -18
View File
@@ -124,17 +124,17 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 h1:ObbB2tzHWWAxzsG5futqeq2Ual2zYlo/+eMkSc5sn8w=
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562/go.mod h1:X2TOTJ+Uamd454RFp7ig2tmP3hQg0Z2Qk8gbVQmU0mk=
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c h1:hLoodLRD4KLWIH8eyAQCLcH8EqIrjac7fCkp/fHnvuQ=
github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -213,8 +213,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -420,6 +421,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 h1:NkHVT1GBVV9hUj1iWglFxAuNlvLHELFn71U2H7aOIog=
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6/go.mod h1:tDI2vlr15+D9cyN3odhFrZJi3oJj8HlHaOEFPrUiGSY=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -439,8 +442,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
@@ -482,9 +485,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -534,10 +536,11 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -545,8 +548,7 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -557,9 +559,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+3 -3
View File
@@ -6,7 +6,7 @@ import (
)
type Controller struct {
GetConfig func(v any) (bool, error)
GetConfig func(v any) bool
FileController
//ContextDialer() (proxy.Dialer, error)
}
@@ -21,8 +21,8 @@ type DefaultFileController struct {
func NewController() *Controller {
return &Controller{
FileController: &DefaultFileController{},
GetConfig: func(v any) (bool, error) {
return false, nil
GetConfig: func(v any) bool {
return false
},
}
}
+9
View File
@@ -57,6 +57,15 @@ func (m *FetcherMeta) SingleFilepath() string {
return path.Join(m.Opts.Path, file.Path, fileName)
}
// RootDirPath return the root dir path of the task file.
func (m *FetcherMeta) RootDirPath() string {
if m.Res.Name != "" {
return m.FolderPath()
} else {
return m.Opts.Path
}
}
// FetcherBuilder defines the interface for a fetcher builder.
type FetcherBuilder interface {
// Schemes returns the schemes supported by the fetcher.
+2 -1
View File
@@ -1,5 +1,6 @@
package bt
type config struct {
Trackers []string `json:"trackers"`
ListenPort int `json:"listenPort"`
Trackers []string `json:"trackers"`
}
+12 -9
View File
@@ -23,7 +23,8 @@ var (
)
type Fetcher struct {
ctl *controller.Controller
ctl *controller.Controller
config *config
torrent *torrent.Torrent
meta *fetcher.FetcherMeta
@@ -42,6 +43,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
exist := f.ctl.GetConfig(&f.config)
if !exist {
f.config = &config{
ListenPort: 0,
Trackers: []string{},
}
}
return
}
@@ -54,7 +62,7 @@ func (f *Fetcher) initClient() (err error) {
}
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
cfg.ListenPort = f.config.ListenPort
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
@@ -231,11 +239,6 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
if err != nil {
return
}
var cfg config
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return
}
// use map to deduplicate
trackers := make(map[string]bool)
@@ -247,8 +250,8 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
}
}
}
if exist && len(cfg.Trackers) > 0 {
for _, tracker := range cfg.Trackers {
if len(f.config.Trackers) > 0 {
for _, tracker := range f.config.Trackers {
trackers[tracker] = true
}
}
+3 -3
View File
@@ -98,11 +98,11 @@ func buildConfigFetcher() fetcher.Fetcher {
"udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce",
}}
newController.GetConfig = func(v any) (bool, error) {
newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false, err
return false
}
return true, nil
return true
}
fetcher.Setup(newController)
return fetcher
+2 -1
View File
@@ -1,5 +1,6 @@
package http
type config struct {
Connections int `json:"connections"`
UserAgent string `json:"userAgent"`
Connections int `json:"connections"`
}
+57 -57
View File
@@ -51,6 +51,7 @@ func newChunk(begin int64, end int64) *chunk {
type Fetcher struct {
ctl *controller.Controller
config *config
doneCh chan error
meta *fetcher.FetcherMeta
@@ -71,6 +72,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
exist := f.ctl.GetConfig(&f.config)
if !exist {
f.config = &config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
Connections: 1,
}
}
return
}
@@ -78,7 +86,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
return err
}
httpReq, err := buildRequest(nil, req)
httpReq, err := f.buildRequest(nil, req)
if err != nil {
return err
}
@@ -160,16 +168,7 @@ func (f *Fetcher) Create(opts *base.Options) error {
}
extra := opts.Extra.(*fhttp.OptsExtra)
if extra.Connections == 0 {
var cfg config
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return err
}
if exist {
extra.Connections = cfg.Connections
} else {
extra.Connections = 1
}
extra.Connections = f.config.Connections
}
return nil
}
@@ -260,7 +259,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
chunk := f.chunks[index]
chunk.retryTimes = 0
httpReq, err := buildRequest(ctx, f.meta.Req)
httpReq, err := f.buildRequest(ctx, f.meta.Req)
if err != nil {
return err
}
@@ -357,6 +356,52 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return
}
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
url, err := url.Parse(req.URL)
if err != nil {
return
}
var (
method string
body io.Reader
)
headers := make(map[string][]string)
if req.Extra == nil {
method = http.MethodGet
} else {
extra := req.Extra.(*fhttp.ReqExtra)
if extra.Method != "" {
method = extra.Method
} else {
method = http.MethodGet
}
if len(extra.Header) > 0 {
for k, v := range extra.Header {
headers[k] = []string{v}
}
}
if extra.Body != "" {
body = bytes.NewBufferString(extra.Body)
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
// load user agent from config
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
} else {
httpReq, err = http.NewRequest(method, url.String(), body)
}
if err != nil {
return
}
httpReq.Header = headers
return httpReq, nil
}
func (f *Fetcher) splitChunk() (chunks []*chunk) {
if f.meta.Res.Range {
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
@@ -393,51 +438,6 @@ func buildClient() *http.Client {
}
}
func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
url, err := url.Parse(req.URL)
if err != nil {
return
}
var (
method string
body io.Reader
)
headers := make(map[string][]string)
if req.Extra == nil {
method = http.MethodGet
} else {
extra := req.Extra.(*fhttp.ReqExtra)
if extra.Method != "" {
method = extra.Method
} else {
method = http.MethodGet
}
if len(extra.Header) > 0 {
for k, v := range extra.Header {
headers[k] = []string{v}
}
}
if extra.Body != "" {
body = bytes.NewBufferString(extra.Body)
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
headers[base.HttpHeaderUserAgent] = []string{base.AgentName}
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
} else {
httpReq, err = http.NewRequest(method, url.String(), body)
}
if err != nil {
return
}
httpReq.Header = headers
return httpReq, nil
}
type fetcherData struct {
Chunks []*chunk
}
+4 -4
View File
@@ -123,7 +123,7 @@ func TestFetcher_DownloadError(t *testing.T) {
}
func TestFetcher_DownloadLimit(t *testing.T) {
listener := test.StartTestLimitServer()
listener := test.StartTestLimitServer(4, 0)
defer listener.Close()
downloadNormal(listener, 1, t)
@@ -314,11 +314,11 @@ func buildConfigFetcher() fetcher.Fetcher {
mockCfg := config{
Connections: 16,
}
newController.GetConfig = func(v any) (bool, error) {
newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false, err
return false
}
return true, nil
return true
}
fetcher.Setup(newController)
return fetcher
+48 -4
View File
@@ -138,7 +138,7 @@ func StartTestErrorServer() net.Listener {
}
// StartTestLimitServer connections limit server
func StartTestLimitServer() net.Listener {
func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
var connections atomic.Int32
return startTestServer(func() http.Handler {
@@ -148,7 +148,7 @@ func StartTestLimitServer() net.Listener {
connections.Add(-1)
}()
connections.Add(1)
if connections.Load() > 4 {
if maxConnections != 0 && connections.Load() > maxConnections {
writer.WriteHeader(403)
return
}
@@ -162,7 +162,7 @@ func StartTestLimitServer() net.Listener {
panic(err)
}
defer file.Close()
io.Copy(writer, file)
slowCopy(writer, file, delay)
} else {
// split range
s := strings.Split(r, "=")
@@ -203,13 +203,57 @@ func StartTestLimitServer() net.Listener {
}
defer file.Close()
file.Seek(start, 0)
io.CopyN(writer, file, end-start+1)
slowCopyN(writer, file, end-start+1, delay)
}
})
return mux
})
}
// 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) {
buf := make([]byte, 32*1024)
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
if delay > 0 {
time.Sleep(time.Millisecond * time.Duration(delay))
}
}
return written, err
}
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
if written == n {
return n, nil
}
if written < n && err == nil {
// src stopped early; must have been EOF.
err = io.EOF
}
return
}
func startTestServer(serverHandle func() http.Handler) net.Listener {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
-4
View File
@@ -23,7 +23,3 @@ const (
HttpHeaderRangeFormat = "bytes=%d-%d"
)
const (
AgentName = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
)
+10 -18
View File
@@ -212,7 +212,7 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
ctl := controller.NewController()
ctl.GetConfig = func(v any) (bool, error) {
ctl.GetConfig = func(v any) bool {
return d.getProtocolConfig(fetcher.Name(), v)
}
fetcher.Setup(ctl)
@@ -540,18 +540,7 @@ func (d *Downloader) GetTask(id string) *Task {
}
func (d *Downloader) GetTasks() []*Task {
tasks := make([]*Task, len(d.tasks))
for i := 0; i < len(d.tasks); i++ {
tasks[i] = d.tasks[i]
}
// sort tasks by status, order by Status(if(running,1,0)) desc, CreatedAt desc
sort.Slice(tasks, func(i, j int) bool {
if tasks[i].Status != tasks[j].Status {
return tasks[i].Status == base.DownloadStatusRunning
}
return tasks[i].CreatedAt.After(tasks[j].CreatedAt)
})
return tasks
return d.tasks
}
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
@@ -563,18 +552,19 @@ func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
return d.storage.Put(bucketConfig, "config", v)
}
func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) {
func (d *Downloader) getProtocolConfig(name string, v any) bool {
cfg, err := d.GetConfig()
if err != nil {
return false, err
return false
}
if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil {
return false, nil
return false
}
if err := util.MapToStruct(cfg.ProtocolConfig[name], v); err != nil {
return false, err
d.Logger.Warn().Err(err).Msgf("get protocol config failed")
return false
}
return true, nil
return true
}
// wait task done
@@ -635,6 +625,8 @@ func (d *Downloader) restoreFetcher(task *Task) error {
task.fetcher.Meta().Res = task.Meta.Res
}
go d.watch(task)
} else if task.Status == base.DownloadStatusError {
go d.watch(task)
}
task.fetcher.Create(task.Meta.Opts)
return nil
+2 -5
View File
@@ -205,10 +205,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
defer downloader.Clear()
var httpCfg map[string]any
exits, err := downloader.getProtocolConfig("http", &httpCfg)
if err != nil {
t.Fatal(err)
}
exits := downloader.getProtocolConfig("http", &httpCfg)
if exits {
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
}
@@ -234,7 +231,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
},
}
if err = downloader.PutConfig(storeCfg); err != nil {
if err := downloader.PutConfig(storeCfg); err != nil {
t.Fatal(err)
}
+47 -6
View File
@@ -3,14 +3,14 @@ package engine
import (
_ "embed"
"errors"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/process"
"github.com/dop251/goja_nodejs/url"
"time"
)
//go:embed polyfill/out/index.js
@@ -27,12 +27,16 @@ type Engine struct {
func (e *Engine) RunString(script string) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
err = r.(error)
}
}()
var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) {
result, err = runtime.RunString(script)
if err == nil {
go e.await(result)
}
})
if err != nil {
return
@@ -45,9 +49,10 @@ func (e *Engine) RunString(script string) (value any, err error) {
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("%v", r)
err = r.(error)
}
}()
var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) {
if args == nil {
@@ -59,13 +64,45 @@ func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err err
}
result, err = fn(nil, jsArgs...)
}
if err == nil {
go e.await(result)
}
})
if err != nil {
return nil, err
return
}
return resolveResult(result)
}
// loop.Run will hang if the script result has a non-stop code, such as setInterval.
// This method will stop the event loop when the promise result is resolved.
func (e *Engine) await(value any) {
if value == nil {
return
}
if v, ok := value.(goja.Value); ok {
// if result is promise, wait for it to be resolved
if p, ok := v.Export().(*goja.Promise); ok {
if p.State() != goja.PromiseStatePending {
return
}
// check promise state every 100 milliseconds, until it is resolved
for {
time.Sleep(time.Millisecond * 100)
if p.State() == goja.PromiseStatePending {
continue
}
break
}
// stop the event loop
e.loop.StopNoWait()
}
}
}
func (e *Engine) Close() {
e.loop.Stop()
}
@@ -78,8 +115,8 @@ func NewEngine() *Engine {
loop.Run(func(runtime *goja.Runtime) {
engine.Runtime = runtime
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime)
url.Enable(runtime)
process.Enable(runtime)
if err := file.Enable(runtime); err != nil {
return
}
@@ -96,6 +133,10 @@ func NewEngine() *Engine {
if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
return
}
// polyfill window.location
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
return
}
return
})
return engine
+60
View File
@@ -23,6 +23,7 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData")
doTestPolyfill(t, "fetch")
doTestPolyfill(t, "__gopeed_create_vm")
}
func TestFetch(t *testing.T) {
@@ -191,6 +192,65 @@ function testTimeout(){
}
}
func TestVm(t *testing.T) {
engine := NewEngine()
value, err := engine.RunString(`
const vm = __gopeed_create_vm()
vm.set('a', 1)
vm.set('b', 2)
const result = vm.runString('a=a+1;b=b+1;a+b;')
const out = {
"a": vm.get('a'),
"b": vm.get('b'),
"result": result
}
out
`)
if err != nil {
t.Fatal(err)
}
want := map[string]any{
"a": 2,
"b": 3,
"result": 5,
}
if !test.JsonEqual(value, want) {
t.Fatalf("vm test failed, want %v, got %v", want, value)
}
}
func TestNonStopLoop(t *testing.T) {
engine := NewEngine()
_, err := engine.RunString(`
function leak(){
setInterval(() => {
},500)
}
function test(){
leak()
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done')
}, 1000)
})
}
`)
if err != nil {
t.Fatal(err)
}
val, err := callTestFun(engine, "test")
if err != nil {
panic(err)
}
if val != "done" {
t.Fatalf("infinite loop test failed, want %s, got %s", "done", val)
}
}
func doTestPolyfill(t *testing.T, module string) {
value, err := Run(fmt.Sprintf(`
!!globalThis['%s']
+44
View File
@@ -0,0 +1,44 @@
package vm
import (
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
)
type Vm struct {
loop *eventloop.EventLoop
}
func (vm *Vm) Set(name string, value any) {
vm.loop.Run(func(runtime *goja.Runtime) {
runtime.Set(name, value)
})
}
func (vm *Vm) Get(name string) (value any) {
vm.loop.Run(func(runtime *goja.Runtime) {
value = runtime.Get(name)
})
return
}
func (vm *Vm) RunString(script string) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
vm.loop.Run(func(runtime *goja.Runtime) {
value, err = runtime.RunString(script)
})
return
}
func Enable(runtime *goja.Runtime) error {
return runtime.Set("__gopeed_create_vm", func(call goja.FunctionCall) goja.Value {
return runtime.ToValue(&Vm{
loop: eventloop.NewEventLoop(),
})
})
}
+6 -6
View File
@@ -598,28 +598,28 @@ type InstanceLogger struct {
logger *logger.Logger
}
func (l *InstanceLogger) Debug(msg ...any) {
func (l *InstanceLogger) Debug(msg ...goja.Value) {
if l.devMode {
l.logger.Debug().Msg(l.append(msg...))
}
}
func (l *InstanceLogger) Info(msg ...any) {
func (l *InstanceLogger) Info(msg ...goja.Value) {
l.logger.Info().Msg(l.append(msg...))
}
func (l *InstanceLogger) Warn(msg ...any) {
func (l *InstanceLogger) Warn(msg ...goja.Value) {
l.logger.Warn().Msg(l.append(msg...))
}
func (l *InstanceLogger) Error(msg ...any) {
func (l *InstanceLogger) Error(msg ...goja.Value) {
l.logger.Error().Msg(l.append(msg...))
}
func (l *InstanceLogger) append(msg ...any) string {
func (l *InstanceLogger) append(msg ...goja.Value) string {
strMsg := make([]string, len(msg))
for i, m := range msg {
strMsg[i] = fmt.Sprint(m)
strMsg[i] = m.String()
}
return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " "))
}
+5 -4
View File
@@ -4,6 +4,7 @@ import (
"errors"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/dop251/goja"
"os"
"testing"
"time"
@@ -356,10 +357,10 @@ func TestDownloader_Extension_Logger(t *testing.T) {
il := newInstanceLogger(&Extension{
Name: "test",
}, logger)
il.Debug("msg1", "msg2")
il.Info("msg1", "msg2")
il.Warn("msg1", "msg2")
il.Error("msg1", "msg2")
il.Debug(goja.NaN(), goja.Undefined())
il.Info(goja.NaN(), goja.Undefined())
il.Warn(goja.NaN(), goja.Undefined())
il.Error(goja.NaN(), goja.Undefined())
}
func setupDownloader(fn func(downloader *Downloader)) {
+41 -14
View File
@@ -105,9 +105,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension)
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
if startCfg.WebEnable {
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
}
@@ -138,30 +139,56 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
return srv, listener, nil
}
func resolvePath(urlPath string, prefix string) (identity string, path string, err error) {
// remove prefix
clearPath := strings.TrimPrefix(urlPath, prefix)
// match extension identity, eg: /fs/extensions/identity/xxx
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
if !reg.MatchString(clearPath) {
err = os.ErrNotExist
return
}
matched := reg.FindStringSubmatch(clearPath)
if len(matched) != 3 {
err = os.ErrNotExist
return
}
return matched[1], matched[2], nil
}
// handle task file resource
type taskFileSystem struct {
}
func (e *taskFileSystem) Open(name string) (http.File, error) {
// get extension identity
identity, path, err := resolvePath(name, "/fs/tasks")
if err != nil {
return nil, err
}
task := Downloader.GetTask(identity)
if task == nil {
return nil, os.ErrNotExist
}
return os.Open(filepath.Join(task.Meta.RootDirPath(), path))
}
// handle extension file resource
type extensionFileSystem struct {
}
func (e *extensionFileSystem) Open(name string) (http.File, error) {
// remove prefix
path := strings.TrimPrefix(name, "/fs/extensions")
// match extension identity, eg: /fs/extensions/identity/xxx
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
if !reg.MatchString(path) {
return nil, os.ErrNotExist
}
matched := reg.FindStringSubmatch(path)
if len(matched) != 3 {
return nil, os.ErrNotExist
}
// get extension identity
identity := matched[1]
identity, path, err := resolvePath(name, "/fs/extensions")
if err != nil {
return nil, err
}
extension, err := Downloader.GetExtension(identity)
if err != nil {
return nil, os.ErrNotExist
}
extensionPath := Downloader.ExtensionPath(extension)
return os.Open(filepath.Join(extensionPath, matched[2]))
return os.Open(filepath.Join(extensionPath, path))
}
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
+1
View File
@@ -491,6 +491,7 @@ func doTest(handler func()) {
cfg.Init()
cfg.Storage = storage
cfg.StorageDir = ".test_storage"
cfg.WebEnable = true
fileListener := doStart(cfg)
defer func() {
if err := fileListener.Close(); err != nil {
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024px" height="1024px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.998" fill="#5dc76c" d="M 951.5,763.5 C 940.739,755.276 929.405,747.776 917.5,741C 908.477,736.319 899.143,732.485 889.5,729.5C 885.844,625.779 840.844,545.613 754.5,489C 674.802,441.453 590.135,430.12 500.5,455C 419.755,480.605 359.588,530.771 320,605.5C 278.585,565.816 230.752,555.649 176.5,575C 131.948,594.935 106.448,629.102 100,677.5C 99.3333,685.833 99.3333,694.167 100,702.5C 102.081,714.824 105.081,726.824 109,738.5C 95.4882,745.492 82.9882,753.826 71.5,763.5C 37.7121,705.639 16.5454,643.306 8,576.5C -9.41698,416.759 35.9164,278.093 144,160.5C 244.661,58.2827 367.161,5.94939 511.5,3.5C 678.975,7.48829 814.142,74.8216 917,205.5C 1011.33,336.426 1040,480.759 1003,638.5C 991.564,682.671 974.397,724.337 951.5,763.5 Z"/></g>
<g><path style="opacity:0.999" fill="#fefefe" d="M 951.5,763.5 C 949.28,768.272 946.78,772.938 944,777.5C 938.305,786.913 932.138,795.913 925.5,804.5C 915.954,796.888 905.288,791.055 893.5,787C 878.282,782.958 863.115,778.791 848,774.5C 854.013,716.891 841.346,663.891 810,615.5C 768.341,553.228 711.174,512.395 638.5,493C 560.311,479.566 490.311,497.566 428.5,547C 411.963,560.536 397.13,575.703 384,592.5C 366.745,616.923 348.911,640.923 330.5,664.5C 310.135,651.302 290.469,637.135 271.5,622C 238.448,602.307 206.782,604.307 176.5,628C 159.375,642.752 148.875,661.252 145,683.5C 143.968,692.716 144.968,701.716 148,710.5C 154.334,725.339 160.167,740.339 165.5,755.5C 138.118,764.688 114.451,779.688 94.5,800.5C 85.653,788.968 77.9863,776.635 71.5,763.5C 82.9882,753.826 95.4882,745.492 109,738.5C 105.081,726.824 102.081,714.824 100,702.5C 99.3333,694.167 99.3333,685.833 100,677.5C 106.448,629.102 131.948,594.935 176.5,575C 230.752,555.649 278.585,565.816 320,605.5C 359.588,530.771 419.755,480.605 500.5,455C 590.135,430.12 674.802,441.453 754.5,489C 840.844,545.613 885.844,625.779 889.5,729.5C 899.143,732.485 908.477,736.319 917.5,741C 929.405,747.776 940.739,755.276 951.5,763.5 Z"/></g>
<g><path style="opacity:0.998" fill="#5dc76c" d="M 925.5,804.5 C 850.791,908.608 750.791,975.775 625.5,1006C 609.308,1009.7 592.975,1012.53 576.5,1014.5C 615.708,969.938 655.042,925.438 694.5,881C 664.168,880.5 633.835,880.333 603.5,880.5C 603.5,811.167 603.5,741.833 603.5,672.5C 542.167,672.5 480.833,672.5 419.5,672.5C 419.5,741.5 419.5,810.5 419.5,879.5C 388.493,879.167 357.493,879.5 326.5,880.5C 326.624,881.107 326.957,881.44 327.5,881.5C 366.805,925.808 406.138,970.141 445.5,1014.5C 318.63,996.317 212.796,939.651 128,844.5C 115.85,830.525 104.683,815.859 94.5,800.5C 114.451,779.688 138.118,764.688 165.5,755.5C 160.167,740.339 154.334,725.339 148,710.5C 144.968,701.716 143.968,692.716 145,683.5C 148.875,661.252 159.375,642.752 176.5,628C 206.782,604.307 238.448,602.307 271.5,622C 290.469,637.135 310.135,651.302 330.5,664.5C 348.911,640.923 366.745,616.923 384,592.5C 397.13,575.703 411.963,560.536 428.5,547C 490.311,497.566 560.311,479.566 638.5,493C 711.174,512.395 768.341,553.228 810,615.5C 841.346,663.891 854.013,716.891 848,774.5C 863.115,778.791 878.282,782.958 893.5,787C 905.288,791.055 915.954,796.888 925.5,804.5 Z"/></g>
<g><path style="opacity:0.999" fill="#fefefe" d="M 576.5,1014.5 C 532.836,1019.82 489.169,1019.82 445.5,1014.5C 406.138,970.141 366.805,925.808 327.5,881.5C 358.167,880.833 388.833,880.167 419.5,879.5C 419.5,810.5 419.5,741.5 419.5,672.5C 480.833,672.5 542.167,672.5 603.5,672.5C 603.5,741.833 603.5,811.167 603.5,880.5C 633.835,880.333 664.168,880.5 694.5,881C 655.042,925.438 615.708,969.938 576.5,1014.5 Z"/></g>
<g><path style="opacity:1" fill="#ade2b5" d="M 419.5,879.5 C 388.833,880.167 358.167,880.833 327.5,881.5C 326.957,881.44 326.624,881.107 326.5,880.5C 357.493,879.5 388.493,879.167 419.5,879.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+22 -14
View File
@@ -1,5 +1,5 @@
{
"label": "中文 (正體) ",
"label": "中文 (繁體) ",
"error": "錯誤",
"tip": "提示",
"confirm": "確定",
@@ -12,6 +12,7 @@
"donate": "斗內",
"exit": "離開",
"create": "新增任務",
"directDownload": "直接下載",
"advancedOptions": "進階選項",
"downloadLink": "下載鏈接",
"downloadLinkValid": "請輸入下載鏈接",
@@ -27,16 +28,16 @@
"general": "通用設定",
"downloadDir": "下載目録",
"downloadDirValid": "請選擇下載目録",
"connections": "同時連接數量",
"connections": "同時連接數",
"maxRunning": "最大同時下載數",
"items": "@count 項",
"subscribeTracker": "已訂閲的 Tracker",
"subscribeTracker": "已訂閲的追蹤者列表",
"subscribeFail": "訂閲失敗",
"update": "更新",
"updateDaily": "每日自動更新",
"updateDaily": "每天自動更新",
"lastUpdate": "上次更新時間: @time",
"addTracker": "增加新的 Tracker",
"addTrackerHit": "請輸入 Tracker 服務器地址,每行一條",
"addTracker": "增加新的追蹤者",
"addTrackerHit": "請輸入追蹤者服務器地址,每行一條",
"ui": "介面",
"theme": "佈景主題",
"themeSystem": "跟隨系統",
@@ -46,12 +47,12 @@
"about": "關於",
"homepage": "訪問主頁",
"version": "版本資訊",
"protocol": "通訊協定",
"protocol": "端口設定",
"port": "端口",
"apiToken": "接口 Token",
"notSet": "尚未設定",
"apiToken": "接口秘鑰",
"notSet": "未設定",
"set": "已設定",
"effectAfterRestart": "該選項將在重新啓動軟體後生效",
"effectAfterRestart": "重啟後生效",
"startAll": "全部開始",
"pauseAll": "全部暫停",
"deleteTask": "刪除任務",
@@ -61,9 +62,16 @@
"newVersionUpdate": "立刻更新",
"newVersionLater": "稍後更新",
"extensions": "外掛程序",
"extensionInstallUrl": "安裝鏈接",
"extensionInstallSuccess": "安裝完成",
"extensionInstallUrl": "安裝URL",
"extensionInstallSuccess": "安裝成功",
"extensionUpdateSuccess": "更新完成",
"extensionDelete": "解除安裝",
"extensionAlreadyLatest": "已是最新版本"
}
"extensionAlreadyLatest": "已是最新版本",
"extensionFind": "取得外掛",
"extensionDevelop": "開發外掛",
"history": "歷史記錄",
"clearHistory": "清空記錄",
"noHistoryFound": "還沒有記錄",
"serviceTitle": "下載服務",
"serviceText": "執行中"
}
@@ -30,6 +30,8 @@ class ProtocolConfig {
@JsonSerializable()
class HttpConfig {
String userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
int connections = 0;
HttpConfig();
@@ -41,6 +43,7 @@ class HttpConfig {
@JsonSerializable()
class BtConfig {
int listenPort = 0;
List<String> trackers = [];
BtConfig();
@@ -33,19 +33,23 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
'bt': instance.bt.toJson(),
};
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) =>
HttpConfig()..connections = json['connections'] as int;
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig()
..userAgent = json['userAgent'] as String
..connections = json['connections'] as int;
Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
<String, dynamic>{
'userAgent': instance.userAgent,
'connections': instance.connections,
};
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
..listenPort = json['listenPort'] as int
..trackers =
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
'listenPort': instance.listenPort,
'trackers': instance.trackers,
};
@@ -209,6 +209,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
Future<void> _initForegroundTask() async {
if (!Util.isMobile()) {
return;
}
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'gopeed_service',
@@ -269,13 +273,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
_defaultStartConfig!.address = "127.0.0.1:0";
} else {
_defaultStartConfig!.network = "unix";
if (Util.isDesktop()) {
_defaultStartConfig!.address = unixSocketPath;
}
if (Util.isMobile()) {
_defaultStartConfig!.address =
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
}
_defaultStartConfig!.address =
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
}
_defaultStartConfig!.apiToken = '';
return _defaultStartConfig!;
@@ -316,6 +315,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
result.addAll(trackers);
} catch (e) {
logger.w("subscribe trackers fail, url: $u", e);
return;
}
}
btExtConfig.subscribeTrackers.clear();
@@ -552,8 +552,8 @@ class CreateView extends GetView<CreateController> {
req: file.req!,
opt: Options(
name: file.name,
path: path.join(
_pathController.text, rr.res.name),
path: path.join(_pathController.text,
rr.res.name, file.path),
selectFiles: [],
extra: optExtra)));
}));
@@ -96,6 +96,24 @@ class SettingView extends GetView<SettingController> {
// http config items start
final httpConfig = downloaderCfg.value.protocolConfig.http;
final buildHttpUa =
_buildConfigItem('User-Agent', () => httpConfig.userAgent, (Key key) {
final uaController = TextEditingController(text: httpConfig.userAgent);
uaController.addListener(() async {
if (uaController.text.isNotEmpty &&
uaController.text != httpConfig.userAgent) {
httpConfig.userAgent = uaController.text;
await debounceSave();
}
});
return TextField(
key: key,
focusNode: FocusNode(),
controller: uaController,
);
});
final buildHttpConnections = _buildConfigItem(
'connections', () => httpConfig.connections.toString(), (Key key) {
final connectionsController =
@@ -122,8 +140,32 @@ class SettingView extends GetView<SettingController> {
});
// bt config items start
final btConfig = downloaderCfg.value.protocolConfig.bt;
final btExtConfig = downloaderCfg.value.extra.bt;
final buildBtListenPort = _buildConfigItem(
'port', () => btConfig.listenPort.toString(), (Key key) {
final listenPortController =
TextEditingController(text: btConfig.listenPort.toString());
listenPortController.addListener(() async {
if (listenPortController.text.isNotEmpty &&
listenPortController.text != btConfig.listenPort.toString()) {
btConfig.listenPort = int.parse(listenPortController.text);
await debounceSave();
}
});
return TextField(
key: key,
focusNode: FocusNode(),
controller: listenPortController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 0, max: 65535),
],
);
});
final buildBtTrackerSubscribeUrls = _buildConfigItem(
'subscribeTracker',
() => 'items'.trParams(
@@ -479,6 +521,7 @@ class SettingView extends GetView<SettingController> {
Card(
child: Column(
children: _addDivider([
buildHttpUa(),
buildHttpConnections(),
]),
)),
@@ -486,6 +529,7 @@ class SettingView extends GetView<SettingController> {
Card(
child: Column(
children: _addDivider([
buildBtListenPort(),
buildBtTrackerSubscribeUrls(),
buildBtTrackers(),
]),
@@ -1,8 +1,7 @@
import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
import '../../../../api/model/task.dart';
class TaskDownloadedController extends TaskListController {
TaskDownloadedController() : super([Status.done]);
TaskDownloadedController() : super([Status.done], SortDirection.asc);
}
@@ -9,5 +9,5 @@ class TaskDownloadingController extends TaskListController {
Status.pause,
Status.wait,
Status.error
]);
], SortDirection.desc);
}
@@ -5,10 +5,13 @@ import 'package:get/get.dart';
import '../../../../api/api.dart';
import '../../../../api/model/task.dart';
enum SortDirection { asc, desc }
abstract class TaskListController extends GetxController {
List<Status> statuses;
SortDirection sortDirection;
TaskListController(this.statuses);
TaskListController(this.statuses, this.sortDirection);
final tasks = <Task>[].obs;
final isRunning = false.obs;
@@ -43,6 +46,15 @@ abstract class TaskListController extends GetxController {
}
getTasksState() async {
tasks.value = await getTasks(statuses);
final tasks = await getTasks(statuses);
// sort tasks by create time
tasks.sort((a, b) {
if (sortDirection == SortDirection.asc) {
return a.createdAt.compareTo(b.createdAt);
} else {
return b.createdAt.compareTo(a.createdAt);
}
});
this.tasks.value = tasks;
}
}
@@ -3,7 +3,10 @@ import 'package:get/get.dart';
import 'package:open_file/open_file.dart';
import 'package:path/path.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../api/api.dart' as api;
import '../../../../util/browser_download/browser_download.dart';
import '../../../../util/file_icon.dart';
import '../../../../util/icons.dart';
import '../../../../util/util.dart';
@@ -21,7 +24,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
icon: const Icon(Icons.arrow_back),
onPressed: () => Get.rootDelegate.popRoute()),
// actions: [],
title: Obx(() => Text(controller.task.value!.meta.res!.name)),
title: Obx(() => Text(controller.task.value?.meta.res?.name ?? "")),
),
body: Obx(() {
final fileList = controller.fileList;
@@ -53,11 +56,12 @@ class TaskFilesView extends GetView<TaskFilesController> {
itemBuilder: (context, index) {
final meta = controller.task.value!.meta;
final file = fileList[index];
final filePath = Util.safePathJoin([
meta.opts.path,
meta.res!.name,
file.filePath(meta.opts.name)
]);
// if resource is single file, use opts.name as file name
final realFileName =
meta.res!.name.isEmpty ? file.name : "";
final fileRelativePath = file.filePath(realFileName);
final filePath = Util.safePathJoin(
[meta.opts.path, meta.res!.name, fileRelativePath]);
final fileName = basename(filePath);
return ListTile(
leading: file.isDirectory
@@ -77,19 +81,39 @@ class TaskFilesView extends GetView<TaskFilesController> {
width: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
IconButton(
icon: const Icon(Icons.play_circle),
onPressed: () {
OpenFile.open(filePath);
}),
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
final xfile = XFile(filePath);
Share.shareXFiles([xfile]);
})
],
children: Util.isWeb()
? () {
final accessUrl = api.join(
"/fs/tasks/${controller.task.value!.id}$fileRelativePath");
return [
IconButton(
icon:
const Icon(Icons.play_circle),
onPressed: () {
launchUrl(Uri.parse(accessUrl),
webOnlyWindowName:
"_blank");
}),
IconButton(
icon: const Icon(Icons.download),
onPressed: () {
download(accessUrl, fileName);
})
];
}()
: [
IconButton(
icon: const Icon(Icons.play_circle),
onPressed: () {
OpenFile.open(filePath);
}),
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
final xfile = XFile(filePath);
Share.shareXFiles([xfile]);
})
],
),
),
onTap: () {
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/controllers/task_downloading_controller.dart';
import '../controllers/task_downloaded_controller.dart';
import 'task_downloading_view.dart';
import 'task_downloaded_view.dart';
import '../controllers/task_controller.dart';
import '../controllers/task_downloaded_controller.dart';
import '../controllers/task_downloading_controller.dart';
import 'task_downloaded_view.dart';
import 'task_downloading_view.dart';
class TaskView extends GetView<TaskController> {
const TaskView({Key? key}) : super(key: key);
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gopeed/api/model/meta.dart';
import 'package:path/path.dart' as path;
import 'package:styled_widget/styled_widget.dart';
import '../../api/api.dart';
import '../../api/model/meta.dart';
import '../../api/model/task.dart';
import '../../util/file_explorer.dart';
import '../../util/file_icon.dart';
@@ -110,19 +110,17 @@ class BuildTaskListView extends GetView {
List<Widget> buildActions() {
final list = <Widget>[];
if (isDone()) {
if (Util.isDesktop() || Util.isMobile()) {
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () {
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
},
));
}
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () {
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
},
));
} else {
if (isRunning()) {
list.add(IconButton(
-2
View File
@@ -4,8 +4,6 @@ import "libgopeed_boot_stub.dart"
if (dart.library.io) 'entry/libgopeed_boot_native.dart';
abstract class LibgopeedBoot {
static const unixSocketPath = 'gopeed.sock';
static LibgopeedBoot? _instance;
static LibgopeedBoot get instance {
+20 -12
View File
@@ -445,7 +445,7 @@ class Locales {
'serviceText': '运行中',
};
static const zh_TW = {
'label': '中文 (正體) ',
'label': '中文 (繁體) ',
'error': '錯誤',
'tip': '提示',
'confirm': '確定',
@@ -458,6 +458,7 @@ class Locales {
'donate': '斗內',
'exit': '離開',
'create': '新增任務',
'directDownload': '直接下載',
'advancedOptions': '進階選項',
'downloadLink': '下載鏈接',
'downloadLinkValid': '請輸入下載鏈接',
@@ -473,16 +474,16 @@ class Locales {
'general': '通用設定',
'downloadDir': '下載目録',
'downloadDirValid': '請選擇下載目録',
'connections': '同時連接數量',
'connections': '同時連接數',
'maxRunning': '最大同時下載數',
'items': '@count 項',
'subscribeTracker': '已訂閲的 Tracker',
'subscribeTracker': '已訂閲的追蹤者列表',
'subscribeFail': '訂閲失敗',
'update': '更新',
'updateDaily': '每日自動更新',
'updateDaily': '每天自動更新',
'lastUpdate': '上次更新時間: @time',
'addTracker': '增加新的 Tracker',
'addTrackerHit': '請輸入 Tracker 服務器地址,每行一條',
'addTracker': '增加新的追蹤者',
'addTrackerHit': '請輸入追蹤者服務器地址,每行一條',
'ui': '介面',
'theme': '佈景主題',
'themeSystem': '跟隨系統',
@@ -492,12 +493,12 @@ class Locales {
'about': '關於',
'homepage': '訪問主頁',
'version': '版本資訊',
'protocol': '通訊協定',
'protocol': '端口設定',
'port': '端口',
'apiToken': '接口 Token',
'notSet': '尚未設定',
'apiToken': '接口秘鑰',
'notSet': '未設定',
'set': '已設定',
'effectAfterRestart': '該選項將在重新啓動軟體後生效',
'effectAfterRestart': '重啟後生效',
'startAll': '全部開始',
'pauseAll': '全部暫停',
'deleteTask': '刪除任務',
@@ -507,10 +508,17 @@ class Locales {
'newVersionUpdate': '立刻更新',
'newVersionLater': '稍後更新',
'extensions': '外掛程序',
'extensionInstallUrl': '安裝鏈接',
'extensionInstallSuccess': '安裝完成',
'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得外掛',
'extensionDevelop': '開發外掛',
'history': '歷史記錄',
'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務',
'serviceText': '執行中',
};
}
+5 -3
View File
@@ -37,11 +37,13 @@ Future<void> init() async {
});
}
if (!Util.isWeb()) {
await Util.initStorageDir();
}
initLogger();
final controller = Get.put(AppController());
try {
if (!Util.isWeb()) {
await Util.initStorageDir();
}
await controller.loadStartConfig();
final startCfg = controller.startConfig.value;
controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
+2 -2
View File
@@ -26,14 +26,14 @@ class GopeedTheme {
});
static final _light = ThemeData(
//TODO useMaterial3: true,
useMaterial3: false,
brightness: Brightness.light,
primarySwatch: _gopeedreen);
static final light = _light.copyWith(
colorScheme: _light.colorScheme.copyWith(secondary: _gopeedreenAccent));
static final _dark = ThemeData(
//TODO useMaterial3: true,
useMaterial3: false,
brightness: Brightness.dark,
primarySwatch: _gopeedreen);
static final dark = _dark.copyWith(
@@ -0,0 +1,4 @@
import 'browser_download_stub.dart'
if (dart.library.html) 'entry/browser_download_browser.dart';
void download(String url, String name) => doDownload(url, name);
@@ -0,0 +1 @@
void doDownload(String url, String name) => throw UnimplementedError();
@@ -0,0 +1,9 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
void doDownload(String url, String name) {
final anchorElement = html.AnchorElement(href: url);
anchorElement.download = name;
anchorElement.target = '_blank';
anchorElement.click();
}
+13 -7
View File
@@ -1,18 +1,24 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'package:path/path.dart' as path;
import 'util.dart';
final logger = Logger(
filter: ProductionFilter(),
printer: SimplePrinter(printTime: true, colors: false),
output: buildOutput(),
);
late final Logger logger;
buildOutput() {
initLogger() {
// if is debug mode, don't log to file
logger = Logger(
filter: ProductionFilter(),
printer: SimplePrinter(printTime: true, colors: false),
output: _buildOutput(),
);
}
_buildOutput() {
// if is debug mode, don't log to file
if (!kDebugMode && Util.isDesktop()) {
const logDirPath = 'logs';
final logDirPath = path.join(Util.getStorageDir(), 'logs');
var logDir = Directory(logDirPath);
if (!logDir.existsSync()) {
logDir.createSync();
+10 -4
View File
@@ -55,6 +55,16 @@ class Util {
storageDir = (await getExternalStorageDirectory())?.path ?? storageDir;
} else if (Platform.isIOS) {
storageDir = (await getLibraryDirectory()).path;
} else if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
// check has write permission, if not, fallback to application support dir
try {
final testFile = File(path.join(storageDir, ".test"));
await testFile.writeAsString("test");
await testFile.delete();
} catch (e) {
storageDir = (await getApplicationSupportDirectory()).path;
}
}
_storageDir = storageDir;
}
@@ -63,10 +73,6 @@ class Util {
return _storageDir!;
}
static String getStorageAsset(String asset) {
return path.join(getStorageDir(), asset);
}
static isAndroid() {
return !kIsWeb && Platform.isAndroid;
}
@@ -0,0 +1,14 @@
[Desktop Entry]
Name=Gopeed
GenericName=Download Manager
GenericName[zh_CN]=下载器
GenericName[zh_TW]=下載器
Comment=High speed downloader that supports all platforms.
Comment[zh_CN]=支持全平台的高速下载器
Comment[zh_TW]=支持全平臺的高速下載器
Terminal=false
Exec=gopeed
Icon=com.gopeed.Gopeed
Type=Application
Categories=Utility;Internet
Keywords=Bittorrent;Downloader;
+27 -19
View File
@@ -181,10 +181,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.17.1"
version: "1.18.0"
context_menus:
dependency: "direct main"
description:
@@ -441,10 +441,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.18.0"
version: "0.18.1"
io:
dependency: transitive
description:
@@ -537,18 +537,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.15"
version: "0.12.16"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.0"
version: "0.5.0"
math_expressions:
dependency: transitive
description:
@@ -569,10 +569,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.10.0"
mime:
dependency: transitive
description:
@@ -894,10 +894,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.9.1"
version: "1.10.0"
sprintf:
dependency: transitive
description:
@@ -910,18 +910,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.0"
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.1"
version: "2.1.2"
stream_transform:
dependency: transitive
description:
@@ -958,10 +958,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.1"
version: "0.6.1"
timing:
dependency: transitive
description:
@@ -1083,6 +1083,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.0"
web_socket_channel:
dependency: transitive
description:
@@ -1140,5 +1148,5 @@ packages:
source: hosted
version: "2.1.1"
sdks:
dart: ">=3.0.0 <4.0.0"
dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.10.0"
+1 -1
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.4.4+1
version: 1.5.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
+6 -1
View File
@@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
# https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
@@ -92,7 +97,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG>
${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM
)
add_custom_target(flutter_assemble DEPENDS
@@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() {
this->Show();
});
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true;
}