mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-11 10:54:08 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c2b81caf7 | ||
|
|
2221332243 | ||
|
|
dc407434d0 | ||
|
|
1c2e86a991 | ||
|
|
c78dc5f18f | ||
|
|
8ea5abb5c3 | ||
|
|
d1a4067dfa | ||
|
|
a1928d202f | ||
|
|
7376e85895 | ||
|
|
eb79dd5ac5 | ||
|
|
5b945b16c1 | ||
|
|
80e60f4190 | ||
|
|
3087796ef5 | ||
|
|
99de0aaf9b | ||
|
|
6bea61cf0a | ||
|
|
6c3553805e | ||
|
|
adc7c7766d | ||
|
|
15d0296307 | ||
|
|
32d2e46118 | ||
|
|
abe3cec13d |
No files matched your search
@@ -5,7 +5,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.19
|
GO_VERSION: 1.19
|
||||||
FLUTTER_VERSION: 3.7.12
|
FLUTTER_VERSION: 3.10.6
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: 1.19
|
GO_VERSION: 1.19
|
||||||
FLUTTER_VERSION: 3.7.12
|
FLUTTER_VERSION: 3.10.6
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# lint:
|
# lint:
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
name: 'translator'
|
||||||
|
|
||||||
|
on:
|
||||||
|
issues:
|
||||||
|
types: [opened, edited]
|
||||||
|
issue_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
discussion:
|
||||||
|
types: [created, edited]
|
||||||
|
discussion_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
pull_request_target:
|
||||||
|
types: [opened, edited]
|
||||||
|
pull_request_review_comment:
|
||||||
|
types: [created, edited]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
translate:
|
||||||
|
permissions:
|
||||||
|
issues: write
|
||||||
|
discussions: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: lizheming/[email protected]
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
with:
|
||||||
|
IS_MODIFY_TITLE: true
|
||||||
|
APPEND_TRANSLATION: true
|
||||||
@@ -21,6 +21,7 @@ bin/
|
|||||||
|
|
||||||
*.data
|
*.data
|
||||||
*.db
|
*.db
|
||||||
|
*.log
|
||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Gopeed コントリビューターガイド
|
||||||
|
|
||||||
|
まず最初に、Gopeed への貢献に興味を持っていただきありがとうございます。このガイドは、あなたが Gopeed の
|
||||||
|
開発に参加するための手助けとなるでしょう。
|
||||||
|
|
||||||
|
## ブランチの説明
|
||||||
|
|
||||||
|
このプロジェクトのメインブランチは `main` ブランチのみです。Gopeed の開発に参加したい場合は、
|
||||||
|
まずこのプロジェクトをフォークし、フォークしたプロジェクトで開発を行ってください。開発が完了したら、
|
||||||
|
このプロジェクトに PR を提出し、`main` ブランチにマージしてください。
|
||||||
|
|
||||||
|
## ローカル開発
|
||||||
|
|
||||||
|
開発およびデバッグはウェブ上で行うことを推奨する。まずバックエンドのサービスを起動し、
|
||||||
|
コマンドライン `go run cmd/api/main.go` で起動する。サービスのデフォルトポートは `9999` で、
|
||||||
|
次にフロントエンドの flutter プロジェクトを `debug` モードで起動して実行します。
|
||||||
|
|
||||||
|
## 翻訳
|
||||||
|
|
||||||
|
Gopeed の国際化ファイルは `ui/flutter/assets/locales` ディレクトリにあります。
|
||||||
|
このディレクトリに対応する言語ファイルを追加するだけでよいです。
|
||||||
|
|
||||||
|
|
||||||
|
ロケール編集後にロケールを生成:
|
||||||
|
|
||||||
|
|
||||||
|
```
|
||||||
|
get generate locales
|
||||||
|
```
|
||||||
|
|
||||||
|
翻訳については `en_us.dart` を参照してください。
|
||||||
|
|
||||||
|
## flutter での開発
|
||||||
|
|
||||||
|
コミットする前に `dart format ./ui/flutter` を実行し、コードを標準の dart フォーマットにしておくことを忘れないでください
|
||||||
|
|
||||||
|
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
-2
@@ -1,4 +1,4 @@
|
|||||||
FROM cirrusci/flutter:3.7.7 AS flutter
|
FROM instrumentisto/flutter:3.10.6 AS flutter
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
|
||||||
RUN flutter pub get
|
RUN flutter pub get
|
||||||
@@ -18,4 +18,4 @@ LABEL maintainer="monkeyWie"
|
|||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY --from=go /app/dist/gopeed ./
|
COPY --from=go /app/dist/gopeed ./
|
||||||
EXPOSE 9999
|
EXPOSE 9999
|
||||||
ENTRYPOINT ["./gopeed"]
|
ENTRYPOINT ["./gopeed"]
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
#### Start directly
|
#### Start directly
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
|
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Using Docker Compose
|
#### Using Docker Compose
|
||||||
@@ -57,8 +57,6 @@ docker-compose up -d
|
|||||||
|
|
||||||
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
When the docker container is running, you can access the web page through `http://localhost:9999`.
|
||||||
|
|
||||||
> Tip: Modify the download path to `/download` on the setting page to access the downloaded files on the host.
|
|
||||||
|
|
||||||
## Donate
|
## Donate
|
||||||
|
|
||||||
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
|
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
|
||||||
@@ -92,7 +90,7 @@ Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
|
|||||||
|
|
||||||
#### Desktop
|
#### Desktop
|
||||||
|
|
||||||
First, you need to configure the environment according to the [flutter desktop official website document](https://docs.flutter.dev/development/platform-integration/desktop), and then you need to prepare the `cgo` environment, which can be searched for yourself.
|
First, you need to configure the environment according to the official [Flutter desktop website documention](https://docs.flutter.dev/development/platform-integration/desktop), then you will need to ensure the cgo environment is set up accordingly. For detailed instructions on setting up the cgo environment, please refer to relevant resources available online.
|
||||||
|
|
||||||
command:
|
command:
|
||||||
|
|
||||||
|
|||||||
+169
@@ -0,0 +1,169 @@
|
|||||||
|
[](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
|
||||||
|
[](https://codecov.io/gh/GopeedLab/gopeed)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/releases)
|
||||||
|
[](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
|
||||||
|
[](https://discord.gg/ZUJqJrwCGB)
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||||
|
|
||||||
|
## はじめに
|
||||||
|
|
||||||
|
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。
|
||||||
|
|
||||||
|
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
|
||||||
|
|
||||||
|
## インストール
|
||||||
|
|
||||||
|
**サポートするプラットフォーム**
|
||||||
|
|
||||||
|
- [x] windows
|
||||||
|
- [x] macos
|
||||||
|
- [x] linux
|
||||||
|
- [x] android
|
||||||
|
- [ ] ios
|
||||||
|
- [x] web
|
||||||
|
- [x] docker
|
||||||
|
|
||||||
|
[ダウンロード](https://github.com/GopeedLab/gopeed/releases/latest)
|
||||||
|
|
||||||
|
> ヒント: macos を開くのに失敗した場合は、ターミナルで `xattr -d com.apple.quarantine /Applications/Gopeed.app` コマンドを実行してください
|
||||||
|
|
||||||
|
### コマンドツール
|
||||||
|
|
||||||
|
`go install` を使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker
|
||||||
|
|
||||||
|
#### 直接始める
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Docker Compose の使用
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker-compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Gopeed へのアクセス
|
||||||
|
|
||||||
|
docker コンテナが起動しているときは、`http://localhost:9999` からウェブページにアクセスできます。
|
||||||
|
|
||||||
|
## 寄付
|
||||||
|
|
||||||
|
もしこのプロジェクトがお気に召しましたら、このプロジェクトの発展を支援するために[寄付](/.donate/index.md#donate)をご検討ください!
|
||||||
|
|
||||||
|
## ショーケース
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 開発
|
||||||
|
|
||||||
|
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
|
||||||
|
|
||||||
|
> フロントコードは `ui/flutter` ディレクトリにあります。
|
||||||
|
|
||||||
|
### 環境
|
||||||
|
|
||||||
|
1. Go 言語 1.19+
|
||||||
|
2. Flutter 3.7+
|
||||||
|
|
||||||
|
### クローン
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone [email protected]:GopeedLab/gopeed.git
|
||||||
|
```
|
||||||
|
|
||||||
|
### コントリビュート
|
||||||
|
|
||||||
|
[CONTRIBUTING.md](/CONTRIBUTING_ja-JP.md) をご参照ください
|
||||||
|
|
||||||
|
### ビルド
|
||||||
|
|
||||||
|
#### デスクトップ
|
||||||
|
|
||||||
|
まず、[flutter デスクトップ公式サイトドキュメント](https://docs.flutter.dev/development/platform-integration/desktop)に従って環境を設定し、自分で検索できる `cgo` 環境を用意します。
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
- windows
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build windows
|
||||||
|
```
|
||||||
|
|
||||||
|
- macos
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build macos
|
||||||
|
```
|
||||||
|
|
||||||
|
- linux
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build linux
|
||||||
|
```
|
||||||
|
|
||||||
|
#### モバイル
|
||||||
|
|
||||||
|
先ほどと同じように、`cgo` 環境を準備し、`gomobile` をインストールする必要があります:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go install golang.org/x/mobile/cmd/gomobile@latest
|
||||||
|
go get golang.org/x/mobile/bind
|
||||||
|
gomobile init
|
||||||
|
```
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
- android
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build apk
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Web
|
||||||
|
|
||||||
|
コマンド:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ui/flutter
|
||||||
|
flutter build web
|
||||||
|
cd ../../
|
||||||
|
rm -rf cmd/web/dist
|
||||||
|
cp -r ui/flutter/build/web cmd/web/dist
|
||||||
|
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
|
||||||
|
go run cmd/web/main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
## 感謝
|
||||||
|
|
||||||
|
### コントリビューター
|
||||||
|
|
||||||
|
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
|
||||||
|
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
### JetBrains
|
||||||
|
|
||||||
|
[](https://www.jetbrains.com/?from=gopeed)
|
||||||
|
|
||||||
|
## ライセンス
|
||||||
|
|
||||||
|
[GPLv3](LICENSE)
|
||||||
+2
-3
@@ -7,7 +7,7 @@
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
[English](/README.md) | [中文](/README_zh-CN.md)
|
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
|
||||||
|
|
||||||
## 介绍
|
## 介绍
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
|
|||||||
#### 直接运行
|
#### 直接运行
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -d -p 9999:9999 -v /path/to/download:/download liwei2633/gopeed
|
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
|
||||||
```
|
```
|
||||||
|
|
||||||
#### 使用 Docker Compose
|
#### 使用 Docker Compose
|
||||||
@@ -56,7 +56,6 @@ docker-compose up -d
|
|||||||
#### 访问服务
|
#### 访问服务
|
||||||
|
|
||||||
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
|
||||||
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
|
|
||||||
|
|
||||||
## 打赏
|
## 打赏
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -7,5 +7,5 @@ services:
|
|||||||
- 9999:9999 # HTTP port (host:container)
|
- 9999:9999 # HTTP port (host:container)
|
||||||
image: liwei2633/gopeed
|
image: liwei2633/gopeed
|
||||||
volumes:
|
volumes:
|
||||||
- ~/Downloads:/download # replace with your prefer path
|
- ~/Downloads:/root/Downloads # replace with your prefer path
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
@@ -3,6 +3,7 @@ module github.com/GopeedLab/gopeed
|
|||||||
go 1.19
|
go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
|
||||||
github.com/anacrolix/torrent v1.52.3
|
github.com/anacrolix/torrent v1.52.3
|
||||||
github.com/gorilla/handlers v1.5.1
|
github.com/gorilla/handlers v1.5.1
|
||||||
github.com/gorilla/mux v1.8.0
|
github.com/gorilla/mux v1.8.0
|
||||||
@@ -12,7 +13,7 @@ require (
|
|||||||
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
|
||||||
)
|
)
|
||||||
|
|
||||||
replace github.com/anacrolix/torrent => github.com/monkeyWie/torrent v1.52.4-alpha
|
replace github.com/anacrolix/torrent => github.com/monkeyWie/torrent v1.52.5-alpha
|
||||||
|
|
||||||
require (
|
require (
|
||||||
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
|
||||||
@@ -27,7 +28,6 @@ require (
|
|||||||
github.com/anacrolix/log v0.14.0 // indirect
|
github.com/anacrolix/log v0.14.0 // indirect
|
||||||
github.com/anacrolix/missinggo v1.3.0 // indirect
|
github.com/anacrolix/missinggo v1.3.0 // indirect
|
||||||
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
github.com/anacrolix/missinggo/perf v1.0.0 // indirect
|
||||||
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 // indirect
|
|
||||||
github.com/anacrolix/mmsg v1.0.0 // indirect
|
github.com/anacrolix/mmsg v1.0.0 // indirect
|
||||||
github.com/anacrolix/multiless v0.3.0 // indirect
|
github.com/anacrolix/multiless v0.3.0 // indirect
|
||||||
github.com/anacrolix/stm v0.4.0 // indirect
|
github.com/anacrolix/stm v0.4.0 // indirect
|
||||||
|
|||||||
@@ -210,8 +210,8 @@ github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJ
|
|||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||||
github.com/monkeyWie/torrent v1.52.4-alpha h1:iir8NMyLkv6X/D4skGUx8Htb8q57cPk9A6bYlRfrrqA=
|
github.com/monkeyWie/torrent v1.52.5-alpha h1:JnGTtfwrnrxaMdpzDgHbzfLEnXhkmldnk0iyka+2Ntw=
|
||||||
github.com/monkeyWie/torrent v1.52.4-alpha/go.mod h1:yBUcBuIOlttGEgAW/6gamzQGD+k2+wUh65fOPPzxvC4=
|
github.com/monkeyWie/torrent v1.52.5-alpha/go.mod h1:yBUcBuIOlttGEgAW/6gamzQGD+k2+wUh65fOPPzxvC4=
|
||||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
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 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/controller"
|
"github.com/GopeedLab/gopeed/internal/controller"
|
||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
"github.com/anacrolix/torrent"
|
"github.com/anacrolix/torrent"
|
||||||
"github.com/anacrolix/torrent/metainfo"
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
"github.com/anacrolix/torrent/storage"
|
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -15,7 +15,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var client *torrent.Client
|
var (
|
||||||
|
client *torrent.Client
|
||||||
|
lock sync.Mutex
|
||||||
|
torrentDirMap = make(map[string]string)
|
||||||
|
ftMap = make(map[string]*fileTorrentImpl)
|
||||||
|
)
|
||||||
|
|
||||||
type Fetcher struct {
|
type Fetcher struct {
|
||||||
ctl *controller.Controller
|
ctl *controller.Controller
|
||||||
@@ -23,10 +28,9 @@ type Fetcher struct {
|
|||||||
torrent *torrent.Torrent
|
torrent *torrent.Torrent
|
||||||
meta *fetcher.FetcherMeta
|
meta *fetcher.FetcherMeta
|
||||||
|
|
||||||
ready atomic.Bool
|
torrentReady atomic.Bool
|
||||||
progress fetcher.Progress
|
create atomic.Bool
|
||||||
|
progress fetcher.Progress
|
||||||
torrentPaths map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Name() string {
|
func (f *Fetcher) Name() string {
|
||||||
@@ -38,42 +42,48 @@ func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
|
|||||||
if f.meta == nil {
|
if f.meta == nil {
|
||||||
f.meta = &fetcher.FetcherMeta{}
|
f.meta = &fetcher.FetcherMeta{}
|
||||||
}
|
}
|
||||||
var once sync.Once
|
return
|
||||||
once.Do(func() {
|
}
|
||||||
cfg := torrent.NewDefaultClientConfig()
|
|
||||||
cfg.ListenPort = 0
|
|
||||||
|
|
||||||
// Support custom download path for each torrent
|
func (f *Fetcher) initClient() (err error) {
|
||||||
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
|
lock.Lock()
|
||||||
if err != nil {
|
defer lock.Unlock()
|
||||||
pieceCompletion = storage.NewMapPieceCompletion()
|
|
||||||
}
|
|
||||||
clientImplCloser := storage.NewFileOpts(storage.NewFileClientOpts{
|
|
||||||
ClientBaseDir: cfg.DataDir,
|
|
||||||
TorrentDirMaker: func(baseDir string, info *metainfo.Info, infoHash metainfo.Hash) string {
|
|
||||||
if dir, ok := f.torrentPaths[infoHash.String()]; ok {
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
return baseDir
|
|
||||||
},
|
|
||||||
PieceCompletion: pieceCompletion,
|
|
||||||
})
|
|
||||||
cfg.DefaultStorage = clientImplCloser
|
|
||||||
client, err = torrent.NewClient(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
f.torrentPaths = make(map[string]string)
|
if client != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := torrent.NewDefaultClientConfig()
|
||||||
|
cfg.ListenPort = 0
|
||||||
|
cfg.DefaultStorage = newFileOpts(newFileClientOpts{
|
||||||
|
ClientBaseDir: cfg.DataDir,
|
||||||
|
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
|
||||||
|
if dir, ok := torrentDirMap[infoHash.String()]; ok {
|
||||||
|
ft.setTorrentDir(dir)
|
||||||
|
}
|
||||||
|
ftMap[infoHash.String()] = ft
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
client, err = torrent.NewClient(cfg)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Resolve(req *base.Request) error {
|
func (f *Fetcher) Resolve(req *base.Request) error {
|
||||||
if err := f.addTorrent(req.URL); err != nil {
|
if err := base.ParseReqExtra[bt.ReqExtra](req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer f.torrent.Drop()
|
|
||||||
|
if err := f.addTorrent(req); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
// recycle unused torrent resource
|
||||||
|
time.Sleep(time.Minute * 3)
|
||||||
|
if !f.create.Load() {
|
||||||
|
f.torrentReady.Store(false)
|
||||||
|
f.safeDrop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
res := &base.Resource{
|
res := &base.Resource{
|
||||||
Name: f.torrent.Name(),
|
Name: f.torrent.Name(),
|
||||||
Range: true,
|
Range: true,
|
||||||
@@ -95,6 +105,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||||
|
f.create.Store(true)
|
||||||
f.meta.Opts = opts
|
f.meta.Opts = opts
|
||||||
if len(opts.SelectFiles) == 0 {
|
if len(opts.SelectFiles) == 0 {
|
||||||
opts.SelectFiles = make([]int, 0)
|
opts.SelectFiles = make([]int, 0)
|
||||||
@@ -103,17 +114,20 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if opts.Path != "" {
|
if opts.Path != "" {
|
||||||
f.torrentPaths[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
|
torrentDirMap[f.meta.Res.Hash] = path.Join(f.meta.Opts.Path, f.meta.Res.RootDir)
|
||||||
|
if ft, ok := ftMap[f.meta.Res.Hash]; ok {
|
||||||
|
// reuse resolve fetcher
|
||||||
|
ft.setTorrentDir(torrentDirMap[f.meta.Res.Hash])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
f.ready.Store(false)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Start() (err error) {
|
func (f *Fetcher) Start() (err error) {
|
||||||
if !f.ready.Load() {
|
if !f.torrentReady.Load() {
|
||||||
if err = f.addTorrent(f.meta.Req.URL); err != nil {
|
if err = f.addTorrent(f.meta.Req); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -130,8 +144,8 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Pause() (err error) {
|
func (f *Fetcher) Pause() (err error) {
|
||||||
f.ready.Store(false)
|
f.torrentReady.Store(false)
|
||||||
f.torrent.Drop()
|
f.safeDrop()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,13 +154,22 @@ func (f *Fetcher) Continue() (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Close() (err error) {
|
func (f *Fetcher) Close() (err error) {
|
||||||
f.torrent.Drop()
|
f.safeDrop()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *Fetcher) safeDrop() {
|
||||||
|
defer func() {
|
||||||
|
// ignore panic
|
||||||
|
_ = recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
f.torrent.Drop()
|
||||||
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
for {
|
for {
|
||||||
if f.ready.Load() {
|
if f.torrentReady.Load() {
|
||||||
done := true
|
done := true
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
@@ -182,7 +205,7 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) Progress() fetcher.Progress {
|
func (f *Fetcher) Progress() fetcher.Progress {
|
||||||
if !f.ready.Load() {
|
if !f.torrentReady.Load() {
|
||||||
return f.progress
|
return f.progress
|
||||||
}
|
}
|
||||||
for i := range f.progress {
|
for i := range f.progress {
|
||||||
@@ -193,12 +216,15 @@ func (f *Fetcher) Progress() fetcher.Progress {
|
|||||||
return f.progress
|
return f.progress
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *Fetcher) addTorrent(url string) (err error) {
|
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
|
||||||
schema := util.ParseSchema(url)
|
if err = f.initClient(); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
schema := util.ParseSchema(req.URL)
|
||||||
if schema == "MAGNET" {
|
if schema == "MAGNET" {
|
||||||
f.torrent, err = client.AddMagnet(url)
|
f.torrent, err = client.AddMagnet(req.URL)
|
||||||
} else {
|
} else {
|
||||||
f.torrent, err = client.AddTorrentFromFile(url)
|
f.torrent, err = client.AddTorrentFromFile(req.URL)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@@ -208,15 +234,31 @@ func (f *Fetcher) addTorrent(url string) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// use map to deduplicate
|
||||||
|
trackers := make(map[string]bool)
|
||||||
|
if req.Extra != nil {
|
||||||
|
extra := req.Extra.(*bt.ReqExtra)
|
||||||
|
if len(extra.Trackers) > 0 {
|
||||||
|
for _, tracker := range extra.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if exist && len(cfg.Trackers) > 0 {
|
if exist && len(cfg.Trackers) > 0 {
|
||||||
announceList := make([][]string, 0)
|
|
||||||
for _, tracker := range cfg.Trackers {
|
for _, tracker := range cfg.Trackers {
|
||||||
|
trackers[tracker] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(trackers) > 0 {
|
||||||
|
announceList := make([][]string, 0)
|
||||||
|
for tracker := range trackers {
|
||||||
announceList = append(announceList, []string{tracker})
|
announceList = append(announceList, []string{tracker})
|
||||||
}
|
}
|
||||||
f.torrent.AddTrackers(announceList)
|
f.torrent.AddTrackers(announceList)
|
||||||
}
|
}
|
||||||
<-f.torrent.GotInfo()
|
<-f.torrent.GotInfo()
|
||||||
f.ready.Store(true)
|
f.torrentReady.Store(true)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/internal/test"
|
"github.com/GopeedLab/gopeed/internal/test"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
|
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
@@ -21,6 +22,12 @@ func TestFetcher_Config(t *testing.T) {
|
|||||||
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
|
||||||
err := fetcher.Resolve(&base.Request{
|
err := fetcher.Resolve(&base.Request{
|
||||||
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
URL: "./testdata/ubuntu-22.04-live-server-amd64.iso.torrent",
|
||||||
|
Extra: bt.ReqExtra{
|
||||||
|
Trackers: []string{
|
||||||
|
"udp://tracker.birkenwald.de:6969/announce",
|
||||||
|
"udp://tracker.bitsearch.to:1337/announce",
|
||||||
|
},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anacrolix/missinggo/v2"
|
||||||
|
|
||||||
|
"github.com/anacrolix/torrent/common"
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/segments"
|
||||||
|
)
|
||||||
|
|
||||||
|
// File-based storage for torrents, that isn't yet bound to a particular torrent.
|
||||||
|
type fileClientImpl struct {
|
||||||
|
opts newFileClientOpts
|
||||||
|
}
|
||||||
|
|
||||||
|
type newFileClientOpts struct {
|
||||||
|
// The base directory for all downloads.
|
||||||
|
ClientBaseDir string
|
||||||
|
HandleFileTorrent func(infoHash metainfo.Hash, ft *fileTorrentImpl)
|
||||||
|
FilePathMaker storage.FilePathMaker
|
||||||
|
PieceCompletion storage.PieceCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFileOpts(opts newFileClientOpts) storage.ClientImplCloser {
|
||||||
|
if opts.FilePathMaker == nil {
|
||||||
|
opts.FilePathMaker = func(opts storage.FilePathMakerOpts) string {
|
||||||
|
var parts []string
|
||||||
|
if opts.Info.Name != metainfo.NoName {
|
||||||
|
parts = append(parts, opts.Info.Name)
|
||||||
|
}
|
||||||
|
return filepath.Join(append(parts, opts.File.Path...)...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if opts.PieceCompletion == nil {
|
||||||
|
ret, err := storage.NewDefaultPieceCompletionForDir(opts.ClientBaseDir)
|
||||||
|
if err != nil {
|
||||||
|
ret = storage.NewMapPieceCompletion()
|
||||||
|
}
|
||||||
|
opts.PieceCompletion = ret
|
||||||
|
}
|
||||||
|
return fileClientImpl{opts}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fileClientImpl) Close() error {
|
||||||
|
return fs.opts.PieceCompletion.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs fileClientImpl) OpenTorrent(info *metainfo.Info, infoHash metainfo.Hash) (_ storage.TorrentImpl, err error) {
|
||||||
|
upvertedFiles := info.UpvertedFiles()
|
||||||
|
files := make([]file, 0, len(upvertedFiles))
|
||||||
|
for _, fileInfo := range upvertedFiles {
|
||||||
|
filePath := filepath.Join("", fs.opts.FilePathMaker(storage.FilePathMakerOpts{
|
||||||
|
Info: info,
|
||||||
|
File: &fileInfo,
|
||||||
|
}))
|
||||||
|
f := file{
|
||||||
|
rawPath: filePath,
|
||||||
|
path: filePath,
|
||||||
|
length: fileInfo.Length,
|
||||||
|
}
|
||||||
|
if f.length == 0 {
|
||||||
|
err = CreateNativeZeroLengthFile(f.path)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("creating zero length file: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = append(files, f)
|
||||||
|
}
|
||||||
|
t := &fileTorrentImpl{
|
||||||
|
files,
|
||||||
|
segments.NewIndex(common.LengthIterFromUpvertedFiles(upvertedFiles)),
|
||||||
|
infoHash,
|
||||||
|
fs.opts.PieceCompletion,
|
||||||
|
}
|
||||||
|
fs.opts.HandleFileTorrent(infoHash, t)
|
||||||
|
return storage.TorrentImpl{
|
||||||
|
Piece: t.Piece,
|
||||||
|
Close: t.Close,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type file struct {
|
||||||
|
// The safe, OS-local file path.
|
||||||
|
rawPath string
|
||||||
|
path string
|
||||||
|
length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileTorrentImpl struct {
|
||||||
|
files []file
|
||||||
|
segmentLocater segments.Index
|
||||||
|
infoHash metainfo.Hash
|
||||||
|
completion storage.PieceCompletion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) Piece(p metainfo.Piece) storage.PieceImpl {
|
||||||
|
// Create a view onto the file-based torrent storage.
|
||||||
|
_io := fileTorrentImplIO{fts}
|
||||||
|
// Return the appropriate segments of this.
|
||||||
|
return &filePieceImpl{
|
||||||
|
fts,
|
||||||
|
p,
|
||||||
|
missinggo.NewSectionWriter(_io, p.Offset(), p.Length()),
|
||||||
|
io.NewSectionReader(_io, p.Offset(), p.Length()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fts *fileTorrentImpl) setTorrentDir(dir string) {
|
||||||
|
for i, f := range fts.files {
|
||||||
|
fts.files[i].path = filepath.Join(dir, f.rawPath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A helper to create zero-length files which won't appear for file-orientated storage since no
|
||||||
|
// writes will ever occur to them (no torrent data is associated with a zero-length file). The
|
||||||
|
// caller should make sure the file name provided is safe/sanitized.
|
||||||
|
func CreateNativeZeroLengthFile(name string) error {
|
||||||
|
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||||
|
var f io.Closer
|
||||||
|
f, err := os.Create(name)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return f.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exposes file-based storage of a torrent, as one big ReadWriterAt.
|
||||||
|
type fileTorrentImplIO struct {
|
||||||
|
fts *fileTorrentImpl
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns EOF on short or missing file.
|
||||||
|
func (fst *fileTorrentImplIO) readFileAt(file file, b []byte, off int64) (n int, err error) {
|
||||||
|
f, err := os.Open(file.path)
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
// File missing is treated the same as a short file.
|
||||||
|
err = io.EOF
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
// Limit the read to within the expected bounds of this file.
|
||||||
|
if int64(len(b)) > file.length-off {
|
||||||
|
b = b[:file.length-off]
|
||||||
|
}
|
||||||
|
for off < file.length && len(b) != 0 {
|
||||||
|
n1, err1 := f.ReadAt(b, off)
|
||||||
|
b = b[n1:]
|
||||||
|
n += n1
|
||||||
|
off += int64(n1)
|
||||||
|
if n1 == 0 {
|
||||||
|
err = err1
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only returns EOF at the end of the torrent. Premature EOF is ErrUnexpectedEOF.
|
||||||
|
func (fst fileTorrentImplIO) ReadAt(b []byte, off int64) (n int, err error) {
|
||||||
|
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(b))}, func(i int, e segments.Extent) bool {
|
||||||
|
n1, err1 := fst.readFileAt(fst.fts.files[i], b[:e.Length], e.Start)
|
||||||
|
n += n1
|
||||||
|
b = b[n1:]
|
||||||
|
err = err1
|
||||||
|
return err == nil // && int64(n1) == e.Length
|
||||||
|
})
|
||||||
|
if len(b) != 0 && err == nil {
|
||||||
|
err = io.EOF
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fst fileTorrentImplIO) WriteAt(p []byte, off int64) (n int, err error) {
|
||||||
|
// log.Printf("write at %v: %v bytes", off, len(p))
|
||||||
|
fst.fts.segmentLocater.Locate(segments.Extent{off, int64(len(p))}, func(i int, e segments.Extent) bool {
|
||||||
|
name := fst.fts.files[i].path
|
||||||
|
os.MkdirAll(filepath.Dir(name), 0o777)
|
||||||
|
var f *os.File
|
||||||
|
f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE, 0o666)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
var n1 int
|
||||||
|
n1, err = f.WriteAt(p[:e.Length], e.Start)
|
||||||
|
// log.Printf("%v %v wrote %v: %v", i, e, n1, err)
|
||||||
|
closeErr := f.Close()
|
||||||
|
n += n1
|
||||||
|
p = p[n1:]
|
||||||
|
if err == nil {
|
||||||
|
err = closeErr
|
||||||
|
}
|
||||||
|
if err == nil && int64(n1) != e.Length {
|
||||||
|
err = io.ErrShortWrite
|
||||||
|
}
|
||||||
|
return err == nil
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file_misc.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import "github.com/anacrolix/torrent/metainfo"
|
||||||
|
|
||||||
|
type requiredLength struct {
|
||||||
|
fileIndex int
|
||||||
|
length int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func extentCompleteRequiredLengths(info *metainfo.Info, off, n int64) (ret []requiredLength) {
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for i, fi := range info.UpvertedFiles() {
|
||||||
|
if off >= fi.Length {
|
||||||
|
off -= fi.Length
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n1 := n
|
||||||
|
if off+n1 > fi.Length {
|
||||||
|
n1 = fi.Length - off
|
||||||
|
}
|
||||||
|
ret = append(ret, requiredLength{
|
||||||
|
fileIndex: i,
|
||||||
|
length: off + n1,
|
||||||
|
})
|
||||||
|
n -= n1
|
||||||
|
if n == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
off = 0
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// modify from github.com/anacrolix/torrent/storage/file_piece.go
|
||||||
|
|
||||||
|
package bt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anacrolix/torrent/metainfo"
|
||||||
|
"github.com/anacrolix/torrent/storage"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type filePieceImpl struct {
|
||||||
|
*fileTorrentImpl
|
||||||
|
p metainfo.Piece
|
||||||
|
io.WriterAt
|
||||||
|
io.ReaderAt
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ storage.PieceImpl = (*filePieceImpl)(nil)
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) pieceKey() metainfo.PieceKey {
|
||||||
|
return metainfo.PieceKey{InfoHash: fs.infoHash, Index: fs.p.Index()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) Completion() storage.Completion {
|
||||||
|
c, err := fs.completion.Get(fs.pieceKey())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("error getting piece completion: %s", err)
|
||||||
|
c.Ok = false
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
verified := true
|
||||||
|
if c.Complete {
|
||||||
|
// If it's allegedly complete, check that its constituent files have the necessary length.
|
||||||
|
for _, fi := range extentCompleteRequiredLengths(fs.p.Info, fs.p.Offset(), fs.p.Length()) {
|
||||||
|
s, err := os.Stat(fs.files[fi.fileIndex].path)
|
||||||
|
if err != nil || s.Size() < fi.length {
|
||||||
|
verified = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !verified {
|
||||||
|
// The completion was wrong, fix it.
|
||||||
|
c.Complete = false
|
||||||
|
fs.completion.Set(fs.pieceKey(), false)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) MarkComplete() error {
|
||||||
|
return fs.completion.Set(fs.pieceKey(), true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (fs *filePieceImpl) MarkNotComplete() error {
|
||||||
|
return fs.completion.Set(fs.pieceKey(), false)
|
||||||
|
}
|
||||||
@@ -72,7 +72,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logPanic(cfg.StorageDir)
|
logPanic(filepath.Join(cfg.StorageDir, "logs"))
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,22 +640,12 @@ func (d *Downloader) doContinue(task *Task) (err error) {
|
|||||||
|
|
||||||
// redirect stderr to log file, when panic happened log it
|
// redirect stderr to log file, when panic happened log it
|
||||||
func logPanic(logDir string) {
|
func logPanic(logDir string) {
|
||||||
fp := filepath.Join(logDir, "crash.log")
|
if err := util.CreateDirIfNotExist(logDir); err != nil {
|
||||||
// if log file not exists, create it
|
return
|
||||||
var f *os.File
|
}
|
||||||
if _, err := os.Stat(fp); err != nil {
|
f, err := os.Create(filepath.Join(logDir, "crash.log"))
|
||||||
if !os.IsNotExist(err) {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
|
||||||
f, err = os.Create(fp)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
f, err = os.OpenFile(fp, os.O_APPEND|os.O_WRONLY, 0644)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
paniclog.RedirectStderr(f)
|
paniclog.RedirectStderr(f)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package bt
|
||||||
|
|
||||||
|
type ReqExtra struct {
|
||||||
|
Trackers []string `json:"trackers"`
|
||||||
|
}
|
||||||
+4
-13
@@ -3,7 +3,6 @@ package util
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
syspath "path"
|
syspath "path"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -83,17 +82,9 @@ func GetSingleDir(paths []string) string {
|
|||||||
return dir
|
return dir
|
||||||
}
|
}
|
||||||
|
|
||||||
// check directory is empty
|
func CreateDirIfNotExist(dir string) error {
|
||||||
func isEmpty(name string) (bool, error) {
|
if _, err := os.Stat(dir); os.IsNotExist(err) {
|
||||||
f, err := os.Open(name)
|
return os.MkdirAll(dir, 0o777)
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
}
|
||||||
defer f.Close()
|
return nil
|
||||||
|
|
||||||
_, err = f.Readdirnames(1) // Or f.Readdir(1)
|
|
||||||
if err == io.EOF {
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
return false, err // Either not empty or error, suits both cases
|
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,6 @@ subprojects {
|
|||||||
project.evaluationDependsOn(':app')
|
project.evaluationDependsOn(':app')
|
||||||
}
|
}
|
||||||
|
|
||||||
task clean(type: Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"exit": "Exit",
|
"exit": "Exit",
|
||||||
"create": "Create Task",
|
"create": "Create Task",
|
||||||
|
"advancedOptions": "Advanced Options",
|
||||||
"downloadLink": "Download Link",
|
"downloadLink": "Download Link",
|
||||||
"downloadLinkValid": "Please enter the download link",
|
"downloadLinkValid": "Please enter the download link",
|
||||||
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
|
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"noFileSelected": "Please select at least one file to continue.",
|
"noFileSelected": "Please select at least one file to continue.",
|
||||||
"noStoragePermission": "Storage permission required",
|
"noStoragePermission": "Storage permission required",
|
||||||
"selectFile": "Select File",
|
"selectFile": "Select File",
|
||||||
|
"rename": "Rename",
|
||||||
"basic": "Basic",
|
"basic": "Basic",
|
||||||
"advanced": "Advanced",
|
"advanced": "Advanced",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"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": "後で"
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"donate": "打赏",
|
"donate": "打赏",
|
||||||
"exit": "退出",
|
"exit": "退出",
|
||||||
"create": "创建任务",
|
"create": "创建任务",
|
||||||
|
"advancedOptions": "高级选项",
|
||||||
"downloadLink": "下载链接",
|
"downloadLink": "下载链接",
|
||||||
"downloadLinkValid": "请输入下载链接",
|
"downloadLinkValid": "请输入下载链接",
|
||||||
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
|
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
|
||||||
@@ -20,6 +21,7 @@
|
|||||||
"noFileSelected": "请至少选择一个文件下载",
|
"noFileSelected": "请至少选择一个文件下载",
|
||||||
"noStoragePermission": "需要开启存储权限",
|
"noStoragePermission": "需要开启存储权限",
|
||||||
"selectFile": "选择文件",
|
"selectFile": "选择文件",
|
||||||
|
"rename": "重命名",
|
||||||
"basic": "基础",
|
"basic": "基础",
|
||||||
"advanced": "高级",
|
"advanced": "高级",
|
||||||
"general": "通用",
|
"general": "通用",
|
||||||
|
|||||||
@@ -147,6 +147,8 @@ Future<Response<String>> proxyRequest<T>(String uri,
|
|||||||
options.headers!["X-Target-Uri"] = uri;
|
options.headers!["X-Target-Uri"] = uri;
|
||||||
|
|
||||||
// add timestamp to avoid cache
|
// add timestamp to avoid cache
|
||||||
return _client.dio.request("/api/v1/proxy?t=${DateTime.now()}",
|
return _client.dio.request(
|
||||||
data: data, options: options);
|
"/api/v1/proxy?t=${DateTime.now().millisecondsSinceEpoch}",
|
||||||
|
data: data,
|
||||||
|
options: options);
|
||||||
}
|
}
|
||||||
@@ -2,17 +2,18 @@ import 'package:json_annotation/json_annotation.dart';
|
|||||||
|
|
||||||
part 'options.g.dart';
|
part 'options.g.dart';
|
||||||
|
|
||||||
@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class Options {
|
class Options {
|
||||||
String name;
|
String name;
|
||||||
String path;
|
String path;
|
||||||
List<int> selectFiles;
|
List<int> selectFiles;
|
||||||
Map<String, dynamic>? extra;
|
Object? extra;
|
||||||
|
|
||||||
Options({
|
Options({
|
||||||
required this.name,
|
required this.name,
|
||||||
required this.path,
|
required this.path,
|
||||||
required this.selectFiles,
|
required this.selectFiles,
|
||||||
|
this.extra,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Options.fromJson(Map<String, dynamic> json) =>
|
factory Options.fromJson(Map<String, dynamic> json) =>
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
|
|||||||
path: json['path'] as String,
|
path: json['path'] as String,
|
||||||
selectFiles:
|
selectFiles:
|
||||||
(json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(),
|
(json['selectFiles'] as List<dynamic>).map((e) => e as int).toList(),
|
||||||
)..extra = json['extra'] as Map<String, dynamic>?;
|
extra: json['extra'],
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$OptionsToJson(Options instance) {
|
Map<String, dynamic> _$OptionsToJson(Options instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ part 'request.g.dart';
|
|||||||
@JsonSerializable(explicitToJson: true)
|
@JsonSerializable(explicitToJson: true)
|
||||||
class Request {
|
class Request {
|
||||||
String url;
|
String url;
|
||||||
Map<String, dynamic>? extra;
|
Object? extra;
|
||||||
|
|
||||||
Request({
|
Request({
|
||||||
required this.url,
|
required this.url,
|
||||||
|
this.extra,
|
||||||
});
|
});
|
||||||
|
|
||||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||||
@@ -20,7 +21,7 @@ class Request {
|
|||||||
@JsonSerializable()
|
@JsonSerializable()
|
||||||
class ReqExtraHttp {
|
class ReqExtraHttp {
|
||||||
String method = 'GET';
|
String method = 'GET';
|
||||||
Map<String, String> headers = {};
|
Map<String, String> header = {};
|
||||||
String body = '';
|
String body = '';
|
||||||
|
|
||||||
ReqExtraHttp();
|
ReqExtraHttp();
|
||||||
@@ -30,3 +31,15 @@ class ReqExtraHttp {
|
|||||||
|
|
||||||
Map<String, dynamic> toJson() => _$ReqExtraHttpToJson(this);
|
Map<String, dynamic> toJson() => _$ReqExtraHttpToJson(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonSerializable()
|
||||||
|
class ReqExtraBt {
|
||||||
|
List<String> trackers = [];
|
||||||
|
|
||||||
|
ReqExtraBt();
|
||||||
|
|
||||||
|
factory ReqExtraBt.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$ReqExtraBtFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$ReqExtraBtToJson(this);
|
||||||
|
}
|
||||||
@@ -8,7 +8,8 @@ part of 'request.dart';
|
|||||||
|
|
||||||
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
||||||
url: json['url'] as String,
|
url: json['url'] as String,
|
||||||
)..extra = json['extra'] as Map<String, dynamic>?;
|
extra: json['extra'],
|
||||||
|
);
|
||||||
|
|
||||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||||
final val = <String, dynamic>{
|
final val = <String, dynamic>{
|
||||||
@@ -27,12 +28,21 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
|||||||
|
|
||||||
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
|
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
|
||||||
..method = json['method'] as String
|
..method = json['method'] as String
|
||||||
..headers = Map<String, String>.from(json['headers'] as Map)
|
..header = Map<String, String>.from(json['header'] as Map)
|
||||||
..body = json['body'] as String;
|
..body = json['body'] as String;
|
||||||
|
|
||||||
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
|
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
|
||||||
<String, dynamic>{
|
<String, dynamic>{
|
||||||
'method': instance.method,
|
'method': instance.method,
|
||||||
'headers': instance.headers,
|
'header': instance.header,
|
||||||
'body': instance.body,
|
'body': instance.body,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
ReqExtraBt _$ReqExtraBtFromJson(Map<String, dynamic> json) => ReqExtraBt()
|
||||||
|
..trackers =
|
||||||
|
(json['trackers'] as List<dynamic>).map((e) => e as String).toList();
|
||||||
|
|
||||||
|
Map<String, dynamic> _$ReqExtraBtToJson(ReqExtraBt instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'trackers': instance.trackers,
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
import 'dart:ui' as ui;
|
import 'dart:ui' as ui;
|
||||||
|
import 'dart:ui';
|
||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
@@ -16,7 +17,7 @@ import '../../../../api/api.dart';
|
|||||||
import '../../../../api/model/downloader_config.dart';
|
import '../../../../api/model/downloader_config.dart';
|
||||||
import '../../../../core/common/start_config.dart';
|
import '../../../../core/common/start_config.dart';
|
||||||
import '../../../../generated/locales.g.dart';
|
import '../../../../generated/locales.g.dart';
|
||||||
import '../../../../util/localeManager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../../util/log_util.dart';
|
import '../../../../util/log_util.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
@@ -42,10 +43,13 @@ const allTrackerSubscribeUrls = [
|
|||||||
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
|
||||||
];
|
];
|
||||||
const allTrackerCdns = [
|
const allTrackerCdns = [
|
||||||
// jsdelivr: https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
|
||||||
["https://cdn.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
|
||||||
// nuaa: https://hub.nuaa.cf/ngosang/trackerslist/raw/master/trackers_all.txt
|
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
|
||||||
["https://hub.nuaa.cf", r".*github.com(/.*)"]
|
[
|
||||||
|
"https://ghproxy.com/https://raw.githubusercontent.com",
|
||||||
|
r".*github.com(/.*)/raw(/.*)"
|
||||||
|
]
|
||||||
];
|
];
|
||||||
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
|
||||||
key: (v) => v as String,
|
key: (v) => v as String,
|
||||||
@@ -265,18 +269,16 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
final btExtConfig = downloaderConfig.value.extra.bt;
|
final btExtConfig = downloaderConfig.value.extra.bt;
|
||||||
final result = <String>[];
|
final result = <String>[];
|
||||||
for (var u in btExtConfig.trackerSubscribeUrls) {
|
for (var u in btExtConfig.trackerSubscribeUrls) {
|
||||||
var allFail = true;
|
final cdns = allTrackerSubscribeUrlCdns[u];
|
||||||
for (var cdn in allTrackerSubscribeUrlCdns[u]!) {
|
if (cdns == null) {
|
||||||
try {
|
continue;
|
||||||
result.addAll(await _fetchTrackers(cdn));
|
|
||||||
allFail = false;
|
|
||||||
break;
|
|
||||||
} catch (e) {
|
|
||||||
logger.w("subscribe trackers fail, url: $cdn", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if (allFail) {
|
try {
|
||||||
throw Exception('subscribe trackers fail, network error');
|
final trackers =
|
||||||
|
await Util.anyOk(cdns.map((cdn) => _fetchTrackers(cdn)));
|
||||||
|
result.addAll(trackers);
|
||||||
|
} catch (e) {
|
||||||
|
logger.w("subscribe trackers fail, url: $u", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btExtConfig.subscribeTrackers.clear();
|
btExtConfig.subscribeTrackers.clear();
|
||||||
@@ -336,7 +338,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
extra.themeMode = ThemeMode.system.name;
|
extra.themeMode = ThemeMode.system.name;
|
||||||
}
|
}
|
||||||
if (extra.locale.isEmpty) {
|
if (extra.locale.isEmpty) {
|
||||||
final systemLocale = getLocaleKey(ui.window.locale);
|
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
|
||||||
extra.locale = AppTranslation.translations.containsKey(systemLocale)
|
extra.locale = AppTranslation.translations.containsKey(systemLocale)
|
||||||
? systemLocale
|
? systemLocale
|
||||||
: getLocaleKey(fallbackLocale);
|
: getLocaleKey(fallbackLocale);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:get/get.dart';
|
|||||||
|
|
||||||
import '../../../../generated/locales.g.dart';
|
import '../../../../generated/locales.g.dart';
|
||||||
import '../../../../theme/theme.dart';
|
import '../../../../theme/theme.dart';
|
||||||
import '../../../../util/localeManager.dart';
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
import '../controllers/app_controller.dart';
|
import '../controllers/app_controller.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,25 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
class CreateController extends GetxController {
|
class CreateController extends GetxController
|
||||||
|
with GetSingleTickerProviderStateMixin {
|
||||||
// final files = [].obs;
|
// final files = [].obs;
|
||||||
final RxList fileInfos = [].obs;
|
final RxList fileInfos = [].obs;
|
||||||
final RxList selectedIndexes = [].obs;
|
final RxList selectedIndexes = [].obs;
|
||||||
final RxList openedFolders = [].obs;
|
final RxList openedFolders = [].obs;
|
||||||
final isResolving = false.obs;
|
final isResolving = false.obs;
|
||||||
|
final showAdvanced = false.obs;
|
||||||
|
late TabController advancedTabController;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onInit() {
|
||||||
|
super.onInit();
|
||||||
|
advancedTabController = TabController(length: 2, vsync: this);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void onClose() {
|
||||||
|
advancedTabController.dispose();
|
||||||
|
super.onClose();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
|
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
|
||||||
import 'package:desktop_drop/desktop_drop.dart';
|
import 'package:desktop_drop/desktop_drop.dart';
|
||||||
import 'package:file_picker/file_picker.dart';
|
import 'package:file_picker/file_picker.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import '../../../../api/model/resolve_result.dart';
|
import '../../../../api/model/resolve_result.dart';
|
||||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||||
@@ -9,6 +11,7 @@ import '../../../../api/api.dart';
|
|||||||
import '../../../../api/model/create_task.dart';
|
import '../../../../api/model/create_task.dart';
|
||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
import '../../../../api/model/request.dart';
|
import '../../../../api/model/request.dart';
|
||||||
|
import '../../../../util/input_formatter.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
@@ -22,6 +25,10 @@ class CreateView extends GetView<CreateController> {
|
|||||||
|
|
||||||
final _urlController = TextEditingController();
|
final _urlController = TextEditingController();
|
||||||
final _confirmController = RoundedLoadingButtonController();
|
final _confirmController = RoundedLoadingButtonController();
|
||||||
|
final _httpUaController = TextEditingController();
|
||||||
|
final _httpCookieController = TextEditingController();
|
||||||
|
final _httpRefererController = TextEditingController();
|
||||||
|
final _btTrackerController = TextEditingController();
|
||||||
|
|
||||||
CreateView({Key? key}) : super(key: key);
|
CreateView({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@@ -48,68 +55,143 @@ class CreateView extends GetView<CreateController> {
|
|||||||
_urlController.text = details.files[0].path;
|
_urlController.text = details.files[0].path;
|
||||||
},
|
},
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
padding:
|
||||||
child: Form(
|
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||||
key: _resolveFormKey,
|
child: Form(
|
||||||
autovalidateMode: AutovalidateMode.always,
|
key: _resolveFormKey,
|
||||||
child: Column(
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
children: [
|
child: Column(children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
controller: _urlController,
|
controller: _urlController,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
maxLines: 30,
|
maxLines: 5,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
hintText: _hitText(),
|
hintText: _hitText(),
|
||||||
hintStyle: const TextStyle(fontSize: 12),
|
hintStyle: const TextStyle(fontSize: 12),
|
||||||
labelText: 'downloadLink'.tr,
|
labelText: 'downloadLink'.tr,
|
||||||
icon: const Icon(Icons.link),
|
icon: const Icon(Icons.link),
|
||||||
suffixIcon: IconButton(
|
suffixIcon: IconButton(
|
||||||
onPressed: _urlController.clear,
|
onPressed: _urlController.clear,
|
||||||
icon: const Icon(Icons.clear),
|
icon: const Icon(Icons.clear),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
validator: (v) {
|
||||||
|
return v!.trim().isNotEmpty
|
||||||
|
? null
|
||||||
|
: 'downloadLinkValid'.tr;
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
Util.isWeb()
|
||||||
|
? null
|
||||||
|
: IconButton(
|
||||||
|
icon: const Icon(Icons.folder_open),
|
||||||
|
onPressed: () async {
|
||||||
|
var pr = await FilePicker.platform.pickFiles(
|
||||||
|
type: FileType.custom,
|
||||||
|
allowedExtensions: ["torrent"]);
|
||||||
|
if (pr != null) {
|
||||||
|
_urlController.text = pr.files[0].path ?? "";
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
].where((e) => e != null).map((e) => e!).toList(),
|
||||||
|
),
|
||||||
|
Obx(() => Visibility(
|
||||||
|
visible: controller.showAdvanced.value,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 40, top: 15),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
TabBar(
|
||||||
|
controller: controller.advancedTabController,
|
||||||
|
tabs: const [
|
||||||
|
Tab(
|
||||||
|
text: 'HTTP',
|
||||||
|
),
|
||||||
|
Tab(
|
||||||
|
text: 'BitTorrent',
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
AutoScaleTabBarView(
|
||||||
|
controller: controller.advancedTabController,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _httpUaController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'User-Agent',
|
||||||
|
)),
|
||||||
|
TextFormField(
|
||||||
|
controller: _httpCookieController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Cookie',
|
||||||
|
)),
|
||||||
|
TextFormField(
|
||||||
|
controller: _httpRefererController,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Referer',
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
children: [
|
||||||
|
TextFormField(
|
||||||
|
controller: _btTrackerController,
|
||||||
|
maxLines: 5,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'Trakers',
|
||||||
|
hintText: 'addTrackerHit'.tr,
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
],
|
||||||
|
),
|
||||||
|
))),
|
||||||
|
Center(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 15),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 10),
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
controller.showAdvanced.value =
|
||||||
|
!controller.showAdvanced.value;
|
||||||
|
},
|
||||||
|
child:
|
||||||
|
Row(mainAxisSize: MainAxisSize.min, children: [
|
||||||
|
Obx(() => Checkbox(
|
||||||
|
value: controller.showAdvanced.value,
|
||||||
|
onChanged: (bool? value) {
|
||||||
|
controller.showAdvanced.value =
|
||||||
|
value ?? false;
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
Text('advancedOptions'.tr),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
validator: (v) {
|
),
|
||||||
return v!.trim().isNotEmpty
|
SizedBox(
|
||||||
? null
|
|
||||||
: 'downloadLinkValid'.tr;
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(Icons.folder_open),
|
|
||||||
onPressed: () async {
|
|
||||||
var pr = await FilePicker.platform.pickFiles(
|
|
||||||
type: FileType.custom,
|
|
||||||
allowedExtensions: ["torrent"]);
|
|
||||||
if (pr != null) {
|
|
||||||
_urlController.text = pr.files[0].path ?? "";
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Center(
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.all(15.0),
|
|
||||||
child: ConstrainedBox(
|
|
||||||
constraints: const BoxConstraints.tightFor(
|
|
||||||
width: 150,
|
width: 150,
|
||||||
height: 40,
|
child: RoundedLoadingButton(
|
||||||
|
color: Get.theme.colorScheme.secondary,
|
||||||
|
onPressed: _doResolve,
|
||||||
|
controller: _confirmController,
|
||||||
|
child: Text('confirm'.tr),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: RoundedLoadingButton(
|
],
|
||||||
color: Get.theme.colorScheme.secondary,
|
),
|
||||||
onPressed: _doResolve,
|
)),
|
||||||
controller: _confirmController,
|
]))),
|
||||||
child: Text('confirm'.tr),
|
|
||||||
),
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -122,8 +204,24 @@ class CreateView extends GetView<CreateController> {
|
|||||||
try {
|
try {
|
||||||
_confirmController.start();
|
_confirmController.start();
|
||||||
if (_resolveFormKey.currentState!.validate()) {
|
if (_resolveFormKey.currentState!.validate()) {
|
||||||
|
Object? extra;
|
||||||
|
if (controller.showAdvanced.value) {
|
||||||
|
final u = Uri.parse(_urlController.text);
|
||||||
|
if (u.scheme.startsWith("http")) {
|
||||||
|
extra = ReqExtraHttp()
|
||||||
|
..header = {
|
||||||
|
"User-Agent": _httpUaController.text,
|
||||||
|
"Cookie": _httpCookieController.text,
|
||||||
|
"Referer": _httpRefererController.text,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
extra = ReqExtraBt()
|
||||||
|
..trackers = Util.textToLines(_btTrackerController.text);
|
||||||
|
}
|
||||||
|
}
|
||||||
final rr = await resolve(Request(
|
final rr = await resolve(Request(
|
||||||
url: _urlController.text,
|
url: _urlController.text,
|
||||||
|
extra: extra,
|
||||||
));
|
));
|
||||||
await _showResolveDialog(rr);
|
await _showResolveDialog(rr);
|
||||||
}
|
}
|
||||||
@@ -146,6 +244,8 @@ class CreateView extends GetView<CreateController> {
|
|||||||
final appController = Get.find<AppController>();
|
final appController = Get.find<AppController>();
|
||||||
|
|
||||||
final createFormKey = GlobalKey<FormState>();
|
final createFormKey = GlobalKey<FormState>();
|
||||||
|
final nameController = TextEditingController();
|
||||||
|
final connectionsController = TextEditingController();
|
||||||
final pathController = TextEditingController(
|
final pathController = TextEditingController(
|
||||||
text: appController.downloaderConfig.value.downloadDir);
|
text: appController.downloaderConfig.value.downloadDir);
|
||||||
final downloadController = RoundedLoadingButtonController();
|
final downloadController = RoundedLoadingButtonController();
|
||||||
@@ -168,6 +268,23 @@ class CreateView extends GetView<CreateController> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Expanded(child: FileListView(files: files)),
|
Expanded(child: FileListView(files: files)),
|
||||||
|
TextFormField(
|
||||||
|
controller: nameController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'rename'.tr,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
TextFormField(
|
||||||
|
controller: connectionsController,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
labelText: 'connections'.tr,
|
||||||
|
),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
inputFormatters: [
|
||||||
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
|
NumericalRangeFormatter(min: 1, max: 256),
|
||||||
|
],
|
||||||
|
),
|
||||||
DirectorySelector(
|
DirectorySelector(
|
||||||
controller: pathController,
|
controller: pathController,
|
||||||
),
|
),
|
||||||
@@ -209,20 +326,18 @@ class CreateView extends GetView<CreateController> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (createFormKey.currentState!.validate()) {
|
if (createFormKey.currentState!.validate()) {
|
||||||
// if (Util.isAndroid()) {
|
|
||||||
// if (!await Permission.storage.request().isGranted) {
|
|
||||||
// Get.snackbar('error'.tr,
|
|
||||||
// 'noStoragePermission'.tr);
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
await createTask(CreateTask(
|
await createTask(CreateTask(
|
||||||
rid: rr.id,
|
rid: rr.id,
|
||||||
opts: Options(
|
opts: Options(
|
||||||
name: '',
|
name: nameController.text,
|
||||||
path: pathController.text,
|
path: pathController.text,
|
||||||
selectFiles: controller.selectedIndexes
|
selectFiles:
|
||||||
.cast<int>())));
|
controller.selectedIndexes.cast<int>(),
|
||||||
|
extra: connectionsController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: (OptsExtraHttp()
|
||||||
|
..connections = int.parse(
|
||||||
|
connectionsController.text)))));
|
||||||
Get.back();
|
Get.back();
|
||||||
Get.rootDelegate.offNamed(Routes.TASK);
|
Get.rootDelegate.offNamed(Routes.TASK);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,17 +28,21 @@ class SettingController extends GetxController {
|
|||||||
void fetchLatestVersion() async {
|
void fetchLatestVersion() async {
|
||||||
String? releaseDataStr;
|
String? releaseDataStr;
|
||||||
try {
|
try {
|
||||||
releaseDataStr =
|
|
||||||
(await proxyRequest("https://gopeed.com/api/release")).data;
|
|
||||||
} catch (e) {
|
|
||||||
releaseDataStr = (await proxyRequest(
|
releaseDataStr = (await proxyRequest(
|
||||||
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
|
||||||
.data;
|
.data;
|
||||||
|
} catch (e) {
|
||||||
|
releaseDataStr =
|
||||||
|
(await proxyRequest("https://gopeed.com/api/release")).data;
|
||||||
}
|
}
|
||||||
if (releaseDataStr == null) {
|
if (releaseDataStr == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final releaseData = jsonDecode(releaseDataStr);
|
final releaseData = jsonDecode(releaseDataStr);
|
||||||
|
final tagName = releaseData["tag_name"];
|
||||||
|
if (tagName == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
latestVersion.value = releaseData["tag_name"].substring(1);
|
latestVersion.value = releaseData["tag_name"].substring(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -9,7 +9,8 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../../../generated/locales.g.dart';
|
import '../../../../generated/locales.g.dart';
|
||||||
import '../../../../util/localeManager.dart';
|
import '../../../../util/input_formatter.dart';
|
||||||
|
import '../../../../util/locale_manager.dart';
|
||||||
import '../../../../util/message.dart';
|
import '../../../../util/message.dart';
|
||||||
import '../../../../util/package_info.dart';
|
import '../../../../util/package_info.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
@@ -35,7 +36,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
debounceSave({bool needRestart = false}) {
|
debounceSave({bool needRestart = false}) {
|
||||||
var completer = Completer<void>();
|
var completer = Completer<void>();
|
||||||
timer?.cancel();
|
timer?.cancel();
|
||||||
timer = Timer(const Duration(milliseconds: 500), () {
|
timer = Timer(const Duration(milliseconds: 1000), () {
|
||||||
appController
|
appController
|
||||||
.saveConfig()
|
.saveConfig()
|
||||||
.then(completer.complete)
|
.then(completer.complete)
|
||||||
@@ -89,7 +90,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
_NumericalRangeFormatter(min: 1, max: 256),
|
NumericalRangeFormatter(min: 1, max: 256),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -116,7 +117,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
_NumericalRangeFormatter(min: 1, max: 256),
|
NumericalRangeFormatter(min: 1, max: 256),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -187,7 +188,6 @@ class SettingView extends GetView<SettingController> {
|
|||||||
(Key key) {
|
(Key key) {
|
||||||
final trackersController = TextEditingController(
|
final trackersController = TextEditingController(
|
||||||
text: btExtConfig.customTrackers.join('\r\n').toString());
|
text: btExtConfig.customTrackers.join('\r\n').toString());
|
||||||
const ls = LineSplitter();
|
|
||||||
return TextField(
|
return TextField(
|
||||||
key: key,
|
key: key,
|
||||||
focusNode: FocusNode(),
|
focusNode: FocusNode(),
|
||||||
@@ -198,7 +198,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
hintText: 'addTrackerHit'.tr,
|
hintText: 'addTrackerHit'.tr,
|
||||||
),
|
),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
btExtConfig.customTrackers = ls.convert(value);
|
btExtConfig.customTrackers = Util.textToLines(value);
|
||||||
appController.refreshTrackers();
|
appController.refreshTrackers();
|
||||||
|
|
||||||
await debounceSave();
|
await debounceSave();
|
||||||
@@ -408,7 +408,7 @@ class SettingView extends GetView<SettingController> {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
inputFormatters: [
|
inputFormatters: [
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
FilteringTextInputFormatter.digitsOnly,
|
||||||
_NumericalRangeFormatter(min: 0, max: 65535),
|
NumericalRangeFormatter(min: 0, max: 65535),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -605,39 +605,3 @@ class SettingView extends GetView<SettingController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// class _ConfigItem {
|
|
||||||
// late String label;
|
|
||||||
// late String Function() text;
|
|
||||||
// late Widget Function(Key key) inputItem;
|
|
||||||
//
|
|
||||||
// _ConfigItem(this.label, this.text, this.inputItem);
|
|
||||||
// }
|
|
||||||
|
|
||||||
class _NumericalRangeFormatter extends TextInputFormatter {
|
|
||||||
final int min;
|
|
||||||
final int max;
|
|
||||||
|
|
||||||
_NumericalRangeFormatter({required this.min, required this.max});
|
|
||||||
|
|
||||||
@override
|
|
||||||
TextEditingValue formatEditUpdate(
|
|
||||||
TextEditingValue oldValue,
|
|
||||||
TextEditingValue newValue,
|
|
||||||
) {
|
|
||||||
if (newValue.text.isEmpty) {
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
var intVal = int.tryParse(newValue.text);
|
|
||||||
if (intVal == null) {
|
|
||||||
return oldValue;
|
|
||||||
}
|
|
||||||
if (intVal < min) {
|
|
||||||
return newValue.copyWith(text: min.toString());
|
|
||||||
} else if (intVal > max) {
|
|
||||||
return oldValue.copyWith(text: max.toString());
|
|
||||||
} else {
|
|
||||||
return newValue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/api/model/meta.dart';
|
||||||
|
import 'package:gopeed/api/model/resource.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import '../../api/api.dart';
|
import '../../api/api.dart';
|
||||||
@@ -190,7 +191,7 @@ class BuildTaskListView extends GetView {
|
|||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
ListTile(
|
ListTile(
|
||||||
// isThreeLine: true,
|
// isThreeLine: true,
|
||||||
title: Text(task.meta.res.name,
|
title: Text(fileName(task.meta),
|
||||||
maxLines: 2,
|
maxLines: 2,
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
style: Get.textTheme.titleSmall),
|
style: Get.textTheme.titleSmall),
|
||||||
@@ -199,9 +200,9 @@ class BuildTaskListView extends GetView {
|
|||||||
style: context.textTheme.bodyLarge
|
style: context.textTheme.bodyLarge
|
||||||
?.copyWith(color: Get.theme.disabledColor),
|
?.copyWith(color: Get.theme.disabledColor),
|
||||||
),
|
),
|
||||||
leading: task.meta.res.name.lastIndexOf('.') == -1
|
leading: (task.meta.res.rootDir.isNotEmpty
|
||||||
? const Icon(FaIcons.file)
|
? const Icon(FaIcons.folder)
|
||||||
: Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]),
|
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])),
|
||||||
|
|
||||||
trailing: SizedBox(
|
trailing: SizedBox(
|
||||||
width: 180,
|
width: 180,
|
||||||
@@ -220,4 +221,11 @@ class BuildTaskListView extends GetView {
|
|||||||
])
|
])
|
||||||
]));
|
]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String fileName(Meta meta) {
|
||||||
|
if (meta.res.files.length > 1) {
|
||||||
|
return meta.res.name;
|
||||||
|
}
|
||||||
|
return meta.opts.name.isEmpty ? meta.res.files[0].name : meta.opts.name;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,6 @@ LibgopeedBoot create() => LibgopeedBootNative();
|
|||||||
|
|
||||||
class LibgopeedBootNative implements LibgopeedBoot {
|
class LibgopeedBootNative implements LibgopeedBoot {
|
||||||
late LibgopeedInterface _libgopeed;
|
late LibgopeedInterface _libgopeed;
|
||||||
late SendPort _childSendPort;
|
|
||||||
|
|
||||||
LibgopeedBootNative() {
|
LibgopeedBootNative() {
|
||||||
if (!Util.isDesktop()) {
|
if (!Util.isDesktop()) {
|
||||||
@@ -44,11 +43,7 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> stop() async {
|
Future<void> stop() async {
|
||||||
if (Util.isDesktop()) {
|
await _libgopeed.stop();
|
||||||
await _ffiStop();
|
|
||||||
} else {
|
|
||||||
await _libgopeed.stop();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ffiInit() {
|
_ffiInit() {
|
||||||
@@ -67,90 +62,9 @@ class LibgopeedBootNative implements LibgopeedBoot {
|
|||||||
|
|
||||||
// FFI run in isolate
|
// FFI run in isolate
|
||||||
Future<int> _ffiStart(StartConfig cfg) async {
|
Future<int> _ffiStart(StartConfig cfg) async {
|
||||||
var startCompleter = Completer<int>();
|
return await Isolate.run(() async {
|
||||||
|
|
||||||
var mainReceive = ReceivePort();
|
|
||||||
var isolate = await Isolate.spawn((SendPort sendPort) {
|
|
||||||
_ffiInit();
|
_ffiInit();
|
||||||
var childReceive = ReceivePort();
|
return await _libgopeed.start(cfg);
|
||||||
sendPort.send(childReceive.sendPort);
|
|
||||||
|
|
||||||
childReceive.listen((message) async {
|
|
||||||
late _Result result;
|
|
||||||
try {
|
|
||||||
if (message is _StartParam) {
|
|
||||||
result = _StartResult();
|
|
||||||
(result as _StartResult).port = await _libgopeed.start(message.cfg);
|
|
||||||
}
|
|
||||||
if (message is _StopParam) {
|
|
||||||
result = _StopResult();
|
|
||||||
await _libgopeed.stop();
|
|
||||||
childReceive.close();
|
|
||||||
Isolate.exit(sendPort);
|
|
||||||
}
|
|
||||||
} on Exception catch (e) {
|
|
||||||
result.exception = e;
|
|
||||||
}
|
|
||||||
sendPort.send(result);
|
|
||||||
});
|
|
||||||
}, mainReceive.sendPort);
|
|
||||||
|
|
||||||
var childCompleter = Completer<void>();
|
|
||||||
mainReceive.listen((message) {
|
|
||||||
if (message is SendPort) {
|
|
||||||
_childSendPort = message;
|
|
||||||
childCompleter.complete();
|
|
||||||
}
|
|
||||||
if (message is _StartResult) {
|
|
||||||
if (message.exception != null) {
|
|
||||||
startCompleter.completeError(message.exception!);
|
|
||||||
} else {
|
|
||||||
startCompleter.complete(message.port!);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (message is _StopResult) {
|
|
||||||
isolate.kill();
|
|
||||||
if (message.exception != null) {
|
|
||||||
_stopCompleter.completeError(message.exception!);
|
|
||||||
} else {
|
|
||||||
_stopCompleter.complete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
await childCompleter.future;
|
|
||||||
_childSendPort.send(_StartParam(cfg: cfg));
|
|
||||||
return startCompleter.future;
|
|
||||||
}
|
|
||||||
|
|
||||||
late Completer<void> _stopCompleter;
|
|
||||||
|
|
||||||
Future<void> _ffiStop() async {
|
|
||||||
_stopCompleter = Completer<void>();
|
|
||||||
_childSendPort.send(_StopParam());
|
|
||||||
return _stopCompleter.future;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Result {
|
|
||||||
Exception? exception;
|
|
||||||
|
|
||||||
_Result();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StartParam {
|
|
||||||
final StartConfig cfg;
|
|
||||||
|
|
||||||
_StartParam({required this.cfg});
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StartResult extends _Result {
|
|
||||||
int? port;
|
|
||||||
|
|
||||||
_StartResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
class _StopParam {}
|
|
||||||
|
|
||||||
class _StopResult extends _Result {
|
|
||||||
_StopResult();
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -6,6 +6,7 @@ class AppTranslation {
|
|||||||
static Map<String, Map<String, String>> translations = {
|
static Map<String, Map<String, String>> translations = {
|
||||||
'en_US': Locales.en_US,
|
'en_US': Locales.en_US,
|
||||||
'fa_IR': Locales.fa_IR,
|
'fa_IR': Locales.fa_IR,
|
||||||
|
'ja_JP': Locales.ja_JP,
|
||||||
'ru_RU': Locales.ru_RU,
|
'ru_RU': Locales.ru_RU,
|
||||||
'zh_CN': Locales.zh_CN,
|
'zh_CN': Locales.zh_CN,
|
||||||
};
|
};
|
||||||
@@ -26,6 +27,7 @@ class LocaleKeys {
|
|||||||
static const donate = 'donate';
|
static const donate = 'donate';
|
||||||
static const exit = 'exit';
|
static const exit = 'exit';
|
||||||
static const create = 'create';
|
static const create = 'create';
|
||||||
|
static const advancedOptions = 'advancedOptions';
|
||||||
static const downloadLink = 'downloadLink';
|
static const downloadLink = 'downloadLink';
|
||||||
static const downloadLinkValid = 'downloadLinkValid';
|
static const downloadLinkValid = 'downloadLinkValid';
|
||||||
static const downloadLinkHit = 'downloadLinkHit';
|
static const downloadLinkHit = 'downloadLinkHit';
|
||||||
@@ -34,6 +36,7 @@ class LocaleKeys {
|
|||||||
static const noFileSelected = 'noFileSelected';
|
static const noFileSelected = 'noFileSelected';
|
||||||
static const noStoragePermission = 'noStoragePermission';
|
static const noStoragePermission = 'noStoragePermission';
|
||||||
static const selectFile = 'selectFile';
|
static const selectFile = 'selectFile';
|
||||||
|
static const rename = 'rename';
|
||||||
static const basic = 'basic';
|
static const basic = 'basic';
|
||||||
static const advanced = 'advanced';
|
static const advanced = 'advanced';
|
||||||
static const general = 'general';
|
static const general = 'general';
|
||||||
@@ -89,6 +92,7 @@ class Locales {
|
|||||||
'donate': 'Donate',
|
'donate': 'Donate',
|
||||||
'exit': 'Exit',
|
'exit': 'Exit',
|
||||||
'create': 'Create Task',
|
'create': 'Create Task',
|
||||||
|
'advancedOptions': 'Advanced Options',
|
||||||
'downloadLink': 'Download Link',
|
'downloadLink': 'Download Link',
|
||||||
'downloadLinkValid': 'Please enter the download link',
|
'downloadLinkValid': 'Please enter the download link',
|
||||||
'downloadLinkHit':
|
'downloadLinkHit':
|
||||||
@@ -98,6 +102,7 @@ class Locales {
|
|||||||
'noFileSelected': 'Please select at least one file to continue.',
|
'noFileSelected': 'Please select at least one file to continue.',
|
||||||
'noStoragePermission': 'Storage permission required',
|
'noStoragePermission': 'Storage permission required',
|
||||||
'selectFile': 'Select File',
|
'selectFile': 'Select File',
|
||||||
|
'rename': 'Rename',
|
||||||
'basic': 'Basic',
|
'basic': 'Basic',
|
||||||
'advanced': 'Advanced',
|
'advanced': 'Advanced',
|
||||||
'general': 'General',
|
'general': 'General',
|
||||||
@@ -197,6 +202,67 @@ class Locales {
|
|||||||
'newVersionUpdate': 'دریافت اکنون',
|
'newVersionUpdate': 'دریافت اکنون',
|
||||||
'newVersionLater': 'بعداً',
|
'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 = {
|
static const ru_RU = {
|
||||||
'label': 'Русский(Россия)',
|
'label': 'Русский(Россия)',
|
||||||
'error': 'Ошибка',
|
'error': 'Ошибка',
|
||||||
@@ -274,6 +340,7 @@ class Locales {
|
|||||||
'donate': '打赏',
|
'donate': '打赏',
|
||||||
'exit': '退出',
|
'exit': '退出',
|
||||||
'create': '创建任务',
|
'create': '创建任务',
|
||||||
|
'advancedOptions': '高级选项',
|
||||||
'downloadLink': '下载链接',
|
'downloadLink': '下载链接',
|
||||||
'downloadLinkValid': '请输入下载链接',
|
'downloadLinkValid': '请输入下载链接',
|
||||||
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
|
||||||
@@ -282,6 +349,7 @@ class Locales {
|
|||||||
'noFileSelected': '请至少选择一个文件下载',
|
'noFileSelected': '请至少选择一个文件下载',
|
||||||
'noStoragePermission': '需要开启存储权限',
|
'noStoragePermission': '需要开启存储权限',
|
||||||
'selectFile': '选择文件',
|
'selectFile': '选择文件',
|
||||||
|
'rename': '重命名',
|
||||||
'basic': '基础',
|
'basic': '基础',
|
||||||
'advanced': '高级',
|
'advanced': '高级',
|
||||||
'general': '通用',
|
'general': '通用',
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import 'app/modules/app/controllers/app_controller.dart';
|
|||||||
import 'app/modules/app/views/app_view.dart';
|
import 'app/modules/app/views/app_view.dart';
|
||||||
import 'core/libgopeed_boot.dart';
|
import 'core/libgopeed_boot.dart';
|
||||||
import 'generated/locales.g.dart';
|
import 'generated/locales.g.dart';
|
||||||
import 'util/localeManager.dart';
|
import 'util/locale_manager.dart';
|
||||||
import 'util/log_util.dart';
|
import 'util/log_util.dart';
|
||||||
import 'util/mac_secure_util.dart';
|
import 'util/mac_secure_util.dart';
|
||||||
import 'util/package_info.dart';
|
import 'util/package_info.dart';
|
||||||
@@ -26,7 +26,7 @@ Future<void> init() async {
|
|||||||
if (Util.isDesktop()) {
|
if (Util.isDesktop()) {
|
||||||
await windowManager.ensureInitialized();
|
await windowManager.ensureInitialized();
|
||||||
const windowOptions = WindowOptions(
|
const windowOptions = WindowOptions(
|
||||||
size: Size(1024, 768),
|
size: Size(800, 600),
|
||||||
center: true,
|
center: true,
|
||||||
skipTaskbar: false,
|
skipTaskbar: false,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
findIcon(String filename) {
|
findIcon(String filename) {
|
||||||
String res = 'doc';
|
String res = 'file';
|
||||||
String ext = filename.substring(filename.lastIndexOf('.') + 1);
|
String ext = filename.substring(filename.lastIndexOf('.') + 1);
|
||||||
for (var iconMap in iconMaps) {
|
for (var iconMap in iconMaps) {
|
||||||
if (iconMap['extensions'].contains(ext)) {
|
if (iconMap['extensions'].contains(ext)) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class FaIcons {
|
|||||||
|
|
||||||
static const _kFontFam = 'FontAwesome';
|
static const _kFontFam = 'FontAwesome';
|
||||||
static const Map<String, IconData> allIcons = {
|
static const Map<String, IconData> allIcons = {
|
||||||
|
'file': file,
|
||||||
'doc': doc,
|
'doc': doc,
|
||||||
'file_image': file_image,
|
'file_image': file_image,
|
||||||
'file_video': file_video,
|
'file_video': file_video,
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
/// A [TextInputFormatter] that restricts input to a numerical range between [min] and [max].
|
||||||
|
class NumericalRangeFormatter extends TextInputFormatter {
|
||||||
|
final int min;
|
||||||
|
final int max;
|
||||||
|
|
||||||
|
NumericalRangeFormatter({required this.min, required this.max});
|
||||||
|
|
||||||
|
@override
|
||||||
|
TextEditingValue formatEditUpdate(
|
||||||
|
TextEditingValue oldValue,
|
||||||
|
TextEditingValue newValue,
|
||||||
|
) {
|
||||||
|
if (newValue.text.isEmpty) {
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
var intVal = int.tryParse(newValue.text);
|
||||||
|
if (intVal == null) {
|
||||||
|
return oldValue;
|
||||||
|
}
|
||||||
|
if (intVal < min) {
|
||||||
|
return newValue.copyWith(text: min.toString());
|
||||||
|
} else if (intVal > max) {
|
||||||
|
return oldValue.copyWith(text: max.toString());
|
||||||
|
} else {
|
||||||
|
return newValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
@@ -1,19 +1,23 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:logger/logger.dart';
|
import 'package:logger/logger.dart';
|
||||||
|
|
||||||
import 'util.dart';
|
import 'util.dart';
|
||||||
|
|
||||||
final logger = Logger(
|
final logger = Logger(
|
||||||
printer: PrettyPrinter(),
|
filter: ProductionFilter(),
|
||||||
|
printer: LogfmtPrinter(),
|
||||||
output: buildOutput(),
|
output: buildOutput(),
|
||||||
);
|
);
|
||||||
|
|
||||||
buildOutput() {
|
buildOutput() {
|
||||||
// if is debug mode, output to console
|
// if is debug mode, don't log to file
|
||||||
if (!kDebugMode && Util.isDesktop()) {
|
if (!kDebugMode && Util.isDesktop()) {
|
||||||
return FileOutput(file: File('log.txt'));
|
const logDirPath = 'logs';
|
||||||
|
var logDir = Directory(logDirPath);
|
||||||
|
if (!logDir.existsSync()) {
|
||||||
|
logDir.createSync();
|
||||||
|
}
|
||||||
|
return FileOutput(file: File('$logDirPath/client.log'));
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ void showErrorMessage(msg) {
|
|||||||
Get.snackbar(title, ((msg as dynamic).message as Result).msg!);
|
Get.snackbar(title, ((msg as dynamic).message as Result).msg!);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Get.snackbar(title, msg);
|
Get.snackbar(title, msg.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
void showMessage(title, msg) {
|
void showMessage(title, msg) {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:convert';
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@@ -11,7 +13,9 @@ class Util {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static String fmtByte(int byte) {
|
static String fmtByte(int byte) {
|
||||||
if (byte < 1024) {
|
if (byte < 0) {
|
||||||
|
return "0 B";
|
||||||
|
} else if (byte < 1024) {
|
||||||
return "$byte B";
|
return "$byte B";
|
||||||
} else if (byte < 1024 * 1024) {
|
} else if (byte < 1024 * 1024) {
|
||||||
return "${(byte / 1024).toStringAsFixed(2)} KB";
|
return "${(byte / 1024).toStringAsFixed(2)} KB";
|
||||||
@@ -59,4 +63,33 @@ class Util {
|
|||||||
static isWeb() {
|
static isWeb() {
|
||||||
return kIsWeb;
|
return kIsWeb;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static List<String> textToLines(String text) {
|
||||||
|
if (text.isEmpty) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const ls = LineSplitter();
|
||||||
|
return ls.convert(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
// if one future complete, return the result, only all future error, return the last error
|
||||||
|
static anyOk<T>(Iterable<Future<T>> futures) {
|
||||||
|
final completer = Completer<T>();
|
||||||
|
Object lastError;
|
||||||
|
var count = futures.length;
|
||||||
|
for (var future in futures) {
|
||||||
|
future.then((value) {
|
||||||
|
if (!completer.isCompleted) {
|
||||||
|
completer.complete(value);
|
||||||
|
}
|
||||||
|
}).catchError((e) {
|
||||||
|
lastError = e;
|
||||||
|
count--;
|
||||||
|
if (count == 0) {
|
||||||
|
completer.completeError(lastError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+227
-212
File diff suppressed because it is too large.
Load diff
@@ -15,10 +15,10 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.3.4+1
|
version: 1.3.7+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=2.17.6 <3.0.0"
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
|
|
||||||
# Dependencies specify other packages that your package needs in order to work.
|
# Dependencies specify other packages that your package needs in order to work.
|
||||||
# To automatically upgrade your package dependencies to the latest versions
|
# To automatically upgrade your package dependencies to the latest versions
|
||||||
@@ -42,11 +42,11 @@ dependencies:
|
|||||||
json_annotation: ^4.6.0
|
json_annotation: ^4.6.0
|
||||||
dio: ^5.2.1
|
dio: ^5.2.1
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.0.11
|
||||||
file_picker: ^5.2.5
|
file_picker: 5.3.1
|
||||||
macos_secure_bookmarks: ^0.2.1
|
macos_secure_bookmarks: ^0.2.1
|
||||||
rounded_loading_button: ^2.1.0
|
rounded_loading_button: ^2.1.0
|
||||||
url_launcher: ^6.1.6
|
url_launcher: ^6.1.6
|
||||||
shared_preferences: ^2.0.15
|
shared_preferences: ^2.2.0
|
||||||
logger: ^1.3.0
|
logger: ^1.3.0
|
||||||
desktop_drop: ^0.3.3
|
desktop_drop: ^0.3.3
|
||||||
package_info_plus: ^4.0.0
|
package_info_plus: ^4.0.0
|
||||||
@@ -57,6 +57,7 @@ dependencies:
|
|||||||
uri_to_file: ^0.2.0
|
uri_to_file: ^0.2.0
|
||||||
window_manager: ^0.3.4
|
window_manager: ^0.3.4
|
||||||
tray_manager: ^0.2.0
|
tray_manager: ^0.2.0
|
||||||
|
autoscale_tabbarview: ^1.0.2
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
@@ -67,14 +68,11 @@ dev_dependencies:
|
|||||||
# package. See that file for information about deactivating specific lint
|
# package. See that file for information about deactivating specific lint
|
||||||
# rules and activating additional ones.
|
# rules and activating additional ones.
|
||||||
flutter_lints: ^2.0.0
|
flutter_lints: ^2.0.0
|
||||||
ffigen: ^6.1.2
|
ffigen: ^8.0.2
|
||||||
# build json model: flutter pub run build_runner build --delete-conflicting-outputs
|
# build json model: flutter pub run build_runner build --delete-conflicting-outputs
|
||||||
build_runner: ^2.2.1
|
build_runner: ^2.2.1
|
||||||
json_serializable: ^6.3.2
|
json_serializable: ^6.3.2
|
||||||
flutter_launcher_icons:
|
flutter_launcher_icons: ^0.13.1
|
||||||
git:
|
|
||||||
url: https://github.com/fluttercommunity/flutter_launcher_icons.git
|
|
||||||
ref: v0.11.0
|
|
||||||
|
|
||||||
# For information on the generic Dart part of this file, see the
|
# For information on the generic Dart part of this file, see the
|
||||||
# following page: https://dart.dev/tools/pub/pubspec
|
# following page: https://dart.dev/tools/pub/pubspec
|
||||||
@@ -128,6 +126,7 @@ flutter_icons:
|
|||||||
generate: true
|
generate: true
|
||||||
image_path: "assets/icon/icon_macos_1024.png"
|
image_path: "assets/icon/icon_macos_1024.png"
|
||||||
|
|
||||||
|
# flutter pub run ffigen
|
||||||
ffigen:
|
ffigen:
|
||||||
name: LibgopeedBind
|
name: LibgopeedBind
|
||||||
description: Bindings to gopeed library.
|
description: Bindings to gopeed library.
|
||||||
|
|||||||
Reference in new issue
Block a user