Compare commits

...
19 Commits
Author SHA1 Message Date
Levi 31228e3edf release: v1.5.3 2023-12-15 19:23:16 +08:00
Levi 31a32d7e14 fix: handle api panic (#302) 2023-12-15 19:20:56 +08:00
Levi 65fd6fb489 fix: http pause handle (#303) 2023-12-15 19:15:38 +08:00
Levi 953b878a89 fix: adapt mobile proxy settings ui (#301) 2023-12-15 19:09:48 +08:00
Elliot ae4ca3da58 feat: improve zh-TW translation (#300) 2023-12-14 18:06:49 +08:00
Levi d09bd180af release: v1.5.2 2023-12-13 09:38:34 +08:00
liwei 3d24e353c5 docs: docker data path mounted 2023-12-13 08:57:57 +08:00
Levi ce7489b0c0 feat: support volume storage dir on docker (#291) 2023-12-13 08:51:22 +08:00
Levi 2b5091ba21 feat: improve extension engine (#298)
1. support onError event
2. add built-in MessageError
2023-12-12 19:00:42 +08:00
Levi ff32971aca fix: repair invalid filename (#296) 2023-12-12 18:40:36 +08:00
Levi f2af61f9aa fix: sort of completed tasks (#297) 2023-12-12 18:36:40 +08:00
Elliot d935b9d631 feat: improve zh_TW translate (#287) 2023-12-04 23:01:49 +08:00
liwei ae66d1d83d docs: fix contributing 2023-12-04 13:41:27 +08:00
madoka773 aa91ca1d50 ci: linux build use libappindicator (#286) 2023-12-04 13:36:27 +08:00
liwei 2d4bdea5cb fix: android file open 2023-12-02 22:51:44 +08:00
liwei 2f2393ac20 fix: i18n word 2023-12-02 19:22:37 +08:00
Levi 5a761e244a release: v1.5.1 2023-12-02 18:47:07 +08:00
Levi 6538766780 fix: incorrect file name in task files view (#285) 2023-12-02 18:45:16 +08:00
Levi 16b7a3d5b3 feat: support proxy (#284) 2023-12-02 18:36:52 +08:00
77 changed files with 1507 additions and 1300 deletions

No files matched your search

+2
View File
@@ -19,6 +19,8 @@ categories:
label: 'doc'
- title: '🚀 CI/CD'
label: 'ci'
- title: '🌎 Internationalization'
label: 'translation'
change-template: '- $TITLE @$AUTHOR (#$NUMBER)'
version-resolver:
major:
+2 -2
View File
@@ -224,7 +224,7 @@ jobs:
flutter-version: ${{ env.FLUTTER_VERSION }}
- run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
sudo apt-get install -y ninja-build libgtk-3-dev libappindicator3-dev
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
@@ -405,7 +405,7 @@ jobs:
plugin: flutter
stage-packages:
- libgtk-3-dev
- libayatana-appindicator3-dev
- libappindicator3-dev
flutter-target: lib/main.dart
EOF
+2 -23
View File
@@ -17,17 +17,9 @@ start the front-end flutter project in `debug` mode to run.
## Translation
The internationalization files of Gopeed are located in the `ui/flutter/assets/locales` directory.
The internationalization files of Gopeed are located in the `ui/flutter/lib/i18n/langs` directory.
You only need to add the corresponding language file in this directory.
Generate locales after you edit locales:
```
get generate locales
```
Please refer to `en_us.dart` for translation.
## flutter development
@@ -38,17 +30,4 @@ Turn on build_runner watcher if you want to edit api/models:
```
flutter pub run build_runner watch
```
get-cli commands usages:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
```
+1 -22
View File
@@ -17,17 +17,9 @@
## 翻訳
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
Gopeed の国際化ファイルは `ui/flutter/lib/i18n/langs` ディレクトリにあります。
このディレクトリに対応する言語ファイルを追加するだけでよいです。
ロケール編集後にロケールを生成:
```
get generate locales
```
翻訳については `en_us.dart` を参照してください。
## flutter での開発
@@ -39,16 +31,3 @@ api/models を編集したい場合は build_runner watcher をオンにしま
```
flutter pub run build_runner watch
```
get-cli コマンドの使用法:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+2 -20
View File
@@ -14,15 +14,9 @@ flutter 项目即可运行。
## 翻译
Gopeed 的国际化文件位于 `ui/flutter/assets/locales` 目录下,只需要在该目录下添加对应的语言文件即可。
Gopeed 的国际化文件位于 `ui/flutter/lib/i18n/langs` 目录下,只需要在该目录下添加对应的语言文件即可。
编辑locales后请运行以下命令:
```
get generate locales
```
请注意以 `en_US.json` 为参照进行翻译。
请注意以 `en_us.dart` 为参照进行翻译。
## flutter开发
@@ -34,15 +28,3 @@ get generate locales
flutter pub run build_runner watch
```
适用get-cli 命令:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+2 -20
View File
@@ -12,15 +12,10 @@
## 翻譯
Gopeed 的翻譯文件位於 `ui/flutter/assets/locales` 目錄中,只需要修改或新建翻譯文件即可。
Gopeed 的翻譯文件位於 `ui/flutter/lib/i18n/langs` 目錄中,只需要修改或新建翻譯文件即可。
修改locales後請執行一下指令:
```
get generate locales
```
請以 `en_US.json` 作為參照。
請以 `en_us.dart` 作為參照。
## flutter開發
@@ -31,16 +26,3 @@ get generate locales
```
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
View File
@@ -17,5 +17,6 @@ FROM alpine:3.14.2
LABEL maintainer="monkeyWie"
WORKDIR /app
COPY --from=go /app/dist/gopeed ./
VOLUME ["/app/storage"]
EXPOSE 9999
ENTRYPOINT ["./gopeed"]
+4 -3
View File
@@ -16,7 +16,7 @@
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Official Docs](https://docs.gopeed.com)
## Install
@@ -47,9 +47,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### Start directly
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[More details for docker](https://docs.gopeed.com/install.html#docker-installation)
#### Using Docker Compose
```bash
@@ -160,7 +162,6 @@ 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
```
## Credits
+3 -2
View File
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接始める
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[Dockerの使用については](https://docs.gopeed.com/install.html#docker-installation)
#### Docker Compose の使用
```bash
@@ -159,7 +161,6 @@ 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
```
## 感謝
+5 -4
View File
@@ -13,9 +13,9 @@
## 介绍
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
Gopeed(全称 Go Speed),直译过来中文名叫做`够快下载器`(不是狗屁下载器!),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [官方文档](https://docs.gopeed.com/zh/)
## 安装
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接运行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[更多关于 Docker 的使用](https://docs.gopeed.com/zh/install.html#docker-%E5%AE%89%E8%A3%85)
#### 使用 Docker Compose
```bash
@@ -159,7 +161,6 @@ 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
```
## 感谢
+3 -2
View File
@@ -46,9 +46,11 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接執行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads -v /path/to/storage:/app/storage liwei2633/gopeed
```
[更多關於 Docker 的使用](https://docs.gopeed.com/zh/install.html#docker-%E5%AE%89%E8%A3%85)
#### 使用 Docker Compose
```bash
@@ -159,7 +161,6 @@ 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
```
## 感謝
+1 -1
View File
@@ -23,7 +23,7 @@ func parse() *args {
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Pwd")
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
+9
View File
@@ -9,6 +9,8 @@ import (
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
"os"
"path/filepath"
)
//go:embed dist/*
@@ -29,10 +31,17 @@ func main() {
}
}
exe, err := os.Executable()
if err != nil {
panic(err)
}
dir := filepath.Dir(exe)
cfg := &model.StartConfig{
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken,
ProductionMode: true,
WebEnable: true,
+3 -2
View File
@@ -4,8 +4,9 @@ services:
gopeed:
container_name: gopeed
ports:
- 9999:9999 # HTTP port (host:container)
- "9999:9999" # HTTP port (host:container)
image: liwei2633/gopeed
volumes:
- ~/Downloads:/root/Downloads # replace with your prefer path
- ~/gopeed/Downloads:/root/Downloads # mount download path
#- ~/gopeed/storage:/app/storage # if you need to mount storage path, uncomment this line
restart: unless-stopped
+1
View File
@@ -5,6 +5,7 @@ go 1.19
require (
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.5
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
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
+2 -13
View File
@@ -8,7 +8,6 @@ crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c/go.mod h1:igAO5JulrQ1Dbd
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
filippo.io/edwards25519 v1.0.0-rc.1 h1:m0VOOB23frXZvAOK44usCgLWvtsxIoMCTBGJZlpmGfU=
filippo.io/edwards25519 v1.0.0-rc.1/go.mod h1:N1IkdkCkiLB6tki+MYJoSx2JTY9NUlxZE7eHn5EwJns=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
@@ -34,7 +33,6 @@ github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuy
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
github.com/anacrolix/chansync v0.3.0 h1:lRu9tbeuw3wl+PhMu/r+JJCRu5ArFXIluOgdF0ao6/U=
github.com/anacrolix/chansync v0.3.0/go.mod h1:DZsatdsdXxD0WiwcGl0nJVwyjCKMDv+knl1q2iBjA2k=
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 h1:8V0K09lrGoeT2KRJNOtspA7q+OMxGwQqK/Ug0IiaaRE=
@@ -74,7 +72,6 @@ github.com/anacrolix/mmsg v1.0.0 h1:btC7YLjOn29aTUAExJiVUhQOuf/8rhm+/nWCMAnL3Hg=
github.com/anacrolix/mmsg v1.0.0/go.mod h1:x8kRaJY/dCrY9Al0PEcj1mb/uFHwP6GCJ9fLl4thEPc=
github.com/anacrolix/multiless v0.3.0 h1:5Bu0DZncjE4e06b9r1Ap2tUY4Au0NToBP5RpuEngSis=
github.com/anacrolix/multiless v0.3.0/go.mod h1:TrCLEZfIDbMVfLoQt5tOoiBS/uq4y8+ojuEVVvTNPX4=
github.com/anacrolix/publicip v0.2.0/go.mod h1:67G1lVkLo8UjdEcJkwScWVTvlJ35OCDsRJoWXl/wi4g=
github.com/anacrolix/stm v0.2.0/go.mod h1:zoVQRvSiGjGoTmbM0vSLIiaKjWtNPeTvXUSdJQA4hsg=
github.com/anacrolix/stm v0.4.0 h1:tOGvuFwaBjeu1u9X1eIh9TX8OEedEiEQ1se1FjhFnXY=
github.com/anacrolix/stm v0.4.0/go.mod h1:GCkwqWoAsP7RfLW+jw+Z0ovrt2OO7wRzcTtFYMYY5t8=
@@ -85,7 +82,6 @@ github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/tagflag v1.3.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/torrent v1.52.5 h1:jWowdx+EU6zFVfBwmnL0d3H4J6vTFEGOrHI35YdfIT8=
github.com/anacrolix/torrent v1.52.5/go.mod h1:CcM8oPMYye5J42cSqJrmUpqwRFgSsJQ1jCEHwygqnqQ=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
@@ -95,6 +91,7 @@ github.com/anacrolix/utp v0.1.0/go.mod h1:MDwc+vsGEq7RMw6lr2GKOEqjWny5hO5OZXRVNa
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=
github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk=
github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg=
github.com/benbjohnson/immutable v0.2.0/go.mod h1:uc6OHo6PN2++n98KHLxW8ef4W42ylHiQSENghE1ezxI=
@@ -144,7 +141,6 @@ github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFP
github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ=
github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q=
github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0=
github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys=
github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=
github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=
github.com/felixge/httpsnoop v1.0.1 h1:lvB5Jl89CsZtGIWuTcDM1E/vkVs49/Ml7JJe07l8SPQ=
@@ -242,7 +238,6 @@ github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq
github.com/ianlancetaylor/demangle v0.0.0-20220319035150-800ac71e25c2/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jessevdk/go-flags v1.5.0/go.mod h1:Fw0T6WPc1dYxT4mKEZRfG5kJhaTDP9pj1c2EWnYs/m4=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
github.com/jtolds/gls v4.2.1+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
@@ -282,7 +277,6 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
github.com/multiformats/go-base36 v0.1.0/go.mod h1:kFGE83c6s80PklsHO9sRn2NCoffoRdUUOENyW/Vv6sM=
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -368,7 +362,6 @@ github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsT
github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A=
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
@@ -404,8 +397,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/tidwall/btree v1.6.0 h1:LDZfKfQIBHGHWSwckhXI0RPSXzlo+KYdjK7FWSqOzzg=
github.com/tidwall/btree v1.6.0/go.mod h1:twD9XRA5jj9VUQGELzDO4HPQTNJsoWWfYEL+EUQ2cKY=
github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
@@ -419,8 +412,6 @@ github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM
github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
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=
@@ -537,8 +528,6 @@ 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.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=
+2
View File
@@ -1,12 +1,14 @@
package controller
import (
"net/url"
"os"
"path/filepath"
)
type Controller struct {
GetConfig func(v any) bool
ProxyUrl *url.URL
FileController
//ContextDialer() (proxy.Dialer, error)
}
+4
View File
@@ -9,6 +9,7 @@ import (
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"net/http"
"path/filepath"
"sync"
"sync/atomic"
@@ -63,6 +64,9 @@ func (f *Fetcher) initClient() (err error) {
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = f.config.ListenPort
if f.ctl.ProxyUrl != nil {
cfg.HTTPProxy = http.ProxyURL(f.ctl.ProxyUrl)
}
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
+13 -2
View File
@@ -8,6 +8,8 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"net/url"
"os"
"reflect"
"testing"
@@ -50,7 +52,15 @@ func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
}
func TestFetcher_Config(t *testing.T) {
doResolve(t, buildConfigFetcher())
doResolve(t, buildConfigFetcher(nil))
}
func TestFetcher_ResolveWithProxy(t *testing.T) {
usr, pwd := "admin", "123"
proxyListener := test.StartSocks5Server(usr, pwd)
defer proxyListener.Close()
doResolve(t, buildConfigFetcher(util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)))
}
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
@@ -90,7 +100,7 @@ func buildFetcher() fetcher.Fetcher {
return fetcher
}
func buildConfigFetcher() fetcher.Fetcher {
func buildConfigFetcher(proxyUrl *url.URL) fetcher.Fetcher {
fetcher := new(FetcherBuilder).Build()
newController := controller.NewController()
mockCfg := config{
@@ -104,6 +114,7 @@ func buildConfigFetcher() fetcher.Fetcher {
}
return true
}
newController.ProxyUrl = proxyUrl
fetcher.Setup(newController)
return fetcher
}
+23 -20
View File
@@ -90,7 +90,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if err != nil {
return err
}
client := buildClient()
client := f.buildClient()
// send Range request to check whether the server supports breakpoint continuation
// just test one byte, Range: bytes=0-0
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
@@ -264,7 +264,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return err
}
var (
client = buildClient()
client = f.buildClient()
buf = make([]byte, 8192)
maxRetries = 3
)
@@ -306,26 +306,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status)
return err
}
return nil
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second)
continue
}
// Http request success, reset retry times
chunk.retryTimes = 0
err = func() error {
defer resp.Body.Close()
// Http request success, reset retry times
chunk.retryTimes = 0
for {
n, err := resp.Body.Read(buf)
if n > 0 {
@@ -342,7 +329,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return err
}
}
return nil
}()
if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second)
continue
}
// if chunk is completed, reset other not complete chunks retry times
if err == nil {
for _, c := range f.chunks {
@@ -430,11 +428,16 @@ func (f *Fetcher) splitChunk() (chunks []*chunk) {
return
}
func buildClient() *http.Client {
func (f *Fetcher) buildClient() *http.Client {
transport := &http.Transport{}
// Cookie handle
jar, _ := cookiejar.New(nil)
if f.ctl.ProxyUrl != nil {
transport.Proxy = http.ProxyURL(f.ctl.ProxyUrl)
}
return &http.Client{
Jar: jar,
Transport: transport,
Jar: jar,
}
}
+30
View File
@@ -8,6 +8,7 @@ import (
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util"
"net"
"reflect"
"testing"
@@ -141,6 +142,15 @@ func TestFetcher_DownloadResume(t *testing.T) {
downloadResume(listener, 16, t)
}
func TestFetcher_DownloadWithProxy(t *testing.T) {
httpListener := test.StartTestFileServer()
defer httpListener.Close()
proxyListener := test.StartSocks5Server("", "")
defer proxyListener.Close()
downloadWithProxy(httpListener, proxyListener, t)
}
func TestFetcher_Config(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
@@ -302,6 +312,26 @@ func downloadResume(listener net.Listener, connections int, t *testing.T) {
}
}
func downloadWithProxy(httpListener net.Listener, proxyListener net.Listener, t *testing.T) {
fetcher := downloadReady(httpListener, 4, t)
ctl := controller.NewController()
ctl.ProxyUrl = util.BuildProxyUrl("socks5", proxyListener.Addr().String(), "", "")
fetcher.Setup(ctl)
err := fetcher.Start()
if err != nil {
t.Fatal(err)
}
err = fetcher.Wait()
if err != nil {
t.Fatal(err)
}
want := test.FileMd5(test.BuildFile)
got := test.FileMd5(test.DownloadFile)
if want != got {
t.Errorf("Download() got = %v, want %v", got, want)
}
}
func buildFetcher() *Fetcher {
fetcher := new(FetcherBuilder).Build()
fetcher.Setup(controller.NewController())
+22
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/armon/go-socks5"
"io"
"math/rand"
"net"
@@ -315,3 +316,24 @@ func ifExistAndRemove(name string) error {
}
return nil
}
func StartSocks5Server(usr, pwd string) net.Listener {
conf := &socks5.Config{}
if usr != "" && pwd != "" {
conf.Credentials = socks5.StaticCredentials{
usr: pwd,
}
}
server, err := socks5.New(conf)
if err != nil {
panic(err)
}
listener, err := net.Listen("tcp", "127.0.0.1:0") // 你可以根据需要更改监听地址
if err != nil {
panic(err)
}
go server.Serve(listener)
return listener
}
+1 -1
View File
@@ -19,7 +19,7 @@ const (
HttpHeaderContentLength = "Content-Length"
HttpHeaderContentRange = "Content-Range"
HttpHeaderContentDisposition = "Content-Disposition"
HttpHeaderUserAgent = "User-Agent"
HttpHeaderUserAgent = "Usr-Agent"
HttpHeaderRangeFormat = "bytes=%d-%d"
)
+43 -34
View File
@@ -8,6 +8,8 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"github.com/virtuald/go-paniclog"
"os"
"path/filepath"
@@ -91,6 +93,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
}
}
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
if cfg.ProductionMode {
@@ -215,6 +218,7 @@ func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
ctl.GetConfig = func(v any) bool {
return d.getProtocolConfig(fetcher.Name(), v)
}
ctl.ProxyUrl = d.cfg.ProxyUrl()
fetcher.Setup(ctl)
}
@@ -224,7 +228,10 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return
}
res := d.triggerOnResolve(req)
res, err := d.triggerOnResolve(req)
if err != nil {
return
}
if res != nil && len(res.Files) > 0 {
rr = &ResolveResult{
Res: res,
@@ -524,9 +531,6 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
Task: task,
Err: err,
})
if eventKey == EventKeyError {
d.emit(EventKeyFinally, task, err)
}
}
}
@@ -571,28 +575,38 @@ func (d *Downloader) getProtocolConfig(name string, v any) bool {
func (d *Downloader) watch(task *Task) {
err := task.fetcher.Wait()
if err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
} else {
task.Progress.Used = task.timer.Used()
if task.Meta.Res.Size == 0 {
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
used = 1
}
totalSize := task.Meta.Res.Size
task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.Status = base.DownloadStatusDone
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
d.doOnError(task, err)
return
}
task.Progress.Used = task.timer.Used()
if task.Meta.Res.Size == 0 {
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
}
used := task.Progress.Used / int64(time.Second)
if used == 0 {
used = 1
}
totalSize := task.Meta.Res.Size
task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.updateStatus(base.DownloadStatusDone)
d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
}
func (d *Downloader) doOnError(task *Task, err error) {
d.Logger.Warn().Err(err).Msgf("task download failed, task id: %s", task.ID)
task.updateStatus(base.DownloadStatusError)
d.triggerOnError(task, err)
if task.Status == base.DownloadStatusError {
d.emit(EventKeyError, task, err)
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
}
}
func (d *Downloader) restoreFetcher(task *Task) error {
if task.fetcher == nil {
fb, err := d.parseFb(task.Meta.Req.URL)
@@ -657,7 +671,6 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
task.fetcher = fetcher
task.Meta = fetcher.Meta()
task.Progress = &Progress{}
task.Status = base.DownloadStatusReady
initTask(task)
if err = fetcher.Create(opts); err != nil {
return
@@ -696,25 +709,18 @@ func (d *Downloader) doStart(task *Task) (err error) {
}
}
cloneTask := task.clone()
isCreate := task.Status == base.DownloadStatusReady
task.Status = base.DownloadStatusRunning
doStart := func() error {
task.lock.Lock()
defer task.lock.Unlock()
req := d.triggerOnStart(cloneTask)
if req != nil {
task.Meta.Req = req
task.fetcher.Meta().Req = req
}
d.triggerOnStart(task)
task.updateStatus(base.DownloadStatusRunning)
if task.Meta.Res == nil {
err := task.fetcher.Resolve(task.Meta.Req)
if err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
return err
}
task.Meta.Res = task.fetcher.Meta().Res
@@ -723,8 +729,10 @@ func (d *Downloader) doStart(task *Task) (err error) {
if isCreate {
d.checkDuplicateLock.Lock()
defer d.checkDuplicateLock.Unlock()
task.Meta.Opts.Name = util.ReplaceInvalidFilename(task.Meta.Opts.Name)
// check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" {
task.Meta.Res.Name = util.ReplaceInvalidFilename(task.Meta.Res.Name)
fullDirPath := task.Meta.FolderPath()
newName, err := util.CheckDuplicateAndRename(fullDirPath)
if err != nil {
@@ -732,6 +740,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
}
task.Meta.Opts.Name = newName
} else {
task.Meta.Res.Files[0].Name = util.ReplaceInvalidFilename(task.Meta.Res.Files[0].Name)
fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil {
@@ -757,7 +766,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
go func() {
err := doStart()
if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
d.doOnError(task, err)
}
}()
@@ -767,7 +776,7 @@ func (d *Downloader) doStart(task *Task) (err error) {
func (d *Downloader) doPause(task *Task) (err error) {
err = func() error {
if task.Status != base.DownloadStatusDone {
task.Status = base.DownloadStatusPause
task.updateStatus(base.DownloadStatusPause)
task.timer.Pause()
if task.fetcher != nil {
if err := task.fetcher.Pause(); err != nil {
+76
View File
@@ -84,6 +84,82 @@ func TestDownloader_Create(t *testing.T) {
}
}
func TestDownloader_CreateWithProxy(t *testing.T) {
// No proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
return nil
})
// Disable proxy
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
proxyCfg.Enable = false
return proxyCfg
})
// Invalid proxy scheme
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
proxyCfg.Scheme = ""
return proxyCfg
})
// Invalid proxy host
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
proxyCfg.Host = ""
return proxyCfg
})
// Use proxy without auth
doTestDownloaderCreateWithProxy(t, false, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
return proxyCfg
})
// Use proxy with auth
doTestDownloaderCreateWithProxy(t, true, func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig {
return proxyCfg
})
}
func doTestDownloaderCreateWithProxy(t *testing.T, auth bool, buildProxyConfig func(proxyCfg *DownloaderProxyConfig) *DownloaderProxyConfig) {
httpListener := test.StartTestFileServer()
defer httpListener.Close()
usr, pwd := "", ""
if auth {
usr, pwd = "admin", "123"
}
proxyListener := test.StartSocks5Server(usr, pwd)
defer proxyListener.Close()
downloader := NewDownloader(nil)
if err := downloader.Setup(); err != nil {
t.Fatal(err)
}
defer downloader.Clear()
downloader.cfg.DownloaderStoreConfig.Proxy = buildProxyConfig(&DownloaderProxyConfig{
Enable: true,
Scheme: "socks5",
Host: proxyListener.Addr().String(),
Usr: usr,
Pwd: pwd,
})
req := &base.Request{
URL: "http://" + httpListener.Addr().String() + "/" + test.BuildName,
}
rr, err := downloader.Resolve(req)
if err != nil {
t.Fatal(err)
}
want := &base.Resource{
Size: test.BuildSize,
Range: true,
Files: []*base.FileInfo{
{
Name: test.BuildName,
Path: "",
Size: test.BuildSize,
},
},
}
if !reflect.DeepEqual(want, rr.Res) {
t.Errorf("Resolve() got = %v, want %v", rr.Res, want)
}
}
func TestDownloader_CreateRename(t *testing.T) {
listener := test.StartTestFileServer()
defer listener.Close()
+22 -6
View File
@@ -3,13 +3,15 @@ package engine
import (
_ "embed"
"errors"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"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/url"
gojaurl "github.com/dop251/goja_nodejs/url"
"net/url"
"time"
)
@@ -107,7 +109,14 @@ func (e *Engine) Close() {
e.loop.Stop()
}
func NewEngine() *Engine {
type Config struct {
ProxyURL *url.URL
}
func NewEngine(cfg *Config) *Engine {
if cfg == nil {
cfg = &Config{}
}
loop := eventloop.NewEventLoop()
engine := &Engine{
loop: loop,
@@ -116,14 +125,17 @@ func NewEngine() *Engine {
engine.Runtime = runtime
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime)
url.Enable(runtime)
gojaurl.Enable(runtime)
if err := gojaerror.Enable(runtime); err != nil {
return
}
if err := file.Enable(runtime); err != nil {
return
}
if err := formdata.Enable(runtime); err != nil {
return
}
if err := xhr.Enable(runtime); err != nil {
if err := xhr.Enable(runtime, cfg.ProxyURL); err != nil {
return
}
if _, err := runtime.RunString(polyfillScript); err != nil {
@@ -143,7 +155,7 @@ func NewEngine() *Engine {
}
func Run(script string) (value any, err error) {
engine := NewEngine()
engine := NewEngine(nil)
return engine.RunString(script)
}
@@ -159,7 +171,11 @@ func resolveResult(value goja.Value) (any, error) {
case goja.PromiseStateFulfilled:
return p.Result().Export(), nil
case goja.PromiseStateRejected:
return nil, errors.New(p.Result().String())
if err, ok := p.Result().Export().(error); ok {
return nil, err
} else {
return nil, errors.New(p.Result().String())
}
}
}
return export, nil
+49 -3
View File
@@ -7,7 +7,10 @@ import (
"errors"
"fmt"
"github.com/GopeedLab/gopeed/internal/test"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja"
"io"
"net"
@@ -19,6 +22,7 @@ import (
)
func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "MessageError")
doTestPolyfill(t, "XMLHttpRequest")
doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData")
@@ -26,10 +30,20 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "__gopeed_create_vm")
}
func TestError(t *testing.T) {
engine := NewEngine(nil)
_, err := engine.RunString(`
throw new MessageError('test');
`)
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); !ok {
t.Fatalf("expect MessageError, but got %v", me)
}
}
func TestFetch(t *testing.T) {
server := startServer()
defer server.Close()
engine := NewEngine()
engine := NewEngine(nil)
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", server.Addr().String())); err != nil {
t.Fatal(err)
}
@@ -192,8 +206,40 @@ function testTimeout(){
}
}
func TestFetchWithProxy(t *testing.T) {
doTestFetchWithProxy(t, "", "")
doTestFetchWithProxy(t, "admin", "123")
}
func doTestFetchWithProxy(t *testing.T, usr, pwd string) {
httpListener := startServer()
defer httpListener.Close()
proxyListener := test.StartSocks5Server(usr, pwd)
defer proxyListener.Close()
engine := NewEngine(&Config{ProxyURL: util.BuildProxyUrl("socks5", proxyListener.Addr().String(), usr, pwd)})
if _, err := engine.RunString(fmt.Sprintf("var host = 'http://%s';", httpListener.Addr().String())); err != nil {
t.Fatal(err)
}
respCode, err := engine.RunString(`
(async function(){
const resp = await fetch(host+'/get');
return resp.status;
})()
`)
if err != nil {
t.Fatal(err)
}
if respCode != int64(200) {
t.Fatalf("fetch with proxy failed, want %d, got %d", 200, respCode)
}
}
func TestVm(t *testing.T) {
engine := NewEngine()
engine := NewEngine(nil)
value, err := engine.RunString(`
const vm = __gopeed_create_vm()
@@ -221,7 +267,7 @@ out
}
func TestNonStopLoop(t *testing.T) {
engine := NewEngine()
engine := NewEngine(nil)
_, err := engine.RunString(`
function leak(){
@@ -0,0 +1,29 @@
package error
import (
"github.com/dop251/goja"
)
type MessageError struct {
Message string `json:"message"`
}
func (e *MessageError) Error() string {
return e.Message
}
func Enable(runtime *goja.Runtime) error {
messageError := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
var message string
if len(call.Arguments) > 0 {
message = call.Arguments[0].String()
}
instance := &MessageError{
Message: message,
}
instanceValue := runtime.ToValue(instance).(*goja.Object)
instanceValue.SetPrototype(call.This.Prototype())
return instanceValue
})
return runtime.Set("MessageError", messageError)
}
-9
View File
@@ -1,9 +0,0 @@
package inject
import (
"github.com/dop251/goja"
)
func ThrowTypeError(vm *goja.Runtime, msg string) {
panic(vm.NewTypeError(msg))
}
+12 -4
View File
@@ -2,14 +2,15 @@ package xhr
import (
"bytes"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject"
"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/util"
"github.com/dop251/goja"
"io"
"mime/multipart"
"net"
"net/http"
"net/url"
"time"
)
@@ -119,6 +120,7 @@ type XMLHttpRequest struct {
requestHeaders map[string]string
responseHeaders map[string]string
aborted bool
proxyUrl *url.URL
Upload *XMLHttpRequestUpload `json:"upload"`
Timeout int `json:"timeout"`
@@ -200,8 +202,13 @@ func (xhr *XMLHttpRequest) Send(data goja.Value) {
for k, v := range xhr.requestHeaders {
req.Header.Set(k, v)
}
transport := &http.Transport{}
if xhr.proxyUrl != nil {
transport.Proxy = http.ProxyURL(xhr.proxyUrl)
}
client := &http.Client{
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
Transport: transport,
Timeout: time.Duration(xhr.Timeout) * time.Millisecond,
}
resp, err := client.Do(req)
if err != nil {
@@ -306,10 +313,10 @@ func (xhr *XMLHttpRequest) parseData(data goja.Value) any {
return data.String()
}
func Enable(runtime *goja.Runtime) error {
func Enable(runtime *goja.Runtime, proxyUrl *url.URL) error {
progressEvent := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
if len(call.Arguments) < 1 {
inject.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
util.ThrowTypeError(runtime, "Failed to construct 'ProgressEvent': 1 argument required, but only 0 present.")
}
instance := &ProgressEvent{
Type: call.Argument(0).String(),
@@ -320,6 +327,7 @@ func Enable(runtime *goja.Runtime) error {
})
xhr := runtime.ToValue(func(call goja.ConstructorCall) *goja.Object {
instance := &XMLHttpRequest{
proxyUrl: proxyUrl,
Upload: &XMLHttpRequestUpload{
EventProp: &EventProp{
eventListeners: make(map[string]func(event *ProgressEvent)),
+24
View File
@@ -0,0 +1,24 @@
package util
import (
"github.com/dop251/goja"
)
func ThrowTypeError(vm *goja.Runtime, msg string) {
panic(vm.NewTypeError(msg))
}
func AssertError[T error](err error) (t T, r bool) {
if err == nil {
return
}
if e, ok := err.(T); ok {
return e, true
}
if e, ok := err.(*goja.Exception); ok {
if ee, okk := e.Value().Export().(T); okk {
return ee, true
}
}
return
}
+78 -17
View File
@@ -6,9 +6,12 @@ import (
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download/engine"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
gojautil "github.com/GopeedLab/gopeed/pkg/download/engine/util"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/dop251/goja"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing/transport"
"io"
"os"
"path"
@@ -34,7 +37,7 @@ type ActivationEvent string
const (
EventOnResolve ActivationEvent = "onResolve"
EventOnStart ActivationEvent = "onStart"
//EventOnError ActivationEvent = "onError"
EventOnError ActivationEvent = "onError"
//EventOnDone ActivationEvent = "onDone"
)
@@ -195,11 +198,19 @@ func (d *Downloader) fetchExtensionByGit(url string, handler func(tempExtPath st
if err := util.RmAndMkDirAll(tempExtDir); err != nil {
return nil, err
}
proxyOptions := transport.ProxyOptions{}
proxyUrl := d.cfg.ProxyUrl()
if proxyUrl != nil {
proxyOptions.URL = proxyUrl.Scheme + "://" + proxyUrl.Host
proxyOptions.Username = proxyUrl.User.Username()
proxyOptions.Password, _ = proxyUrl.User.Password()
}
// clone project to extension temp dir
gitUrl := parentPath + projectPath + gitSuffix
if _, err := git.PlainClone(tempExtDir, false, &git.CloneOptions{
URL: gitUrl,
Depth: 1,
URL: gitUrl,
Depth: 1,
ProxyOptions: proxyOptions,
}); err != nil {
return nil, err
}
@@ -229,8 +240,8 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
return &ext, nil
}
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
doTrigger(d,
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, err error) {
err = doTrigger(d,
EventOnResolve,
req,
&OnResolveContext{
@@ -243,6 +254,10 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
return
}
ctx.Res.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
for _, file := range ctx.Res.Files {
file.Name = util.ReplaceInvalidFilename(file.Name)
}
ctx.Res.CalcSize(nil)
}
res = ctx.Res
@@ -251,12 +266,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
return
}
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
func (d *Downloader) triggerOnStart(task *Task) {
doTrigger(d,
EventOnStart,
task.Meta.Req,
&OnStartContext{
Task: task,
Task: NewExtensionTask(d, task),
},
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
// Validate request structure
@@ -265,14 +280,25 @@ func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
return
}
req = ctx.Task.Meta.Req
}
},
)
return
}
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) {
func (d *Downloader) triggerOnError(task *Task, err error) {
doTrigger(d,
EventOnError,
task.Meta.Req,
&OnErrorContext{
Task: NewExtensionTask(d, task),
Error: err,
},
nil,
)
}
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) error {
// init extension global object
gopeed := &Instance{
Events: make(InstanceEvents),
@@ -314,7 +340,9 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
if req.Labels == nil {
req.Labels = make(map[string]string)
}
engine := engine.NewEngine()
engine := engine.NewEngine(&engine.Config{
ProxyURL: d.cfg.ProxyUrl(),
})
defer engine.Close()
err = engine.Runtime.Set("gopeed", gopeed)
if err != nil {
@@ -332,13 +360,20 @@ func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, c
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
return
}
handler(ext, gopeed, ctx)
if handler != nil {
handler(ext, gopeed, ctx)
}
}
}()
}
}
}
return
// Only return MessageError
if me, ok := gojautil.AssertError[*gojaerror.MessageError](err); ok {
return me
}
return nil
}
func (d *Downloader) ExtensionPath(ext *Extension) string {
@@ -566,10 +601,10 @@ func (h InstanceEvents) OnStart(fn goja.Callable) {
h.register(EventOnStart, fn)
}
//func (h InstanceEvents) OnError(fn goja.Callable) {
// h.register(HookEventOnError, fn)
//}
//
func (h InstanceEvents) OnError(fn goja.Callable) {
h.register(EventOnError, fn)
}
//func (h InstanceEvents) OnDone(fn goja.Callable) {
// h.register(HookEventOnDone, fn)
//}
@@ -638,7 +673,33 @@ type OnResolveContext struct {
}
type OnStartContext struct {
Task *Task `json:"task"`
Task *ExtensionTask `json:"task"`
}
type OnErrorContext struct {
Task *ExtensionTask `json:"task"`
Error error `json:"error"`
}
type ExtensionTask struct {
*Task
download *Downloader
}
func NewExtensionTask(download *Downloader, task *Task) *ExtensionTask {
return &ExtensionTask{
Task: task.clone(),
download: download,
}
}
func (t *ExtensionTask) Continue() error {
return t.download.Continue(t.ID)
}
func (t *ExtensionTask) Pause() error {
return t.download.Pause(t.ID)
}
func parseSettings(settings []*Setting) map[string]any {
+66 -20
View File
@@ -4,6 +4,7 @@ import (
"errors"
"github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base"
gojaerror "github.com/GopeedLab/gopeed/pkg/download/engine/inject/error"
"github.com/dop251/goja"
"os"
"testing"
@@ -165,7 +166,7 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
})
}
func TestDownloader_ExtensionByOnStart(t *testing.T) {
func TestDownloader_Extension_OnStart(t *testing.T) {
downloadAndCheck := func(req *base.Request) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
@@ -192,10 +193,10 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
}
task := downloader.GetTask(id)
if task.Meta.Req.URL != "https://github.com" {
panic("extension on start modify url error")
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
}
if task.Meta.Req.Labels["modified"] != "true" {
panic("extension on start modify label error")
t.Fatalf("except label: modified=true, actual: %s", task.Meta.Req.Labels["modified"])
}
})
}
@@ -207,13 +208,54 @@ func TestDownloader_ExtensionByOnStart(t *testing.T) {
// label match
downloadAndCheck(&base.Request{
URL: "https://xxx.com",
URL: "https://test.com",
Labels: map[string]string{
"test": "true",
},
})
}
func TestDownloader_Extension_OnError(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_error", false); err != nil {
t.Fatal(err)
}
errCh := make(chan error, 1)
downloader.Listener(func(event *Event) {
if event.Key == EventKeyFinally {
errCh <- event.Err
}
})
id, err := downloader.CreateDirect(&base.Request{
URL: "https://github.com/gopeed/test/404",
Labels: map[string]string{
"test": "true",
},
}, nil)
if err != nil {
t.Fatal(err)
}
select {
case err = <-errCh:
break
case <-time.After(time.Second * 10):
err = errors.New("timeout")
}
if err != nil {
panic("extension on error download error: " + err.Error())
}
// extension on error modify url and continue download
task := downloader.GetTask(id)
if task.Meta.Req.URL != "https://github.com" {
t.Fatalf("except url: https://github.com, actual: %s", task.Meta.Req.URL)
}
if task.Status != base.DownloadStatusDone {
t.Fatalf("except status is done, actual: %s", task.Status)
}
})
}
func TestDownloader_Extension_Errors(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
@@ -244,6 +286,26 @@ func TestDownloader_Extension_Errors(t *testing.T) {
t.Fatal("function error catch failed")
}
})
setupDownloader(func(downloader *Downloader) {
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/message_error", false); err != nil {
t.Fatal(err)
}
_, err := downloader.Resolve(&base.Request{
URL: "https://github.com/test",
})
if err == nil {
t.Fatalf("except error, but got nil")
}
me, ok := err.(*gojaerror.MessageError)
if !ok {
t.Fatalf("except MessageError type, but got %s", err)
}
want := "test"
if me.Error() != want {
t.Fatalf("except MessageError message %s, but got %s", want, me.Message)
}
})
}
func TestDownloader_Extension_Settings(t *testing.T) {
@@ -336,22 +398,6 @@ func TestDownloader_DeleteExtension(t *testing.T) {
})
}
func TestDownloader_Extension_OnResolve(t *testing.T) {
setupDownloader(func(downloader *Downloader) {
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/settings_all", false)
if err != nil {
t.Fatal(err)
}
if err := downloader.DeleteExtension(installedExt.Identity); err != nil {
t.Fatal(err)
}
extensions := downloader.GetExtensions()
if len(extensions) != 0 {
t.Fatal("extension delete fail")
}
})
}
func TestDownloader_Extension_Logger(t *testing.T) {
logger := logger.NewLogger(false, "")
il := newInstanceLogger(&Extension{
+35 -4
View File
@@ -8,6 +8,7 @@ import (
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2"
"net/url"
"sync"
"time"
)
@@ -23,6 +24,7 @@ type Task struct {
Status base.Status `json:"status"`
Progress *Progress `json:"progress"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
fetcherBuilder fetcher.FetcherBuilder
fetcher fetcher.Fetcher
@@ -40,9 +42,15 @@ func NewTask() *Task {
ID: id,
Status: base.DownloadStatusReady,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
}
func (t *Task) updateStatus(status base.Status) {
t.UpdatedAt = time.Now()
t.Status = status
}
func (t *Task) clone() *Task {
return &Task{
ID: t.ID,
@@ -50,6 +58,7 @@ func (t *Task) clone() *Task {
Status: t.Status,
Progress: t.Progress,
CreatedAt: t.CreatedAt,
UpdatedAt: t.UpdatedAt,
}
}
@@ -101,15 +110,37 @@ func (cfg *DownloaderConfig) Init() *DownloaderConfig {
type DownloaderStoreConfig struct {
FirstLoad bool `json:"-"` // FirstLoad is the flag that the config is first time init and not from store
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"` // Extra is the extra config
DownloadDir string `json:"downloadDir"` // DownloadDir is the default directory to save the downloaded files
MaxRunning int `json:"maxRunning"` // MaxRunning is the max running download count
ProtocolConfig map[string]any `json:"protocolConfig"` // ProtocolConfig is special config for each protocol
Extra map[string]any `json:"extra"`
Proxy *DownloaderProxyConfig `json:"proxy"`
}
func (cfg *DownloaderStoreConfig) Init() *DownloaderStoreConfig {
if cfg.MaxRunning == 0 {
cfg.MaxRunning = 5
}
if cfg.Proxy == nil {
cfg.Proxy = &DownloaderProxyConfig{}
}
return cfg
}
func (cfg *DownloaderStoreConfig) ProxyUrl() *url.URL {
if cfg.Proxy == nil {
return nil
}
if cfg.Proxy.Enable == false || cfg.Proxy.Scheme == "" || cfg.Proxy.Host == "" {
return nil
}
return util.BuildProxyUrl(cfg.Proxy.Scheme, cfg.Proxy.Host, cfg.Proxy.Usr, cfg.Proxy.Pwd)
}
type DownloaderProxyConfig struct {
Enable bool `json:"enable"`
Scheme string `json:"scheme"`
Host string `json:"host"`
Usr string `json:"usr"`
Pwd string `json:"pwd"`
}
@@ -1,5 +1,6 @@
gopeed.events.onResolve(async function (ctx) {
const aaa = {};
// access undefined property
gopeed.logger.info(aaa.bbb.ccc);
ctx.res = {
@@ -0,0 +1,3 @@
gopeed.events.onResolve(async function (ctx) {
throw new MessageError("test");
});
@@ -0,0 +1,16 @@
{
"name": "message-error",
"title": "gopeed extension message error test",
"version": "0.0.1",
"scripts": [
{
"event": "onResolve",
"match": {
"urls": [
"*://github.com/*"
]
},
"entry": "index.js"
}
]
}
+8
View File
@@ -0,0 +1,8 @@
gopeed.events.onError(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
gopeed.logger.info("error", ctx.error);
ctx.task.meta.req.url = "https://github.com";
ctx.task.pause();
ctx.task.continue();
});
+16
View File
@@ -0,0 +1,16 @@
{
"name": "on-error",
"title": "gopeed extension on error event test",
"version": "0.0.1",
"scripts": [
{
"event": "onError",
"match": {
"labels": [
"test"
]
},
"entry": "index.js"
}
]
}
+1
View File
@@ -1,4 +1,5 @@
gopeed.events.onStart(async function (ctx) {
gopeed.logger.info("url", ctx.task.meta.req.url);
ctx.task.meta.req.url = "https://github.com";
ctx.task.meta.req.labels['modified'] = 'true';
});
+16
View File
@@ -3,11 +3,13 @@ package rest
import (
"context"
"encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/pkg/errors"
"net"
"net/http"
"os"
@@ -131,6 +133,20 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
})
}
// recover panic
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
err := errors.WithStack(fmt.Errorf("%v", v))
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
}
}()
h.ServeHTTP(w, r)
})
})
srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
+3 -1
View File
@@ -27,7 +27,7 @@ var (
Extra: map[string]any{
"method": "",
"header": map[string]string{
"User-Agent": "gopeed",
"Usr-Agent": "gopeed",
},
"body": "",
},
@@ -204,12 +204,14 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
// continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
time.Sleep(time.Millisecond * 100)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
if len(tasks) != cfg.MaxRunning {
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
}
// pause all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
time.Sleep(time.Millisecond * 100)
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
+11
View File
@@ -183,3 +183,14 @@ func IsExistsFile(path string) bool {
}
return false
}
// ReplaceInvalidFilename replace invalid path characters
func ReplaceInvalidFilename(path string) string {
if path == "" {
return ""
}
for _, char := range invalidPathChars {
path = strings.ReplaceAll(path, char, "_")
}
return path
}
+6
View File
@@ -0,0 +1,6 @@
//go:build !windows
// +build !windows
package util
var invalidPathChars = []string{`/`, `:`}
+47
View File
@@ -178,3 +178,50 @@ func TestIsExistsFile(t *testing.T) {
})
}
}
func TestReplaceInvalidFilename(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want string
}{
{
name: "blank",
args: args{
path: "",
},
want: "",
},
{
name: "normal",
args: args{
path: "test.txt",
},
want: "test.txt",
},
{
name: "case1",
args: args{
path: "te/st.txt",
},
want: "te_st.txt",
},
{
name: "case2",
args: args{
path: "te/st:.txt",
},
want: "te_st_.txt",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ReplaceInvalidFilename(tt.args.path); got != tt.want {
t.Errorf("ReplaceInvalidFilename() = %v, want %v", got, tt.want)
}
})
}
}
+3
View File
@@ -0,0 +1,3 @@
package util
var invalidPathChars = []string{`\`, `/`, `:`, `*`, `?`, `"`, `<`, `>`, `|`}
+14
View File
@@ -2,6 +2,7 @@ package util
import (
"encoding/base64"
"net/url"
"regexp"
"strings"
)
@@ -37,3 +38,16 @@ func ParseDataUri(uri string) (string, []byte) {
}
return mime, data
}
// BuildProxyUrl builds a proxy url with given host, username and password.
func BuildProxyUrl(scheme, host, usr, pwd string) *url.URL {
var user *url.Userinfo
if usr != "" && pwd != "" {
user = url.UserPassword(usr, pwd)
}
return &url.URL{
Scheme: scheme,
User: user,
Host: host,
}
}
-77
View File
@@ -1,77 +0,0 @@
{
"label": "English",
"error": "Error",
"tip": "Tip",
"confirm": "Confirm",
"cancel": "Cancel",
"selectAll": "Select All",
"task": "Tasks",
"downloading": "downloading",
"downloaded": "downloaded",
"setting": "Settings",
"donate": "Donate",
"exit": "Exit",
"create": "Create Task",
"directDownload": "Direct Download",
"advancedOptions": "Advanced Options",
"downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link",
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
"downloadLinkHitDesktop": ", or drag the torrent file here directly",
"download": "Download",
"noFileSelected": "Please select at least one file to continue.",
"noStoragePermission": "Storage permission required",
"selectFile": "Select File",
"rename": "Rename",
"basic": "Basic",
"advanced": "Advanced",
"general": "General",
"downloadDir": "Download Directory",
"downloadDirValid": "Please select the download directory",
"connections": "Connections",
"maxRunning": "Max Running Tasks",
"items": "@count items",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
"update": "Update",
"updateDaily": "Update daily",
"lastUpdate": "Last update: @time",
"addTracker": "Add Tracker",
"addTrackerHit": "Please enter the tracker server url, one per line",
"ui": "UI",
"theme": "Theme",
"themeSystem": "System",
"themeLight": "Light",
"themeDark": "Dark",
"locale": "Language",
"about": "About",
"homepage": "Homepage",
"version": "Version",
"protocol": "Protocol",
"port": "Port",
"apiToken": "API Token",
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "Start All",
"pauseAll": "Pause All",
"deleteTask": "Delete Task",
"deleteTaskTip": "Keep downloaded files",
"delete": "Delete",
"newVersionTitle": "Discover new version @version",
"newVersionUpdate": "Update Now",
"newVersionLater": "Later",
"extensions": "Extensions",
"extensionInstallUrl": "Install URL",
"extensionInstallSuccess": "Installed successfully",
"extensionUpdateSuccess": "Updated successfully",
"extensionDelete": "Delete Extension",
"extensionAlreadyLatest": "It's already the latest version",
"extensionFind": "Find Extensions",
"extensionDevelop": "Develop Extensions",
"history": "History",
"clearHistory": "Clear History",
"noHistoryFound": "No History Found",
"serviceTitle": "Download Service",
"serviceText": "Running"
}
-58
View File
@@ -1,58 +0,0 @@
{
"label": "فارسی",
"error": "اررور",
"tip": "Tip",
"confirm": "تایید",
"cancel": "انصراف",
"selectAll": "انتخاب همه",
"task": "کار",
"setting": "تنظیمات",
"donate": "کمک مالی",
"exit": "خروج",
"create": "ایجاد کار",
"downloading": "دانلود کردن",
"downloaded": "دانلود شدن",
"downloadLink": "لینک دانلود",
"downloadLinkValid": "لطفا لینک دانلود را وارد کنید",
"download": "دانلود",
"noFileSelected": "لطفا حداقل یک فایل را جهت دانلود انتخاب کنید.",
"noStoragePermission": "مجوز ذخیره سازی مورد نیاز است",
"selectFile": "انتخاب فایل",
"basic": "پایه",
"advanced": "پیشرفته",
"general": "عمومی",
"downloadDir": "دایرکتوری دانلود",
"downloadDirValid": "لطفا دایرکتوری دانلود را انتخاب کنید",
"connections": "اتصالات",
"items": "@count آیتم ها",
"subscribeTracker": "Subscribe Tracker",
"subscribeFail": "Subscribe failed, please check network or try again later",
"update": "بروزرسانی",
"updateDaily": "بروزرسانی روزانه",
"lastUpdate": "Last update: @time",
"addTracker": "Add Tracker",
"addTrackerHit": "Please enter the tracker server url, one per line",
"ui": "رابط کاربری",
"theme": "پوسته",
"themeSystem": "سیستم",
"themeLight": "روشن",
"themeDark": "تاریک",
"locale": "Language",
"about": "درباره ی",
"homepage": "صفحه اصلی",
"version": "نسخه",
"protocol": "پروتکل",
"port": "پورت",
"apiToken": "API توکن",
"notSet": "NS",
"set": "SET",
"effectAfterRestart": "Effect after restart",
"startAll": "شروع همه",
"pauseAll": "توقف همه",
"deleteTask": "پاک کردن کار",
"deleteTaskTip": "فایل های دانلود شده را نگه دارد",
"delete": "پاک کردن",
"newVersionTitle": "@version عنوان: کشف نسخه جدید",
"newVersionUpdate": "بروزرسانی",
"newVersionLater": "بعداً"
}
-61
View File
@@ -1,61 +0,0 @@
{
"label": "日本語",
"error": "エラー",
"tip": "ヒント",
"confirm": "確認",
"cancel": "キャンセル",
"selectAll": "すべてを選択",
"task": "タスク",
"downloading": "ダウンロード中",
"downloaded": "ダウンロード済",
"setting": "セッティング",
"donate": "寄付する",
"exit": "終了",
"create": "タスクを作成",
"downloadLink": "ダウンロードリンク",
"downloadLinkValid": "ダウンロードリンクを入力してください",
"downloadLinkHit": "ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append",
"downloadLinkHitDesktop": "または、torrent ファイルを直接ここにドラッグします",
"download": "ダウンロード",
"noFileSelected": "続行するには少なくとも 1 つのファイルを選択してください。",
"noStoragePermission": "ストレージのパーミッションが必要",
"selectFile": "ファイルを選択",
"basic": "ベーシック",
"advanced": "アドバンスド",
"general": "ジェネラル",
"downloadDir": "ディレクトリのダウンロード",
"downloadDirValid": "ダウンロードディレクトリを選択してください",
"connections": "接続",
"maxRunning": "最大実行タスク",
"items": "@count アイテム",
"subscribeTracker": "トラッカーを購読",
"subscribeFail": "登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください",
"update": "アップデート",
"updateDaily": "毎日アップデート",
"lastUpdate": "最終アップデート: @time",
"addTracker": "トラッカーを追加",
"addTrackerHit": "トラッカーサーバーの URL を 1 行に 1 つずつ入力してください",
"ui": "UI",
"theme": "テーマ",
"themeSystem": "システム",
"themeLight": "ライト",
"themeDark": "ダーク",
"locale": "言語",
"about": "概要",
"homepage": "ホームページ",
"version": "バージョン",
"protocol": "プロトコル",
"port": "ポート",
"apiToken": "API トークン",
"notSet": "未設置",
"set": "設置",
"effectAfterRestart": "再起動後の効果",
"startAll": "すべてを開始",
"pauseAll": "すべてを一時停止",
"deleteTask": "タスクを削除",
"deleteTaskTip": "ダウンロードしたファイルを保持",
"delete": "削除",
"newVersionTitle": "新しいバージョン @version を発見する",
"newVersionUpdate": "アップデート",
"newVersionLater": "後で"
}
-73
View File
@@ -1,73 +0,0 @@
{
"label": "Русский(Россия)",
"error": "Ошибка",
"tip": "Подсказка",
"confirm": "Подтвердить",
"cancel": "Отмена",
"selectAll": "Выбрать все",
"task": "Задачи",
"downloading": "Загрузка",
"downloaded": "Загружено",
"setting": "Настройки",
"donate": "Пожертвовать",
"exit": "Выход",
"create": "Создать задачу",
"advancedOptions": "Расширенные опции",
"downloadLink": "Ссылка на скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append",
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
"download": "Скачать",
"noFileSelected": "Файл не выбран",
"noStoragePermission": "Требуется доступ к хранилищу",
"selectFile": "Выбрать файл",
"rename": "Переименовать",
"basic": "Основные параметры",
"advanced": "Расширенные",
"general": "Общие",
"downloadDir": "Папка загрузки",
"downloadDirValid": "Пожалуйста, выберите папку для загрузки",
"connections": "Соединений",
"maxRunning": "Максимальное количество активных задач",
"items": "@подсчет элементов",
"subscribeTracker": "Подпись трекера",
"subscribeFail": "Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже",
"update": "Обновить",
"updateDaily": "Обновлять ежедневно",
"lastUpdate": "Последнее обновление: @время",
"addTracker": "Добавить трекер",
"addTrackerHit": "Пожалуйста, введите url трекера, по одному в строке",
"ui": "UI",
"theme": "Тема",
"themeSystem": "Системная",
"themeLight": "Светлая",
"themeDark": "Темная",
"locale": "Язык",
"about": "О приложении",
"homepage": "Домашняя страница",
"version": "Версия",
"protocol": "Протокол",
"port": "Порт",
"apiToken": "API Токен",
"notSet": "Не установлено",
"set": "Установлено",
"effectAfterRestart": "Эффект после перезагрузки",
"startAll": "Запустить все",
"pauseAll": "Приостановить все",
"deleteTask": "Удалить задачу",
"deleteTaskTip": "Сохранить загруженные файлы",
"delete": "Удалить",
"newVersionTitle": "Обнаружена новая версия @version",
"newVersionUpdate": "Обновить",
"newVersionLater": "позже",
"extensions": "Расширения",
"extensionInstallSuccess": "Установка завершена",
"extensionUpdateSuccess": "Обновление завершено",
"extensionDelete": "Удалить расширение",
"extensionAlreadyLatest": "Это последняя версия",
"extensionFind": "Найти расширения",
"extensionDevelop": "Разработка расширений",
"history": "История",
"clearHistory": "Очистить историю",
"noHistoryFound": "История не найдена"
}
-77
View File
@@ -1,77 +0,0 @@
{
"label": "中文(简体)",
"error": "错误",
"tip": "提示",
"confirm": "确认",
"cancel": "取消",
"selectAll": "全选",
"task": "任务",
"downloading": "下载中",
"downloaded": "已完成",
"setting": "设置",
"donate": "打赏",
"exit": "退出",
"create": "创建任务",
"directDownload": "直接下载",
"advancedOptions": "高级选项",
"downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接",
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
"downloadLinkHitDesktop": ",也可以直接拖拽种子文件到此处",
"download": "下载",
"noFileSelected": "请至少选择一个文件下载",
"noStoragePermission": "需要开启存储权限",
"selectFile": "选择文件",
"rename": "重命名",
"basic": "基础",
"advanced": "高级",
"general": "通用",
"downloadDir": "下载目录",
"downloadDirValid": "请选择下载目录",
"connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 项",
"subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试",
"update": "更新",
"updateDaily": "每天自动更新",
"lastUpdate": "上次更新:@time",
"addTracker": "添加 Tracker",
"addTrackerHit": "请输入 tracker 服务器地址,每行一条",
"ui": "界面",
"theme": "主题",
"themeSystem": "跟随系统",
"themeLight": "明亮主题",
"themeDark": "暗黑主题",
"locale": "语言",
"about": "关于",
"homepage": "主页",
"version": "版本",
"protocol": "通讯协议",
"port": "端口",
"apiToken": "接口令牌",
"notSet": "未设置",
"set": "已设置",
"effectAfterRestart": "此配置项将在重启应用后生效",
"startAll": "全部开始",
"pauseAll": "全部暂停",
"deleteTask": "删除任务",
"deleteTaskTip": "保留已下载的文件",
"delete": "删除",
"newVersionTitle": "发现新版本 @version",
"newVersionUpdate": "立即更新",
"newVersionLater": "稍后再说",
"extensions": "扩展",
"extensionInstallUrl": "安装链接",
"extensionInstallSuccess": "安装成功",
"extensionUpdateSuccess": "更新成功",
"extensionDelete": "删除扩展",
"extensionAlreadyLatest": "已经是最新版本",
"extensionFind": "获取扩展",
"extensionDevelop": "开发扩展",
"history": "历史记录",
"clearHistory": "清空历史记录",
"noHistoryFound": "暂无历史记录",
"serviceTitle": "下载服务",
"serviceText": "运行中"
}
-77
View File
@@ -1,77 +0,0 @@
{
"label": "中文 (繁體) ",
"error": "錯誤",
"tip": "提示",
"confirm": "確定",
"cancel": "取消",
"selectAll": "全選",
"task": "任務",
"downloading": "下載中",
"downloaded": "已完成",
"setting": "設定",
"donate": "斗內",
"exit": "離開",
"create": "新增任務",
"directDownload": "直接下載",
"advancedOptions": "進階選項",
"downloadLink": "下載鏈接",
"downloadLinkValid": "請輸入下載鏈接",
"downloadLinkHit": "請輸入下載鏈接,支援 HTTP/HTTPS/MAGNET@append",
"downloadLinkHitDesktop": ",也可以通過拖拽Torrent文件至此以新增任務",
"download": "下載",
"noFileSelected": "沒有選取的文件",
"noStoragePermission": "沒有存儲權限",
"selectFile": "選擇文件",
"rename": "重命名",
"basic": "基礎",
"advanced": "進階",
"general": "通用設定",
"downloadDir": "下載目録",
"downloadDirValid": "請選擇下載目録",
"connections": "同時連接數",
"maxRunning": "最大同時下載數",
"items": "@count 項",
"subscribeTracker": "已訂閲的追蹤者列表",
"subscribeFail": "訂閲失敗",
"update": "更新",
"updateDaily": "每天自動更新",
"lastUpdate": "上次更新時間: @time",
"addTracker": "增加新的追蹤者",
"addTrackerHit": "請輸入追蹤者服務器地址,每行一條",
"ui": "介面",
"theme": "佈景主題",
"themeSystem": "跟隨系統",
"themeLight": "亮色主題",
"themeDark": "深色主題",
"locale": "語言",
"about": "關於",
"homepage": "訪問主頁",
"version": "版本資訊",
"protocol": "端口設定",
"port": "端口",
"apiToken": "接口秘鑰",
"notSet": "未設定",
"set": "已設定",
"effectAfterRestart": "重啟後生效",
"startAll": "全部開始",
"pauseAll": "全部暫停",
"deleteTask": "刪除任務",
"deleteTaskTip": "保留已經下載的文件",
"delete": "刪除",
"newVersionTitle": "發現新版本 @version",
"newVersionUpdate": "立刻更新",
"newVersionLater": "稍後更新",
"extensions": "外掛程序",
"extensionInstallUrl": "安裝URL",
"extensionInstallSuccess": "安裝成功",
"extensionUpdateSuccess": "更新完成",
"extensionDelete": "解除安裝",
"extensionAlreadyLatest": "已是最新版本",
"extensionFind": "取得外掛",
"extensionDevelop": "開發外掛",
"history": "歷史記錄",
"clearHistory": "清空記錄",
"noHistoryFound": "還沒有記錄",
"serviceTitle": "下載服務",
"serviceText": "執行中"
}
@@ -8,6 +8,7 @@ class DownloaderConfig {
int maxRunning = 0;
ProtocolConfig protocolConfig = ProtocolConfig();
ExtraConfig extra = ExtraConfig();
ProxyConfig proxy = ProxyConfig();
DownloaderConfig();
@@ -66,6 +67,22 @@ class ExtraConfig {
Map<String, dynamic> toJson() => _$ExtraConfigToJson(this);
}
@JsonSerializable()
class ProxyConfig {
bool enable = false;
String scheme = '';
String host = '';
String usr = '';
String pwd = '';
ProxyConfig();
factory ProxyConfig.fromJson(Map<String, dynamic> json) =>
_$ProxyConfigFromJson(json);
Map<String, dynamic> toJson() => _$ProxyConfigToJson(this);
}
@JsonSerializable()
class ExtraConfigBt {
List<String> trackerSubscribeUrls = [];
@@ -12,7 +12,8 @@ DownloaderConfig _$DownloaderConfigFromJson(Map<String, dynamic> json) =>
..maxRunning = json['maxRunning'] as int
..protocolConfig = ProtocolConfig.fromJson(
json['protocolConfig'] as Map<String, dynamic>?)
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?);
..extra = ExtraConfig.fromJson(json['extra'] as Map<String, dynamic>?)
..proxy = ProxyConfig.fromJson(json['proxy'] as Map<String, dynamic>);
Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
<String, dynamic>{
@@ -20,6 +21,7 @@ Map<String, dynamic> _$DownloaderConfigToJson(DownloaderConfig instance) =>
'maxRunning': instance.maxRunning,
'protocolConfig': instance.protocolConfig.toJson(),
'extra': instance.extra.toJson(),
'proxy': instance.proxy.toJson(),
};
ProtocolConfig _$ProtocolConfigFromJson(Map<String, dynamic> json) =>
@@ -65,6 +67,22 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) =>
'bt': instance.bt.toJson(),
};
ProxyConfig _$ProxyConfigFromJson(Map<String, dynamic> json) => ProxyConfig()
..enable = json['enable'] as bool
..scheme = json['scheme'] as String
..host = json['host'] as String
..usr = json['usr'] as String
..pwd = json['pwd'] as String;
Map<String, dynamic> _$ProxyConfigToJson(ProxyConfig instance) =>
<String, dynamic>{
'enable': instance.enable,
'scheme': instance.scheme,
'host': instance.host,
'usr': instance.usr,
'pwd': instance.pwd,
};
ExtraConfigBt _$ExtraConfigBtFromJson(Map<String, dynamic> json) =>
ExtraConfigBt()
..trackerSubscribeUrls = (json['trackerSubscribeUrls'] as List<dynamic>)
+2
View File
@@ -13,6 +13,7 @@ class Task {
Status status;
Progress progress;
DateTime createdAt;
DateTime updatedAt;
Task({
required this.id,
@@ -20,6 +21,7 @@ class Task {
required this.status,
required this.progress,
required this.createdAt,
required this.updatedAt,
});
factory Task.fromJson(Map<String, dynamic> json) => _$TaskFromJson(json);
+2
View File
@@ -12,6 +12,7 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
status: $enumDecode(_$StatusEnumMap, json['status']),
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
createdAt: DateTime.parse(json['createdAt'] as String),
updatedAt: DateTime.parse(json['updatedAt'] as String),
);
Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
@@ -20,6 +21,7 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'status': _$StatusEnumMap[instance.status]!,
'progress': instance.progress.toJson(),
'createdAt': instance.createdAt.toIso8601String(),
'updatedAt': instance.updatedAt.toIso8601String(),
};
const _$StatusEnumMap = {
@@ -16,7 +16,7 @@ import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../core/common/start_config.dart';
import '../../../../generated/locales.g.dart';
import '../../../../i18n/message.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
@@ -185,9 +185,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
MenuItem(
label: 'donate'.tr,
onClick: (menuItem) => {
launchUrl(
Uri.parse(
"https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate"),
launchUrl(Uri.parse("https://docs.gopeed.com/donate.html"),
mode: LaunchMode.externalApplication)
},
),
@@ -376,7 +374,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
}
if (extra.locale.isEmpty) {
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
extra.locale = AppTranslation.translations.containsKey(systemLocale)
extra.locale = messages.keys.containsKey(systemLocale)
? systemLocale
: getLocaleKey(fallbackLocale);
}
@@ -384,6 +382,10 @@ class AppController extends GetxController with WindowListener, TrayListener {
// default select all tracker subscribe urls
extra.bt.trackerSubscribeUrls.addAll(allTrackerSubscribeUrls);
}
final proxy = config.proxy;
if (proxy.scheme.isEmpty) {
proxy.scheme = 'http';
}
if (config.downloadDir.isEmpty) {
if (Util.isDesktop()) {
@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import '../../../../generated/locales.g.dart';
import '../../../../i18n/message.dart';
import '../../../../theme/theme.dart';
import '../../../../util/locale_manager.dart';
import '../../../routes/app_pages.dart';
@@ -20,8 +20,7 @@ class AppView extends GetView<AppController> {
theme: GopeedTheme.light,
darkTheme: GopeedTheme.dark,
themeMode: ThemeMode.values.byName(config.extra.themeMode),
// translations: messages,
translationsKeys: AppTranslation.translations,
translations: messages,
locale: toLocale(config.extra.locale),
fallbackLocale: fallbackLocale,
localizationsDelegates: const [
@@ -29,8 +28,7 @@ class AppView extends GetView<AppController> {
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales:
AppTranslation.translations.keys.map((e) => toLocale(e)).toList(),
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
getPages: AppPages.routes,
);
}
@@ -41,10 +41,8 @@ class ExtensionView extends GetView<ExtensionController> {
children: [
TextButton.icon(
onPressed: () {
launchUrl(
Uri.parse(
'https://github.com/search?q=topic%3Agopeed-extension&type=repositories'),
mode: LaunchMode.externalApplication);
launchUrl(Uri.parse(
'https://github.com/search?q=topic%3Agopeed-extension&type=repositories'));
},
icon: const Icon(Icons.search),
label: Text('extensionFind'.tr),
@@ -53,8 +51,7 @@ class ExtensionView extends GetView<ExtensionController> {
TextButton.icon(
onPressed: () {
launchUrl(
Uri.parse('https://docs.gopeed.com/dev-extension.html'),
mode: LaunchMode.externalApplication);
Uri.parse('https://docs.gopeed.com/dev-extension.html'));
},
icon: const Icon(Icons.edit),
label: Text('extensionDevelop'.tr),
@@ -208,20 +205,15 @@ class ExtensionView extends GetView<ExtensionController> {
? IconButton(
onPressed: () {
launchUrl(
Uri.parse(extension.homepage),
mode: LaunchMode
.externalApplication);
Uri.parse(extension.homepage));
},
icon: const Icon(Icons.home))
: null,
extension.repository?.url.isNotEmpty == true
? IconButton(
onPressed: () {
launchUrl(
Uri.parse(
extension.repository!.url),
mode: LaunchMode
.externalApplication);
launchUrl(Uri.parse(
extension.repository!.url));
},
icon: const Icon(Icons.code))
: null,
@@ -7,7 +7,7 @@ import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../generated/locales.g.dart';
import '../../../../i18n/message.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/message.dart';
@@ -272,8 +272,7 @@ class SettingView extends GetView<SettingController> {
));
final buildLocale = _buildConfigItem(
'locale',
() => AppTranslation
.translations[downloaderCfg.value.extra.locale]!['label']!,
() => messages.keys[downloaderCfg.value.extra.locale]!['label']!,
(Key key) => DropdownButton<String>(
key: key,
value: downloaderCfg.value.extra.locale,
@@ -287,23 +286,26 @@ class SettingView extends GetView<SettingController> {
await debounceSave();
},
items: AppTranslation.translations.keys
items: messages.keys.keys
.map((e) => DropdownMenuItem<String>(
value: e,
child: Text(AppTranslation.translations[e]!['label']!),
child: Text(messages.keys[e]!['label']!),
))
.toList(),
));
// about config items start
buildHomepage() => ListTile(
title: Text('homepage'.tr),
subtitle: const Text('https://github.com/GopeedLab/gopeed'),
onTap: () {
launchUrl(Uri.parse('https://github.com/GopeedLab/gopeed'),
mode: LaunchMode.externalApplication);
},
);
buildHomepage() {
const homePage = 'https://gopeed.com';
return ListTile(
title: Text('homepage'.tr),
subtitle: const Text(homePage),
onTap: () {
launchUrl(Uri.parse(homePage), mode: LaunchMode.externalApplication);
},
);
}
buildVersion() {
bool isNewVersion(String current, String latest) {
if (latest == "") {
@@ -363,7 +365,161 @@ class SettingView extends GetView<SettingController> {
);
}
// advanced config items start
// advanced config proxy items start
final buildProxy = _buildConfigItem(
'proxy',
() => downloaderCfg.value.proxy.enable
? '${downloaderCfg.value.proxy.scheme}://${downloaderCfg.value.proxy.host}'
: 'notSet'.tr,
(Key key) {
final proxy = downloaderCfg.value.proxy;
final switcher = Switch(
value: proxy.enable,
onChanged: (bool value) async {
if (value != proxy.enable) {
downloaderCfg.update((val) {
val!.proxy.enable = value;
});
await debounceSave();
}
},
);
final scheme = SizedBox(
width: 100,
child: DropdownButtonFormField<String>(
value: proxy.scheme,
onChanged: (value) async {
if (value != null && value != proxy.scheme) {
proxy.scheme = value;
await debounceSave();
}
},
items: const [
DropdownMenuItem<String>(
value: 'http',
child: Text('HTTP'),
),
DropdownMenuItem<String>(
value: 'https',
child: Text('HTTPS'),
),
DropdownMenuItem<String>(
value: 'socks5',
child: Text('SOCKS5'),
),
],
),
);
final arr = proxy.host.split(':');
var ip = '';
var port = '';
if (arr.length > 1) {
ip = arr[0];
port = arr[1];
}
final ipController = TextEditingController(text: ip);
final portController = TextEditingController(text: port);
updateAddress() async {
final newAddress = '${ipController.text}:${portController.text}';
if (newAddress != startCfg.value.address) {
proxy.host = newAddress;
await debounceSave();
}
}
ipController.addListener(updateAddress);
portController.addListener(updateAddress);
final host = [
Flexible(
child: TextFormField(
controller: ipController,
decoration: const InputDecoration(
labelText: 'IP',
contentPadding: EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
),
),
const Padding(padding: EdgeInsets.only(left: 10)),
Flexible(
child: TextFormField(
controller: portController,
decoration: InputDecoration(
labelText: 'port'.tr,
contentPadding: const EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 0, max: 65535),
],
),
),
];
final usrController = TextEditingController(text: proxy.usr);
final pwdController = TextEditingController(text: proxy.pwd);
final auth = [
Flexible(
child: TextFormField(
controller: usrController,
decoration: InputDecoration(
labelText: 'username'.tr,
contentPadding: const EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
),
),
const Padding(padding: EdgeInsets.only(left: 10)),
Flexible(
child: TextFormField(
controller: pwdController,
decoration: InputDecoration(
labelText: 'password'.tr,
contentPadding: const EdgeInsets.all(0.0),
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 0, max: 65535),
],
),
),
];
return Form(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([
switcher,
scheme,
Row(
children: host,
),
Row(
children: auth,
),
]),
),
);
},
);
// advanced config API items start
final buildApiProtocol = _buildConfigItem(
'protocol',
() => startCfg.value.network == 'tcp'
@@ -372,7 +528,7 @@ class SettingView extends GetView<SettingController> {
(Key key) {
final items = <Widget>[
SizedBox(
width: 150,
width: 80,
child: DropdownButtonFormField<String>(
value: startCfg.value.network,
onChanged: Util.isDesktop()
@@ -423,8 +579,7 @@ class SettingView extends GetView<SettingController> {
portController.addListener(updateAddress);
items.addAll([
const Padding(padding: EdgeInsets.only(left: 20)),
SizedBox(
width: 200,
Flexible(
child: TextFormField(
controller: ipController,
decoration: const InputDecoration(
@@ -438,8 +593,7 @@ class SettingView extends GetView<SettingController> {
),
),
const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 200,
Flexible(
child: TextFormField(
controller: portController,
decoration: InputDecoration(
@@ -553,21 +707,42 @@ class SettingView extends GetView<SettingController> {
]),
),
),
Column(
children: [
// Column(
// children: [
// Card(
// child: Column(
// children: [
// ..._addDivider([
// buildApiProtocol(),
// Util.isDesktop() && startCfg.value.network == 'tcp'
// ? buildApiToken()
// : null,
// ]),
// ],
// )),
// ],
// ),
SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([
Text('network'.tr),
Card(
child: Column(
children: [
..._addDivider([
buildApiProtocol(),
Util.isDesktop() && startCfg.value.network == 'tcp'
? buildApiToken()
: null,
]),
],
children: _addDivider([buildProxy()]),
)),
],
),
const Text('API'),
Card(
child: Column(
children: _addDivider([
buildApiProtocol(),
Util.isDesktop() && startCfg.value.network == 'tcp'
? buildApiToken()
: null,
]),
)),
]),
))
],
).paddingOnly(left: 16, right: 16, top: 16, bottom: 16)),
),
@@ -3,5 +3,6 @@ import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
import '../../../../api/model/task.dart';
class TaskDownloadedController extends TaskListController {
TaskDownloadedController() : super([Status.done], SortDirection.asc);
TaskDownloadedController()
: super([Status.done], (a, b) => b.updatedAt.compareTo(a.updatedAt));
}
@@ -9,5 +9,5 @@ class TaskDownloadingController extends TaskListController {
Status.pause,
Status.wait,
Status.error
], SortDirection.desc);
], (a, b) => b.createdAt.compareTo(a.createdAt));
}
@@ -5,13 +5,11 @@ 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;
int Function(Task a, Task b) compare;
TaskListController(this.statuses, this.sortDirection);
TaskListController(this.statuses, this.compare);
final tasks = <Task>[].obs;
final isRunning = false.obs;
@@ -48,13 +46,7 @@ abstract class TaskListController extends GetxController {
getTasksState() async {
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);
}
});
tasks.sort(compare);
this.tasks.value = tasks;
}
}
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:open_file/open_file.dart';
import 'package:open_filex/open_filex.dart';
import 'package:path/path.dart';
import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -57,8 +57,9 @@ class TaskFilesView extends GetView<TaskFilesController> {
final meta = controller.task.value!.meta;
final file = fileList[index];
// if resource is single file, use opts.name as file name
final realFileName =
meta.res!.name.isEmpty ? file.name : "";
final realFileName = meta.res!.name.isEmpty
? (meta.opts.name.isEmpty ? file.name : meta.opts.name)
: "";
final fileRelativePath = file.filePath(realFileName);
final filePath = Util.safePathJoin(
[meta.opts.path, meta.res!.name, fileRelativePath]);
@@ -66,7 +67,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
return ListTile(
leading: file.isDirectory
? const Icon(Icons.folder)
: Icon(FaIcons.allIcons[findIcon(file.name)]),
: Icon(FaIcons.allIcons[findIcon(fileName)]),
title: Text(fileName),
subtitle: file.isDirectory
? Text('items'.trParams({
@@ -104,8 +105,13 @@ class TaskFilesView extends GetView<TaskFilesController> {
: [
IconButton(
icon: const Icon(Icons.play_circle),
onPressed: () {
OpenFile.open(filePath);
onPressed: () async {
print(filePath);
try {
await OpenFilex.open(filePath);
} catch (e) {
print(e);
}
}),
IconButton(
icon: const Icon(Icons.share),
@@ -205,9 +205,11 @@ class BuildTaskListView extends GetView {
)),
],
),
LinearProgressIndicator(
value: getProgress(),
),
isDone()
? Container()
: LinearProgressIndicator(
value: getProgress(),
),
],
),
)).padding(horizontal: 14, top: 8);
-524
View File
@@ -1,524 +0,0 @@
// DO NOT EDIT. This is code generated via package:get_cli/get_cli.dart
// ignore_for_file: lines_longer_than_80_chars
// ignore: avoid_classes_with_only_static_members
class AppTranslation {
static Map<String, Map<String, String>> translations = {
'en_US': Locales.en_US,
'fa_IR': Locales.fa_IR,
'ja_JP': Locales.ja_JP,
'ru_RU': Locales.ru_RU,
'zh_CN': Locales.zh_CN,
'zh_TW': Locales.zh_TW,
};
}
class LocaleKeys {
LocaleKeys._();
static const label = 'label';
static const error = 'error';
static const tip = 'tip';
static const confirm = 'confirm';
static const cancel = 'cancel';
static const selectAll = 'selectAll';
static const task = 'task';
static const downloading = 'downloading';
static const downloaded = 'downloaded';
static const setting = 'setting';
static const donate = 'donate';
static const exit = 'exit';
static const create = 'create';
static const directDownload = 'directDownload';
static const advancedOptions = 'advancedOptions';
static const downloadLink = 'downloadLink';
static const downloadLinkValid = 'downloadLinkValid';
static const downloadLinkHit = 'downloadLinkHit';
static const downloadLinkHitDesktop = 'downloadLinkHitDesktop';
static const download = 'download';
static const noFileSelected = 'noFileSelected';
static const noStoragePermission = 'noStoragePermission';
static const selectFile = 'selectFile';
static const rename = 'rename';
static const basic = 'basic';
static const advanced = 'advanced';
static const general = 'general';
static const downloadDir = 'downloadDir';
static const downloadDirValid = 'downloadDirValid';
static const connections = 'connections';
static const maxRunning = 'maxRunning';
static const items = 'items';
static const subscribeTracker = 'subscribeTracker';
static const subscribeFail = 'subscribeFail';
static const update = 'update';
static const updateDaily = 'updateDaily';
static const lastUpdate = 'lastUpdate';
static const addTracker = 'addTracker';
static const addTrackerHit = 'addTrackerHit';
static const ui = 'ui';
static const theme = 'theme';
static const themeSystem = 'themeSystem';
static const themeLight = 'themeLight';
static const themeDark = 'themeDark';
static const locale = 'locale';
static const about = 'about';
static const homepage = 'homepage';
static const version = 'version';
static const protocol = 'protocol';
static const port = 'port';
static const apiToken = 'apiToken';
static const notSet = 'notSet';
static const set = 'set';
static const effectAfterRestart = 'effectAfterRestart';
static const startAll = 'startAll';
static const pauseAll = 'pauseAll';
static const deleteTask = 'deleteTask';
static const deleteTaskTip = 'deleteTaskTip';
static const delete = 'delete';
static const newVersionTitle = 'newVersionTitle';
static const newVersionUpdate = 'newVersionUpdate';
static const newVersionLater = 'newVersionLater';
static const extensions = 'extensions';
static const extensionInstallUrl = 'extensionInstallUrl';
static const extensionInstallSuccess = 'extensionInstallSuccess';
static const extensionUpdateSuccess = 'extensionUpdateSuccess';
static const extensionDelete = 'extensionDelete';
static const extensionAlreadyLatest = 'extensionAlreadyLatest';
static const extensionFind = 'extensionFind';
static const extensionDevelop = 'extensionDevelop';
static const history = 'history';
static const clearHistory = 'clearHistory';
static const noHistoryFound = 'noHistoryFound';
static const serviceTitle = 'serviceTitle';
static const serviceText = 'serviceText';
}
class Locales {
static const en_US = {
'label': 'English',
'error': 'Error',
'tip': 'Tip',
'confirm': 'Confirm',
'cancel': 'Cancel',
'selectAll': 'Select All',
'task': 'Tasks',
'downloading': 'downloading',
'downloaded': 'downloaded',
'setting': 'Settings',
'donate': 'Donate',
'exit': 'Exit',
'create': 'Create Task',
'directDownload': 'Direct Download',
'advancedOptions': 'Advanced Options',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link',
'downloadLinkHit':
'Please enter the download link, HTTP/HTTPS/MAGNET supported@append',
'downloadLinkHitDesktop': ', or drag the torrent file here directly',
'download': 'Download',
'noFileSelected': 'Please select at least one file to continue.',
'noStoragePermission': 'Storage permission required',
'selectFile': 'Select File',
'rename': 'Rename',
'basic': 'Basic',
'advanced': 'Advanced',
'general': 'General',
'downloadDir': 'Download Directory',
'downloadDirValid': 'Please select the download directory',
'connections': 'Connections',
'maxRunning': 'Max Running Tasks',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
'Subscribe failed, please check network or try again later',
'update': 'Update',
'updateDaily': 'Update daily',
'lastUpdate': 'Last update: @time',
'addTracker': 'Add Tracker',
'addTrackerHit': 'Please enter the tracker server url, one per line',
'ui': 'UI',
'theme': 'Theme',
'themeSystem': 'System',
'themeLight': 'Light',
'themeDark': 'Dark',
'locale': 'Language',
'about': 'About',
'homepage': 'Homepage',
'version': 'Version',
'protocol': 'Protocol',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'Start All',
'pauseAll': 'Pause All',
'deleteTask': 'Delete Task',
'deleteTaskTip': 'Keep downloaded files',
'delete': 'Delete',
'newVersionTitle': 'Discover new version @version',
'newVersionUpdate': 'Update Now',
'newVersionLater': 'Later',
'extensions': 'Extensions',
'extensionInstallUrl': 'Install URL',
'extensionInstallSuccess': 'Installed successfully',
'extensionUpdateSuccess': 'Updated successfully',
'extensionDelete': 'Delete Extension',
'extensionAlreadyLatest': 'It\'s already the latest version',
'extensionFind': 'Find Extensions',
'extensionDevelop': 'Develop Extensions',
'history': 'History',
'clearHistory': 'Clear History',
'noHistoryFound': 'No History Found',
'serviceTitle': 'Download Service',
'serviceText': 'Running',
};
static const fa_IR = {
'label': 'فارسی',
'error': 'اررور',
'tip': 'Tip',
'confirm': 'تایید',
'cancel': 'انصراف',
'selectAll': 'انتخاب همه',
'task': 'کار',
'setting': 'تنظیمات',
'donate': 'کمک مالی',
'exit': 'خروج',
'create': 'ایجاد کار',
'downloading': 'دانلود کردن',
'downloaded': 'دانلود شدن',
'downloadLink': 'لینک دانلود',
'downloadLinkValid': 'لطفا لینک دانلود را وارد کنید',
'download': 'دانلود',
'noFileSelected': 'لطفا حداقل یک فایل را جهت دانلود انتخاب کنید.',
'noStoragePermission': 'مجوز ذخیره سازی مورد نیاز است',
'selectFile': 'انتخاب فایل',
'basic': 'پایه',
'advanced': 'پیشرفته',
'general': 'عمومی',
'downloadDir': 'دایرکتوری دانلود',
'downloadDirValid': 'لطفا دایرکتوری دانلود را انتخاب کنید',
'connections': 'اتصالات',
'items': '@count آیتم ها',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
'Subscribe failed, please check network or try again later',
'update': 'بروزرسانی',
'updateDaily': 'بروزرسانی روزانه',
'lastUpdate': 'Last update: @time',
'addTracker': 'Add Tracker',
'addTrackerHit': 'Please enter the tracker server url, one per line',
'ui': 'رابط کاربری',
'theme': 'پوسته',
'themeSystem': 'سیستم',
'themeLight': 'روشن',
'themeDark': 'تاریک',
'locale': 'Language',
'about': 'درباره ی',
'homepage': 'صفحه اصلی',
'version': 'نسخه',
'protocol': 'پروتکل',
'port': 'پورت',
'apiToken': 'API توکن',
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'شروع همه',
'pauseAll': 'توقف همه',
'deleteTask': 'پاک کردن کار',
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
'delete': 'پاک کردن',
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
'newVersionUpdate': 'بروزرسانی',
'newVersionLater': 'بعداً',
};
static const ja_JP = {
'label': '日本語',
'error': 'エラー',
'tip': 'ヒント',
'confirm': '確認',
'cancel': 'キャンセル',
'selectAll': 'すべてを選択',
'task': 'タスク',
'downloading': 'ダウンロード中',
'downloaded': 'ダウンロード済',
'setting': 'セッティング',
'donate': '寄付する',
'exit': '終了',
'create': 'タスクを作成',
'downloadLink': 'ダウンロードリンク',
'downloadLinkValid': 'ダウンロードリンクを入力してください',
'downloadLinkHit': 'ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append',
'downloadLinkHitDesktop': 'または、torrent ファイルを直接ここにドラッグします',
'download': 'ダウンロード',
'noFileSelected': '続行するには少なくとも 1 つのファイルを選択してください。',
'noStoragePermission': 'ストレージのパーミッションが必要',
'selectFile': 'ファイルを選択',
'basic': 'ベーシック',
'advanced': 'アドバンスド',
'general': 'ジェネラル',
'downloadDir': 'ディレクトリのダウンロード',
'downloadDirValid': 'ダウンロードディレクトリを選択してください',
'connections': '接続',
'maxRunning': '最大実行タスク',
'items': '@count アイテム',
'subscribeTracker': 'トラッカーを購読',
'subscribeFail': '登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください',
'update': 'アップデート',
'updateDaily': '毎日アップデート',
'lastUpdate': '最終アップデート: @time',
'addTracker': 'トラッカーを追加',
'addTrackerHit': 'トラッカーサーバーの URL を 1 行に 1 つずつ入力してください',
'ui': 'UI',
'theme': 'テーマ',
'themeSystem': 'システム',
'themeLight': 'ライト',
'themeDark': 'ダーク',
'locale': '言語',
'about': '概要',
'homepage': 'ホームページ',
'version': 'バージョン',
'protocol': 'プロトコル',
'port': 'ポート',
'apiToken': 'API トークン',
'notSet': '未設置',
'set': '設置',
'effectAfterRestart': '再起動後の効果',
'startAll': 'すべてを開始',
'pauseAll': 'すべてを一時停止',
'deleteTask': 'タスクを削除',
'deleteTaskTip': 'ダウンロードしたファイルを保持',
'delete': '削除',
'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': 'アップデート',
'newVersionLater': '後で',
};
static const ru_RU = {
'label': 'Русский(Россия)',
'error': 'Ошибка',
'tip': 'Подсказка',
'confirm': 'Подтвердить',
'cancel': 'Отмена',
'selectAll': 'Выбрать все',
'task': 'Задачи',
'downloading': 'Загрузка',
'downloaded': 'Загружено',
'setting': 'Настройки',
'donate': 'Пожертвовать',
'exit': 'Выход',
'create': 'Создать задачу',
'advancedOptions': 'Расширенные опции',
'downloadLink': 'Ссылка на скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
'downloadLinkHit':
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append',
'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл',
'download': 'Скачать',
'noFileSelected': 'Файл не выбран',
'noStoragePermission': 'Требуется доступ к хранилищу',
'selectFile': 'Выбрать файл',
'rename': 'Переименовать',
'basic': 'Основные параметры',
'advanced': 'Расширенные',
'general': 'Общие',
'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений',
'maxRunning': 'Максимальное количество активных задач',
'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера',
'subscribeFail':
'Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже',
'update': 'Обновить',
'updateDaily': 'Обновлять ежедневно',
'lastUpdate': 'Последнее обновление: @время',
'addTracker': 'Добавить трекер',
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
'ui': 'UI',
'theme': 'Тема',
'themeSystem': 'Системная',
'themeLight': 'Светлая',
'themeDark': 'Темная',
'locale': 'Язык',
'about': 'О приложении',
'homepage': 'Домашняя страница',
'version': 'Версия',
'protocol': 'Протокол',
'port': 'Порт',
'apiToken': 'API Токен',
'notSet': 'Не установлено',
'set': 'Установлено',
'effectAfterRestart': 'Эффект после перезагрузки',
'startAll': 'Запустить все',
'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить задачу',
'deleteTaskTip': 'Сохранить загруженные файлы',
'delete': 'Удалить',
'newVersionTitle': 'Обнаружена новая версия @version',
'newVersionUpdate': 'Обновить',
'newVersionLater': 'позже',
'extensions': 'Расширения',
'extensionInstallSuccess': 'Установка завершена',
'extensionUpdateSuccess': 'Обновление завершено',
'extensionDelete': 'Удалить расширение',
'extensionAlreadyLatest': 'Это последняя версия',
'extensionFind': 'Найти расширения',
'extensionDevelop': 'Разработка расширений',
'history': 'История',
'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена',
};
static const zh_CN = {
'label': '中文(简体)',
'error': '错误',
'tip': '提示',
'confirm': '确认',
'cancel': '取消',
'selectAll': '全选',
'task': '任务',
'downloading': '下载中',
'downloaded': '已完成',
'setting': '设置',
'donate': '打赏',
'exit': '退出',
'create': '创建任务',
'directDownload': '直接下载',
'advancedOptions': '高级选项',
'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接',
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
'downloadLinkHitDesktop': ',也可以直接拖拽种子文件到此处',
'download': '下载',
'noFileSelected': '请至少选择一个文件下载',
'noStoragePermission': '需要开启存储权限',
'selectFile': '选择文件',
'rename': '重命名',
'basic': '基础',
'advanced': '高级',
'general': '通用',
'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录',
'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 项',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
'update': '更新',
'updateDaily': '每天自动更新',
'lastUpdate': '上次更新:@time',
'addTracker': '添加 Tracker',
'addTrackerHit': '请输入 tracker 服务器地址,每行一条',
'ui': '界面',
'theme': '主题',
'themeSystem': '跟随系统',
'themeLight': '明亮主题',
'themeDark': '暗黑主题',
'locale': '语言',
'about': '关于',
'homepage': '主页',
'version': '版本',
'protocol': '通讯协议',
'port': '端口',
'apiToken': '接口令牌',
'notSet': '未设置',
'set': '已设置',
'effectAfterRestart': '此配置项将在重启应用后生效',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务',
'deleteTaskTip': '保留已下载的文件',
'delete': '删除',
'newVersionTitle': '发现新版本 @version',
'newVersionUpdate': '立即更新',
'newVersionLater': '稍后再说',
'extensions': '扩展',
'extensionInstallUrl': '安装链接',
'extensionInstallSuccess': '安装成功',
'extensionUpdateSuccess': '更新成功',
'extensionDelete': '删除扩展',
'extensionAlreadyLatest': '已经是最新版本',
'extensionFind': '获取扩展',
'extensionDevelop': '开发扩展',
'history': '历史记录',
'clearHistory': '清空历史记录',
'noHistoryFound': '暂无历史记录',
'serviceTitle': '下载服务',
'serviceText': '运行中',
};
static const zh_TW = {
'label': '中文 (繁體) ',
'error': '錯誤',
'tip': '提示',
'confirm': '確定',
'cancel': '取消',
'selectAll': '全選',
'task': '任務',
'downloading': '下載中',
'downloaded': '已完成',
'setting': '設定',
'donate': '斗內',
'exit': '離開',
'create': '新增任務',
'directDownload': '直接下載',
'advancedOptions': '進階選項',
'downloadLink': '下載鏈接',
'downloadLinkValid': '請輸入下載鏈接',
'downloadLinkHit': '請輸入下載鏈接,支援 HTTP/HTTPS/MAGNET@append',
'downloadLinkHitDesktop': ',也可以通過拖拽Torrent文件至此以新增任務',
'download': '下載',
'noFileSelected': '沒有選取的文件',
'noStoragePermission': '沒有存儲權限',
'selectFile': '選擇文件',
'rename': '重命名',
'basic': '基礎',
'advanced': '進階',
'general': '通用設定',
'downloadDir': '下載目録',
'downloadDirValid': '請選擇下載目録',
'connections': '同時連接數',
'maxRunning': '最大同時下載數',
'items': '@count 項',
'subscribeTracker': '已訂閲的追蹤者列表',
'subscribeFail': '訂閲失敗',
'update': '更新',
'updateDaily': '每天自動更新',
'lastUpdate': '上次更新時間: @time',
'addTracker': '增加新的追蹤者',
'addTrackerHit': '請輸入追蹤者服務器地址,每行一條',
'ui': '介面',
'theme': '佈景主題',
'themeSystem': '跟隨系統',
'themeLight': '亮色主題',
'themeDark': '深色主題',
'locale': '語言',
'about': '關於',
'homepage': '訪問主頁',
'version': '版本資訊',
'protocol': '端口設定',
'port': '端口',
'apiToken': '接口秘鑰',
'notSet': '未設定',
'set': '已設定',
'effectAfterRestart': '重啟後生效',
'startAll': '全部開始',
'pauseAll': '全部暫停',
'deleteTask': '刪除任務',
'deleteTaskTip': '保留已經下載的文件',
'delete': '刪除',
'newVersionTitle': '發現新版本 @version',
'newVersionUpdate': '立刻更新',
'newVersionLater': '稍後更新',
'extensions': '外掛程序',
'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得外掛',
'extensionDevelop': '開發外掛',
'history': '歷史記錄',
'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務',
'serviceText': '執行中',
};
}
+85
View File
@@ -0,0 +1,85 @@
const enUS = {
'en_US': {
'label': 'English',
'error': 'Error',
'tip': 'Tip',
'confirm': 'Confirm',
'cancel': 'Cancel',
'selectAll': 'Select All',
'task': 'Tasks',
'downloading': 'downloading',
'downloaded': 'downloaded',
'setting': 'Settings',
'donate': 'Donate',
'exit': 'Exit',
'create': 'Create Task',
'directDownload': 'Direct Download',
'advancedOptions': 'Advanced Options',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link',
'downloadLinkHit':
'Please enter the download link, HTTP/HTTPS/MAGNET supported@append',
'downloadLinkHitDesktop': ', or drag the torrent file here directly',
'download': 'Download',
'noFileSelected': 'Please select at least one file to continue.',
'noStoragePermission': 'Storage permission required',
'selectFile': 'Select File',
'rename': 'Rename',
'basic': 'Basic',
'advanced': 'Advanced',
'general': 'General',
'downloadDir': 'Download Directory',
'downloadDirValid': 'Please select the download directory',
'connections': 'Connections',
'maxRunning': 'Max Running Tasks',
'items': '@count items',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
'Subscribe failed, please check network or try again later',
'update': 'Update',
'updateDaily': 'Update daily',
'lastUpdate': 'Last update: @time',
'addTracker': 'Add Tracker',
'addTrackerHit': 'Please enter the tracker server url, one per line',
'ui': 'UI',
'theme': 'Theme',
'themeSystem': 'System',
'themeLight': 'Light',
'themeDark': 'Dark',
'locale': 'Language',
'about': 'About',
'homepage': 'Homepage',
'version': 'Version',
'protocol': 'Protocol',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'Start All',
'pauseAll': 'Pause All',
'deleteTask': 'Delete Task',
'deleteTaskTip': 'Keep downloaded files',
'delete': 'Delete',
'newVersionTitle': 'Discover new version @version',
'newVersionUpdate': 'Update Now',
'newVersionLater': 'Later',
'extensions': 'Extensions',
'extensionInstallUrl': 'Install URL',
'extensionInstallSuccess': 'Installed successfully',
'extensionUpdateSuccess': 'Updated successfully',
'extensionDelete': 'Delete Extension',
'extensionAlreadyLatest': 'It\'s already the latest version',
'extensionFind': 'Find Extensions',
'extensionDevelop': 'Develop Extensions',
'history': 'History',
'clearHistory': 'Clear History',
'noHistoryFound': 'No History Found',
'serviceTitle': 'Download Service',
'serviceText': 'Running',
'network': 'Network',
'proxy': 'Proxy',
'username': 'Username',
'password': 'Password',
},
};
+61
View File
@@ -0,0 +1,61 @@
const faIR = {
"fa_IR": {
'label': 'فارسی',
'error': 'اررور',
'tip': 'Tip',
'confirm': 'تایید',
'cancel': 'انصراف',
'selectAll': 'انتخاب همه',
'task': 'کار',
'setting': 'تنظیمات',
'donate': 'کمک مالی',
'exit': 'خروج',
'create': 'ایجاد کار',
'downloading': 'دانلود کردن',
'downloaded': 'دانلود شدن',
'downloadLink': 'لینک دانلود',
'downloadLinkValid': 'لطفا لینک دانلود را وارد کنید',
'download': 'دانلود',
'noFileSelected': 'لطفا حداقل یک فایل را جهت دانلود انتخاب کنید.',
'noStoragePermission': 'مجوز ذخیره سازی مورد نیاز است',
'selectFile': 'انتخاب فایل',
'basic': 'پایه',
'advanced': 'پیشرفته',
'general': 'عمومی',
'downloadDir': 'دایرکتوری دانلود',
'downloadDirValid': 'لطفا دایرکتوری دانلود را انتخاب کنید',
'connections': 'اتصالات',
'items': '@count آیتم ها',
'subscribeTracker': 'Subscribe Tracker',
'subscribeFail':
'Subscribe failed, please check network or try again later',
'update': 'بروزرسانی',
'updateDaily': 'بروزرسانی روزانه',
'lastUpdate': 'Last update: @time',
'addTracker': 'Add Tracker',
'addTrackerHit': 'Please enter the tracker server url, one per line',
'ui': 'رابط کاربری',
'theme': 'پوسته',
'themeSystem': 'سیستم',
'themeLight': 'روشن',
'themeDark': 'تاریک',
'locale': 'Language',
'about': 'درباره ی',
'homepage': 'صفحه اصلی',
'version': 'نسخه',
'protocol': 'پروتکل',
'port': 'پورت',
'apiToken': 'API توکن',
'notSet': 'NS',
'set': 'SET',
'effectAfterRestart': 'Effect after restart',
'startAll': 'شروع همه',
'pauseAll': 'توقف همه',
'deleteTask': 'پاک کردن کار',
'deleteTaskTip': 'فایل های دانلود شده را نگه دارد',
'delete': 'پاک کردن',
'newVersionTitle': '@version عنوان: کشف نسخه جدید',
'newVersionUpdate': 'بروزرسانی',
'newVersionLater': 'بعداً',
}
};
+63
View File
@@ -0,0 +1,63 @@
const jaJP = {
"ja_JP": {
'label': '日本語',
'error': 'エラー',
'tip': 'ヒント',
'confirm': '確認',
'cancel': 'キャンセル',
'selectAll': 'すべてを選択',
'task': 'タスク',
'downloading': 'ダウンロード中',
'downloaded': 'ダウンロード済',
'setting': 'セッティング',
'donate': '寄付する',
'exit': '終了',
'create': 'タスクを作成',
'downloadLink': 'ダウンロードリンク',
'downloadLinkValid': 'ダウンロードリンクを入力してください',
'downloadLinkHit': 'ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append',
'downloadLinkHitDesktop': 'または、torrent ファイルを直接ここにドラッグします',
'download': 'ダウンロード',
'noFileSelected': '続行するには少なくとも 1 つのファイルを選択してください。',
'noStoragePermission': 'ストレージのパーミッションが必要',
'selectFile': 'ファイルを選択',
'basic': 'ベーシック',
'advanced': 'アドバンスド',
'general': 'ジェネラル',
'downloadDir': 'ディレクトリのダウンロード',
'downloadDirValid': 'ダウンロードディレクトリを選択してください',
'connections': '接続',
'maxRunning': '最大実行タスク',
'items': '@count アイテム',
'subscribeTracker': 'トラッカーを購読',
'subscribeFail': '登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください',
'update': 'アップデート',
'updateDaily': '毎日アップデート',
'lastUpdate': '最終アップデート: @time',
'addTracker': 'トラッカーを追加',
'addTrackerHit': 'トラッカーサーバーの URL を 1 行に 1 つずつ入力してください',
'ui': 'UI',
'theme': 'テーマ',
'themeSystem': 'システム',
'themeLight': 'ライト',
'themeDark': 'ダーク',
'locale': '言語',
'about': '概要',
'homepage': 'ホームページ',
'version': 'バージョン',
'protocol': 'プロトコル',
'port': 'ポート',
'apiToken': 'API トークン',
'notSet': '未設置',
'set': '設置',
'effectAfterRestart': '再起動後の効果',
'startAll': 'すべてを開始',
'pauseAll': 'すべてを一時停止',
'deleteTask': 'タスクを削除',
'deleteTaskTip': 'ダウンロードしたファイルを保持',
'delete': '削除',
'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': 'アップデート',
'newVersionLater': '後で',
}
};
+77
View File
@@ -0,0 +1,77 @@
const ruRU = {
'ru_RU': {
'label': 'Русский(Россия)',
'error': 'Ошибка',
'tip': 'Подсказка',
'confirm': 'Подтвердить',
'cancel': 'Отмена',
'selectAll': 'Выбрать все',
'task': 'Задачи',
'downloading': 'Загрузка',
'downloaded': 'Загружено',
'setting': 'Настройки',
'donate': 'Пожертвовать',
'exit': 'Выход',
'create': 'Создать задачу',
'advancedOptions': 'Расширенные опции',
'downloadLink': 'Ссылка на скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
'downloadLinkHit':
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append',
'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл',
'download': 'Скачать',
'noFileSelected': 'Файл не выбран',
'noStoragePermission': 'Требуется доступ к хранилищу',
'selectFile': 'Выбрать файл',
'rename': 'Переименовать',
'basic': 'Основные параметры',
'advanced': 'Расширенные',
'general': 'Общие',
'downloadDir': 'Папка загрузки',
'downloadDirValid': 'Пожалуйста, выберите папку для загрузки',
'connections': 'Соединений',
'maxRunning': 'Максимальное количество активных задач',
'items': '@подсчет элементов',
'subscribeTracker': 'Подпись трекера',
'subscribeFail':
'Подписка не удалась, пожалуйста, проверьте сеть или повторите попытку позже',
'update': 'Обновить',
'updateDaily': 'Обновлять ежедневно',
'lastUpdate': 'Последнее обновление: @время',
'addTracker': 'Добавить трекер',
'addTrackerHit': 'Пожалуйста, введите url трекера, по одному в строке',
'ui': 'UI',
'theme': 'Тема',
'themeSystem': 'Системная',
'themeLight': 'Светлая',
'themeDark': 'Темная',
'locale': 'Язык',
'about': 'О приложении',
'homepage': 'Домашняя страница',
'version': 'Версия',
'protocol': 'Протокол',
'port': 'Порт',
'apiToken': 'API Токен',
'notSet': 'Не установлено',
'set': 'Установлено',
'effectAfterRestart': 'Эффект после перезагрузки',
'startAll': 'Запустить все',
'pauseAll': 'Приостановить все',
'deleteTask': 'Удалить задачу',
'deleteTaskTip': 'Сохранить загруженные файлы',
'delete': 'Удалить',
'newVersionTitle': 'Обнаружена новая версия @version',
'newVersionUpdate': 'Обновить',
'newVersionLater': 'позже',
'extensions': 'Расширения',
'extensionInstallSuccess': 'Установка завершена',
'extensionUpdateSuccess': 'Обновление завершено',
'extensionDelete': 'Удалить расширение',
'extensionAlreadyLatest': 'Это последняя версия',
'extensionFind': 'Найти расширения',
'extensionDevelop': 'Разработка расширений',
'history': 'История',
'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена',
}
};
+83
View File
@@ -0,0 +1,83 @@
const zhCN = {
'zh_CN': {
'label': '中文(简体)',
'error': '错误',
'tip': '提示',
'confirm': '确认',
'cancel': '取消',
'selectAll': '全选',
'task': '任务',
'downloading': '下载中',
'downloaded': '已完成',
'setting': '设置',
'donate': '打赏',
'exit': '退出',
'create': '创建任务',
'directDownload': '直接下载',
'advancedOptions': '高级选项',
'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接',
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
'downloadLinkHitDesktop': ',也可以直接拖拽种子文件到此处',
'download': '下载',
'noFileSelected': '请至少选择一个文件下载',
'noStoragePermission': '需要开启存储权限',
'selectFile': '选择文件',
'rename': '重命名',
'basic': '基础',
'advanced': '高级',
'general': '通用',
'downloadDir': '下载目录',
'downloadDirValid': '请选择下载目录',
'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 项',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
'update': '更新',
'updateDaily': '每天自动更新',
'lastUpdate': '上次更新:@time',
'addTracker': '添加 Tracker',
'addTrackerHit': '请输入 tracker 服务器地址,每行一条',
'ui': '界面',
'theme': '主题',
'themeSystem': '跟随系统',
'themeLight': '明亮主题',
'themeDark': '暗黑主题',
'locale': '语言',
'about': '关于',
'homepage': '主页',
'version': '版本',
'protocol': '通讯协议',
'port': '端口',
'apiToken': '接口令牌',
'notSet': '未设置',
'set': '已设置',
'effectAfterRestart': '此配置项将在重启应用后生效',
'startAll': '全部开始',
'pauseAll': '全部暂停',
'deleteTask': '删除任务',
'deleteTaskTip': '保留已下载的文件',
'delete': '删除',
'newVersionTitle': '发现新版本 @version',
'newVersionUpdate': '立即更新',
'newVersionLater': '稍后再说',
'extensions': '扩展',
'extensionInstallUrl': '安装链接',
'extensionInstallSuccess': '安装成功',
'extensionUpdateSuccess': '更新成功',
'extensionDelete': '删除扩展',
'extensionAlreadyLatest': '已经是最新版本',
'extensionFind': '获取扩展',
'extensionDevelop': '开发扩展',
'history': '历史记录',
'clearHistory': '清空历史记录',
'noHistoryFound': '暂无历史记录',
'serviceTitle': '下载服务',
'serviceText': '运行中',
'network': '网络',
'proxy': '代理',
'username': '用户名',
'password': '密码',
}
};
+83
View File
@@ -0,0 +1,83 @@
const zhTW = {
"zh_TW": {
'label': '中文 (繁體) ',
'error': '錯誤',
'tip': '提示',
'confirm': '確定',
'cancel': '取消',
'selectAll': '全選',
'task': '任務',
'downloading': '下載中',
'downloaded': '已完成',
'setting': '設定',
'donate': '斗內',
'exit': '離開',
'create': '新增任務',
'directDownload': '直接下載',
'advancedOptions': '進階選項',
'downloadLink': '下載鏈接',
'downloadLinkValid': '請輸入下載鏈接',
'downloadLinkHit': '請輸入下載鏈接,支援 HTTP/HTTPS/MAGNET@append',
'downloadLinkHitDesktop': ',也可以通過拖拽Torrent文件至此以新增任務',
'download': '下載',
'noFileSelected': '沒有選取的文件',
'noStoragePermission': '沒有存儲權限',
'selectFile': '選擇文件',
'rename': '重命名',
'basic': '基礎',
'advanced': '進階',
'general': '通用設定',
'downloadDir': '下載目録',
'downloadDirValid': '請選擇下載目録',
'connections': '同時連接數',
'maxRunning': '最大同時下載數',
'items': '@count 項',
'subscribeTracker': '已訂閲的追蹤者列表',
'subscribeFail': '訂閲失敗',
'update': '更新',
'updateDaily': '每天自動更新',
'lastUpdate': '上次更新時間: @time',
'addTracker': '增加新的追蹤者',
'addTrackerHit': '請輸入追蹤者服務器地址,每行一條',
'ui': '介面',
'theme': '佈景主題',
'themeSystem': '跟隨系統',
'themeLight': '亮色主題',
'themeDark': '深色主題',
'locale': '語言',
'about': '關於',
'homepage': '訪問主頁',
'version': '版本資訊',
'protocol': '端口設定',
'port': '端口',
'apiToken': '接口秘鑰',
'notSet': '未設定',
'set': '已設定',
'effectAfterRestart': '重啟後生效',
'startAll': '全部開始',
'pauseAll': '全部暫停',
'deleteTask': '刪除任務',
'deleteTaskTip': '保留已經下載的文件',
'delete': '刪除',
'newVersionTitle': '現新版本 @version 已發佈',
'newVersionUpdate': '下載',
'newVersionLater': '取消',
'extensions': '擴充程序',
'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得擴充程序',
'extensionDevelop': '開發擴充程序',
'history': '歷史記錄',
'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務',
'serviceText': '執行中',
'network': '網路',
'proxy': '代理',
'username': '名稱',
'password': '密碼',
}
};
+23
View File
@@ -0,0 +1,23 @@
import 'package:get/get.dart';
import 'package:gopeed/i18n/langs/fa_ir.dart';
import 'package:gopeed/i18n/langs/ja_jp.dart';
import 'package:gopeed/i18n/langs/zh_tw.dart';
import 'langs/en_us.dart';
import 'langs/ru_ru.dart';
import 'langs/zh_cn.dart';
final messages = _Messages();
class _Messages extends Translations {
// just include available locales here
@override
Map<String, Map<String, String>> get keys => {
...zhCN,
...enUS,
...ruRU,
...zhTW,
...faIR,
...jaJP,
};
}
+5 -7
View File
@@ -7,7 +7,7 @@ import 'api/api.dart' as api;
import 'app/modules/app/controllers/app_controller.dart';
import 'app/modules/app/views/app_view.dart';
import 'core/libgopeed_boot.dart';
import 'generated/locales.g.dart';
import 'i18n/message.dart';
import 'util/locale_manager.dart';
import 'util/log_util.dart';
import 'util/mac_secure_util.dart';
@@ -72,12 +72,10 @@ Future<void> onStart() async {
// if is debug mode, check language message is complete,change debug locale to your comfortable language if you want
if (kDebugMode) {
final mainLang = getLocaleKey(debugLocale);
final fullMessages = AppTranslation.translations[mainLang];
AppTranslation.translations.keys
.where((e) => e != mainLang)
.forEach((lang) {
final langMessages = AppTranslation.translations[lang];
final debugLang = getLocaleKey(debugLocale);
final fullMessages = messages.keys[debugLang];
messages.keys.keys.where((e) => e != debugLang).forEach((lang) {
final langMessages = messages.keys[lang];
if (langMessages == null) {
logger.w("missing language: $lang");
return;
+23 -31
View File
@@ -181,10 +181,10 @@ packages:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.18.0"
version: "1.17.1"
context_menus:
dependency: "direct main"
description:
@@ -441,10 +441,10 @@ packages:
dependency: transitive
description:
name: intl
sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.18.1"
version: "0.18.0"
io:
dependency: transitive
description:
@@ -537,18 +537,18 @@ packages:
dependency: transitive
description:
name: matcher
sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.12.16"
version: "0.12.15"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.5.0"
version: "0.2.0"
math_expressions:
dependency: transitive
description:
@@ -569,10 +569,10 @@ packages:
dependency: transitive
description:
name: meta
sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3"
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
version: "1.9.1"
mime:
dependency: transitive
description:
@@ -581,14 +581,14 @@ packages:
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.0.4"
open_file:
open_filex:
dependency: "direct main"
description:
name: open_file
sha256: a5a32d44acb7c899987d0999e1e3cbb0a0f1adebbf41ac813ec6d2d8faa0af20
name: open_filex
sha256: a6c95237767c5647e68b71a476602fcf4f1bfc530c126265e53addae22ef5fc2
url: "https://pub.flutter-io.cn"
source: hosted
version: "3.3.2"
version: "4.3.4"
package_config:
dependency: transitive
description:
@@ -894,10 +894,10 @@ packages:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.10.0"
version: "1.9.1"
sprintf:
dependency: transitive
description:
@@ -910,18 +910,18 @@ packages:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5
url: "https://pub.flutter-io.cn"
source: hosted
version: "1.11.1"
version: "1.11.0"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8"
url: "https://pub.flutter-io.cn"
source: hosted
version: "2.1.2"
version: "2.1.1"
stream_transform:
dependency: transitive
description:
@@ -958,10 +958,10 @@ packages:
dependency: transitive
description:
name: test_api
sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.6.1"
version: "0.5.1"
timing:
dependency: transitive
description:
@@ -1083,14 +1083,6 @@ 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:
@@ -1148,5 +1140,5 @@ packages:
source: hosted
version: "2.1.1"
sdks:
dart: ">=3.2.0-194.0.dev <4.0.0"
dart: ">=3.0.0 <4.0.0"
flutter: ">=3.10.0"
+2 -2
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.5.0+1
version: 1.5.3+1
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -62,10 +62,10 @@ dependencies:
ref: fix/libayatana-set-icon-deprecation
autoscale_tabbarview: ^1.0.2
share_plus: ^7.1.0
open_file: ^3.3.2
flutter_form_builder: ^9.1.1
form_builder_validators: ^9.0.0
flutter_foreground_task: ^6.1.2
open_filex: ^4.3.4
dev_dependencies:
flutter_test:
sdk: flutter