Compare commits

...
35 Commits
Author SHA1 Message Date
Levi 09aea55b49 release: v1.3.11 2023-09-04 13:57:47 +08:00
Levi c0cd2d7255 feat: task file manage (#196)
1. support view task file list
2. support share file on android and iOS
2023-09-04 13:51:30 +08:00
Levi b00f4a1ed0 feat: ios support (#194) 2023-09-04 11:28:07 +08:00
Levi 06766eea56 feat: auto get create url from clipboard (#193) 2023-09-02 11:32:28 +08:00
Levi 0f0c8f27bd feat: adjust size of resource dialog (#192) 2023-09-02 11:26:26 +08:00
liwei 45cf62f88d release: v1.3.10 2023-08-26 09:17:22 +08:00
Levi 6e9d3499d9 fix: drop file crash on Windows (#189) 2023-08-26 08:58:10 +08:00
Levi b6d60f6bdf fix: http download timeout (#188) 2023-08-25 18:42:10 +08:00
Levi e1684e755a fix: go install fail (#184) 2023-08-16 17:05:20 +08:00
Tracey Lesley e63cfca82a fix:current writes map (#182) 2023-08-12 13:45:01 +08:00
Levi 85ef8d3be4 release: v1.3.9 2023-08-01 18:57:15 +08:00
Levi bf2ccdb5a7 fix: torrent file path resolve (#173) 2023-08-01 18:56:39 +08:00
Levi a75bfd6118 release: v1.3.8 2023-07-28 21:50:26 +08:00
Levi a5d8713ff7 feat: torrent file support on web (#171) 2023-07-28 21:48:20 +08:00
Levi 81518d794f feat: web support http basic authorization (#170) 2023-07-27 21:02:33 +08:00
Levi 7c2b81caf7 release: v1.3.7 2023-07-23 10:00:47 +08:00
Levi 2221332243 fix: bt file real time speed (#163) 2023-07-23 09:58:42 +08:00
Levi dc407434d0 fix: file_picker crash on windows (#164) 2023-07-23 00:43:22 +08:00
Levi 1c2e86a991 fix: flutter docker image 2023-07-19 23:35:02 +08:00
Levi c78dc5f18f fix: flutter image 2023-07-19 23:26:53 +08:00
Levi 8ea5abb5c3 fix: Dockerfile flutter version 2023-07-19 23:19:24 +08:00
Levi d1a4067dfa release: v1.3.6 2023-07-19 23:05:01 +08:00
Levi a1928d202f fix: high dpi display (#158) 2023-07-19 23:01:25 +08:00
Levi 7376e85895 feat: create task with advanced options (#157) 2023-07-19 22:52:51 +08:00
Li Wei eb79dd5ac5 chore: update docker command 2023-07-19 06:11:02 +00:00
Li Wei 5b945b16c1 ci: auto translate 2023-07-18 09:17:07 +00:00
Levi 80e60f4190 feat: improve tracker cdn (#155) 2023-07-15 17:01:52 +08:00
Levi 3087796ef5 feat: support flutter 3.10 (#154) 2023-07-15 14:52:59 +08:00
Liam Segura 99de0aaf9b chore: Update README.md (#144) 2023-07-07 20:23:41 +08:00
liwei 6bea61cf0a release: v1.3.5 2023-07-07 16:23:31 +08:00
liwei 6c3553805e fix: include Japanese 2023-07-07 16:16:43 +08:00
liwei adc7c7766d feat: include Japanese 2023-07-07 16:10:50 +08:00
Ikko Eltociear Ashimine 15d0296307 feat: add Japanese support (#139) 2023-07-07 09:55:23 +08:00
Li Wei 32d2e46118 feat: bt fetcher reuse (#142) 2023-07-06 17:21:34 +08:00
Li Wei abe3cec13d fix: client log is not recorded (#141) 2023-07-06 17:15:48 +08:00
101 changed files with 3415 additions and 816 deletions

No files matched your search

+39 -7
View File
@@ -5,7 +5,7 @@ on:
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
FLUTTER_VERSION: 3.10.6
permissions:
contents: write
@@ -39,7 +39,7 @@ jobs:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
Invoke-WebRequest -Uri "https://github.com/jrsoftware/issrc/raw/main/Files/Languages/Unofficial/ChineseSimplified.isl" -OutFile "ChineseSimplified.isl"
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
mv ChineseSimplified.isl "C:\Program Files (x86)\Inno Setup 6\Languages\"
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
@@ -90,7 +90,7 @@ jobs:
SolidCompression=yes
WizardStyle=modern
LanguageDetectionMethod=uilanguage
ShowLanguageDialog=yes
ShowLanguageDialog=yes
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
@@ -166,7 +166,7 @@ jobs:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
PROJECT_DIR=$(pwd)
# amd64 lib
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/lib-amd64/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
# universal binary
@@ -221,13 +221,13 @@ jobs:
mkdir -p debian/gui
cp assets/icon/icon_1024.png debian/gui/gopeed.png
cat>debian/debian.yaml<<EOF
flutter_app:
flutter_app:
command: gopeed
arch: x64
parent: /usr/local/lib
control:
Package: gopeed
Version: ${VERSION:1}
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
@@ -238,7 +238,7 @@ jobs:
cat>debian/gui/gopeed.desktop<<EOF
[Desktop Entry]
Version=${VERSION:1}
Version=${VERSION:1}
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
@@ -294,6 +294,38 @@ jobs:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/dist/*
overwrite: true
build-ios:
runs-on: macos-latest
needs: [get-release]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
mkdir Payload
cp -r build/ios/iphoneos/Runner.app Payload
zip -r -y Payload.zip Payload/Runner.app
mkdir dist
mv Payload.zip dist/Gopeed-$VERSION-ios.ipa
- name: Upload
uses: shogo82148/actions-upload-release-asset@v1
with:
upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/dist/*
overwrite: true
build-web:
runs-on: ubuntu-latest
needs: [get-release]
+1 -1
View File
@@ -28,7 +28,7 @@ on:
env:
GO_VERSION: 1.19
FLUTTER_VERSION: 3.7.12
FLUTTER_VERSION: 3.10.6
jobs:
# lint:
+30
View File
@@ -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
+1
View File
@@ -21,6 +21,7 @@ bin/
*.data
*.db
*.log
.DS_Store
+54
View File
@@ -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
View File
@@ -1,4 +1,4 @@
FROM cirrusci/flutter:3.7.7 AS flutter
FROM instrumentisto/flutter:3.10.6 AS flutter
WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get
@@ -18,4 +18,4 @@ LABEL maintainer="monkeyWie"
WORKDIR /app
COPY --from=go /app/dist/gopeed ./
EXPOSE 9999
ENTRYPOINT ["./gopeed"]
ENTRYPOINT ["./gopeed"]
+13 -7
View File
@@ -7,7 +7,7 @@
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## Introduction
@@ -23,7 +23,7 @@ Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://d
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] ios
- [x] web
- [x] docker
@@ -44,7 +44,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### Start directly
```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
@@ -57,8 +57,6 @@ docker-compose up -d
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
If you like this project, please consider [donating](/.donate/index.md#donate) to support the development of this project, thank you!
@@ -76,7 +74,7 @@ This project is divided into two parts, the front end uses `flutter`, the back e
### Environment
1. Golang 1.19+
2. Flutter 3.7+
2. Flutter 3.10+
### Clone
@@ -92,7 +90,7 @@ Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md)
#### 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:
@@ -140,6 +138,14 @@ cd ui/flutter
flutter build apk
```
- ios
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### Web
command:
+177
View File
@@ -0,0 +1,177 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://github.com/GopeedLab/gopeed/blob/main/.donate/index.md#donate)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[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
- [x] 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)をご検討ください!
## ショーケース
![](_docs/img/ui-demo.png)
## 開発
このプロジェクトは二つの部分に分かれており、フロントエンドでは `flutter` を、バックエンドでは `Golang` を使用し、両者は `http` プロトコルで通信する。ユニックスシステムでは `unix socket` を、ウィンドウズシステムでは `tcp` プロトコルを使用します。
> フロントコードは `ui/flutter` ディレクトリにあります。
### 環境
1. Go 言語 1.19+
2. Flutter 3.10+
### クローン
```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
```
- ios
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### 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
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## ライセンス
[GPLv3](LICENSE)
+12 -5
View File
@@ -7,7 +7,7 @@
![](_docs/img/banner.png)
[English](/README.md) | [中文](/README_zh-CN.md)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md)
## 介绍
@@ -23,7 +23,7 @@ Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下
- [x] macos
- [x] linux
- [x] android
- [ ] ios
- [x] ios
- [x] web
- [x] docker
@@ -44,7 +44,7 @@ go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
#### 直接运行
```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
@@ -56,7 +56,6 @@ docker-compose up -d
#### 访问服务
当 docker 容器运行时,可以通过 `http://localhost:9999` 访问 web 页面。
> 提示:在设置页面把下载路径修改为 `/download` 以便在宿主机访问下载完的文件。
## 打赏
@@ -75,7 +74,7 @@ docker-compose up -d
### 环境要求
1. Golang 1.19+
2. Flutter 3.7+
2. Flutter 3.10+
### 克隆项目
@@ -139,6 +138,14 @@ cd ui/flutter
flutter build apk
```
- ios
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### Web 端
构建命令:
+77
View File
@@ -0,0 +1,77 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path/filepath"
)
type args struct {
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
configPath *string
}
func parse() *args {
var cliArgs args
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "HTTP Basic Auth Username")
cliArgs.Password = flag.String("p", "", "HTTP Basic Auth Password")
cliArgs.ApiToken = flag.String("T", "", "API token, that can only be used when basic authentication is enabled.")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
fmt.Println("parse")
// args priority: config file > cli args
cfgArgs := loadConfig(*cliArgs.configPath)
if cfgArgs.Address == nil {
cfgArgs.Address = cliArgs.Address
}
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
return cfgArgs
}
func loadConfig(path string) *args {
var args args
if !filepath.IsAbs(path) {
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return &args
}
path = filepath.Join(dir, path)
}
file, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return &args
}
fmt.Println("config file read failed, reason:" + err.Error())
return &args
}
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error())
return &args
}
fmt.Printf("config file loaded: %s\n", path)
return &args
}
+22 -5
View File
@@ -5,6 +5,7 @@ package main
import (
"embed"
"fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
@@ -18,12 +19,28 @@ func main() {
if err != nil {
panic(err)
}
args := parse()
var webBasicAuth *model.WebBasicAuth
if isNotBlank(args.Username) && isNotBlank(args.Password) {
webBasicAuth = &model.WebBasicAuth{
Username: *args.Username,
Password: *args.Password,
}
}
cfg := &model.StartConfig{
Network: "tcp",
Address: "0.0.0.0:9999",
Storage: model.StorageBolt,
WebEnable: true,
WebFS: sub,
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
ApiToken: *args.ApiToken,
WebEnable: true,
WebFS: sub,
WebBasicAuth: webBasicAuth,
}
cmd.Start(cfg)
}
func isNotBlank(str *string) bool {
return str != nil && *str != ""
}
+1 -1
View File
@@ -7,5 +7,5 @@ services:
- 9999:9999 # HTTP port (host:container)
image: liwei2633/gopeed
volumes:
- ~/Downloads:/download # replace with your prefer path
- ~/Downloads:/root/Downloads # replace with your prefer path
restart: unless-stopped
+3 -5
View File
@@ -3,7 +3,8 @@ module github.com/GopeedLab/gopeed
go 1.19
require (
github.com/anacrolix/torrent v1.52.3
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.5
github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0
@@ -12,8 +13,6 @@ require (
golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde
)
replace github.com/anacrolix/torrent => github.com/monkeyWie/torrent v1.52.4-alpha
require (
crawshaw.io/sqlite v0.3.3-0.20220618202545-d1964889ea3c // indirect
github.com/RoaringBitmap/roaring v1.2.3 // indirect
@@ -23,11 +22,10 @@ require (
github.com/anacrolix/dht/v2 v2.19.2-0.20221121215055-066ad8494444 // indirect
github.com/anacrolix/envpprof v1.2.1 // indirect
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 // indirect
github.com/anacrolix/go-libutp v1.2.0 // indirect
github.com/anacrolix/go-libutp v1.3.1 // indirect
github.com/anacrolix/log v0.14.0 // indirect
github.com/anacrolix/missinggo v1.3.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/multiless v0.3.0 // indirect
github.com/anacrolix/stm v0.4.0 // indirect
+7 -6
View File
@@ -35,12 +35,12 @@ github.com/anacrolix/envpprof v1.2.1 h1:25TJe6t/i0AfzzldiGFKCpD+s+dk8lONBcacJZB2
github.com/anacrolix/envpprof v1.2.1/go.mod h1:My7T5oSqVfEn4MD4Meczkw/f5lSIndGAKu/0SM/rkf4=
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68 h1:fyXlBfnlFzZSFckJ8QLb2lfmWfY++4RiUnae7ZMuv0A=
github.com/anacrolix/generics v0.0.0-20230428105757-683593396d68/go.mod h1:ff2rHB/joTV03aMSSn/AZNnaIpUw0h3njetGsaXcMy8=
github.com/anacrolix/go-libutp v1.2.0 h1:sjxoB+/ARiKUR7IK/6wLWyADIBqGmu1fm0xo+8Yy7u0=
github.com/anacrolix/go-libutp v1.2.0/go.mod h1:RrJ3KcaDcf9Jqp33YL5V/5CBEc6xMc7aJL8wXfuWL50=
github.com/anacrolix/go-libutp v1.3.1 h1:idJzreNLl+hNjGC3ZnUOjujEaryeOGgkwHLqSGoige0=
github.com/anacrolix/go-libutp v1.3.1/go.mod h1:heF41EC8kN0qCLMokLBVkB8NXiLwx3t8R8810MTNI5o=
github.com/anacrolix/log v0.3.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.6.0/go.mod h1:lWvLTqzAnCWPJA08T2HCstZi0L1y2Wyvm3FJgwU9jwU=
github.com/anacrolix/log v0.10.0/go.mod h1:s5yBP/j046fm9odtUTbHOfDUq/zh1W8OkPpJtnX0oQI=
github.com/anacrolix/log v0.10.1-0.20220123034749-3920702c17f8/go.mod h1:GmnE2c0nvz8pOIPUSC9Rawgefy1sDXqposC2wgtBZE4=
github.com/anacrolix/log v0.13.1/go.mod h1:D4+CvN8SnruK6zIFS/xPoRJmtvtnxs+CSfDQ+BFxZ68=
github.com/anacrolix/log v0.14.0 h1:mYhTSemILe/Z8tIxbGdTIWWpPspI8W/fhZHpoFbDaL0=
github.com/anacrolix/log v0.14.0/go.mod h1:1OmJESOtxQGNMlUO5rcv96Vpp9mfMqXXbe2RdinFLdY=
github.com/anacrolix/lsan v0.0.0-20211126052245-807000409a62 h1:P04VG6Td13FHMgS5ZBcJX23NPC/fiC4cp9bXwYujdYM=
@@ -73,6 +73,8 @@ github.com/anacrolix/sync v0.4.0/go.mod h1:BbecHL6jDSExojhNtgTFSBcdGerzNc64tz3DC
github.com/anacrolix/tagflag v0.0.0-20180109131632-2146c8d41bf0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.0.0/go.mod h1:1m2U/K6ZT+JZG0+bdMK6qauP49QT4wE5pmhJXOKKCHw=
github.com/anacrolix/tagflag v1.1.0/go.mod h1:Scxs9CV10NQatSmbyjqmqmeQNwGzlNe0CMUMIxqHIG8=
github.com/anacrolix/torrent v1.52.5 h1:jWowdx+EU6zFVfBwmnL0d3H4J6vTFEGOrHI35YdfIT8=
github.com/anacrolix/torrent v1.52.5/go.mod h1:CcM8oPMYye5J42cSqJrmUpqwRFgSsJQ1jCEHwygqnqQ=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96 h1:QAVZ3pN/J4/UziniAhJR2OZ9Ox5kOY2053tBbbqUPYA=
github.com/anacrolix/upnp v0.1.3-0.20220123035249-922794e51c96/go.mod h1:Wa6n8cYIdaG35x15aH3Zy6d03f7P728QfdcDeD/IEOs=
github.com/anacrolix/utp v0.1.0 h1:FOpQOmIwYsnENnz7tAGohA+r6iXpRjrq8ssKSre2Cp4=
@@ -210,8 +212,6 @@ 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/reflect2 v0.0.0-20180701023420-4b7aa43c6742/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.4-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.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
@@ -365,7 +365,6 @@ golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTk
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20180524181706-dfa909b99c79/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -441,6 +440,7 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -448,6 +448,7 @@ golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+101 -49
View File
@@ -1,13 +1,14 @@
package bt
import (
"bytes"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"github.com/GopeedLab/gopeed/pkg/util"
"github.com/anacrolix/torrent"
"github.com/anacrolix/torrent/metainfo"
"github.com/anacrolix/torrent/storage"
"path"
"path/filepath"
"sync"
@@ -15,7 +16,12 @@ import (
"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 {
ctl *controller.Controller
@@ -23,10 +29,9 @@ type Fetcher struct {
torrent *torrent.Torrent
meta *fetcher.FetcherMeta
ready atomic.Bool
progress fetcher.Progress
torrentPaths map[string]string
torrentReady atomic.Bool
create atomic.Bool
progress fetcher.Progress
}
func (f *Fetcher) Name() string {
@@ -38,42 +43,48 @@ func (f *Fetcher) Setup(ctl *controller.Controller) (err error) {
if f.meta == nil {
f.meta = &fetcher.FetcherMeta{}
}
var once sync.Once
once.Do(func() {
cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0
return
}
// Support custom download path for each torrent
pieceCompletion, err := storage.NewDefaultPieceCompletionForDir(cfg.DataDir)
if err != nil {
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
}
func (f *Fetcher) initClient() (err error) {
lock.Lock()
defer lock.Unlock()
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
}
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
}
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{
Name: f.torrent.Name(),
Range: true,
@@ -95,6 +106,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
}
func (f *Fetcher) Create(opts *base.Options) (err error) {
f.create.Store(true)
f.meta.Opts = opts
if len(opts.SelectFiles) == 0 {
opts.SelectFiles = make([]int, 0)
@@ -103,17 +115,20 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
}
}
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.ready.Store(false)
return nil
}
func (f *Fetcher) Start() (err error) {
if !f.ready.Load() {
if err = f.addTorrent(f.meta.Req.URL); err != nil {
if !f.torrentReady.Load() {
if err = f.addTorrent(f.meta.Req); err != nil {
return
}
}
@@ -130,8 +145,8 @@ func (f *Fetcher) Start() (err error) {
}
func (f *Fetcher) Pause() (err error) {
f.ready.Store(false)
f.torrent.Drop()
f.torrentReady.Store(false)
f.safeDrop()
return
}
@@ -140,13 +155,22 @@ func (f *Fetcher) Continue() (err error) {
}
func (f *Fetcher) Close() (err error) {
f.torrent.Drop()
f.safeDrop()
return nil
}
func (f *Fetcher) safeDrop() {
defer func() {
// ignore panic
_ = recover()
}()
f.torrent.Drop()
}
func (f *Fetcher) Wait() (err error) {
for {
if f.ready.Load() {
if f.torrentReady.Load() {
done := true
for _, selectIndex := range f.meta.Opts.SelectFiles {
file := f.torrent.Files()[selectIndex]
@@ -182,7 +206,7 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
}
func (f *Fetcher) Progress() fetcher.Progress {
if !f.ready.Load() {
if !f.torrentReady.Load() {
return f.progress
}
for i := range f.progress {
@@ -193,12 +217,24 @@ func (f *Fetcher) Progress() fetcher.Progress {
return f.progress
}
func (f *Fetcher) addTorrent(url string) (err error) {
schema := util.ParseSchema(url)
func (f *Fetcher) addTorrent(req *base.Request) (err error) {
if err = f.initClient(); err != nil {
return
}
schema := util.ParseSchema(req.URL)
if schema == "MAGNET" {
f.torrent, err = client.AddMagnet(url)
f.torrent, err = client.AddMagnet(req.URL)
} else if schema == "APPLICATION/X-BITTORRENT" {
_, data := util.ParseDataUri(req.URL)
buf := bytes.NewBuffer(data)
var metaInfo *metainfo.MetaInfo
metaInfo, err = metainfo.Load(buf)
if err != nil {
return err
}
f.torrent, err = client.AddTorrent(metaInfo)
} else {
f.torrent, err = client.AddTorrentFromFile(url)
f.torrent, err = client.AddTorrentFromFile(req.URL)
}
if err != nil {
return
@@ -208,22 +244,38 @@ func (f *Fetcher) addTorrent(url string) (err error) {
if err != nil {
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 {
announceList := make([][]string, 0)
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})
}
f.torrent.AddTrackers(announceList)
}
<-f.torrent.GotInfo()
f.ready.Store(true)
f.torrentReady.Store(true)
return
}
type FetcherBuilder struct {
}
var schemes = []string{"FILE", "MAGNET"}
var schemes = []string{"FILE", "MAGNET", "APPLICATION/X-BITTORRENT"}
func (fb *FetcherBuilder) Schemes() []string {
return schemes
+42
View File
@@ -1,11 +1,14 @@
package bt
import (
"encoding/base64"
"encoding/json"
"github.com/GopeedLab/gopeed/internal/controller"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/test"
"github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/bt"
"os"
"reflect"
"testing"
)
@@ -14,6 +17,39 @@ func TestFetcher_Resolve_Torrent(t *testing.T) {
doResolve(t, buildFetcher())
}
func TestFetcher_Resolve_DataUri_Torrent(t *testing.T) {
fetcher := buildFetcher()
buf, err := os.ReadFile("./testdata/ubuntu-22.04-live-server-amd64.iso.torrent")
if err != nil {
t.Fatal(err)
}
// convert to data uri
dataUri := "data:application/x-bittorrent;base64," + base64.StdEncoding.EncodeToString(buf)
err = fetcher.Resolve(&base.Request{
URL: dataUri,
})
if err != nil {
panic(err)
}
want := &base.Resource{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
Range: true,
RootDir: "ubuntu-22.04-live-server-amd64.iso",
Files: []*base.FileInfo{
{
Name: "ubuntu-22.04-live-server-amd64.iso",
Size: 1466714112,
},
},
Hash: "8a55cfbd5ca5d11507364765936c4f9e55b253ed",
}
if !reflect.DeepEqual(want, fetcher.Meta().Res) {
t.Errorf("Resolve() got = %v, want %v", fetcher.Meta().Res, want)
}
}
func TestFetcher_Config(t *testing.T) {
doResolve(t, buildConfigFetcher())
}
@@ -21,6 +57,12 @@ func TestFetcher_Config(t *testing.T) {
func doResolve(t *testing.T, fetcher fetcher.Fetcher) {
err := fetcher.Resolve(&base.Request{
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 {
panic(err)
+214
View File
@@ -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
}
+36
View File
@@ -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
}
+61
View File
@@ -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)
}
+4 -5
View File
@@ -17,7 +17,6 @@ import (
"os"
"path"
"strconv"
"time"
)
type RequestError struct {
@@ -85,13 +84,14 @@ func (f *Fetcher) Resolve(req *base.Request) error {
return err
}
client := buildClient()
// 只访问一个字节,测试资源是否支持Range请求
// send Range request to check whether the server supports breakpoint continuation
// just test one byte, Range: bytes=0-0
httpReq.Header.Set(base.HttpHeaderRange, fmt.Sprintf(base.HttpHeaderRangeFormat, 0, 0))
httpResp, err := client.Do(httpReq)
if err != nil {
return err
}
// 拿到响应头就关闭,不用加defer
// close response body immediately
httpResp.Body.Close()
res := &base.Resource{
Range: false,
@@ -395,8 +395,7 @@ func buildClient() *http.Client {
// Cookie handle
jar, _ := cookiejar.New(nil)
return &http.Client{
Jar: jar,
Timeout: time.Second * 60,
Jar: jar,
}
}
+25 -26
View File
@@ -47,8 +47,9 @@ type Downloader struct {
tasks []*Task
listener Listener
lock *sync.Mutex
closed atomic.Bool
lock *sync.Mutex
fetcherMapLock *sync.RWMutex
closed atomic.Bool
}
func NewDownloader(cfg *DownloaderConfig) *Downloader {
@@ -63,8 +64,9 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
fetchBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherMap: make(map[string]fetcher.Fetcher),
storage: cfg.Storage,
lock: &sync.Mutex{},
storage: cfg.Storage,
lock: &sync.Mutex{},
fetcherMapLock: &sync.RWMutex{},
}
for _, f := range cfg.FetchBuilders {
for _, p := range f.Schemes() {
@@ -72,7 +74,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
}
}
logPanic(cfg.StorageDir)
logPanic(filepath.Join(cfg.StorageDir, "logs"))
return d
}
@@ -169,9 +171,6 @@ func (d *Downloader) Setup() error {
func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
schema := util.ParseSchema(url)
fetchBuilder, ok := d.fetchBuilders[schema]
if !ok {
fetchBuilder, ok = d.fetchBuilders[util.FileSchema]
}
if ok {
return fetchBuilder, nil
}
@@ -201,7 +200,9 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
if err != nil {
return
}
d.fetcherMapLock.Lock()
d.fetcherMap[rrId] = fetcher
d.fetcherMapLock.Unlock()
rr = &ResolveResult{
ID: rrId,
Res: fetcher.Meta().Res,
@@ -221,11 +222,17 @@ func (d *Downloader) Create(rrId string, opts *base.Options) (taskId string, err
if opts == nil {
opts = &base.Options{}
}
d.fetcherMapLock.RLock()
fetcher, ok := d.fetcherMap[rrId]
d.fetcherMapLock.RUnlock()
if !ok {
return "", errors.New("invalid resource id")
}
defer delete(d.fetcherMap, rrId)
defer func() {
d.fetcherMapLock.Lock()
delete(d.fetcherMap, rrId)
d.fetcherMapLock.Unlock()
}()
meta := fetcher.Meta()
meta.Opts = opts
@@ -521,7 +528,9 @@ func (d *Downloader) watch(task *Task) {
d.emit(EventKeyDone, task)
}
d.emit(EventKeyFinally, task, err)
d.notifyRunning()
if task.Status == EventKeyDone {
d.notifyRunning()
}
}
func (d *Downloader) restoreFetcher(task *Task) error {
@@ -640,22 +649,12 @@ func (d *Downloader) doContinue(task *Task) (err error) {
// redirect stderr to log file, when panic happened log it
func logPanic(logDir string) {
fp := filepath.Join(logDir, "crash.log")
// if log file not exists, create it
var f *os.File
if _, err := os.Stat(fp); err != nil {
if !os.IsNotExist(err) {
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
}
if err := util.CreateDirIfNotExist(logDir); err != nil {
return
}
f, err := os.Create(filepath.Join(logDir, "crash.log"))
if err != nil {
return
}
paniclog.RedirectStderr(f)
}
+5
View File
@@ -0,0 +1,5 @@
package bt
type ReqExtra struct {
Trackers []string `json:"trackers"`
}
+18 -3
View File
@@ -1,6 +1,9 @@
package model
import "io/fs"
import (
"encoding/base64"
"io/fs"
)
type Storage string
@@ -17,8 +20,9 @@ type StartConfig struct {
StorageDir string `json:"storageDir"`
ApiToken string `json:"apiToken"`
WebEnable bool
WebFS fs.FS
WebEnable bool
WebFS fs.FS
WebBasicAuth *WebBasicAuth
}
func (cfg *StartConfig) Init() *StartConfig {
@@ -39,3 +43,14 @@ func (cfg *StartConfig) Init() *StartConfig {
}
return cfg
}
type WebBasicAuth struct {
Username string
Password string
}
// Authorization returns the value of the Authorization header to be used in HTTP requests.
func (cfg *WebBasicAuth) Authorization() string {
userId := cfg.Username + ":" + cfg.Password
return "Basic " + base64.StdEncoding.EncodeToString([]byte(userId))
}
+16 -5
View File
@@ -98,14 +98,21 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
}
if startCfg.ApiToken != "" {
if startCfg.ApiToken != "" || (startCfg.WebEnable && startCfg.WebBasicAuth != nil) {
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("X-Api-Token") != startCfg.ApiToken {
WriteJson(w, model.NewErrorResult("invalid token", model.CodeUnauthorized))
if startCfg.ApiToken != "" && r.Header.Get("X-Api-Token") == startCfg.ApiToken {
h.ServeHTTP(w, r)
return
}
h.ServeHTTP(w, r)
if startCfg.WebEnable && startCfg.WebBasicAuth != nil {
if r.Header.Get("Authorization") == startCfg.WebBasicAuth.Authorization() {
h.ServeHTTP(w, r)
return
}
w.Header().Set("WWW-Authenticate", "Basic realm=\"gopeed web\"")
}
WriteStatusJson(w, http.StatusUnauthorized, model.NewErrorResult("unauthorized", model.CodeUnauthorized))
})
})
}
@@ -127,7 +134,11 @@ func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
}
func WriteJson(w http.ResponseWriter, v any) {
WriteStatusJson(w, http.StatusOK, v)
}
func WriteStatusJson(w http.ResponseWriter, statusCode int, v any) {
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(v)
}
+46 -5
View File
@@ -273,7 +273,7 @@ func TestDoProxy(t *testing.T) {
})
}
func TestAuthorization(t *testing.T) {
func TestApiToken(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.ApiToken = "123456"
@@ -285,16 +285,57 @@ func TestAuthorization(t *testing.T) {
Stop()
}()
code, _ := doHttpRequest[any](http.MethodGet, "/api/v1/config", nil, nil)
checkCode(code, model.CodeUnauthorized)
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusUnauthorized)
}
code, _ = doHttpRequest[any](http.MethodGet, "/api/v1/config", map[string]string{
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken,
}, nil)
checkCode(code, model.CodeOk)
if status != http.StatusOK {
t.Errorf("TestApiToken() got = %v, want %v", status, http.StatusOK)
}
}
func TestAuthorization(t *testing.T) {
var cfg = &model.StartConfig{}
cfg.Init()
cfg.ApiToken = "123456"
cfg.WebEnable = true
cfg.WebBasicAuth = &model.WebBasicAuth{
Username: "admin",
Password: "123456",
}
fileListener := doStart(cfg)
defer func() {
if err := fileListener.Close(); err != nil {
panic(err)
}
Stop()
}()
status, _ := doHttpRequest0(http.MethodGet, "/api/v1/config", nil, nil)
if status != http.StatusUnauthorized {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusUnauthorized)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"Authorization": cfg.WebBasicAuth.Authorization(),
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
status, _ = doHttpRequest0(http.MethodGet, "/api/v1/config", map[string]string{
"X-Api-Token": cfg.ApiToken,
}, nil)
if status != http.StatusOK {
t.Errorf("TestAuthorization() got = %v, want %v", status, http.StatusOK)
}
}
func doTest(handler func()) {
testFunc := func(storage model.Storage) {
var cfg = &model.StartConfig{}
+12 -11
View File
@@ -3,7 +3,6 @@ package util
import (
"errors"
"fmt"
"io"
"os"
syspath "path"
"strings"
@@ -83,17 +82,19 @@ func GetSingleDir(paths []string) string {
return dir
}
// check directory is empty
func isEmpty(name string) (bool, error) {
f, err := os.Open(name)
if err != nil {
return false, err
func CreateDirIfNotExist(dir string) error {
if _, err := os.Stat(dir); os.IsNotExist(err) {
return os.MkdirAll(dir, 0o777)
}
defer f.Close()
return nil
}
_, err = f.Readdirnames(1) // Or f.Readdir(1)
if err == io.EOF {
return true, nil
// IsExistsFile check file exists and is a file
func IsExistsFile(path string) bool {
info, err := os.Stat(path)
// 判断路径是否存在,并且是一个文件
if err == nil && !info.IsDir() {
return true
}
return false, err // Either not empty or error, suits both cases
return false
}
+40
View File
@@ -199,3 +199,43 @@ func doCheckDuplicateAndRename(t *testing.T, exitsPaths []string, path string, e
t.Errorf("CheckDuplicateAndRename() = %v, want %v", got, except)
}
}
func TestIsExistsFile(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "exist",
args: args{
path: "./path.go",
},
want: true,
},
{
name: "not exist",
args: args{
path: "./path_not_exist.go",
},
want: false,
},
{
name: "is dir",
args: args{
path: "../util",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := IsExistsFile(tt.args.path); got != tt.want {
t.Errorf("IsExistsFile() = %v, want %v", got, tt.want)
}
})
}
}
+29 -3
View File
@@ -1,13 +1,39 @@
package util
import "strings"
import (
"encoding/base64"
"regexp"
"strings"
)
const FileSchema = "FILE"
func ParseSchema(url string) string {
index := strings.Index(url, ":")
if index == -1 {
// if no schema or windows path like C:\a.txt, return FILE
if index == -1 || index == 1 {
return FileSchema
}
return strings.ToUpper(url[:index])
schema := url[:index]
if schema == "data" {
schema, _ = ParseDataUri(url)
}
return strings.ToUpper(schema)
}
// ParseDataUri parses a data URI and returns the MIME type and decode data.
func ParseDataUri(uri string) (string, []byte) {
re := regexp.MustCompile(`^data:(.*);base64,(.*)$`)
matches := re.FindStringSubmatch(uri)
if len(matches) != 3 {
return "", nil
}
mime := matches[1]
base64Data := matches[2]
// 解码Base64数据
data, err := base64.StdEncoding.DecodeString(base64Data)
if err != nil {
return "", nil
}
return mime, data
}
+82 -2
View File
@@ -1,6 +1,10 @@
package util
import "testing"
import (
"encoding/base64"
"reflect"
"testing"
)
func TestParseSchema(t *testing.T) {
type args struct {
@@ -35,7 +39,21 @@ func TestParseSchema(t *testing.T) {
{
name: "file-no-scheme",
args: args{
url: "/home/bt.torrent",
url: "./url.go",
},
want: "FILE",
},
{
name: "data-uri",
args: args{
url: "data:application/x-bittorrent;base64,test",
},
want: "APPLICATION/X-BITTORRENT",
},
{
name: "windows-path",
args: args{
url: "D:\\bt.torrent",
},
want: "FILE",
},
@@ -48,3 +66,65 @@ func TestParseSchema(t *testing.T) {
})
}
}
func TestParseDataUri(t *testing.T) {
type args struct {
uri string
}
type result struct {
mime string
data []byte
}
testData := []byte("test")
testData64 := base64.StdEncoding.EncodeToString(testData)
tests := []struct {
name string
args args
want result
}{
{
name: "success",
args: args{
uri: "data:application/x-bittorrent;base64," + testData64,
},
want: result{
mime: "application/x-bittorrent",
data: testData,
},
},
{
name: "fail-dirty-data",
args: args{
uri: "data::application/x-bittorrent;base64,!@$",
},
want: result{
mime: "",
data: nil,
},
},
{
name: "fail-miss-data",
args: args{
uri: ":application/x-bittorrent;base64," + testData64,
},
want: result{
mime: "",
data: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mime, data := ParseDataUri(tt.args.uri)
got := result{
mime: mime,
data: data,
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("ParseDataUri() = %v, want %v", got, tt.want)
}
})
}
}
@@ -31,12 +31,8 @@ class MainActivity : FlutterActivity() {
}
}
"stop" -> {
try {
Libgopeed.stop()
result.success(null)
} catch (e: Exception) {
result.error("ERROR", e.message, null)
}
Libgopeed.stop()
result.success(null)
}
else -> {
result.notImplemented()
+1 -1
View File
@@ -26,6 +26,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
tasks.register("clean", Delete) {
delete rootProject.buildDir
}
+2
View File
@@ -12,6 +12,7 @@
"donate": "Donate",
"exit": "Exit",
"create": "Create Task",
"advancedOptions": "Advanced Options",
"downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link",
"downloadLinkHit": "Please enter the download link, HTTP/HTTPS/MAGNET supported@append",
@@ -20,6 +21,7 @@
"noFileSelected": "Please select at least one file to continue.",
"noStoragePermission": "Storage permission required",
"selectFile": "Select File",
"rename": "Rename",
"basic": "Basic",
"advanced": "Advanced",
"general": "General",
+61
View File
@@ -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": "後で"
}
+3 -1
View File
@@ -12,6 +12,7 @@
"donate": "打赏",
"exit": "退出",
"create": "创建任务",
"advancedOptions": "高级选项",
"downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接",
"downloadLinkHit": "请输入下载链接,支持 HTTP/HTTPS/MAGNET@append",
@@ -20,6 +21,7 @@
"noFileSelected": "请至少选择一个文件下载",
"noStoragePermission": "需要开启存储权限",
"selectFile": "选择文件",
"rename": "重命名",
"basic": "基础",
"advanced": "高级",
"general": "通用",
@@ -27,7 +29,7 @@
"downloadDirValid": "请选择下载目录",
"connections": "连接数",
"maxRunning": "最大下载数",
"items": "@count 条",
"items": "@count 项",
"subscribeTracker": "订阅 Tracker",
"subscribeFail": "订阅失败,请检查网络或稍后重试",
"update": "更新",
@@ -21,6 +21,6 @@
<key>CFBundleVersion</key>
<string>1.0</string>
<key>MinimumOSVersion</key>
<string>9.0</string>
<string>11.0</string>
</dict>
</plist>
+1 -1
View File
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
# platform :ios, '9.0'
# platform :ios, '11.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+52
View File
@@ -0,0 +1,52 @@
PODS:
- file_selector_ios (0.0.1):
- Flutter
- Flutter (1.0.0)
- package_info_plus (0.4.5):
- Flutter
- path_provider_ios (0.0.1):
- Flutter
- permission_handler_apple (9.0.4):
- Flutter
- shared_preferences_ios (0.0.1):
- Flutter
- url_launcher_ios (0.0.1):
- Flutter
DEPENDENCIES:
- file_selector_ios (from `.symlinks/plugins/file_selector_ios/ios`)
- Flutter (from `Flutter`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- shared_preferences_ios (from `.symlinks/plugins/shared_preferences_ios/ios`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
EXTERNAL SOURCES:
file_selector_ios:
:path: ".symlinks/plugins/file_selector_ios/ios"
Flutter:
:path: Flutter
package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios"
path_provider_ios:
:path: ".symlinks/plugins/path_provider_ios/ios"
permission_handler_apple:
:path: ".symlinks/plugins/permission_handler_apple/ios"
shared_preferences_ios:
:path: ".symlinks/plugins/shared_preferences_ios/ios"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
SPEC CHECKSUMS:
file_selector_ios: 6e9b6d7300556c730f224905726f0a2d5d874e51
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: 44366e37eaf29454a1e7b1b7d736c2cceaeb17ce
shared_preferences_ios: 548a61f8053b9b8a49ac19c1ffbc8b92c50d68ad
url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de
PODFILE CHECKSUM: ef19549a9bc3046e7bb7d2fab4d021637c0c58a3
COCOAPODS: 1.11.3
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 50;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -13,28 +13,36 @@
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = D081C25B294826C0006EB10B /* libc++.tbd */; };
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; };
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D0E623A729482D030001185E /* Libgopeed.xcframework */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
buildActionMask = 8;
dstPath = "";
dstSubfolderSpec = 10;
files = (
D0E623AA29482D160001185E /* Libgopeed.xcframework in Embed Frameworks */,
);
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
runOnlyForDeploymentPostprocessing = 1;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
97232923D357E56CAA39281D /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -42,6 +50,9 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BC90D28B9068C663815415CC /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
D081C25B294826C0006EB10B /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
D0E623A729482D030001185E /* Libgopeed.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Libgopeed.xcframework; path = Frameworks/Libgopeed.xcframework; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -49,6 +60,9 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
D0E623A929482D160001185E /* Libgopeed.xcframework in Frameworks */,
D081C25C294826C0006EB10B /* libc++.tbd in Frameworks */,
DCA5B0FA95A8007ADEDACA1B /* Pods_Runner.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -72,6 +86,8 @@
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
ADF7F433B4BC23D6C18AB78B /* Pods */,
B4A1CBAC5AA50208793CFF34 /* Frameworks */,
);
sourceTree = "<group>";
};
@@ -98,6 +114,26 @@
path = Runner;
sourceTree = "<group>";
};
ADF7F433B4BC23D6C18AB78B /* Pods */ = {
isa = PBXGroup;
children = (
97232923D357E56CAA39281D /* Pods-Runner.debug.xcconfig */,
BC90D28B9068C663815415CC /* Pods-Runner.release.xcconfig */,
25B61E78A388C87913468FD6 /* Pods-Runner.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
};
B4A1CBAC5AA50208793CFF34 /* Frameworks */ = {
isa = PBXGroup;
children = (
D0E623A729482D030001185E /* Libgopeed.xcframework */,
D081C25B294826C0006EB10B /* libc++.tbd */,
70BC8D37FAC641C7F20125E8 /* Pods_Runner.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -105,12 +141,14 @@
isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
F3093A8F123671F8F587626F /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */,
);
buildRules = (
);
@@ -127,7 +165,7 @@
97C146E61CF9000F007C117D /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1300;
LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
@@ -171,10 +209,12 @@
/* Begin PBXShellScriptBuildPhase section */
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -185,6 +225,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -197,6 +238,45 @@
shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
};
DDA801E9D83E2E88398514D3 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
F3093A8F123671F8F587626F /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
@@ -272,7 +352,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -288,6 +368,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -349,7 +430,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -398,7 +479,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.0;
IPHONEOS_DEPLOYMENT_TARGET = 11.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -416,6 +497,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -438,6 +520,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
DEVELOPMENT_TEAM = JH48DS925K;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
LD_RUNPATH_SEARCH_PATHS = (
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1300"
LastUpgradeVersion = "1430"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -4,4 +4,7 @@
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace>
+32 -7
View File
@@ -1,13 +1,38 @@
import UIKit
import Flutter
import Libgopeed
@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let batteryChannel = FlutterMethodChannel(name: "gopeed.com/libgopeed",
binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "start":
let args = call.arguments as? Dictionary<String, Any>
let cfg = args?["cfg"] as? String
let portPrt = UnsafeMutablePointer<Int>.allocate(capacity: MemoryLayout<Int>.stride)
var error: NSError?
if LibgopeedStart(cfg, portPrt, &error){
result(portPrt.pointee)
}else{
result(FlutterError(code: "ERROR", message: error.debugDescription, details: nil))
}
case "stop":
LibgopeedStop()
result(nil)
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 564 B

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

+8 -3
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
@@ -12,8 +14,6 @@
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>gopeed</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
@@ -24,6 +24,11 @@
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -43,7 +48,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict>
</plist>
+4 -2
View File
@@ -147,6 +147,8 @@ Future<Response<String>> proxyRequest<T>(String uri,
options.headers!["X-Target-Uri"] = uri;
// add timestamp to avoid cache
return _client.dio.request("/api/v1/proxy?t=${DateTime.now()}",
data: data, options: options);
return _client.dio.request(
"/api/v1/proxy?t=${DateTime.now().millisecondsSinceEpoch}",
data: data,
options: options);
}
+3 -2
View File
@@ -2,17 +2,18 @@ import 'package:json_annotation/json_annotation.dart';
part 'options.g.dart';
@JsonSerializable(explicitToJson: true, genericArgumentFactories: true)
@JsonSerializable(explicitToJson: true)
class Options {
String name;
String path;
List<int> selectFiles;
Map<String, dynamic>? extra;
Object? extra;
Options({
required this.name,
required this.path,
required this.selectFiles,
this.extra,
});
factory Options.fromJson(Map<String, dynamic> json) =>
+2 -1
View File
@@ -11,7 +11,8 @@ Options _$OptionsFromJson(Map<String, dynamic> json) => Options(
path: json['path'] as String,
selectFiles:
(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) {
final val = <String, dynamic>{
+15 -2
View File
@@ -5,10 +5,11 @@ part 'request.g.dart';
@JsonSerializable(explicitToJson: true)
class Request {
String url;
Map<String, dynamic>? extra;
Object? extra;
Request({
required this.url,
this.extra,
});
factory Request.fromJson(Map<String, dynamic> json) =>
@@ -20,7 +21,7 @@ class Request {
@JsonSerializable()
class ReqExtraHttp {
String method = 'GET';
Map<String, String> headers = {};
Map<String, String> header = {};
String body = '';
ReqExtraHttp();
@@ -30,3 +31,15 @@ class ReqExtraHttp {
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);
}
+13 -3
View File
@@ -8,7 +8,8 @@ part of 'request.dart';
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
url: json['url'] as String,
)..extra = json['extra'] as Map<String, dynamic>?;
extra: json['extra'],
);
Map<String, dynamic> _$RequestToJson(Request instance) {
final val = <String, dynamic>{
@@ -27,12 +28,21 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
ReqExtraHttp _$ReqExtraHttpFromJson(Map<String, dynamic> json) => ReqExtraHttp()
..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;
Map<String, dynamic> _$ReqExtraHttpToJson(ReqExtraHttp instance) =>
<String, dynamic>{
'method': instance.method,
'headers': instance.headers,
'header': instance.header,
'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,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ui' as ui;
import 'dart:ui';
import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart';
@@ -16,7 +16,7 @@ import '../../../../api/api.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../core/common/start_config.dart';
import '../../../../generated/locales.g.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart';
import '../../../../util/util.dart';
@@ -42,10 +42,13 @@ const allTrackerSubscribeUrls = [
'https://github.com/XIU2/TrackersListCollection/raw/master/http.txt',
];
const allTrackerCdns = [
// jsdelivr: https://cdn.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
["https://cdn.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
// nuaa: https://hub.nuaa.cf/ngosang/trackerslist/raw/master/trackers_all.txt
["https://hub.nuaa.cf", r".*github.com(/.*)"]
// jsdelivr: https://fastly.jsdelivr.net/gh/ngosang/trackerslist/trackers_all.txt
["https://fastly.jsdelivr.net/gh", r".*github.com(/.*)/raw/master(/.*)"],
// ghproxy: https://ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
[
"https://ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
]
];
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
key: (v) => v as String,
@@ -222,7 +225,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
return _defaultStartConfig!;
}
_defaultStartConfig = StartConfig();
if (!Util.isUnix()) {
if (!Util.supportUnixSocket()) {
// not support unix socket, use tcp
_defaultStartConfig!.network = "tcp";
_defaultStartConfig!.address = "127.0.0.1:0";
@@ -265,18 +268,16 @@ class AppController extends GetxController with WindowListener, TrayListener {
final btExtConfig = downloaderConfig.value.extra.bt;
final result = <String>[];
for (var u in btExtConfig.trackerSubscribeUrls) {
var allFail = true;
for (var cdn in allTrackerSubscribeUrlCdns[u]!) {
try {
result.addAll(await _fetchTrackers(cdn));
allFail = false;
break;
} catch (e) {
logger.w("subscribe trackers fail, url: $cdn", e);
}
final cdns = allTrackerSubscribeUrlCdns[u];
if (cdns == null) {
continue;
}
if (allFail) {
throw Exception('subscribe trackers fail, network error');
try {
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();
@@ -336,7 +337,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
extra.themeMode = ThemeMode.system.name;
}
if (extra.locale.isEmpty) {
final systemLocale = getLocaleKey(ui.window.locale);
final systemLocale = getLocaleKey(PlatformDispatcher.instance.locale);
extra.locale = AppTranslation.translations.containsKey(systemLocale)
? systemLocale
: getLocaleKey(fallbackLocale);
@@ -4,7 +4,7 @@ import 'package:get/get.dart';
import '../../../../generated/locales.g.dart';
import '../../../../theme/theme.dart';
import '../../../../util/localeManager.dart';
import '../../../../util/locale_manager.dart';
import '../../../routes/app_pages.dart';
import '../controllers/app_controller.dart';
@@ -1,9 +1,38 @@
import 'dart:convert';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class CreateController extends GetxController {
class CreateController extends GetxController
with GetSingleTickerProviderStateMixin {
// final files = [].obs;
final RxList fileInfos = [].obs;
final RxList selectedIndexes = [].obs;
final RxList openedFolders = [].obs;
final isResolving = false.obs;
final showAdvanced = false.obs;
late TabController advancedTabController;
final fileDataUri = "".obs;
@override
void onInit() {
super.onInit();
advancedTabController = TabController(length: 2, vsync: this);
}
@override
void onClose() {
advancedTabController.dispose();
super.onClose();
}
void setFileDataUri(Uint8List bytes) {
fileDataUri.value =
"data:application/x-bittorrent;base64,${base64.encode(bytes)}";
}
void clearFileDataUri() {
fileDataUri.value = "";
}
}
@@ -1,6 +1,8 @@
import 'package:autoscale_tabbarview/autoscale_tabbarview.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import '../../../../api/model/resolve_result.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/options.dart';
import '../../../../api/model/request.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart';
import '../../../../util/util.dart';
import '../../../routes/app_pages.dart';
@@ -22,6 +25,12 @@ class CreateView extends GetView<CreateController> {
final _urlController = TextEditingController();
final _confirmController = RoundedLoadingButtonController();
final _httpUaController = TextEditingController();
final _httpCookieController = TextEditingController();
final _httpRefererController = TextEditingController();
final _btTrackerController = TextEditingController();
final _availableSchemes = ["http:", "https:", "magnet:"];
CreateView({Key? key}) : super(key: key);
@@ -30,8 +39,24 @@ class CreateView extends GetView<CreateController> {
final String? filePath = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty && filePath != null) {
_urlController.text = filePath;
WidgetsBinding.instance.addPostFrameCallback((_) async {
await _doResolve();
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
}
// if no file path, read from clipboard
if (filePath?.isEmpty ?? true) {
Clipboard.getData('text/plain').then((value) {
if (value?.text?.isNotEmpty ?? false) {
if (_availableSchemes
.where((e) =>
value!.text!.startsWith(e) ||
value.text!.startsWith(e.toUpperCase()))
.isNotEmpty) {
_urlController.text = value!.text!;
_urlController.selection = TextSelection.fromPosition(
TextPosition(offset: _urlController.text.length));
}
}
});
}
@@ -44,31 +69,44 @@ class CreateView extends GetView<CreateController> {
title: Text('create'.tr),
),
body: DropTarget(
onDragDone: (details) {
_urlController.text = details.files[0].path;
onDragDone: (details) async {
if (!Util.isWeb()) {
_urlController.text = details.files[0].path;
return;
}
_urlController.text = details.files[0].name;
final bytes = await details.files[0].readAsBytes();
controller.setFileDataUri(bytes);
},
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form(
key: _resolveFormKey,
autovalidateMode: AutovalidateMode.always,
child: Column(
children: [
Row(
children: [
Expanded(
child: TextFormField(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () {
FocusScope.of(context).requestFocus(FocusNode());
},
child: Padding(
padding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form(
key: _resolveFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(children: [
Row(children: [
Expanded(
child: TextFormField(
autofocus: true,
controller: _urlController,
minLines: 1,
maxLines: 30,
maxLines: 5,
decoration: InputDecoration(
hintText: _hitText(),
hintStyle: const TextStyle(fontSize: 12),
labelText: 'downloadLink'.tr,
icon: const Icon(Icons.link),
suffixIcon: IconButton(
onPressed: _urlController.clear,
onPressed: () {
_urlController.clear();
controller.clearFileDataUri();
},
icon: const Icon(Icons.clear),
),
),
@@ -76,39 +114,124 @@ class CreateView extends GetView<CreateController> {
return v!.trim().isNotEmpty
? null
: 'downloadLinkValid'.tr;
},
onChanged: (v) async {
controller.clearFileDataUri();
},
),
),
IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
var pr = await FilePicker.platform.pickFiles(
type: FileType.custom,
allowedExtensions: ["torrent"]);
if (pr != null) {
if (!Util.isWeb()) {
_urlController.text = pr.files[0].path ?? "";
return;
}
_urlController.text = pr.files[0].name;
controller.setFileDataUri(pr.files[0].bytes!);
}
}),
),
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,
height: 40,
]
//.where((e) => e != null).map((e) => e!).toList(),
),
child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary,
onPressed: _doResolve,
controller: _confirmController,
child: Text('confirm'.tr),
),
)),
),
],
),
),
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),
]),
),
),
SizedBox(
width: 150,
child: RoundedLoadingButton(
color: Get.theme.colorScheme.secondary,
onPressed: _doResolve,
controller: _confirmController,
child: Text('confirm'.tr),
),
),
],
),
)),
]))),
),
),
);
@@ -122,8 +245,27 @@ class CreateView extends GetView<CreateController> {
try {
_confirmController.start();
if (_resolveFormKey.currentState!.validate()) {
Object? extra;
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
? controller.fileDataUri.value
: _urlController.text;
if (controller.showAdvanced.value) {
final u = Uri.parse(submitUrl);
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(
url: _urlController.text,
url: submitUrl,
extra: extra,
));
await _showResolveDialog(rr);
}
@@ -137,7 +279,8 @@ class CreateView extends GetView<CreateController> {
String _hitText() {
return 'downloadLinkHit'.trParams({
'append': Util.isDesktop() ? 'downloadLinkHitDesktop'.tr : '',
'append':
Util.isDesktop() || Util.isWeb() ? 'downloadLinkHitDesktop'.tr : '',
});
}
@@ -146,6 +289,8 @@ class CreateView extends GetView<CreateController> {
final appController = Get.find<AppController>();
final createFormKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final connectionsController = TextEditingController();
final pathController = TextEditingController(
text: appController.downloaderConfig.value.downloadDir);
final downloadController = RoundedLoadingButtonController();
@@ -160,7 +305,7 @@ class CreateView extends GetView<CreateController> {
var width = MediaQuery.of(context).size.width;
return SizedBox(
height: height * 0.5,
height: height * 0.75,
width: width,
child: Form(
key: createFormKey,
@@ -168,6 +313,23 @@ class CreateView extends GetView<CreateController> {
child: Column(
children: [
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(
controller: pathController,
),
@@ -209,20 +371,18 @@ class CreateView extends GetView<CreateController> {
return;
}
if (createFormKey.currentState!.validate()) {
// if (Util.isAndroid()) {
// if (!await Permission.storage.request().isGranted) {
// Get.snackbar('error'.tr,
// 'noStoragePermission'.tr);
// return;
// }
// }
await createTask(CreateTask(
rid: rr.id,
opts: Options(
name: '',
name: nameController.text,
path: pathController.text,
selectFiles: controller.selectedIndexes
.cast<int>())));
selectFiles:
controller.selectedIndexes.cast<int>(),
extra: connectionsController.text.isEmpty
? null
: (OptsExtraHttp()
..connections = int.parse(
connectionsController.text)))));
Get.back();
Get.rootDelegate.offNamed(Routes.TASK);
}
@@ -28,17 +28,21 @@ class SettingController extends GetxController {
void fetchLatestVersion() async {
String? releaseDataStr;
try {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
} catch (e) {
releaseDataStr = (await proxyRequest(
"https://api.github.com/repos/GopeedLab/gopeed/releases/latest"))
.data;
} catch (e) {
releaseDataStr =
(await proxyRequest("https://gopeed.com/api/release")).data;
}
if (releaseDataStr == null) {
return;
}
final releaseData = jsonDecode(releaseDataStr);
final tagName = releaseData["tag_name"];
if (tagName == null) {
return;
}
latestVersion.value = releaseData["tag_name"].substring(1);
}
}
@@ -9,7 +9,8 @@ import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.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/package_info.dart';
import '../../../../util/util.dart';
@@ -35,7 +36,7 @@ class SettingView extends GetView<SettingController> {
debounceSave({bool needRestart = false}) {
var completer = Completer<void>();
timer?.cancel();
timer = Timer(const Duration(milliseconds: 500), () {
timer = Timer(const Duration(milliseconds: 1000), () {
appController
.saveConfig()
.then(completer.complete)
@@ -89,7 +90,7 @@ class SettingView extends GetView<SettingController> {
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
_NumericalRangeFormatter(min: 1, max: 256),
NumericalRangeFormatter(min: 1, max: 256),
],
);
});
@@ -116,7 +117,7 @@ class SettingView extends GetView<SettingController> {
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
_NumericalRangeFormatter(min: 1, max: 256),
NumericalRangeFormatter(min: 1, max: 256),
],
);
});
@@ -187,7 +188,6 @@ class SettingView extends GetView<SettingController> {
(Key key) {
final trackersController = TextEditingController(
text: btExtConfig.customTrackers.join('\r\n').toString());
const ls = LineSplitter();
return TextField(
key: key,
focusNode: FocusNode(),
@@ -198,7 +198,7 @@ class SettingView extends GetView<SettingController> {
hintText: 'addTrackerHit'.tr,
),
onChanged: (value) async {
btExtConfig.customTrackers = ls.convert(value);
btExtConfig.customTrackers = Util.textToLines(value);
appController.refreshTrackers();
await debounceSave();
@@ -334,21 +334,21 @@ class SettingView extends GetView<SettingController> {
width: 150,
child: DropdownButtonFormField<String>(
value: startCfg.value.network,
onChanged: (value) async {
startCfg.update((val) {
val!.network = value!;
});
onChanged: Util.isDesktop()
? (value) async {
startCfg.update((val) {
val!.network = value!;
});
await debounceSave(needRestart: true);
},
await debounceSave(needRestart: true);
}
: null,
items: [
!Util.isMobile()
? const DropdownMenuItem<String>(
value: 'tcp',
child: Text('TCP'),
)
: null,
Util.isUnix()
const DropdownMenuItem<String>(
value: 'tcp',
child: Text('TCP'),
),
Util.supportUnixSocket()
? const DropdownMenuItem<String>(
value: 'unix',
child: Text('Unix'),
@@ -408,7 +408,7 @@ class SettingView extends GetView<SettingController> {
keyboardType: TextInputType.number,
inputFormatters: [
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;
}
}
}
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/task_files_controller.dart';
class TaskFilesBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<TaskFilesController>(
() => TaskFilesController(),
);
}
}
@@ -0,0 +1,86 @@
import 'package:get/get.dart';
import '../../../../api/api.dart';
import '../../../../api/model/resource.dart';
import '../../../../api/model/task.dart';
class FileItem {
final bool isDirectory;
final String path;
final String name;
final int size;
FileItem(this.isDirectory, this.path, this.name, this.size);
String get fullPath => "${path == "/" ? path : "$path/"}$name";
}
class TaskFilesController extends GetxController {
final Map<String, List<FileItem>> _dirMap = {};
final task = Rx<Task?>(null);
final fileList = <FileItem>[].obs;
@override
void onInit() async {
super.onInit();
final taskId = Get.rootDelegate.parameters['id'];
final tasks = await getTasks([]);
task.value = tasks.firstWhere((element) => element.id == taskId);
parseDirMap(task.value!.meta.res.files);
toDir("/");
}
void parseDirMap(List<FileInfo> fileList) {
for (final file in fileList) {
String dir = file.path;
if (!dir.startsWith("/")) {
dir = "/$dir";
}
if (!_dirMap.containsKey(dir)) {
_dirMap[dir] = [];
}
_dirMap[dir]!.add(FileItem(false, dir, file.name, file.size));
void findParent(String dir) {
final parentDirIndex = dir.lastIndexOf("/");
String parentDir =
parentDirIndex == 0 ? "/" : dir.substring(0, parentDirIndex);
if (!_dirMap.containsKey(parentDir)) {
_dirMap[parentDir] = [];
}
String dirName = dir.substring(dir.lastIndexOf("/") + 1);
if (!_dirMap[parentDir]!.any((element) => element.name == dirName)) {
_dirMap[parentDir]!.add(FileItem(true, parentDir, dirName, 0));
}
}
while (dir != "/" && dir != "") {
findParent(dir);
dir = dir.substring(0, dir.lastIndexOf("/"));
}
}
// sort children, directories first then files, alphabetically
_dirMap.forEach((key, value) {
value.sort((a, b) {
if (a.isDirectory && !b.isDirectory) {
return -1;
} else if (!a.isDirectory && b.isDirectory) {
return 1;
} else {
return a.name.compareTo(b.name);
}
});
});
}
void toDir(String dir) {
fileList.value = _dirMap[dir]?.toList() ?? [];
}
int dirItemCount(String dir) {
return _dirMap[dir]?.length ?? 0;
}
}
@@ -0,0 +1,92 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:share_plus/share_plus.dart';
import '../../../../util/file_icon.dart';
import '../../../../util/icons.dart';
import '../../../../util/util.dart';
import '../../../views/breadcrumb_view.dart';
import '../controllers/task_files_controller.dart';
class TaskFilesView extends GetView<TaskFilesController> {
const TaskFilesView({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_back),
onPressed: () => Get.rootDelegate.popRoute()),
// actions: [],
title: Obx(() => Text(controller.task.value?.meta.res.name ?? "")),
),
body: Obx(() {
final fileList = controller.fileList;
final breadcrumbItems = ["/"];
if (fileList.isNotEmpty) {
final file = fileList.first;
final path = file.path.substring(1);
if (path.isNotEmpty) {
final pathArr = path.split("/");
for (int i = 0; i < pathArr.length; i++) {
breadcrumbItems.add(pathArr[i]);
}
}
}
return Column(
children: [
Breadcrumb(
items: breadcrumbItems,
onItemTap: (index) {
final targetDirArr = <String>[];
for (int i = 0; i <= index; i++) {
targetDirArr.add(breadcrumbItems[i]);
}
controller
.toDir(targetDirArr.join("/").replaceFirst('//', '/'));
}).paddingOnly(left: 16, top: 16, bottom: 8),
Expanded(
child: ListView.builder(
itemBuilder: (context, index) {
final meta = controller.task.value!.meta;
final file = fileList[index];
return ListTile(
leading: file.isDirectory
? const Icon(Icons.folder)
: Icon(FaIcons.allIcons[findIcon(file.name)]),
title: Text(fileList[index].name),
subtitle: file.isDirectory
? Text('items'.trParams({
'count': controller
.dirItemCount(file.fullPath)
.toString()
}))
: Text(Util.fmtByte(file.size)),
trailing: file.isDirectory
? null
: IconButton(
icon: const Icon(Icons.share),
onPressed: () {
final xfile = XFile(Util.safePathJoin([
meta.opts.path,
meta.res.rootDir,
file.fullPath
]));
Share.shareXFiles([xfile]);
}),
onTap: () {
if (file.isDirectory) {
controller.toDir(file.fullPath);
}
},
);
},
itemCount: controller.fileList.length,
),
)
],
);
}));
}
}
+10 -1
View File
@@ -1,4 +1,5 @@
import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/views/task_files_view.dart';
import 'package:gopeed/app/modules/task/views/task_view.dart';
import '../modules/create/bindings/create_binding.dart';
@@ -10,6 +11,7 @@ import '../modules/root/views/root_view.dart';
import '../modules/setting/bindings/setting_binding.dart';
import '../modules/setting/views/setting_view.dart';
import '../modules/task/bindings/task_binding.dart';
import '../modules/task/bindings/task_files_binding.dart';
part 'app_routes.dart';
@@ -37,7 +39,14 @@ class AppPages {
name: _Paths.TASK,
page: () => const TaskView(),
transition: Transition.noTransition,
binding: TaskBinding()),
binding: TaskBinding(),
children: [
GetPage(
name: _Paths.TASK_FILES,
page: () => const TaskFilesView(),
transition: Transition.noTransition,
binding: TaskFilesBinding()),
]),
GetPage(
name: _Paths.SETTING,
page: () => const SettingView(),
@@ -7,6 +7,7 @@ abstract class Routes {
static const HOME = _Paths.HOME;
static const CREATE = _Paths.CREATE;
static const TASK = _Paths.HOME + _Paths.TASK;
static const TASK_FILES = TASK + _Paths.TASK_FILES;
static const SETTING = _Paths.HOME + _Paths.SETTING;
}
@@ -16,5 +17,6 @@ abstract class _Paths {
static const HOME = '/home';
static const CREATE = '/create';
static const TASK = '/task';
static const TASK_FILES = '/files';
static const SETTING = '/setting';
}
@@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
class Breadcrumb extends StatelessWidget {
final List<String> items;
final Function(int)? onItemTap;
final TextStyle textStyle;
final TextStyle activeTextStyle;
const Breadcrumb({
super.key,
required this.items,
this.onItemTap,
this.textStyle = const TextStyle(fontSize: 16),
this.activeTextStyle =
const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
});
@override
Widget build(BuildContext context) {
List<Widget> children = [];
for (int i = 0; i < items.length; i++) {
children.add(
GestureDetector(
onTap: () {
if (onItemTap != null) {
onItemTap!(i);
}
},
child: Text(
items[i],
style: i == items.length - 1 ? activeTextStyle : textStyle,
),
),
);
if (i != items.length - 1) {
children.add(const Text(" > "));
}
}
return Row(
children: [
...(children.length == 1
? children.sublist(0, 1)
: children.sublist(0, 2)),
children.length > 2
? Expanded(
child: SingleChildScrollView(
reverse: true,
scrollDirection: Axis.horizontal,
child: Row(
children: children.sublist(2),
),
),
)
: null,
].where((e) => e != null).map((e) => e!).toList(),
).paddingOnly(right: 12);
}
}
@@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gopeed/api/model/meta.dart';
import 'package:path/path.dart' as path;
import '../../api/api.dart';
@@ -108,11 +108,16 @@ class BuildTaskListView extends GetView {
List<Widget> buildActions() {
final list = <Widget>[];
if (isDone()) {
if (Util.isDesktop()) {
if (Util.isDesktop() || Util.isMobile()) {
list.add(IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
await FileExplorer.openAndSelectFile(buildExplorerUrl(task));
onPressed: () {
if (Util.isDesktop()) {
FileExplorer.openAndSelectFile(buildExplorerUrl(task));
} else {
Get.rootDelegate
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
}
},
));
}
@@ -190,7 +195,7 @@ class BuildTaskListView extends GetView {
: const SizedBox.shrink(),
ListTile(
// isThreeLine: true,
title: Text(task.meta.res.name,
title: Text(fileName(task.meta),
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: Get.textTheme.titleSmall),
@@ -199,9 +204,9 @@ class BuildTaskListView extends GetView {
style: context.textTheme.bodyLarge
?.copyWith(color: Get.theme.disabledColor),
),
leading: task.meta.res.name.lastIndexOf('.') == -1
? const Icon(FaIcons.file)
: Icon(FaIcons.allIcons[findIcon(task.meta.res.name)]),
leading: (task.meta.res.rootDir.isNotEmpty
? const Icon(FaIcons.folder)
: Icon(FaIcons.allIcons[findIcon(fileName(task.meta))])),
trailing: SizedBox(
width: 180,
@@ -220,4 +225,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;
}
}
@@ -35,7 +35,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
},
)),
Util.isDesktop() || Util.isAndroid()
!Util.isWeb() && !Util.isIOS()
? IconButton(
icon: const Icon(Icons.folder_open),
onPressed: () async {
@@ -17,7 +17,6 @@ LibgopeedBoot create() => LibgopeedBootNative();
class LibgopeedBootNative implements LibgopeedBoot {
late LibgopeedInterface _libgopeed;
late SendPort _childSendPort;
LibgopeedBootNative() {
if (!Util.isDesktop()) {
@@ -44,11 +43,7 @@ class LibgopeedBootNative implements LibgopeedBoot {
@override
Future<void> stop() async {
if (Util.isDesktop()) {
await _ffiStop();
} else {
await _libgopeed.stop();
}
await _libgopeed.stop();
}
_ffiInit() {
@@ -67,90 +62,9 @@ class LibgopeedBootNative implements LibgopeedBoot {
// FFI run in isolate
Future<int> _ffiStart(StartConfig cfg) async {
var startCompleter = Completer<int>();
var mainReceive = ReceivePort();
var isolate = await Isolate.spawn((SendPort sendPort) {
return await Isolate.run(() async {
_ffiInit();
var childReceive = ReceivePort();
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();
}
}
return await _libgopeed.start(cfg);
});
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
+69 -1
View File
@@ -6,6 +6,7 @@ class AppTranslation {
static Map<String, Map<String, String>> translations = {
'en_US': Locales.en_US,
'fa_IR': Locales.fa_IR,
'ja_JP': Locales.ja_JP,
'ru_RU': Locales.ru_RU,
'zh_CN': Locales.zh_CN,
};
@@ -26,6 +27,7 @@ class LocaleKeys {
static const donate = 'donate';
static const exit = 'exit';
static const create = 'create';
static const advancedOptions = 'advancedOptions';
static const downloadLink = 'downloadLink';
static const downloadLinkValid = 'downloadLinkValid';
static const downloadLinkHit = 'downloadLinkHit';
@@ -34,6 +36,7 @@ class LocaleKeys {
static const noFileSelected = 'noFileSelected';
static const noStoragePermission = 'noStoragePermission';
static const selectFile = 'selectFile';
static const rename = 'rename';
static const basic = 'basic';
static const advanced = 'advanced';
static const general = 'general';
@@ -89,6 +92,7 @@ class Locales {
'donate': 'Donate',
'exit': 'Exit',
'create': 'Create Task',
'advancedOptions': 'Advanced Options',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link',
'downloadLinkHit':
@@ -98,6 +102,7 @@ class Locales {
'noFileSelected': 'Please select at least one file to continue.',
'noStoragePermission': 'Storage permission required',
'selectFile': 'Select File',
'rename': 'Rename',
'basic': 'Basic',
'advanced': 'Advanced',
'general': 'General',
@@ -197,6 +202,67 @@ class Locales {
'newVersionUpdate': 'دریافت اکنون',
'newVersionLater': 'بعداً',
};
static const ja_JP = {
'label': '日本語',
'error': 'エラー',
'tip': 'ヒント',
'confirm': '確認',
'cancel': 'キャンセル',
'selectAll': 'すべてを選択',
'task': 'タスク',
'downloading': 'ダウンロード中',
'downloaded': 'ダウンロード済',
'setting': 'セッティング',
'donate': '寄付する',
'exit': '終了',
'create': 'タスクを作成',
'downloadLink': 'ダウンロードリンク',
'downloadLinkValid': 'ダウンロードリンクを入力してください',
'downloadLinkHit': 'ダウンロード リンクを入力してください、HTTP/HTTPS/MAGNET support@append',
'downloadLinkHitDesktop': 'または、torrent ファイルを直接ここにドラッグします',
'download': 'ダウンロード',
'noFileSelected': '続行するには少なくとも 1 つのファイルを選択してください。',
'noStoragePermission': 'ストレージのパーミッションが必要',
'selectFile': 'ファイルを選択',
'basic': 'ベーシック',
'advanced': 'アドバンスド',
'general': 'ジェネラル',
'downloadDir': 'ディレクトリのダウンロード',
'downloadDirValid': 'ダウンロードディレクトリを選択してください',
'connections': '接続',
'maxRunning': '最大実行タスク',
'items': '@count アイテム',
'subscribeTracker': 'トラッカーを購読',
'subscribeFail': '登録に失敗しました。ネットワークを確認するか、後でもう一度お試しください',
'update': 'アップデート',
'updateDaily': '毎日アップデート',
'lastUpdate': '最終アップデート: @time',
'addTracker': 'トラッカーを追加',
'addTrackerHit': 'トラッカーサーバーの URL を 1 行に 1 つずつ入力してください',
'ui': 'UI',
'theme': 'テーマ',
'themeSystem': 'システム',
'themeLight': 'ライト',
'themeDark': 'ダーク',
'locale': '言語',
'about': '概要',
'homepage': 'ホームページ',
'version': 'バージョン',
'protocol': 'プロトコル',
'port': 'ポート',
'apiToken': 'API トークン',
'notSet': '未設置',
'set': '設置',
'effectAfterRestart': '再起動後の効果',
'startAll': 'すべてを開始',
'pauseAll': 'すべてを一時停止',
'deleteTask': 'タスクを削除',
'deleteTaskTip': 'ダウンロードしたファイルを保持',
'delete': '削除',
'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': '今すぐダウンロード',
'newVersionLater': '後で',
};
static const ru_RU = {
'label': 'Русский(Россия)',
'error': 'Ошибка',
@@ -274,6 +340,7 @@ class Locales {
'donate': '打赏',
'exit': '退出',
'create': '创建任务',
'advancedOptions': '高级选项',
'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接',
'downloadLinkHit': '请输入下载链接,支持 HTTP/HTTPS/MAGNET@append',
@@ -282,6 +349,7 @@ class Locales {
'noFileSelected': '请至少选择一个文件下载',
'noStoragePermission': '需要开启存储权限',
'selectFile': '选择文件',
'rename': '重命名',
'basic': '基础',
'advanced': '高级',
'general': '通用',
@@ -289,7 +357,7 @@ class Locales {
'downloadDirValid': '请选择下载目录',
'connections': '连接数',
'maxRunning': '最大下载数',
'items': '@count 条',
'items': '@count 项',
'subscribeTracker': '订阅 Tracker',
'subscribeFail': '订阅失败,请检查网络或稍后重试',
'update': '更新',
+2 -2
View File
@@ -8,7 +8,7 @@ import 'app/modules/app/controllers/app_controller.dart';
import 'app/modules/app/views/app_view.dart';
import 'core/libgopeed_boot.dart';
import 'generated/locales.g.dart';
import 'util/localeManager.dart';
import 'util/locale_manager.dart';
import 'util/log_util.dart';
import 'util/mac_secure_util.dart';
import 'util/package_info.dart';
@@ -26,7 +26,7 @@ Future<void> init() async {
if (Util.isDesktop()) {
await windowManager.ensureInitialized();
const windowOptions = WindowOptions(
size: Size(1024, 768),
size: Size(800, 600),
center: true,
skipTaskbar: false,
);
+1 -1
View File
@@ -1,5 +1,5 @@
findIcon(String filename) {
String res = 'doc';
String res = 'file';
String ext = filename.substring(filename.lastIndexOf('.') + 1);
for (var iconMap in iconMaps) {
if (iconMap['extensions'].contains(ext)) {
+1
View File
@@ -28,6 +28,7 @@ class FaIcons {
static const _kFontFam = 'FontAwesome';
static const Map<String, IconData> allIcons = {
'file': file,
'doc': doc,
'file_image': file_image,
'file_video': file_video,
+30
View File
@@ -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;
}
}
}
+9 -5
View File
@@ -1,19 +1,23 @@
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart';
import 'util.dart';
final logger = Logger(
printer: PrettyPrinter(),
filter: ProductionFilter(),
printer: LogfmtPrinter(),
output: buildOutput(),
);
buildOutput() {
// if is debug mode, output to console
// if is debug mode, don't log to file
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;
}
+1 -1
View File
@@ -12,7 +12,7 @@ void showErrorMessage(msg) {
Get.snackbar(title, ((msg as dynamic).message as Result).msg!);
return;
}
Get.snackbar(title, msg);
Get.snackbar(title, msg.toString());
}
void showMessage(title, msg) {
+48 -8
View File
@@ -1,3 +1,5 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:flutter/foundation.dart';
@@ -10,8 +12,17 @@ class Util {
return path;
}
static String safePathJoin(List<String> paths) {
return paths
.map((e) => safeDir(e))
.join("/")
.replaceAll(RegExp(r'//'), "/");
}
static String fmtByte(int byte) {
if (byte < 1024) {
if (byte < 0) {
return "0 B";
} else if (byte < 1024) {
return "$byte B";
} else if (byte < 1024 * 1024) {
return "${(byte / 1024).toStringAsFixed(2)} KB";
@@ -49,14 +60,43 @@ class Util {
return !kIsWeb && Platform.isMacOS;
}
static isUnix() {
if (kIsWeb) {
return false;
}
return !Platform.isWindows;
}
static isWeb() {
return kIsWeb;
}
static supportUnixSocket() {
if (kIsWeb) {
return false;
}
return Platform.isLinux || Platform.isMacOS || Platform.isAndroid;
}
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;
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 672 B

After

Width:  |  Height:  |  Size: 709 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 71 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

+253 -214
View File
File diff suppressed because it is too large. Load diff
Loaded 100 of 101 files, more files were not shown because too many files have changed in this diff. Show more