Compare commits

...
34 Commits
Author SHA1 Message Date
Levi ef97528114 ci: fix window 2023-11-29 21:12:06 +08:00
Levi b58669bb46 ci: fix windows build 2023-11-29 20:35:20 +08:00
Levi 51d28c5e53 release: v1.5.0 2023-11-29 20:18:08 +08:00
Levi db15650664 feat: support browse files on web (#283) 2023-11-29 18:24:01 +08:00
Levi ee7da179c1 chore: upgrade flutter to v3.16.1 (#282) 2023-11-29 13:32:07 +08:00
Levi 6bb32372ab feat: support more configurations (#281) 2023-11-29 12:52:51 +08:00
Levi 7d6428c967 fix: task can not completed (#280) 2023-11-28 18:27:36 +08:00
Levi bba5107dd5 fix: crash on android arm-32 (#279) 2023-11-27 11:10:50 +08:00
liwei ddfe76b635 fix: script engine event loop stop 2023-11-25 00:43:40 +08:00
Levi aad7395dde feat: enhanced extension script engine (#277)
1. update goja version
2. add node vm polyfill
3. non-stop event loop handle
4 improve logger print
2023-11-24 22:41:16 +08:00
Levi 6f541715df fix: extension download path (#274) 2023-11-24 18:16:03 +08:00
neovali 848797b2fd fix: .deb build (#269) 2023-11-15 21:01:28 +08:00
Levi 0a05bf41e7 fix: incorrect storage path in linux (#270) 2023-11-15 16:26:29 +08:00
liwei 485656d1d4 ci: fix linux desktop entry 2023-11-13 16:23:26 +08:00
liwei c074e0c2bc chore: generate locales 2023-11-13 15:55:02 +08:00
yuloury 0db0a2c987 ci: fix deb desktop entry (#268) 2023-11-11 09:23:17 +08:00
Elliot b06a79b5c7 feat: improve zh_TW translate 2023-11-10 09:11:50 +08:00
liwei 4840c4b8a4 ci: fix build env 2023-11-09 18:49:35 +08:00
Levi e72b097aa2 release: v1.4.4 2023-11-09 17:09:15 +08:00
Levi af52c4ce7e feat: support create task directly (#262) 2023-11-09 16:52:02 +08:00
Levi aae79c2449 feat: running foreground service on mobile (#261) 2023-11-07 17:52:10 +08:00
Levi 83838afce9 feat: support more linux distributions (#254) 2023-11-01 14:54:24 +08:00
Levi 4690107e32 fix: gopeed cli BT download (#258) 2023-10-31 10:16:28 +08:00
liwei 74b08b3631 chore: build locales.g.dart 2023-10-31 09:24:55 +08:00
Kiber_kotan 953d348c0c feat: improve Russian translation (#257) 2023-10-30 11:27:43 +08:00
Levi 00cea68b6e feat: add extension intro links (#252) 2023-10-26 18:35:22 +08:00
Levi daa9d4c707 release: v1.4.3 2023-10-24 16:17:48 +08:00
liwei 6bc587752a chore: update introduction 2023-10-24 16:16:16 +08:00
Levi 11befc09bd refractor: downloader task action dispatch (#249) 2023-10-24 16:02:39 +08:00
liwei ff6c0da262 chore: locales build 2023-10-23 13:55:13 +08:00
Levi aab4a37a7e feat: adapt mobile extension list (#242) 2023-10-21 16:36:22 +08:00
Levi 74c9491893 fix: linux system tray tooltip (#243) 2023-10-21 16:29:15 +08:00
Levi 256faf2cbd feat: add extension setting tooltip (#247) 2023-10-21 16:16:46 +08:00
Om Jogani 32e3620f5e feat: Implemented History Panel for reusing previously used Magnet & Torrent Links (#245) 2023-10-21 15:52:34 +08:00
79 changed files with 2478 additions and 1037 deletions

No files matched your search

+5 -1
View File
@@ -32,6 +32,10 @@ version-resolver:
- 'patch' - 'patch'
default: patch default: patch
template: | template: |
## Changes # Changes
$CHANGES
# 更新日志
$CHANGES $CHANGES
+201 -36
View File
@@ -2,10 +2,19 @@ name: build
on: on:
workflow_dispatch: workflow_dispatch:
inputs:
platform:
description: 'Build platform'
required: true
default: 'all'
channel:
description: 'Snap channel'
required: true
default: 'stable'
env: env:
GO_VERSION: "1.19" GO_VERSION: "1.19"
FLUTTER_VERSION: "3.10.6" FLUTTER_VERSION: "3.16.1"
permissions: permissions:
contents: write contents: write
@@ -24,6 +33,7 @@ jobs:
myToken: ${{ github.token }} myToken: ${{ github.token }}
build-windows: build-windows:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
runs-on: windows-latest runs-on: windows-latest
needs: [ get-release ] needs: [ get-release ]
steps: steps:
@@ -46,7 +56,7 @@ jobs:
flutter build windows flutter build windows
$mingw = "C:\Program Files\Git\mingw64\bin" $mingw = "C:\Program Files\Git\mingw64\bin"
$system = "C:\Windows\System32" $system = "C:\Windows\System32"
$release = "build\windows\runner\Release\" $release = "build\windows\x64\runner\Release\"
cp $mingw\libstdc++-6.dll $release cp $mingw\libstdc++-6.dll $release
cp $mingw\libgcc_s_seh-1.dll $release cp $mingw\libgcc_s_seh-1.dll $release
cp $mingw\libwinpthread-1.dll $release cp $mingw\libwinpthread-1.dll $release
@@ -100,7 +110,7 @@ jobs:
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files] [Files]
Source: ".\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs Source: ".\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs
; NOTE: Don't use "Flags: ignoreversion" on any shared system files ; NOTE: Don't use "Flags: ignoreversion" on any shared system files
[UninstallDelete] [UninstallDelete]
@@ -124,6 +134,7 @@ jobs:
asset_path: ui/flutter/build/windows/Output/* asset_path: ui/flutter/build/windows/Output/*
overwrite: true overwrite: true
build-macos-arm64-lib: build-macos-arm64-lib:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [ get-release ]
steps: steps:
@@ -134,13 +145,14 @@ jobs:
- name: Build - name: Build
run: | run: |
go install src.techknowlogick.com/xgo@latest go install src.techknowlogick.com/xgo@latest
xgo -go go-1.19.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed . xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
mv libgopeed-*.dylib libgopeed.dylib mv libgopeed-*.dylib libgopeed.dylib
- uses: actions/upload-artifact@v3 - uses: actions/upload-artifact@v3
with: with:
name: macos-arm64-lib name: macos-arm64-lib
path: libgopeed.dylib path: libgopeed.dylib
build-macos: build-macos:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
runs-on: macos-latest runs-on: macos-latest
needs: [ get-release, build-macos-arm64-lib ] needs: [ get-release, build-macos-arm64-lib ]
steps: steps:
@@ -155,7 +167,9 @@ jobs:
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Install appdmg - name: Install appdmg
run: npm install -g appdmg run: |
python3 -m pip install setuptools
npm install -g appdmg
- uses: actions/download-artifact@v3 - uses: actions/download-artifact@v3
with: with:
name: macos-arm64-lib name: macos-arm64-lib
@@ -197,7 +211,8 @@ jobs:
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/* asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
overwrite: true overwrite: true
build-linux: build-linux:
runs-on: ubuntu-latest if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
runs-on: ubuntu-20.04
needs: [ get-release ] needs: [ get-release ]
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
@@ -217,46 +232,193 @@ jobs:
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter cd ui/flutter
flutter build linux flutter build linux
mkdir -p debian/gui mkdir -p build/dist
cp assets/icon/icon_1024.png debian/gui/gopeed.png
cat>debian/debian.yaml<<EOF # Build portable
flutter_app: tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./
command: gopeed
arch: x64 # Build deb package
parent: /usr/local/lib mkdir -p debian/gopeed/opt/gopeed
control: mkdir -p debian/gopeed/usr/share/applications
Package: gopeed mkdir -p debian/gopeed/usr/share/icons/hicolor/scalable/apps
Version: ${VERSION:1} mkdir -p debian/gopeed/DEBIAN
Architecture: amd64
Essential: no cp -r build/linux/x64/release/bundle/* debian/gopeed/opt/gopeed/
Priority: optional cp assets/icon/icon.svg debian/gopeed/usr/share/icons/hicolor/scalable/apps/com.gopeed.Gopeed.svg
Depends:
Maintainer: monkeyWie cat>debian/gopeed/DEBIAN/postinst<<EOF
Description: High speed downloader that supports all platforms. #!/bin/bash
ln -sf /opt/gopeed/gopeed /usr/bin/gopeed
EOF EOF
cat>debian/gui/gopeed.desktop<<EOF cat>debian/gopeed/DEBIAN/postrm<<EOF
[Desktop Entry] #!/bin/bash
Version=${VERSION:1} rm -rf /usr/bin/gopeed
Name=Gopeed
GenericName=Gopeed
Comment=High speed downloader that supports all platforms.
Terminal=false
Type=Application
Categories=Utility
Keywords=Flutter;Downloader;
EOF EOF
dart pub global activate flutter_to_debian chmod 0755 debian/gopeed/DEBIAN/postinst
flutter_to_debian chmod 0755 debian/gopeed/DEBIAN/postrm
mv debian/packages/gopeed*.deb debian/packages/Gopeed-$VERSION-linux-amd64.deb
cat>debian/gopeed/DEBIAN/control<<EOF
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cp linux/assets/com.gopeed.Gopeed.desktop debian/gopeed/usr/share/applications/gopeed.desktop
dpkg-deb --build debian/gopeed
mv debian/gopeed.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
# Build AppImage
sudo apt-get -y install libfuse2
wget -O appimage-builder https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder
mkdir AppDir
cp -r build/linux/x64/release/bundle/* AppDir
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
cp assets/icon/icon.svg AppDir/usr/share/icons/hicolor/scalable/apps/gopeed.svg
cat>AppImageBuilder.yml<<EOF
version: 1
AppDir:
path: ${PWD}/AppDir
app_info:
id: com.gopeed.gopeed
name: Gopeed
icon: gopeed
version: ${VERSION:1}
exec: gopeed
exec_args: \$@
apt:
arch: amd64
allow_unauthenticated: true
sources:
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted
universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main
restricted universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports
main restricted universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted
universe multiverse
include: []
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: x86_64
update-information: guess
EOF
./appimage-builder --skip-tests --recipe AppImageBuilder.yml
mv Gopeed-*-x86_64.AppImage build/dist/Gopeed-$VERSION-linux-amd64.AppImage
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/debian/packages/* asset_path: ui/flutter/build/dist/*
overwrite: true overwrite: true
build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
runs-on: ubuntu-latest
needs: [ get-release ]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
with:
go-version: ${{ env.GO_VERSION }}
- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Setup LXD
uses: canonical/[email protected]
with:
channel: latest/stable
- name: Build
env:
VERSION: ${{ needs.get-release.outputs.tag_name }}
run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
sudo snap install snapcraft --classic
mkdir -p snap/gui
cp assets/icon/icon.svg snap/gui/gopeed.svg
cat>snap/snapcraft.yaml<<EOF
name: gopeed
version: ${VERSION:1}
summary: A modern download manager
description: High speed downloader that supports all platforms.
confinement: strict
base: core22
grade: stable
architectures:
- build-on: amd64
build-for: amd64
slots:
dbus-gopeed:
interface: dbus
bus: session
name: com.gopeed.gopeed
apps:
gopeed:
command: gopeed
extensions: [gnome]
plugs:
- network
slots:
- dbus-gopeed
parts:
gopeed:
source: .
plugin: flutter
stage-packages:
- libgtk-3-dev
- libayatana-appindicator3-dev
flutter-target: lib/main.dart
EOF
cp linux/assets/com.gopeed.Gopeed.desktop snap/gui/gopeed.desktop
sed -i 's/Icon=com.gopeed.Gopeed/Icon=\${SNAP}\/meta\/gui\/gopeed.svg/g' snap/gui/gopeed.desktop
snapcraft --use-lxd
# Snapcraft login
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
snapcraft upload --release=${{ github.event.inputs.channel }} gopeed_${VERSION:1}_amd64.snap
build-android: build-android:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [ get-release ]
steps: steps:
@@ -294,6 +456,7 @@ jobs:
asset_path: ui/flutter/dist/* asset_path: ui/flutter/dist/*
overwrite: true overwrite: true
build-ios: build-ios:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
runs-on: macos-latest runs-on: macos-latest
needs: [ get-release ] needs: [ get-release ]
steps: steps:
@@ -326,6 +489,7 @@ jobs:
asset_path: ui/flutter/dist/* asset_path: ui/flutter/dist/*
overwrite: true overwrite: true
build-web: build-web:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [ get-release ]
steps: steps:
@@ -373,6 +537,7 @@ jobs:
asset_path: dist/zip/* asset_path: dist/zip/*
overwrite: true overwrite: true
build-docker: build-docker:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
needs: [ get-release ] needs: [ get-release ]
steps: steps:
+1 -1
View File
@@ -28,7 +28,7 @@ on:
env: env:
GO_VERSION: "1.19" GO_VERSION: "1.19"
FLUTTER_VERSION: "3.10.6" FLUTTER_VERSION: "3.16.1"
jobs: jobs:
# lint: # lint:
+46
View File
@@ -0,0 +1,46 @@
# Gopeed 協助指南
首先感謝您願意幫助我們改進並優化該項目,這份指南將會幫助您更好的參與 Gopeed 的開發。
## 分支說明
本項目只有一個分支,即 `main` 分支,如果您想要參與 Gopeed 的開發,請先 fork 該項目,再在您自己的 fork 中進行開發,開發完成後再開啟PR,以合併至 `main` 分支。
## 離線開發
建議使用 web 端進行開發與調試,首先啟動服務,使用指令 `go run cmd/api/main.go` 啟動 ,該服務默認連接埠為 `9999`,接著以 `debug` 模式啟動前端 flutter 項目即可。
## 翻譯
Gopeed 的翻譯文件位於 `ui/flutter/assets/locales` 目錄中,只需要修改或新建翻譯文件即可。
修改locales後請執行一下指令:
```
get generate locales
```
請以 `en_US.json` 作為參照。
## flutter開發
每次提交PR前請務必執行 `dart format ./ui/flutter`
如果需要編輯 api/models,請打開build_runner watcher:
```
flutter pub run build_runner watch
```
適用於 get-cli 的指令:
```
create:
controller: Generate controller
page: Use to generate pages
view: Generate view
generate:
locales: Generate translation file from json files
model: generate Class model from json
```
+1 -1
View File
@@ -1,4 +1,4 @@
FROM instrumentisto/flutter:3.10.6 AS flutter FROM instrumentisto/flutter:3.16.1 AS flutter
WORKDIR /app WORKDIR /app
COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./ COPY ./ui/flutter/pubspec.yaml ./ui/flutter/pubspec.lock ./
RUN flutter pub get RUN flutter pub get
+3 -2
View File
@@ -9,11 +9,12 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## Introduction ## Introduction
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com) Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
+2 -2
View File
@@ -9,11 +9,11 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## はじめに ## はじめに
Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。 Gopeed (正式名 Go Speed) は `Golang` + `Flutter` によって開発された高速ダウンローダーで、(HTTP、BitTorrent、Magnet) プロトコルをサポートし、すべてのプラットフォームをサポートします。基本的なダウンロード機能に加え、[APIs](https://docs.gopeed.com/dev-api.html)との連動や[拡張機能](https://docs.gopeed.com/dev-extension.html)のインストール・開発による追加機能にも対応した、カスタマイズ性の高いダウンローダーです。
見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com) 見て下さい ✈ [公式ウェブサイト](https://gopeed.com) | 📖 [開発ドキュメント](https://docs.gopeed.com)
+2 -2
View File
@@ -9,11 +9,11 @@
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) [English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## 介绍 ## 介绍
Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。 Gopeed(全称 Go Speed),是一款由`Golang`+`Flutter`开发的高速下载器,支持(HTTP、BitTorrent、Magnet)协议下载,并且支持全平台使用。除了基本的下载功能外,Gopeed 还是一款高度可定制化的下载器,支持通过对接[APIs](https://docs.gopeed.com/zh/dev-api.html)或者安装和开发[扩展](https://docs.gopeed.com/zh/dev-extension.html)来实现更多的功能。
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/) 访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
+179
View File
@@ -0,0 +1,179 @@
[![Test Status](https://github.com/GopeedLab/gopeed/workflows/test/badge.svg)](https://github.com/GopeedLab/gopeed/actions?query=workflow%3Atest)
[![Codecov](https://codecov.io/gh/GopeedLab/gopeed/branch/main/graph/badge.svg)](https://codecov.io/gh/GopeedLab/gopeed)
[![Release](https://img.shields.io/github/release/GopeedLab/gopeed.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Download](https://img.shields.io/github/downloads/GopeedLab/gopeed/total.svg)](https://github.com/GopeedLab/gopeed/releases)
[![Donate](https://img.shields.io/badge/%24-donate-ff69b4.svg)](https://docs.gopeed.com/donate.html)
[![Discord](https://img.shields.io/discord/1037992631881449472?label=Discord&logo=discord&style=social)](https://discord.gg/ZUJqJrwCGB)
![](_docs/img/banner.png)
[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R6IJGN6)
[English](/README.md) | [中文](/README_zh-CN.md) | [日本語](/README_ja-JP.md) | [正體中文](/README_zh-TW.md)
## 簡介
Gopeed(全稱 Go Speed),是一款使用`Golang`+`Flutter`編寫的高速下載軟體,支援(HTTP、BitTorrent、Magnet)協定,同時支援所有的平台。
前往 ✈ [主頁](https://gopeed.com/zh-CN) | 📖 [文檔](https://docs.gopeed.com/zh/)
## 安裝
**已支援的平台**
- [x] Windows
- [x] MacOS
- [x] Linux
- [x] Android
- [x] iOS
- [x] Web
- [x] Docker
[下載](https://github.com/GopeedLab/gopeed/releases/latest)
> 註:MacOS 版運行時若提示損壞,請在終端中執行 `xattr -d com.apple.quarantine /Applications/Gopeed.app`
### 使用CLI安裝
使用`go install`安裝:
```bash
go install github.com/GopeedLab/gopeed/cmd/gopeed@latest
```
### Docker
#### 直接執行
```bash
docker run -d -p 9999:9999 -v /path/to/download:/root/Downloads liwei2633/gopeed
```
#### 使用 Docker Compose
```bash
docker-compose up -d
```
#### 訪問服務
當 docker 容器運作時,可以通過 `http://localhost:9999` 訪問 web 頁面。
## 贊助
如果你認為該項目對你有所幫助,請考慮[贊助](https://docs.gopeed.com/zh/donate)以支持該項目的持續發展,謝謝!
## 軟體介面
![](_docs/img/ui-demo.png)
## 開發
該項目分為前端與後端,前端使用`flutter`編寫,後端使用`Golang`編寫,兩邊通過`http`協定進行通訊,在 unix 系統下,則使用`unix socket`,在 windows 系統下,則使用`tcp`協定。
> 前端代碼位於`ui/flutter`目錄內。
### 開發環境
1. Golang 1.19+
2. Flutter 3.10+
### 克隆項目
```bash
git clone [email protected]:GopeedLab/gopeed.git
```
### 協助開發
請參考[協助指南](CONTRIBUTING_zh-TW.md)
### 編譯
#### 桌面端
首先需要按照[flutter desktop 官方文檔](https://docs.flutter.dev/development/platform-integration/desktop)配置開發環境,並準備好`cgo`環境,具體方法可以自行搜索。
組建指令:
- windows
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/windows/libgopeed.dll github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build windows
```
- macos
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/macos/Frameworks/libgopeed.dylib github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build macos
```
- linux
```bash
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
cd ui/flutter
flutter build linux
```
#### 移動設備
需要`cgo`環境,並安裝`gomobile`:
```bash
go install golang.org/x/mobile/cmd/gomobile@latest
go get golang.org/x/mobile/bind
gomobile init
```
組建指令:
- android
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 19 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build apk
```
- ios
```bash
gomobile bind -tags nosqlite -ldflags="-w -s" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
cd ui/flutter
flutter build ios --no-codesign
```
#### 網頁端
組建指令:
```bash
cd ui/flutter
flutter build web
cd ../../
rm -rf cmd/web/dist
cp -r ui/flutter/build/web cmd/web/dist
go build -tags nosqlite,web -ldflags="-s -w" -o bin/ github.com/GopeedLab/gopeed/cmd/web
go run cmd/web/main.go
```
## 感謝
### 貢獻者
<a href="https://github.com/GopeedLab/gopeed/graphs/contributors">
<img src="https://contrib.rocks/image?repo=GopeedLab/gopeed" />
</a>
### JetBrains
[![goland](_docs/img/goland.svg)](https://www.jetbrains.com/?from=gopeed)
## 軟體許可
該軟體遵循 [GPLv3](LICENSE) 。
+4 -3
View File
@@ -8,9 +8,10 @@ import (
// only for local development // only for local development
func main() { func main() {
cfg := &model.StartConfig{ cfg := &model.StartConfig{
Network: "tcp", Network: "tcp",
Address: "127.0.0.1:9999", Address: "127.0.0.1:9999",
Storage: model.StorageBolt, Storage: model.StorageBolt,
WebEnable: true,
} }
cmd.Start(cfg) cmd.Start(cfg)
} }
+14 -1
View File
@@ -2,6 +2,7 @@ package main
import ( import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/protocol/http" "github.com/GopeedLab/gopeed/pkg/protocol/http"
@@ -47,16 +48,28 @@ func main() {
if err != nil { if err != nil {
panic(err) panic(err)
} }
printProgress(emptyTask, "downloading...")
wg.Wait() wg.Wait()
} }
var ( var (
lastLineLen = 0 lastLineLen = 0
sb = new(strings.Builder) sb = new(strings.Builder)
emptyTask = &download.Task{
Progress: &download.Progress{},
Meta: &fetcher.FetcherMeta{
Res: &base.Resource{},
},
}
) )
func printProgress(task *download.Task, title string) { func printProgress(task *download.Task, title string) {
rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size) var rate float64
if task.Meta.Res.Size <= 0 {
rate = 0
} else {
rate = float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
}
completeWidth := int(progressWidth * rate) completeWidth := int(progressWidth * rate)
speed := util.ByteFmt(task.Progress.Speed) speed := util.ByteFmt(task.Progress.Speed)
totalSize := util.ByteFmt(task.Meta.Res.Size) totalSize := util.ByteFmt(task.Meta.Res.Size)
+11 -11
View File
@@ -5,14 +5,16 @@ go 1.19
require ( require (
github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9 github.com/anacrolix/missinggo/v2 v2.7.2-0.20230527121029-a582b4f397b9
github.com/anacrolix/torrent v1.52.5 github.com/anacrolix/torrent v1.52.5
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c
github.com/go-git/go-git/v5 v5.8.1 github.com/go-git/go-git/v5 v5.8.1
github.com/gorilla/handlers v1.5.1 github.com/gorilla/handlers v1.5.1
github.com/gorilla/mux v1.8.0 github.com/gorilla/mux v1.8.0
github.com/matoous/go-nanoid/v2 v2.0.0 github.com/matoous/go-nanoid/v2 v2.0.0
github.com/rs/zerolog v1.31.0
github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459 github.com/virtuald/go-paniclog v0.0.0-20190812204905-43a7fa316459
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 go.etcd.io/bbolt v1.3.8
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d
golang.org/x/sync v0.3.0 golang.org/x/sync v0.3.0
) )
@@ -45,7 +47,7 @@ require (
github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect github.com/bradfitz/iter v0.0.0-20191230175014-e8f45d346db8 // indirect
github.com/cloudflare/circl v1.3.3 // indirect github.com/cloudflare/circl v1.3.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dlclark/regexp2 v1.7.0 // indirect github.com/dlclark/regexp2 v1.10.0 // indirect
github.com/dustin/go-humanize v1.0.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect
github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/edsrzf/mmap-go v1.1.0 // indirect
github.com/emirpasic/gods v1.18.1 // indirect github.com/emirpasic/gods v1.18.1 // indirect
@@ -57,7 +59,7 @@ require (
github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect github.com/go-sourcemap/sourcemap v2.1.3+incompatible // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
github.com/google/btree v1.1.2 // indirect github.com/google/btree v1.1.2 // indirect
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 // indirect github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/google/uuid v1.3.0 // indirect github.com/google/uuid v1.3.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect
github.com/huandu/xstrings v1.3.2 // indirect github.com/huandu/xstrings v1.3.2 // indirect
@@ -88,19 +90,17 @@ require (
github.com/pjbgf/sha1cd v0.3.0 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect github.com/rs/dnscache v0.0.0-20211102005908-e0241e321417 // indirect
github.com/rs/zerolog v1.31.0 // indirect
github.com/sergi/go-diff v1.1.0 // indirect github.com/sergi/go-diff v1.1.0 // indirect
github.com/skeema/knownhosts v1.2.0 // indirect github.com/skeema/knownhosts v1.2.0 // indirect
github.com/tidwall/btree v1.6.0 // indirect github.com/tidwall/btree v1.6.0 // indirect
github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect
go.opentelemetry.io/otel v1.8.0 // indirect go.opentelemetry.io/otel v1.8.0 // indirect
go.opentelemetry.io/otel/trace v1.8.0 // indirect go.opentelemetry.io/otel/trace v1.8.0 // indirect
golang.org/x/crypto v0.11.0 // indirect golang.org/x/crypto v0.14.0 // indirect
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d // indirect
golang.org/x/mod v0.8.0 // indirect golang.org/x/mod v0.8.0 // indirect
golang.org/x/net v0.12.0 // indirect golang.org/x/net v0.17.0 // indirect
golang.org/x/sys v0.13.0 // indirect golang.org/x/sys v0.14.0 // indirect
golang.org/x/text v0.11.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect
golang.org/x/tools v0.6.0 // indirect golang.org/x/tools v0.6.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect
+19 -18
View File
@@ -124,17 +124,17 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc= github.com/dlclark/regexp2 v1.4.1-0.20201116162257-a2a8dda75c91/go.mod h1:2pZnwuY/m+8K6iRw6wQdMtk+rH5tNGR1i55kozfMjCc=
github.com/dlclark/regexp2 v1.7.0 h1:7lJfhqlPssTb1WQx4yvTHN0uElPEv52sbaECrAQxjAo=
github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dlclark/regexp2 v1.7.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/dlclark/regexp2 v1.10.0 h1:+/GIL799phkJqYW+3YbOd8LCcbHzT0Pbo8zl70MHsq0=
github.com/dlclark/regexp2 v1.10.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE=
github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk= github.com/dop251/goja v0.0.0-20211022113120-dc8c55024d06/go.mod h1:R9ET47fwRVRPZnOGvHxxhuZcbrMCuiqOz3Rlrh4KSnk=
github.com/dop251/goja v0.0.0-20230531210528-d7324b2d74f7/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4= github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d h1:wi6jN5LVt/ljaBG4ue79Ekzb12QfJ52L9Q98tl8SWhw=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e h1:UvQD6hTSfeM6hhTQ24Dlw2RppP05W7SWbWb6kubJAog= github.com/dop251/goja v0.0.0-20231027120936-b396bb4c349d/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja v0.0.0-20230828202809-3dbe69dd2b8e/go.mod h1:QMWlm50DNe14hD7t24KEqZuUdC9sOTy8W6XbCU1mlw4=
github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y= github.com/dop251/goja_nodejs v0.0.0-20210225215109-d91c329300e7/go.mod h1:hn7BA7c8pLvoGndExHudxTDKZ84Pyvv+90pbBjbTz0Y=
github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM= github.com/dop251/goja_nodejs v0.0.0-20211022123610-8dd9abb0616d/go.mod h1:DngW8aVqWbuLRMHItjPUyqdj+HWPvnQe8V8y1nDpIbM=
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562 h1:ObbB2tzHWWAxzsG5futqeq2Ual2zYlo/+eMkSc5sn8w= github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c h1:hLoodLRD4KLWIH8eyAQCLcH8EqIrjac7fCkp/fHnvuQ=
github.com/dop251/goja_nodejs v0.0.0-20230821135201-94e508132562/go.mod h1:X2TOTJ+Uamd454RFp7ig2tmP3hQg0Z2Qk8gbVQmU0mk= github.com/dop251/goja_nodejs v0.0.0-20231122114759-e84d9a924c5c/go.mod h1:bhGPmCgCCTSRfiMYWjpS46IDo9EUZXlsuUaPXSWGbv0=
github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v0.0.0-20180421182945-02af3965c54e/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
@@ -213,8 +213,9 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg= github.com/google/pprof v0.0.0-20230207041349-798e818bf904/go.mod h1:uglQLonpP8qtYCYyzA+8c/9qtqgA3qsXGYqCPKARAFg=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@@ -420,6 +421,8 @@ github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9dec
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 h1:NkHVT1GBVV9hUj1iWglFxAuNlvLHELFn71U2H7aOIog= go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6 h1:NkHVT1GBVV9hUj1iWglFxAuNlvLHELFn71U2H7aOIog=
go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6/go.mod h1:tDI2vlr15+D9cyN3odhFrZJi3oJj8HlHaOEFPrUiGSY= go.etcd.io/bbolt v1.3.7-0.20231005155708-db45d0d6b1f6/go.mod h1:tDI2vlr15+D9cyN3odhFrZJi3oJj8HlHaOEFPrUiGSY=
go.etcd.io/bbolt v1.3.8 h1:xs88BrvEv273UsB79e0hcVrlUWmS0a8upikMFhSyAtA=
go.etcd.io/bbolt v1.3.8/go.mod h1:N9Mkw9X8x5fupy0IKsmuqVtoGDyxsaDlbk4Rd05IAQw=
go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk=
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
@@ -439,8 +442,8 @@ golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d h1:vtUKgx8dahOomfFzLREU8nSv25YHnTgLBn4rDnWZdU0=
golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA= golang.org/x/exp v0.0.0-20220613132600-b0d781184e0d/go.mod h1:Kr81I6Kryrl9sr8s2FK3vxD90NdsKWRuOIl2O4CvYbA=
@@ -482,9 +485,8 @@ golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -534,10 +536,11 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.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.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
@@ -545,8 +548,7 @@ golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek=
golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 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.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -557,9 +559,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/time v0.0.0-20220609170525-579cf78fd858 h1:Dpdu/EMxGMFgq0CeYMh4fazTD2vtlZRYE7wyynxJb9U= 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/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= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+3 -3
View File
@@ -6,7 +6,7 @@ import (
) )
type Controller struct { type Controller struct {
GetConfig func(v any) (bool, error) GetConfig func(v any) bool
FileController FileController
//ContextDialer() (proxy.Dialer, error) //ContextDialer() (proxy.Dialer, error)
} }
@@ -21,8 +21,8 @@ type DefaultFileController struct {
func NewController() *Controller { func NewController() *Controller {
return &Controller{ return &Controller{
FileController: &DefaultFileController{}, FileController: &DefaultFileController{},
GetConfig: func(v any) (bool, error) { GetConfig: func(v any) bool {
return false, nil return false
}, },
} }
} }
+9
View File
@@ -57,6 +57,15 @@ func (m *FetcherMeta) SingleFilepath() string {
return path.Join(m.Opts.Path, file.Path, fileName) return path.Join(m.Opts.Path, file.Path, fileName)
} }
// RootDirPath return the root dir path of the task file.
func (m *FetcherMeta) RootDirPath() string {
if m.Res.Name != "" {
return m.FolderPath()
} else {
return m.Opts.Path
}
}
// FetcherBuilder defines the interface for a fetcher builder. // FetcherBuilder defines the interface for a fetcher builder.
type FetcherBuilder interface { type FetcherBuilder interface {
// Schemes returns the schemes supported by the fetcher. // Schemes returns the schemes supported by the fetcher.
+2 -1
View File
@@ -1,5 +1,6 @@
package bt package bt
type config struct { type config struct {
Trackers []string `json:"trackers"` ListenPort int `json:"listenPort"`
Trackers []string `json:"trackers"`
} }
+23 -13
View File
@@ -23,7 +23,8 @@ var (
) )
type Fetcher struct { type Fetcher struct {
ctl *controller.Controller ctl *controller.Controller
config *config
torrent *torrent.Torrent torrent *torrent.Torrent
meta *fetcher.FetcherMeta meta *fetcher.FetcherMeta
@@ -42,6 +43,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
if f.meta == nil { if f.meta == nil {
f.meta = &fetcher.FetcherMeta{} f.meta = &fetcher.FetcherMeta{}
} }
exist := f.ctl.GetConfig(&f.config)
if !exist {
f.config = &config{
ListenPort: 0,
Trackers: []string{},
}
}
return return
} }
@@ -54,7 +62,7 @@ func (f *Fetcher) initClient() (err error) {
} }
cfg := torrent.NewDefaultClientConfig() cfg := torrent.NewDefaultClientConfig()
cfg.ListenPort = 0 cfg.ListenPort = f.config.ListenPort
cfg.DefaultStorage = newFileOpts(newFileClientOpts{ cfg.DefaultStorage = newFileOpts(newFileClientOpts{
ClientBaseDir: cfg.DataDir, ClientBaseDir: cfg.DataDir,
HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) { HandleFileTorrent: func(infoHash metainfo.Hash, ft *fileTorrentImpl) {
@@ -87,7 +95,6 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
if f.meta.Res != nil { if f.meta.Res != nil {
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath() torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
} }
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
return nil return nil
} }
@@ -101,6 +108,14 @@ func (f *Fetcher) Start() (err error) {
ft.setTorrentDir(f.meta.FolderPath()) ft.setTorrentDir(f.meta.FolderPath())
} }
files := f.torrent.Files() files := f.torrent.Files()
// If the user does not specify the file to download, all files will be downloaded by default
if len(f.meta.Opts.SelectFiles) == 0 {
f.meta.Opts.SelectFiles = make([]int, len(files))
for i := range files {
f.meta.Opts.SelectFiles[i] = i
}
}
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
if len(f.meta.Opts.SelectFiles) == len(files) { if len(f.meta.Opts.SelectFiles) == len(files) {
f.torrent.DownloadAll() f.torrent.DownloadAll()
} else { } else {
@@ -134,7 +149,7 @@ func (f *Fetcher) safeDrop() {
func (f *Fetcher) Wait() (err error) { func (f *Fetcher) Wait() (err error) {
for { for {
if f.torrentReady.Load() { if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
done := true done := true
for _, selectIndex := range f.meta.Opts.SelectFiles { for _, selectIndex := range f.meta.Opts.SelectFiles {
file := f.torrent.Files()[selectIndex] file := f.torrent.Files()[selectIndex]
@@ -154,7 +169,7 @@ func (f *Fetcher) Wait() (err error) {
} }
} }
if !selected { if !selected {
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path())) util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
} }
} }
break break
@@ -195,7 +210,7 @@ func (f *Fetcher) updateRes() {
Size: file.Length(), Size: file.Length(),
} }
} }
res.CalcSize() res.CalcSize(nil)
f.meta.Res = res f.meta.Res = res
if f.meta.Opts != nil { if f.meta.Opts != nil {
f.meta.Opts.InitSelectFiles(len(res.Files)) f.meta.Opts.InitSelectFiles(len(res.Files))
@@ -224,11 +239,6 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
if err != nil { if err != nil {
return return
} }
var cfg config
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return
}
// use map to deduplicate // use map to deduplicate
trackers := make(map[string]bool) trackers := make(map[string]bool)
@@ -240,8 +250,8 @@ func (f *Fetcher) addTorrent(req *base.Request) (err error) {
} }
} }
} }
if exist && len(cfg.Trackers) > 0 { if len(f.config.Trackers) > 0 {
for _, tracker := range cfg.Trackers { for _, tracker := range f.config.Trackers {
trackers[tracker] = true trackers[tracker] = true
} }
} }
+3 -3
View File
@@ -98,11 +98,11 @@ func buildConfigFetcher() fetcher.Fetcher {
"udp://tracker.birkenwald.de:6969/announce", "udp://tracker.birkenwald.de:6969/announce",
"udp://tracker.bitsearch.to:1337/announce", "udp://tracker.bitsearch.to:1337/announce",
}} }}
newController.GetConfig = func(v any) (bool, error) { newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil { if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false, err return false
} }
return true, nil return true
} }
fetcher.Setup(newController) fetcher.Setup(newController)
return fetcher return fetcher
+2 -1
View File
@@ -1,5 +1,6 @@
package http package http
type config struct { type config struct {
Connections int `json:"connections"` UserAgent string `json:"userAgent"`
Connections int `json:"connections"`
} }
+62 -60
View File
@@ -51,6 +51,7 @@ func newChunk(begin int64, end int64) *chunk {
type Fetcher struct { type Fetcher struct {
ctl *controller.Controller ctl *controller.Controller
config *config
doneCh chan error doneCh chan error
meta *fetcher.FetcherMeta meta *fetcher.FetcherMeta
@@ -71,6 +72,13 @@ func (f *Fetcher) Setup(ctl *controller.Controller) {
if f.meta == nil { if f.meta == nil {
f.meta = &fetcher.FetcherMeta{} f.meta = &fetcher.FetcherMeta{}
} }
exist := f.ctl.GetConfig(&f.config)
if !exist {
f.config = &config{
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
Connections: 1,
}
}
return return
} }
@@ -78,7 +86,7 @@ func (f *Fetcher) Resolve(req *base.Request) error {
if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil { if err := base.ParseReqExtra[fhttp.ReqExtra](req); err != nil {
return err return err
} }
httpReq, err := buildRequest(nil, req) httpReq, err := f.buildRequest(nil, req)
if err != nil { if err != nil {
return err return err
} }
@@ -160,16 +168,7 @@ func (f *Fetcher) Create(opts *base.Options) error {
} }
extra := opts.Extra.(*fhttp.OptsExtra) extra := opts.Extra.(*fhttp.OptsExtra)
if extra.Connections == 0 { if extra.Connections == 0 {
var cfg config extra.Connections = f.config.Connections
exist, err := f.ctl.GetConfig(&cfg)
if err != nil {
return err
}
if exist {
extra.Connections = cfg.Connections
} else {
extra.Connections = 1
}
} }
return nil return nil
} }
@@ -260,7 +259,7 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
chunk := f.chunks[index] chunk := f.chunks[index]
chunk.retryTimes = 0 chunk.retryTimes = 0
httpReq, err := buildRequest(ctx, f.meta.Req) httpReq, err := f.buildRequest(ctx, f.meta.Req)
if err != nil { if err != nil {
return err return err
} }
@@ -314,6 +313,10 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return nil return nil
}() }()
if err != nil { if err != nil {
// If canceled, do not retry
if errors.Is(err, context.Canceled) {
return
}
// retry request after 1 second // retry request after 1 second
chunk.retryTimes = chunk.retryTimes + 1 chunk.retryTimes = chunk.retryTimes + 1
time.Sleep(time.Second) time.Sleep(time.Second)
@@ -353,6 +356,52 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return return
} }
func (f *Fetcher) buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
url, err := url.Parse(req.URL)
if err != nil {
return
}
var (
method string
body io.Reader
)
headers := make(map[string][]string)
if req.Extra == nil {
method = http.MethodGet
} else {
extra := req.Extra.(*fhttp.ReqExtra)
if extra.Method != "" {
method = extra.Method
} else {
method = http.MethodGet
}
if len(extra.Header) > 0 {
for k, v := range extra.Header {
headers[k] = []string{v}
}
}
if extra.Body != "" {
body = bytes.NewBufferString(extra.Body)
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
// load user agent from config
headers[base.HttpHeaderUserAgent] = []string{f.config.UserAgent}
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
} else {
httpReq, err = http.NewRequest(method, url.String(), body)
}
if err != nil {
return
}
httpReq.Header = headers
return httpReq, nil
}
func (f *Fetcher) splitChunk() (chunks []*chunk) { func (f *Fetcher) splitChunk() (chunks []*chunk) {
if f.meta.Res.Range { if f.meta.Res.Range {
connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections connections := f.meta.Opts.Extra.(*fhttp.OptsExtra).Connections
@@ -389,51 +438,6 @@ func buildClient() *http.Client {
} }
} }
func buildRequest(ctx context.Context, req *base.Request) (httpReq *http.Request, err error) {
url, err := url.Parse(req.URL)
if err != nil {
return
}
var (
method string
body io.Reader
)
headers := make(map[string][]string)
if req.Extra == nil {
method = http.MethodGet
} else {
extra := req.Extra.(*fhttp.ReqExtra)
if extra.Method != "" {
method = extra.Method
} else {
method = http.MethodGet
}
if len(extra.Header) > 0 {
for k, v := range extra.Header {
headers[k] = []string{v}
}
}
if extra.Body != "" {
body = bytes.NewBufferString(extra.Body)
}
}
if _, ok := headers[base.HttpHeaderUserAgent]; !ok {
headers[base.HttpHeaderUserAgent] = []string{base.AgentName}
}
if ctx != nil {
httpReq, err = http.NewRequestWithContext(ctx, method, url.String(), body)
} else {
httpReq, err = http.NewRequest(method, url.String(), body)
}
if err != nil {
return
}
httpReq.Header = headers
return httpReq, nil
}
type fetcherData struct { type fetcherData struct {
Chunks []*chunk Chunks []*chunk
} }
@@ -466,9 +470,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
fetcher.meta = meta fetcher.meta = meta
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req) base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts) base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
if len(fd.Chunks) == 0 { if len(fd.Chunks) > 0 {
fetcher.chunks = fetcher.splitChunk()
} else {
fetcher.chunks = fd.Chunks fetcher.chunks = fd.Chunks
} }
return fetcher return fetcher
+4 -4
View File
@@ -123,7 +123,7 @@ func TestFetcher_DownloadError(t *testing.T) {
} }
func TestFetcher_DownloadLimit(t *testing.T) { func TestFetcher_DownloadLimit(t *testing.T) {
listener := test.StartTestLimitServer() listener := test.StartTestLimitServer(4, 0)
defer listener.Close() defer listener.Close()
downloadNormal(listener, 1, t) downloadNormal(listener, 1, t)
@@ -314,11 +314,11 @@ func buildConfigFetcher() fetcher.Fetcher {
mockCfg := config{ mockCfg := config{
Connections: 16, Connections: 16,
} }
newController.GetConfig = func(v any) (bool, error) { newController.GetConfig = func(v any) bool {
if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil { if err := json.Unmarshal([]byte(test.ToJson(mockCfg)), v); err != nil {
return false, err return false
} }
return true, nil return true
} }
fetcher.Setup(newController) fetcher.Setup(newController)
return fetcher return fetcher
+48 -4
View File
@@ -138,7 +138,7 @@ func StartTestErrorServer() net.Listener {
} }
// StartTestLimitServer connections limit server // StartTestLimitServer connections limit server
func StartTestLimitServer() net.Listener { func StartTestLimitServer(maxConnections int32, delay int64) net.Listener {
var connections atomic.Int32 var connections atomic.Int32
return startTestServer(func() http.Handler { return startTestServer(func() http.Handler {
@@ -148,7 +148,7 @@ func StartTestLimitServer() net.Listener {
connections.Add(-1) connections.Add(-1)
}() }()
connections.Add(1) connections.Add(1)
if connections.Load() > 4 { if maxConnections != 0 && connections.Load() > maxConnections {
writer.WriteHeader(403) writer.WriteHeader(403)
return return
} }
@@ -162,7 +162,7 @@ func StartTestLimitServer() net.Listener {
panic(err) panic(err)
} }
defer file.Close() defer file.Close()
io.Copy(writer, file) slowCopy(writer, file, delay)
} else { } else {
// split range // split range
s := strings.Split(r, "=") s := strings.Split(r, "=")
@@ -203,13 +203,57 @@ func StartTestLimitServer() net.Listener {
} }
defer file.Close() defer file.Close()
file.Seek(start, 0) file.Seek(start, 0)
io.CopyN(writer, file, end-start+1) slowCopyN(writer, file, end-start+1, delay)
} }
}) })
return mux return mux
}) })
} }
// slowCopyN copies n bytes from src to dst, speed limit is bytes per second
func slowCopy(dst io.Writer, src io.Reader, delay int64) (written int64, err error) {
buf := make([]byte, 32*1024)
for {
nr, er := src.Read(buf)
if nr > 0 {
nw, ew := dst.Write(buf[0:nr])
if nw > 0 {
written += int64(nw)
}
if ew != nil {
err = ew
break
}
if nr != nw {
err = io.ErrShortWrite
break
}
}
if er != nil {
if er != io.EOF {
err = er
}
break
}
if delay > 0 {
time.Sleep(time.Millisecond * time.Duration(delay))
}
}
return written, err
}
func slowCopyN(dst io.Writer, src io.Reader, n int64, delay int64) (written int64, err error) {
written, err = slowCopy(dst, io.LimitReader(src, n), delay)
if written == n {
return n, nil
}
if written < n && err == nil {
// src stopped early; must have been EOF.
err = io.EOF
}
return
}
func startTestServer(serverHandle func() http.Handler) net.Listener { func startTestServer(serverHandle func() http.Handler) net.Listener {
listener, err := net.Listen("tcp", "127.0.0.1:0") listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil { if err != nil {
-4
View File
@@ -23,7 +23,3 @@ const (
HttpHeaderRangeFormat = "bytes=%d-%d" HttpHeaderRangeFormat = "bytes=%d-%d"
) )
const (
AgentName = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36"
)
+6 -3
View File
@@ -3,6 +3,7 @@ package base
import ( import (
"fmt" "fmt"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"golang.org/x/exp/slices"
) )
// Request download request // Request download request
@@ -47,10 +48,12 @@ func (r *Resource) Validate() error {
return nil return nil
} }
func (r *Resource) CalcSize() { func (r *Resource) CalcSize(selectFiles []int) {
var size int64 var size int64
for _, file := range r.Files { for i, file := range r.Files {
size += file.Size if len(selectFiles) == 0 || slices.Contains(selectFiles, i) {
size += file.Size
}
} }
r.Size = size r.Size = size
} }
+289 -218
View File
@@ -9,7 +9,6 @@ import (
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2" gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/virtuald/go-paniclog" "github.com/virtuald/go-paniclog"
"math"
"os" "os"
"path/filepath" "path/filepath"
"sort" "sort"
@@ -56,11 +55,13 @@ type Downloader struct {
fetcherCache map[string]fetcher.Fetcher fetcherCache map[string]fetcher.Fetcher
storage Storage storage Storage
tasks []*Task tasks []*Task
waitTasks []*Task
listener Listener listener Listener
lock *sync.Mutex lock *sync.Mutex
fetcherMapLock *sync.RWMutex fetcherMapLock *sync.RWMutex
closed atomic.Bool checkDuplicateLock *sync.Mutex
closed atomic.Bool
extensions []*Extension extensions []*Extension
} }
@@ -72,14 +73,15 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
cfg.Init() cfg.Init()
d := &Downloader{ d := &Downloader{
cfg: cfg, cfg: cfg,
fetcherBuilders: make(map[string]fetcher.FetcherBuilder), fetcherBuilders: make(map[string]fetcher.FetcherBuilder),
fetcherCache: make(map[string]fetcher.Fetcher), fetcherCache: make(map[string]fetcher.Fetcher),
waitTasks: make([]*Task, 0),
storage: cfg.Storage,
storage: cfg.Storage, lock: &sync.Mutex{},
lock: &sync.Mutex{}, fetcherMapLock: &sync.RWMutex{},
fetcherMapLock: &sync.RWMutex{}, checkDuplicateLock: &sync.Mutex{},
extensions: make([]*Extension, 0), extensions: make([]*Extension, 0),
} }
@@ -210,7 +212,7 @@ func (d *Downloader) parseFb(url string) (fetcher.FetcherBuilder, error) {
func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) { func (d *Downloader) setupFetcher(fetcher fetcher.Fetcher) {
ctl := controller.NewController() ctl := controller.NewController()
ctl.GetConfig = func(v any) (bool, error) { ctl.GetConfig = func(v any) bool {
return d.getProtocolConfig(fetcher.Name(), v) return d.getProtocolConfig(fetcher.Name(), v)
} }
fetcher.Setup(ctl) fetcher.Setup(ctl)
@@ -248,6 +250,33 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
return return
} }
func (d *Downloader) notifyRunning() {
go func() {
d.lock.Lock()
defer d.lock.Unlock()
remainRunningCount := d.remainRunningCount()
if remainRunningCount == 0 {
return
}
if len(d.waitTasks) > 0 {
wt := d.waitTasks[0]
d.waitTasks = d.waitTasks[1:]
d.doStart(wt)
}
}()
}
func (d *Downloader) remainRunningCount() int {
runningCount := 0
for _, t := range d.tasks {
if t.Status == base.DownloadStatusRunning {
runningCount++
}
}
return d.cfg.MaxRunning - runningCount
}
func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) { func (d *Downloader) CreateDirect(req *base.Request, opts *base.Options) (taskId string, err error) {
var fetcher fetcher.Fetcher var fetcher fetcher.Fetcher
fetcher, err = d.buildFetcher(req.URL) fetcher, err = d.buildFetcher(req.URL)
@@ -281,10 +310,21 @@ func (d *Downloader) Pause(id string) (err error) {
if task == nil { if task == nil {
return ErrTaskNotFound return ErrTaskNotFound
} }
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return err func() {
} task.lock.Lock()
return d.notifyRunning() defer task.lock.Unlock()
if task.Status == base.DownloadStatusPause {
return
}
err = d.doPause(task)
if err != nil {
return
}
d.notifyRunning()
}()
return
} }
func (d *Downloader) Continue(id string) (err error) { func (d *Downloader) Continue(id string) (err error) {
@@ -292,109 +332,164 @@ func (d *Downloader) Continue(id string) (err error) {
if task == nil { if task == nil {
return ErrTaskNotFound return ErrTaskNotFound
} }
d.tryRunning(func(remain int) {
if remain == 0 { func() {
// no more task can be running, need to doPause a running task d.lock.Lock()
defer d.lock.Unlock()
remainRunningCount := d.remainRunningCount()
if remainRunningCount == 0 {
for _, t := range d.tasks { for _, t := range d.tasks {
if t.Status == base.DownloadStatusRunning { if t.Status == base.DownloadStatusRunning {
err = d.doPause(t, base.DownloadStatusWait) if err = d.doPause(t); err != nil {
return
}
t.Status = base.DownloadStatusWait
d.waitTasks = append(d.waitTasks, t)
break break
} }
} }
} }
if err != nil { }()
func() {
task.lock.Lock()
defer task.lock.Unlock()
if task.Status == base.DownloadStatusRunning {
return return
} }
})
if err != nil { if err = d.doStart(task); err != nil {
return return
} }
return d.doContinue(task) }()
return
} }
func (d *Downloader) PauseAll() (err error) { func (d *Downloader) PauseAll() (err error) {
d.lock.Lock() func() {
defer d.lock.Unlock() d.lock.Lock()
return d.doPauseAll() defer d.lock.Unlock()
}
// Clear wait tasks
d.waitTasks = d.waitTasks[:0]
}()
func (d *Downloader) ContinueAll() (err error) {
d.lock.Lock()
defer d.lock.Unlock()
runningCount := 0
continueTasks := make([]*Task, 0)
for _, task := range d.tasks { for _, task := range d.tasks {
if task.Status == base.DownloadStatusRunning { err = func() error {
runningCount++ task.lock.Lock()
} else if task.Status != base.DownloadStatusDone { defer task.lock.Unlock()
continueTasks = append(continueTasks, task)
} return d.doPause(task)
} }()
// calculate how many tasks can be continued, can't exceed maxRunning if err != nil {
continueCount := int(math.Min(float64(d.cfg.MaxRunning-runningCount), float64(len(continueTasks))))
for i := 0; i < continueCount; i++ {
if err = d.doContinue(continueTasks[i]); err != nil {
return return
} }
} }
return
}
func (d *Downloader) ContinueAll() (err error) {
continuedTasks := make([]*Task, 0)
func() {
d.lock.Lock()
defer d.lock.Unlock()
// calculate how many tasks can be continued, can't exceed maxRunning
remainCount := d.remainRunningCount()
for _, task := range d.tasks {
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
if len(continuedTasks) < remainCount {
continuedTasks = append(continuedTasks, task)
} else {
task.Status = base.DownloadStatusWait
d.waitTasks = append(d.waitTasks, task)
}
}
}
}()
for _, task := range continuedTasks {
tt := task
err = func() error {
tt.lock.Lock()
defer tt.lock.Unlock()
return d.doStart(tt)
}()
if err != nil {
return
}
}
return return
} }
func (d *Downloader) Delete(id string, force bool) (err error) { func (d *Downloader) Delete(id string, force bool) (err error) {
d.lock.Lock()
defer d.lock.Unlock()
task := d.GetTask(id) task := d.GetTask(id)
if task == nil { if task == nil {
return ErrTaskNotFound return ErrTaskNotFound
} }
task.lock.Lock()
defer task.lock.Unlock() func() {
if task.fetcher != nil { d.lock.Lock()
err = task.fetcher.Close() defer d.lock.Unlock()
if err != nil {
return for i, t := range d.tasks {
} if t.ID == id {
} d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
for i, t := range d.tasks {
if t.ID == id {
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
break
}
}
err = d.storage.Delete(bucketTask, id)
if err != nil {
return
}
err = d.storage.Delete(bucketSave, id)
if err != nil {
return
}
if force && task.Meta.Res != nil {
if task.Meta.Res.Name != "" {
if err = os.RemoveAll(task.Meta.FolderPath()); err != nil {
return
} }
} else { }
if err = util.SafeRemove(task.Meta.SingleFilepath()); err != nil { }()
err = d.doDelete(task, force)
if err != nil {
return
}
d.notifyRunning()
return
}
func (d *Downloader) doDelete(task *Task, force bool) (err error) {
err = func() error {
if task.fetcher != nil {
if err := task.fetcher.Close(); err != nil {
return err return err
} }
} }
} if err := d.storage.Delete(bucketTask, task.ID); err != nil {
d.emit(EventKeyDelete, task)
if task.Status == base.DownloadStatusRunning {
if err = d.doNotifyRunning(); err != nil {
return err return err
} }
if err := d.storage.Delete(bucketSave, task.ID); err != nil {
return err
}
if force && task.Meta.Res != nil {
if task.Meta.Res.Name != "" {
if err := os.RemoveAll(task.Meta.FolderPath()); err != nil {
return err
}
} else {
if err := util.SafeRemove(task.Meta.SingleFilepath()); err != nil {
return err
}
}
}
d.emit(EventKeyDelete, task)
task = nil
return nil
}()
if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("delete task failed, task id: %s", task.ID)
} }
task = nil
return return
} }
func (d *Downloader) Close() error { func (d *Downloader) Close() error {
d.lock.Lock()
defer d.lock.Unlock()
d.closed.Store(true) d.closed.Store(true)
if err := d.doPauseAll(); err != nil { if err := d.PauseAll(); err != nil {
return err return err
} }
return d.storage.Close() return d.storage.Close()
@@ -445,12 +540,7 @@ func (d *Downloader) GetTask(id string) *Task {
} }
func (d *Downloader) GetTasks() []*Task { func (d *Downloader) GetTasks() []*Task {
// reverse tasks, so that the latest task is in the front return d.tasks
tasks := make([]*Task, len(d.tasks))
for i := 0; i < len(d.tasks); i++ {
tasks[i] = d.tasks[len(d.tasks)-i-1]
}
return tasks
} }
func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) { func (d *Downloader) GetConfig() (*DownloaderStoreConfig, error) {
@@ -462,18 +552,19 @@ func (d *Downloader) PutConfig(v *DownloaderStoreConfig) error {
return d.storage.Put(bucketConfig, "config", v) return d.storage.Put(bucketConfig, "config", v)
} }
func (d *Downloader) getProtocolConfig(name string, v any) (bool, error) { func (d *Downloader) getProtocolConfig(name string, v any) bool {
cfg, err := d.GetConfig() cfg, err := d.GetConfig()
if err != nil { if err != nil {
return false, err return false
} }
if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil { if cfg.ProtocolConfig == nil || cfg.ProtocolConfig[name] == nil {
return false, nil return false
} }
if err := util.MapToStruct(cfg.ProtocolConfig[name], v); err != nil { if err := util.MapToStruct(cfg.ProtocolConfig[name], v); err != nil {
return false, err d.Logger.Warn().Err(err).Msgf("get protocol config failed")
return false
} }
return true, nil return true
} }
// wait task done // wait task done
@@ -484,24 +575,22 @@ func (d *Downloader) watch(task *Task) {
d.emit(EventKeyError, task, err) d.emit(EventKeyError, task, err)
} else { } else {
task.Progress.Used = task.timer.Used() task.Progress.Used = task.timer.Used()
if task.Size == 0 { if task.Meta.Res.Size == 0 {
task.Size = task.fetcher.Progress().TotalDownloaded() task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
task.Meta.Res.Size = task.Size
} }
used := task.Progress.Used / int64(time.Second) used := task.Progress.Used / int64(time.Second)
if used == 0 { if used == 0 {
used = 1 used = 1
} }
task.Progress.Speed = task.Size / used totalSize := task.Meta.Res.Size
task.Progress.Downloaded = task.Size task.Progress.Speed = totalSize / used
task.Progress.Downloaded = totalSize
task.Status = base.DownloadStatusDone task.Status = base.DownloadStatusDone
d.storage.Put(bucketTask, task.ID, task.clone()) d.storage.Put(bucketTask, task.ID, task.clone())
d.emit(EventKeyDone, task) d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err) d.emit(EventKeyFinally, task, err)
} }
if task.Status == EventKeyDone { d.notifyRunning()
d.notifyRunning()
}
} }
func (d *Downloader) restoreFetcher(task *Task) error { func (d *Downloader) restoreFetcher(task *Task) error {
@@ -529,6 +618,14 @@ func (d *Downloader) restoreFetcher(task *Task) error {
task.fetcher = fb.Build() task.fetcher = fb.Build()
} }
d.setupFetcher(task.fetcher) d.setupFetcher(task.fetcher)
if task.fetcher.Meta().Req == nil {
task.fetcher.Meta().Req = task.Meta.Req
}
if task.fetcher.Meta().Res == nil {
task.fetcher.Meta().Res = task.Meta.Res
}
go d.watch(task)
} else if task.Status == base.DownloadStatusError {
go d.watch(task) go d.watch(task)
} }
task.fetcher.Create(task.Meta.Opts) task.fetcher.Create(task.Meta.Opts)
@@ -573,147 +670,122 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
func() { func() {
d.lock.Lock() d.lock.Lock()
defer d.lock.Unlock() defer d.lock.Unlock()
d.tasks = append(d.tasks, task) d.tasks = append(d.tasks, task)
}()
go d.watch(task)
go d.tryRunning(func(remain int) {
if remain > 0 {
if err = d.start(task); err != nil {
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
return
}
}
})
return
}
func (d *Downloader) tryRunning(cb func(remain int)) { remainRunningCount := d.remainRunningCount()
runningCount := 0 if remainRunningCount == 0 {
for _, task := range d.tasks { task.Status = base.DownloadStatusWait
if task.Status == base.DownloadStatusRunning { d.waitTasks = append(d.waitTasks, task)
runningCount++
}
}
cb(d.cfg.MaxRunning - runningCount)
}
func (d *Downloader) notifyRunning() error {
d.lock.Lock()
defer d.lock.Unlock()
return d.doNotifyRunning()
}
func (d *Downloader) doNotifyRunning() error {
for _, task := range d.tasks {
if task.Status == base.DownloadStatusReady || task.Status == base.DownloadStatusWait {
return d.doContinue(task)
}
}
return nil
}
func (d *Downloader) doPauseAll() (err error) {
for _, task := range d.tasks {
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
return return
} }
}
d.doStart(task)
}()
go d.watch(task)
return return
} }
func (d *Downloader) start(task *Task) error { func (d *Downloader) doStart(task *Task) (err error) {
task.lock.Lock() if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
defer task.lock.Unlock() err := d.restoreFetcher(task)
d.triggerOnStart(task)
isCreate := task.Status == base.DownloadStatusReady
task.Status = base.DownloadStatusRunning
if task.Meta.Res == nil {
err := task.fetcher.Resolve(task.Meta.Req)
if err != nil { if err != nil {
task.Status = base.DownloadStatusError d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
d.emit(EventKeyError, task, err)
return err return err
} }
isCreate = true
} }
if isCreate { cloneTask := task.clone()
d.lock.Lock() isCreate := task.Status == base.DownloadStatusReady
defer d.lock.Unlock() task.Status = base.DownloadStatusRunning
// check if the download file is duplicated and rename it automatically.
if task.Meta.Res.Name != "" { doStart := func() error {
fullDirPath := task.Meta.FolderPath() task.lock.Lock()
newName, err := util.CheckDuplicateAndRename(fullDirPath) defer task.lock.Unlock()
if err != nil {
return err req := d.triggerOnStart(cloneTask)
} if req != nil {
task.Meta.Opts.Name = newName task.Meta.Req = req
} else { task.fetcher.Meta().Req = req
fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil {
return err
}
task.Meta.Opts.Name = newName
} }
}
if err := d.doStart(task); err != nil {
task.Status = base.DownloadStatusError
d.emit(EventKeyError, task, err)
return err
}
return nil
}
func (d *Downloader) doStart(task *Task) error { if task.Meta.Res == nil {
task.Progress.Speed = 0 err := task.fetcher.Resolve(task.Meta.Req)
task.timer.Start() if err != nil {
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil { task.Status = base.DownloadStatusError
return err d.emit(EventKeyError, task, err)
} return err
if err := task.fetcher.Start(); err != nil { }
return err task.Meta.Res = task.fetcher.Meta().Res
} }
d.emit(EventKeyStart, task)
return nil
}
func (d *Downloader) doPause(task *Task, status base.Status) (err error) { if isCreate {
task.lock.Lock() d.checkDuplicateLock.Lock()
defer task.lock.Unlock() defer d.checkDuplicateLock.Unlock()
if task.Status != base.DownloadStatusDone { // check if the download file is duplicated and rename it automatically.
task.Status = status if task.Meta.Res.Name != "" {
task.timer.Pause() fullDirPath := task.Meta.FolderPath()
task.fetcher.Pause() newName, err := util.CheckDuplicateAndRename(fullDirPath)
d.storage.Put(bucketTask, task.ID, task.clone()) if err != nil {
d.emit(EventKeyPause, task) return err
}
task.Meta.Opts.Name = newName
} else {
fullFilePath := task.Meta.SingleFilepath()
newName, err := util.CheckDuplicateAndRename(fullFilePath)
if err != nil {
return err
}
task.Meta.Opts.Name = newName
}
task.Meta.Res.CalcSize(task.Meta.Opts.SelectFiles)
}
task.Progress.Speed = 0
task.timer.Start()
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err
}
if err := task.fetcher.Start(); err != nil {
return err
}
d.emit(EventKeyStart, task)
return nil
} }
go func() {
err := doStart()
if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
}
}()
return return
} }
func (d *Downloader) doContinue(task *Task) (err error) { func (d *Downloader) doPause(task *Task) (err error) {
err = func() error { err = func() error {
task.lock.Lock() if task.Status != base.DownloadStatusDone {
defer task.lock.Unlock() task.Status = base.DownloadStatusPause
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone { task.timer.Pause()
err := d.restoreFetcher(task) if task.fetcher != nil {
if err != nil { if err := task.fetcher.Pause(); err != nil {
return err
}
}
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
return err return err
} }
d.emit(EventKeyPause, task)
} }
return nil return nil
}() }()
if err != nil { if err != nil {
d.Logger.Error().Stack().Err(err).Msgf("pause task failed, task id: %s", task.ID)
return return
} }
go func() {
if err := d.start(task); err != nil {
d.Logger.Error().Stack().Err(err).Msgf("start task failed, task id: %s", task.ID)
return
}
}()
return return
} }
@@ -776,12 +848,11 @@ func (b *boot) Listener(listener Listener) *boot {
} }
func (b *boot) Create(opts *base.Options) (string, error) { func (b *boot) Create(opts *base.Options) (string, error) {
rr, err := b.Resolve()
if err != nil {
return "", err
}
defaultDownloader.Listener(b.listener) defaultDownloader.Listener(b.listener)
return defaultDownloader.Create(rr.ID, opts) return defaultDownloader.CreateDirect(&base.Request{
URL: b.url,
Extra: b.extra,
}, opts)
} }
func Boot() *boot { func Boot() *boot {
+2 -5
View File
@@ -205,10 +205,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
defer downloader.Clear() defer downloader.Clear()
var httpCfg map[string]any var httpCfg map[string]any
exits, err := downloader.getProtocolConfig("http", &httpCfg) exits := downloader.getProtocolConfig("http", &httpCfg)
if err != nil {
t.Fatal(err)
}
if exits { if exits {
t.Errorf("getProtocolConfig() got = %v, want %v", exits, false) t.Errorf("getProtocolConfig() got = %v, want %v", exits, false)
} }
@@ -234,7 +231,7 @@ func TestDownloader_Protocol_Config(t *testing.T) {
}, },
} }
if err = downloader.PutConfig(storeCfg); err != nil { if err := downloader.PutConfig(storeCfg); err != nil {
t.Fatal(err) t.Fatal(err)
} }
+47 -6
View File
@@ -3,14 +3,14 @@ package engine
import ( import (
_ "embed" _ "embed"
"errors" "errors"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/file" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/file"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/formdata"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/vm"
"github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr" "github.com/GopeedLab/gopeed/pkg/download/engine/inject/xhr"
"github.com/dop251/goja" "github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop" "github.com/dop251/goja_nodejs/eventloop"
"github.com/dop251/goja_nodejs/process"
"github.com/dop251/goja_nodejs/url" "github.com/dop251/goja_nodejs/url"
"time"
) )
//go:embed polyfill/out/index.js //go:embed polyfill/out/index.js
@@ -27,12 +27,16 @@ type Engine struct {
func (e *Engine) RunString(script string) (value any, err error) { func (e *Engine) RunString(script string) (value any, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("%v", r) err = r.(error)
} }
}() }()
var result goja.Value var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) { e.loop.Run(func(runtime *goja.Runtime) {
result, err = runtime.RunString(script) result, err = runtime.RunString(script)
if err == nil {
go e.await(result)
}
}) })
if err != nil { if err != nil {
return return
@@ -45,9 +49,10 @@ func (e *Engine) RunString(script string) (value any, err error) {
func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) { func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err error) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
err = fmt.Errorf("%v", r) err = r.(error)
} }
}() }()
var result goja.Value var result goja.Value
e.loop.Run(func(runtime *goja.Runtime) { e.loop.Run(func(runtime *goja.Runtime) {
if args == nil { if args == nil {
@@ -59,13 +64,45 @@ func (e *Engine) CallFunction(fn goja.Callable, args ...any) (value any, err err
} }
result, err = fn(nil, jsArgs...) result, err = fn(nil, jsArgs...)
} }
if err == nil {
go e.await(result)
}
}) })
if err != nil { if err != nil {
return nil, err return
} }
return resolveResult(result) return resolveResult(result)
} }
// loop.Run will hang if the script result has a non-stop code, such as setInterval.
// This method will stop the event loop when the promise result is resolved.
func (e *Engine) await(value any) {
if value == nil {
return
}
if v, ok := value.(goja.Value); ok {
// if result is promise, wait for it to be resolved
if p, ok := v.Export().(*goja.Promise); ok {
if p.State() != goja.PromiseStatePending {
return
}
// check promise state every 100 milliseconds, until it is resolved
for {
time.Sleep(time.Millisecond * 100)
if p.State() == goja.PromiseStatePending {
continue
}
break
}
// stop the event loop
e.loop.StopNoWait()
}
}
}
func (e *Engine) Close() { func (e *Engine) Close() {
e.loop.Stop() e.loop.Stop()
} }
@@ -78,8 +115,8 @@ func NewEngine() *Engine {
loop.Run(func(runtime *goja.Runtime) { loop.Run(func(runtime *goja.Runtime) {
engine.Runtime = runtime engine.Runtime = runtime
runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true)) runtime.SetFieldNameMapper(goja.TagFieldNameMapper("json", true))
vm.Enable(runtime)
url.Enable(runtime) url.Enable(runtime)
process.Enable(runtime)
if err := file.Enable(runtime); err != nil { if err := file.Enable(runtime); err != nil {
return return
} }
@@ -96,6 +133,10 @@ func NewEngine() *Engine {
if err := runtime.Set("global", runtime.GlobalObject()); err != nil { if err := runtime.Set("global", runtime.GlobalObject()); err != nil {
return return
} }
// polyfill window.location
if _, err := runtime.RunString("global.location = new URL('http://localhost');"); err != nil {
return
}
return return
}) })
return engine return engine
+60
View File
@@ -23,6 +23,7 @@ func TestPolyfill(t *testing.T) {
doTestPolyfill(t, "Blob") doTestPolyfill(t, "Blob")
doTestPolyfill(t, "FormData") doTestPolyfill(t, "FormData")
doTestPolyfill(t, "fetch") doTestPolyfill(t, "fetch")
doTestPolyfill(t, "__gopeed_create_vm")
} }
func TestFetch(t *testing.T) { func TestFetch(t *testing.T) {
@@ -191,6 +192,65 @@ function testTimeout(){
} }
} }
func TestVm(t *testing.T) {
engine := NewEngine()
value, err := engine.RunString(`
const vm = __gopeed_create_vm()
vm.set('a', 1)
vm.set('b', 2)
const result = vm.runString('a=a+1;b=b+1;a+b;')
const out = {
"a": vm.get('a'),
"b": vm.get('b'),
"result": result
}
out
`)
if err != nil {
t.Fatal(err)
}
want := map[string]any{
"a": 2,
"b": 3,
"result": 5,
}
if !test.JsonEqual(value, want) {
t.Fatalf("vm test failed, want %v, got %v", want, value)
}
}
func TestNonStopLoop(t *testing.T) {
engine := NewEngine()
_, err := engine.RunString(`
function leak(){
setInterval(() => {
},500)
}
function test(){
leak()
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('done')
}, 1000)
})
}
`)
if err != nil {
t.Fatal(err)
}
val, err := callTestFun(engine, "test")
if err != nil {
panic(err)
}
if val != "done" {
t.Fatalf("infinite loop test failed, want %s, got %s", "done", val)
}
}
func doTestPolyfill(t *testing.T, module string) { func doTestPolyfill(t *testing.T, module string) {
value, err := Run(fmt.Sprintf(` value, err := Run(fmt.Sprintf(`
!!globalThis['%s'] !!globalThis['%s']
+44
View File
@@ -0,0 +1,44 @@
package vm
import (
"github.com/dop251/goja"
"github.com/dop251/goja_nodejs/eventloop"
)
type Vm struct {
loop *eventloop.EventLoop
}
func (vm *Vm) Set(name string, value any) {
vm.loop.Run(func(runtime *goja.Runtime) {
runtime.Set(name, value)
})
}
func (vm *Vm) Get(name string) (value any) {
vm.loop.Run(func(runtime *goja.Runtime) {
value = runtime.Get(name)
})
return
}
func (vm *Vm) RunString(script string) (value any, err error) {
defer func() {
if r := recover(); r != nil {
err = r.(error)
}
}()
vm.loop.Run(func(runtime *goja.Runtime) {
value, err = runtime.RunString(script)
})
return
}
func Enable(runtime *goja.Runtime) error {
return runtime.Set("__gopeed_create_vm", func(call goja.FunctionCall) goja.Value {
return runtime.ToValue(&Vm{
loop: eventloop.NewEventLoop(),
})
})
}
+54 -13
View File
@@ -243,7 +243,7 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity()) gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
return return
} }
ctx.Res.CalcSize() ctx.Res.CalcSize(nil)
} }
res = ctx.Res res = ctx.Res
}, },
@@ -251,12 +251,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
return return
} }
func (d *Downloader) triggerOnStart(task *Task) { func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
doTrigger(d, doTrigger(d,
EventOnStart, EventOnStart,
task.Meta.Req, task.Meta.Req,
&OnStartContext{ &OnStartContext{
Task: task.clone(), Task: task,
}, },
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) { func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
// Validate request structure // Validate request structure
@@ -265,8 +265,7 @@ func (d *Downloader) triggerOnStart(task *Task) {
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity()) gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
return return
} }
// Modify real task request req = ctx.Task.Meta.Req
task.Meta.Req = ctx.Task.Meta.Req
} }
}, },
) )
@@ -422,8 +421,50 @@ func (e *Extension) update(newExt *Extension) error {
e.Homepage = newExt.Homepage e.Homepage = newExt.Homepage
e.Repository = newExt.Repository e.Repository = newExt.Repository
e.Scripts = newExt.Scripts e.Scripts = newExt.Scripts
// don't override settings // merge settings
//e.Settings = newExt.Settings // if new setting not exist in old settings, append it
for _, newSetting := range newExt.Settings {
var exist bool
for _, setting := range e.Settings {
if setting.Name == newSetting.Name {
exist = true
break
}
}
if !exist {
e.Settings = append(e.Settings, newSetting)
}
}
// if old setting not exist in new settings, remove it
for i := 0; i < len(e.Settings); i++ {
var exist bool
for _, setting := range newExt.Settings {
if setting.Name == e.Settings[i].Name {
exist = true
break
}
}
if !exist {
e.Settings = append(e.Settings[:i], e.Settings[i+1:]...)
i--
}
}
// if new setting exist in old settings, update it
for _, newSetting := range newExt.Settings {
for _, setting := range e.Settings {
if setting.Name == newSetting.Name {
setting.Title = newSetting.Title
setting.Description = newSetting.Description
setting.Options = newSetting.Options
// if type changed, reset value
if setting.Type != newSetting.Type {
setting.Type = newSetting.Type
setting.Value = newSetting.Value
}
break
}
}
}
e.UpdatedAt = time.Now() e.UpdatedAt = time.Now()
return nil return nil
} }
@@ -557,28 +598,28 @@ type InstanceLogger struct {
logger *logger.Logger logger *logger.Logger
} }
func (l *InstanceLogger) Debug(msg ...any) { func (l *InstanceLogger) Debug(msg ...goja.Value) {
if l.devMode { if l.devMode {
l.logger.Debug().Msg(l.append(msg...)) l.logger.Debug().Msg(l.append(msg...))
} }
} }
func (l *InstanceLogger) Info(msg ...any) { func (l *InstanceLogger) Info(msg ...goja.Value) {
l.logger.Info().Msg(l.append(msg...)) l.logger.Info().Msg(l.append(msg...))
} }
func (l *InstanceLogger) Warn(msg ...any) { func (l *InstanceLogger) Warn(msg ...goja.Value) {
l.logger.Warn().Msg(l.append(msg...)) l.logger.Warn().Msg(l.append(msg...))
} }
func (l *InstanceLogger) Error(msg ...any) { func (l *InstanceLogger) Error(msg ...goja.Value) {
l.logger.Error().Msg(l.append(msg...)) l.logger.Error().Msg(l.append(msg...))
} }
func (l *InstanceLogger) append(msg ...any) string { func (l *InstanceLogger) append(msg ...goja.Value) string {
strMsg := make([]string, len(msg)) strMsg := make([]string, len(msg))
for i, m := range msg { for i, m := range msg {
strMsg[i] = fmt.Sprint(m) strMsg[i] = m.String()
} }
return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " ")) return fmt.Sprintf("[%s] %s", l.identity, strings.Join(strMsg, " "))
} }
+38 -7
View File
@@ -4,6 +4,7 @@ import (
"errors" "errors"
"github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/dop251/goja"
"os" "os"
"testing" "testing"
"time" "time"
@@ -95,6 +96,15 @@ func TestDownloader_InstallExtensionByGitFull(t *testing.T) {
} }
func TestDownloader_UpgradeExtension(t *testing.T) { func TestDownloader_UpgradeExtension(t *testing.T) {
getSetting := func(settings []*Setting, name string) *Setting {
for _, setting := range settings {
if setting.Name == name {
return setting
}
}
return nil
}
setupDownloader(func(downloader *Downloader) { setupDownloader(func(downloader *Downloader) {
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false) installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false)
if err != nil { if err != nil {
@@ -122,14 +132,35 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
t.Fatal("extension update fail") t.Fatal("extension update fail")
} }
// check setting update
s1 := getSetting(upgradeExt.Settings, "s1")
if s1.Title == "S1 old" {
t.Fatal("setting update fail")
}
// check setting type update
s2 := getSetting(upgradeExt.Settings, "s2")
if s2.Type == "number" {
t.Fatal("setting type update fail")
}
// check setting remove
d1 := getSetting(upgradeExt.Settings, "d1")
if d1 != nil {
t.Fatal("setting remove fail")
}
// check setting add
s3 := getSetting(upgradeExt.Settings, "s3")
if s3 == nil {
t.Fatal("setting add fail")
}
rr, err := downloader.Resolve(&base.Request{ rr, err := downloader.Resolve(&base.Request{
URL: "https://github.com/GopeedLab/gopeed/releases", URL: "https://test.com",
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if len(rr.Res.Files) == 1 { if rr.Res.Name != "test" {
t.Fatal("resolve error") t.Fatal("script update fail")
} }
}) })
} }
@@ -326,10 +357,10 @@ func TestDownloader_Extension_Logger(t *testing.T) {
il := newInstanceLogger(&Extension{ il := newInstanceLogger(&Extension{
Name: "test", Name: "test",
}, logger) }, logger)
il.Debug("msg1", "msg2") il.Debug(goja.NaN(), goja.Undefined())
il.Info("msg1", "msg2") il.Info(goja.NaN(), goja.Undefined())
il.Warn("msg1", "msg2") il.Warn(goja.NaN(), goja.Undefined())
il.Error("msg1", "msg2") il.Error(goja.NaN(), goja.Undefined())
} }
func setupDownloader(fn func(downloader *Downloader)) { func setupDownloader(fn func(downloader *Downloader)) {
-2
View File
@@ -22,7 +22,6 @@ type Task struct {
Meta *fetcher.FetcherMeta `json:"meta"` Meta *fetcher.FetcherMeta `json:"meta"`
Status base.Status `json:"status"` Status base.Status `json:"status"`
Progress *Progress `json:"progress"` Progress *Progress `json:"progress"`
Size int64 `json:"size"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
fetcherBuilder fetcher.FetcherBuilder fetcherBuilder fetcher.FetcherBuilder
@@ -50,7 +49,6 @@ func (t *Task) clone() *Task {
Meta: t.Meta, Meta: t.Meta,
Status: t.Status, Status: t.Status,
Progress: t.Progress, Progress: t.Progress,
Size: t.Size,
CreatedAt: t.CreatedAt, CreatedAt: t.CreatedAt,
} }
} }
+3
View File
@@ -222,6 +222,9 @@ func (b *BoltStorage) Pop(bucket string, key string, v any) error {
if err != nil { if err != nil {
return err return err
} }
if len(data) == 0 {
return nil
}
return json.Unmarshal(data, v) return json.Unmarshal(data, v)
} }
+28 -4
View File
@@ -1,13 +1,13 @@
{ {
"name": "github-release", "name": "extension-test",
"author": "gopeed", "author": "gopeed",
"title": "Github Release", "title": "Gopeed Extension Test",
"description": "Download github release assets", "description": "Test extension settings and upgrade",
"version": "0.0.1", "version": "0.0.1",
"homepage": "https://gopeed.com", "homepage": "https://gopeed.com",
"repository": { "repository": {
"url": "https://github.com/GopeedLab/gopeed-extension-samples", "url": "https://github.com/GopeedLab/gopeed-extension-samples",
"directory": "github-release-sample" "directory": "extension-test"
}, },
"scripts": [ "scripts": [
{ {
@@ -19,5 +19,29 @@
}, },
"entry": "index.js" "entry": "index.js"
} }
],
"settings": [
{
"name": "s1",
"title": "S1 old",
"description": "Test setting update",
"type": "string",
"required": true
},
{
"name": "s2",
"title": "s2 number old",
"description": "Test setting type update",
"type": "number",
"required": true,
"value": 1
},
{
"name": "d1",
"title": "Delete test",
"description": "Test setting delete",
"type": "string",
"required": true
}
] ]
} }
+41 -14
View File
@@ -105,9 +105,10 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension) r.Methods(http.MethodDelete).Path("/api/v1/extensions/{identity}").HandlerFunc(DeleteExtension)
r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension) r.Methods(http.MethodGet).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateCheckExtension)
r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension) r.Methods(http.MethodPost).Path("/api/v1/extensions/{identity}/update").HandlerFunc(UpdateExtension)
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.Path("/api/v1/proxy").HandlerFunc(DoProxy) r.Path("/api/v1/proxy").HandlerFunc(DoProxy)
if startCfg.WebEnable { if startCfg.WebEnable {
r.PathPrefix("/fs/tasks").Handler(http.FileServer(new(taskFileSystem)))
r.PathPrefix("/fs/extensions").Handler(http.FileServer(new(extensionFileSystem)))
r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS))) r.PathPrefix("/").Handler(http.FileServer(http.FS(startCfg.WebFS)))
} }
@@ -138,30 +139,56 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
return srv, listener, nil return srv, listener, nil
} }
func resolvePath(urlPath string, prefix string) (identity string, path string, err error) {
// remove prefix
clearPath := strings.TrimPrefix(urlPath, prefix)
// match extension identity, eg: /fs/extensions/identity/xxx
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
if !reg.MatchString(clearPath) {
err = os.ErrNotExist
return
}
matched := reg.FindStringSubmatch(clearPath)
if len(matched) != 3 {
err = os.ErrNotExist
return
}
return matched[1], matched[2], nil
}
// handle task file resource
type taskFileSystem struct {
}
func (e *taskFileSystem) Open(name string) (http.File, error) {
// get extension identity
identity, path, err := resolvePath(name, "/fs/tasks")
if err != nil {
return nil, err
}
task := Downloader.GetTask(identity)
if task == nil {
return nil, os.ErrNotExist
}
return os.Open(filepath.Join(task.Meta.RootDirPath(), path))
}
// handle extension file resource // handle extension file resource
type extensionFileSystem struct { type extensionFileSystem struct {
} }
func (e *extensionFileSystem) Open(name string) (http.File, error) { func (e *extensionFileSystem) Open(name string) (http.File, error) {
// remove prefix
path := strings.TrimPrefix(name, "/fs/extensions")
// match extension identity, eg: /fs/extensions/identity/xxx
reg := regexp.MustCompile(`^/([^/]+)/(.*)$`)
if !reg.MatchString(path) {
return nil, os.ErrNotExist
}
matched := reg.FindStringSubmatch(path)
if len(matched) != 3 {
return nil, os.ErrNotExist
}
// get extension identity // get extension identity
identity := matched[1] identity, path, err := resolvePath(name, "/fs/extensions")
if err != nil {
return nil, err
}
extension, err := Downloader.GetExtension(identity) extension, err := Downloader.GetExtension(identity)
if err != nil { if err != nil {
return nil, os.ErrNotExist return nil, os.ErrNotExist
} }
extensionPath := Downloader.ExtensionPath(extension) extensionPath := Downloader.ExtensionPath(extension)
return os.Open(filepath.Join(extensionPath, matched[2])) return os.Open(filepath.Join(extensionPath, path))
} }
func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool { func ReadJson(r *http.Request, w http.ResponseWriter, v any) bool {
+1 -3
View File
@@ -204,19 +204,16 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
// continue all // continue all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil) httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
time.Sleep(time.Millisecond * 100)
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil) tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
if len(tasks) != cfg.MaxRunning { if len(tasks) != cfg.MaxRunning {
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning) t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
} }
// pause all // pause all
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil) httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
time.Sleep(time.Millisecond * 500)
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil) tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
if len(tasks) != total { if len(tasks) != total {
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total) t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
} }
}) })
} }
@@ -494,6 +491,7 @@ func doTest(handler func()) {
cfg.Init() cfg.Init()
cfg.Storage = storage cfg.Storage = storage
cfg.StorageDir = ".test_storage" cfg.StorageDir = ".test_storage"
cfg.WebEnable = true
fileListener := doStart(cfg) fileListener := doStart(cfg)
defer func() { defer func() {
if err := fileListener.Close(); err != nil { if err := fileListener.Close(); err != nil {
+1 -1
View File
@@ -47,7 +47,7 @@ android {
applicationId "com.gopeed" applicationId "com.gopeed"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
minSdkVersion 19 minSdkVersion 21
targetSdkVersion 29 targetSdkVersion 29
versionCode flutterVersionCode.toInteger() versionCode flutterVersionCode.toInteger()
versionName flutterVersionName versionName flutterVersionName
@@ -72,6 +72,12 @@
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
</activity> </activity>
<!-- Add android:stopWithTask option only when necessary. -->
<service
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
android:stopWithTask="true" />
<!-- Don't delete the meta-data below. <!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java --> This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data <meta-data
+9
View File
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="1024px" height="1024px" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd" xmlns:xlink="http://www.w3.org/1999/xlink">
<g><path style="opacity:0.998" fill="#5dc76c" d="M 951.5,763.5 C 940.739,755.276 929.405,747.776 917.5,741C 908.477,736.319 899.143,732.485 889.5,729.5C 885.844,625.779 840.844,545.613 754.5,489C 674.802,441.453 590.135,430.12 500.5,455C 419.755,480.605 359.588,530.771 320,605.5C 278.585,565.816 230.752,555.649 176.5,575C 131.948,594.935 106.448,629.102 100,677.5C 99.3333,685.833 99.3333,694.167 100,702.5C 102.081,714.824 105.081,726.824 109,738.5C 95.4882,745.492 82.9882,753.826 71.5,763.5C 37.7121,705.639 16.5454,643.306 8,576.5C -9.41698,416.759 35.9164,278.093 144,160.5C 244.661,58.2827 367.161,5.94939 511.5,3.5C 678.975,7.48829 814.142,74.8216 917,205.5C 1011.33,336.426 1040,480.759 1003,638.5C 991.564,682.671 974.397,724.337 951.5,763.5 Z"/></g>
<g><path style="opacity:0.999" fill="#fefefe" d="M 951.5,763.5 C 949.28,768.272 946.78,772.938 944,777.5C 938.305,786.913 932.138,795.913 925.5,804.5C 915.954,796.888 905.288,791.055 893.5,787C 878.282,782.958 863.115,778.791 848,774.5C 854.013,716.891 841.346,663.891 810,615.5C 768.341,553.228 711.174,512.395 638.5,493C 560.311,479.566 490.311,497.566 428.5,547C 411.963,560.536 397.13,575.703 384,592.5C 366.745,616.923 348.911,640.923 330.5,664.5C 310.135,651.302 290.469,637.135 271.5,622C 238.448,602.307 206.782,604.307 176.5,628C 159.375,642.752 148.875,661.252 145,683.5C 143.968,692.716 144.968,701.716 148,710.5C 154.334,725.339 160.167,740.339 165.5,755.5C 138.118,764.688 114.451,779.688 94.5,800.5C 85.653,788.968 77.9863,776.635 71.5,763.5C 82.9882,753.826 95.4882,745.492 109,738.5C 105.081,726.824 102.081,714.824 100,702.5C 99.3333,694.167 99.3333,685.833 100,677.5C 106.448,629.102 131.948,594.935 176.5,575C 230.752,555.649 278.585,565.816 320,605.5C 359.588,530.771 419.755,480.605 500.5,455C 590.135,430.12 674.802,441.453 754.5,489C 840.844,545.613 885.844,625.779 889.5,729.5C 899.143,732.485 908.477,736.319 917.5,741C 929.405,747.776 940.739,755.276 951.5,763.5 Z"/></g>
<g><path style="opacity:0.998" fill="#5dc76c" d="M 925.5,804.5 C 850.791,908.608 750.791,975.775 625.5,1006C 609.308,1009.7 592.975,1012.53 576.5,1014.5C 615.708,969.938 655.042,925.438 694.5,881C 664.168,880.5 633.835,880.333 603.5,880.5C 603.5,811.167 603.5,741.833 603.5,672.5C 542.167,672.5 480.833,672.5 419.5,672.5C 419.5,741.5 419.5,810.5 419.5,879.5C 388.493,879.167 357.493,879.5 326.5,880.5C 326.624,881.107 326.957,881.44 327.5,881.5C 366.805,925.808 406.138,970.141 445.5,1014.5C 318.63,996.317 212.796,939.651 128,844.5C 115.85,830.525 104.683,815.859 94.5,800.5C 114.451,779.688 138.118,764.688 165.5,755.5C 160.167,740.339 154.334,725.339 148,710.5C 144.968,701.716 143.968,692.716 145,683.5C 148.875,661.252 159.375,642.752 176.5,628C 206.782,604.307 238.448,602.307 271.5,622C 290.469,637.135 310.135,651.302 330.5,664.5C 348.911,640.923 366.745,616.923 384,592.5C 397.13,575.703 411.963,560.536 428.5,547C 490.311,497.566 560.311,479.566 638.5,493C 711.174,512.395 768.341,553.228 810,615.5C 841.346,663.891 854.013,716.891 848,774.5C 863.115,778.791 878.282,782.958 893.5,787C 905.288,791.055 915.954,796.888 925.5,804.5 Z"/></g>
<g><path style="opacity:0.999" fill="#fefefe" d="M 576.5,1014.5 C 532.836,1019.82 489.169,1019.82 445.5,1014.5C 406.138,970.141 366.805,925.808 327.5,881.5C 358.167,880.833 388.833,880.167 419.5,879.5C 419.5,810.5 419.5,741.5 419.5,672.5C 480.833,672.5 542.167,672.5 603.5,672.5C 603.5,741.833 603.5,811.167 603.5,880.5C 633.835,880.333 664.168,880.5 694.5,881C 655.042,925.438 615.708,969.938 576.5,1014.5 Z"/></g>
<g><path style="opacity:1" fill="#ade2b5" d="M 419.5,879.5 C 388.833,880.167 358.167,880.833 327.5,881.5C 326.957,881.44 326.624,881.107 326.5,880.5C 357.493,879.5 388.493,879.167 419.5,879.5 Z"/></g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

+11 -4
View File
@@ -5,13 +5,14 @@
"confirm": "Confirm", "confirm": "Confirm",
"cancel": "Cancel", "cancel": "Cancel",
"selectAll": "Select All", "selectAll": "Select All",
"task": "Task", "task": "Tasks",
"downloading": "downloading", "downloading": "downloading",
"downloaded": "downloaded", "downloaded": "downloaded",
"setting": "Setting", "setting": "Settings",
"donate": "Donate", "donate": "Donate",
"exit": "Exit", "exit": "Exit",
"create": "Create Task", "create": "Create Task",
"directDownload": "Direct Download",
"advancedOptions": "Advanced Options", "advancedOptions": "Advanced Options",
"downloadLink": "Download Link", "downloadLink": "Download Link",
"downloadLinkValid": "Please enter the download link", "downloadLinkValid": "Please enter the download link",
@@ -65,6 +66,12 @@
"extensionInstallSuccess": "Installed successfully", "extensionInstallSuccess": "Installed successfully",
"extensionUpdateSuccess": "Updated successfully", "extensionUpdateSuccess": "Updated successfully",
"extensionDelete": "Delete Extension", "extensionDelete": "Delete Extension",
"extensionAlreadyLatest": "It's already the latest version" "extensionAlreadyLatest": "It's already the latest version",
"extensionFind": "Find Extensions",
"extensionDevelop": "Develop Extensions",
"history": "History",
"clearHistory": "Clear History",
"noHistoryFound": "No History Found",
"serviceTitle": "Download Service",
"serviceText": "Running"
} }
+17 -5
View File
@@ -12,14 +12,16 @@
"donate": "Пожертвовать", "donate": "Пожертвовать",
"exit": "Выход", "exit": "Выход",
"create": "Создать задачу", "create": "Создать задачу",
"downloadLink": "Ссылка для скачивание", "advancedOptions": "Расширенные опции",
"downloadLink": "Ссылка на скачивание",
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания", "downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append", "downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append",
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл", "downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
"download": "Скачать", "download": "Скачать",
"noFileSelected": "Файл не выбран", "noFileSelected": "Файл не выбран",
"noStoragePermission": "Требуется доступ к хранилищу", "noStoragePermission": "Требуется доступ к хранилищу",
"selectFile": "Выберать файл", "selectFile": "Выбрать файл",
"rename": "Переименовать",
"basic": "Основные параметры", "basic": "Основные параметры",
"advanced": "Расширенные", "advanced": "Расширенные",
"general": "Общие", "general": "Общие",
@@ -57,5 +59,15 @@
"delete": "Удалить", "delete": "Удалить",
"newVersionTitle": "Обнаружена новая версия @version", "newVersionTitle": "Обнаружена новая версия @version",
"newVersionUpdate": "Обновить", "newVersionUpdate": "Обновить",
"newVersionLater": "позже" "newVersionLater": "позже",
} "extensions": "Расширения",
"extensionInstallSuccess": "Установка завершена",
"extensionUpdateSuccess": "Обновление завершено",
"extensionDelete": "Удалить расширение",
"extensionAlreadyLatest": "Это последняя версия",
"extensionFind": "Найти расширения",
"extensionDevelop": "Разработка расширений",
"history": "История",
"clearHistory": "Очистить историю",
"noHistoryFound": "История не найдена"
}
+9 -1
View File
@@ -12,6 +12,7 @@
"donate": "打赏", "donate": "打赏",
"exit": "退出", "exit": "退出",
"create": "创建任务", "create": "创建任务",
"directDownload": "直接下载",
"advancedOptions": "高级选项", "advancedOptions": "高级选项",
"downloadLink": "下载链接", "downloadLink": "下载链接",
"downloadLinkValid": "请输入下载链接", "downloadLinkValid": "请输入下载链接",
@@ -65,5 +66,12 @@
"extensionInstallSuccess": "安装成功", "extensionInstallSuccess": "安装成功",
"extensionUpdateSuccess": "更新成功", "extensionUpdateSuccess": "更新成功",
"extensionDelete": "删除扩展", "extensionDelete": "删除扩展",
"extensionAlreadyLatest": "已经是最新版本" "extensionAlreadyLatest": "已经是最新版本",
"extensionFind": "获取扩展",
"extensionDevelop": "开发扩展",
"history": "历史记录",
"clearHistory": "清空历史记录",
"noHistoryFound": "暂无历史记录",
"serviceTitle": "下载服务",
"serviceText": "运行中"
} }
+21 -13
View File
@@ -1,5 +1,5 @@
{ {
"label": "中文 (正體) ", "label": "中文 (繁體) ",
"error": "錯誤", "error": "錯誤",
"tip": "提示", "tip": "提示",
"confirm": "確定", "confirm": "確定",
@@ -12,6 +12,7 @@
"donate": "斗內", "donate": "斗內",
"exit": "離開", "exit": "離開",
"create": "新增任務", "create": "新增任務",
"directDownload": "直接下載",
"advancedOptions": "進階選項", "advancedOptions": "進階選項",
"downloadLink": "下載鏈接", "downloadLink": "下載鏈接",
"downloadLinkValid": "請輸入下載鏈接", "downloadLinkValid": "請輸入下載鏈接",
@@ -27,16 +28,16 @@
"general": "通用設定", "general": "通用設定",
"downloadDir": "下載目録", "downloadDir": "下載目録",
"downloadDirValid": "請選擇下載目録", "downloadDirValid": "請選擇下載目録",
"connections": "同時連接數量", "connections": "同時連接數",
"maxRunning": "最大同時下載數", "maxRunning": "最大同時下載數",
"items": "@count 項", "items": "@count 項",
"subscribeTracker": "已訂閲的 Tracker", "subscribeTracker": "已訂閲的追蹤者列表",
"subscribeFail": "訂閲失敗", "subscribeFail": "訂閲失敗",
"update": "更新", "update": "更新",
"updateDaily": "每日自動更新", "updateDaily": "每天自動更新",
"lastUpdate": "上次更新時間: @time", "lastUpdate": "上次更新時間: @time",
"addTracker": "增加新的 Tracker", "addTracker": "增加新的追蹤者",
"addTrackerHit": "請輸入 Tracker 服務器地址,每行一條", "addTrackerHit": "請輸入追蹤者服務器地址,每行一條",
"ui": "介面", "ui": "介面",
"theme": "佈景主題", "theme": "佈景主題",
"themeSystem": "跟隨系統", "themeSystem": "跟隨系統",
@@ -46,12 +47,12 @@
"about": "關於", "about": "關於",
"homepage": "訪問主頁", "homepage": "訪問主頁",
"version": "版本資訊", "version": "版本資訊",
"protocol": "通訊協定", "protocol": "端口設定",
"port": "端口", "port": "端口",
"apiToken": "接口 Token", "apiToken": "接口秘鑰",
"notSet": "尚未設定", "notSet": "未設定",
"set": "已設定", "set": "已設定",
"effectAfterRestart": "該選項將在重新啓動軟體後生效", "effectAfterRestart": "重啟後生效",
"startAll": "全部開始", "startAll": "全部開始",
"pauseAll": "全部暫停", "pauseAll": "全部暫停",
"deleteTask": "刪除任務", "deleteTask": "刪除任務",
@@ -61,9 +62,16 @@
"newVersionUpdate": "立刻更新", "newVersionUpdate": "立刻更新",
"newVersionLater": "稍後更新", "newVersionLater": "稍後更新",
"extensions": "外掛程序", "extensions": "外掛程序",
"extensionInstallUrl": "安裝鏈接", "extensionInstallUrl": "安裝URL",
"extensionInstallSuccess": "安裝完成", "extensionInstallSuccess": "安裝成功",
"extensionUpdateSuccess": "更新完成", "extensionUpdateSuccess": "更新完成",
"extensionDelete": "解除安裝", "extensionDelete": "解除安裝",
"extensionAlreadyLatest": "已是最新版本" "extensionAlreadyLatest": "已是最新版本",
"extensionFind": "取得外掛",
"extensionDevelop": "開發外掛",
"history": "歷史記錄",
"clearHistory": "清空記錄",
"noHistoryFound": "還沒有記錄",
"serviceTitle": "下載服務",
"serviceText": "執行中"
} }
+9
View File
@@ -33,6 +33,15 @@ import Libgopeed
}) })
GeneratedPluginRegistrant.register(with: self) GeneratedPluginRegistrant.register(with: self)
SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback(registerPlugins)
if #available(iOS 10.0, *) {
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
}
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
} }
func registerPlugins(registry: FlutterPluginRegistry) {
GeneratedPluginRegistrant.register(with: registry)
}
@@ -1 +1,2 @@
#import "GeneratedPluginRegistrant.h" #import "GeneratedPluginRegistrant.h"
#import <flutter_foreground_task/FlutterForegroundTaskPlugin.h>
@@ -30,6 +30,8 @@ class ProtocolConfig {
@JsonSerializable() @JsonSerializable()
class HttpConfig { class HttpConfig {
String userAgent =
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36';
int connections = 0; int connections = 0;
HttpConfig(); HttpConfig();
@@ -41,6 +43,7 @@ class HttpConfig {
@JsonSerializable() @JsonSerializable()
class BtConfig { class BtConfig {
int listenPort = 0;
List<String> trackers = []; List<String> trackers = [];
BtConfig(); BtConfig();
@@ -33,19 +33,23 @@ Map<String, dynamic> _$ProtocolConfigToJson(ProtocolConfig instance) =>
'bt': instance.bt.toJson(), 'bt': instance.bt.toJson(),
}; };
HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig _$HttpConfigFromJson(Map<String, dynamic> json) => HttpConfig()
HttpConfig()..connections = json['connections'] as int; ..userAgent = json['userAgent'] as String
..connections = json['connections'] as int;
Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) => Map<String, dynamic> _$HttpConfigToJson(HttpConfig instance) =>
<String, dynamic>{ <String, dynamic>{
'userAgent': instance.userAgent,
'connections': instance.connections, 'connections': instance.connections,
}; };
BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig() BtConfig _$BtConfigFromJson(Map<String, dynamic> json) => BtConfig()
..listenPort = json['listenPort'] as int
..trackers = ..trackers =
(json['trackers'] as List<dynamic>).map((e) => e as String).toList(); (json['trackers'] as List<dynamic>).map((e) => e as String).toList();
Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{ Map<String, dynamic> _$BtConfigToJson(BtConfig instance) => <String, dynamic>{
'listenPort': instance.listenPort,
'trackers': instance.trackers, 'trackers': instance.trackers,
}; };
-2
View File
@@ -12,7 +12,6 @@ class Task {
Meta meta; Meta meta;
Status status; Status status;
Progress progress; Progress progress;
int size;
DateTime createdAt; DateTime createdAt;
Task({ Task({
@@ -20,7 +19,6 @@ class Task {
required this.meta, required this.meta,
required this.status, required this.status,
required this.progress, required this.progress,
required this.size,
required this.createdAt, required this.createdAt,
}); });
-2
View File
@@ -11,7 +11,6 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>), meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
status: $enumDecode(_$StatusEnumMap, json['status']), status: $enumDecode(_$StatusEnumMap, json['status']),
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>), progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
size: json['size'] as int,
createdAt: DateTime.parse(json['createdAt'] as String), createdAt: DateTime.parse(json['createdAt'] as String),
); );
@@ -20,7 +19,6 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
'meta': instance.meta.toJson(), 'meta': instance.meta.toJson(),
'status': _$StatusEnumMap[instance.status]!, 'status': _$StatusEnumMap[instance.status]!,
'progress': instance.progress.toJson(), 'progress': instance.progress.toJson(),
'size': instance.size,
'createdAt': instance.createdAt.toIso8601String(), 'createdAt': instance.createdAt.toIso8601String(),
}; };
@@ -4,6 +4,7 @@ import 'dart:ui';
import 'package:app_links/app_links.dart'; import 'package:app_links/app_links.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
@@ -79,21 +80,18 @@ class AppController extends GetxController with WindowListener, TrayListener {
@override @override
void onReady() { void onReady() {
super.onReady(); super.onReady();
try {
_initDeepLinks(); _initDeepLinks().onError((error, stackTrace) =>
} catch (e) { logger.w("initDeepLinks error", error, stackTrace));
logger.w("initDeepLinks error", e);
} _initWindows().onError((error, stackTrace) =>
try { logger.w("initWindows error", error, stackTrace));
_initWindows();
} catch (e) { _initTray().onError(
logger.w("initWindows error", e); (error, stackTrace) => logger.w("initTray error", error, stackTrace));
}
try { _initForegroundTask().onError((error, stackTrace) =>
_initTray(); logger.w("initForegroundTask error", error, stackTrace));
} catch (e) {
logger.w("initTray error", e);
}
} }
@override @override
@@ -202,11 +200,54 @@ class AppController extends GetxController with WindowListener, TrayListener {
onClick: (menuItem) => {windowManager.destroy()}, onClick: (menuItem) => {windowManager.destroy()},
), ),
]); ]);
await trayManager.setToolTip('Gopeed'); if (!Util.isLinux()) {
// Linux seems not support setToolTip, refer to: https://github.com/GopeedLab/gopeed/issues/241
await trayManager.setToolTip('Gopeed');
}
await trayManager.setContextMenu(menu); await trayManager.setContextMenu(menu);
trayManager.addListener(this); trayManager.addListener(this);
} }
Future<void> _initForegroundTask() async {
if (!Util.isMobile()) {
return;
}
FlutterForegroundTask.init(
androidNotificationOptions: AndroidNotificationOptions(
channelId: 'gopeed_service',
channelName: 'Gopeed Background Service',
channelImportance: NotificationChannelImportance.LOW,
showWhen: true,
priority: NotificationPriority.LOW,
iconData: const NotificationIconData(
resType: ResourceType.mipmap,
resPrefix: ResourcePrefix.ic,
name: 'launcher',
)),
iosNotificationOptions: const IOSNotificationOptions(
showNotification: true,
playSound: false,
),
foregroundTaskOptions: const ForegroundTaskOptions(
interval: 5000,
isOnceEvent: false,
autoRunOnBoot: true,
allowWakeLock: true,
allowWifiLock: true,
),
);
if (await FlutterForegroundTask.isRunningService) {
FlutterForegroundTask.restartService();
} else {
FlutterForegroundTask.startService(
notificationTitle: "serviceTitle".tr,
notificationText: "serviceText".tr,
);
}
}
Future<void> _toCreate(Uri uri) async { Future<void> _toCreate(Uri uri) async {
final path = uri.scheme == "magnet" final path = uri.scheme == "magnet"
? uri.toString() ? uri.toString()
@@ -232,13 +273,8 @@ class AppController extends GetxController with WindowListener, TrayListener {
_defaultStartConfig!.address = "127.0.0.1:0"; _defaultStartConfig!.address = "127.0.0.1:0";
} else { } else {
_defaultStartConfig!.network = "unix"; _defaultStartConfig!.network = "unix";
if (Util.isDesktop()) { _defaultStartConfig!.address =
_defaultStartConfig!.address = unixSocketPath; "${(await getTemporaryDirectory()).path}/$unixSocketPath";
}
if (Util.isMobile()) {
_defaultStartConfig!.address =
"${(await getTemporaryDirectory()).path}/$unixSocketPath";
}
} }
_defaultStartConfig!.apiToken = ''; _defaultStartConfig!.apiToken = '';
return _defaultStartConfig!; return _defaultStartConfig!;
@@ -279,6 +315,7 @@ class AppController extends GetxController with WindowListener, TrayListener {
result.addAll(trackers); result.addAll(trackers);
} catch (e) { } catch (e) {
logger.w("subscribe trackers fail, url: $u", e); logger.w("subscribe trackers fail, url: $u", e);
return;
} }
} }
btExtConfig.subscribeTrackers.clear(); btExtConfig.subscribeTrackers.clear();
@@ -10,8 +10,9 @@ class CreateController extends GetxController
final RxList fileInfos = [].obs; final RxList fileInfos = [].obs;
final RxList openedFolders = [].obs; final RxList openedFolders = [].obs;
final selectedIndexes = <int>[].obs; final selectedIndexes = <int>[].obs;
final isResolving = false.obs; final isConfirming = false.obs;
final showAdvanced = false.obs; final showAdvanced = false.obs;
final directDownload = false.obs;
late TabController advancedTabController; late TabController advancedTabController;
final oldUrl = "".obs; final oldUrl = "".obs;
final fileDataUri = "".obs; final fileDataUri = "".obs;
@@ -4,10 +4,10 @@ import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
import 'package:gopeed/app/modules/history/views/history_view.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:rounded_loading_button/rounded_loading_button.dart'; import 'package:rounded_loading_button/rounded_loading_button.dart';
import '../../../../api/api.dart'; import '../../../../api/api.dart';
import '../../../../api/model/create_task.dart'; import '../../../../api/model/create_task.dart';
import '../../../../api/model/options.dart'; import '../../../../api/model/options.dart';
@@ -24,14 +24,18 @@ import '../../app/controllers/app_controller.dart';
import '../controllers/create_controller.dart'; import '../controllers/create_controller.dart';
class CreateView extends GetView<CreateController> { class CreateView extends GetView<CreateController> {
final _resolveFormKey = GlobalKey<FormState>(); final _confirmFormKey = GlobalKey<FormState>();
final _urlController = TextEditingController(); final _urlController = TextEditingController();
final _renameController = TextEditingController();
final _connectionsController = TextEditingController();
final _pathController = TextEditingController();
final _confirmController = RoundedLoadingButtonController(); final _confirmController = RoundedLoadingButtonController();
final _httpUaController = TextEditingController(); final _httpUaController = TextEditingController();
final _httpCookieController = TextEditingController(); final _httpCookieController = TextEditingController();
final _httpRefererController = TextEditingController(); final _httpRefererController = TextEditingController();
final _btTrackerController = TextEditingController(); final _btTrackerController = TextEditingController();
final _historyController = HistoryController();
final _availableSchemes = ["http:", "https:", "magnet:"]; final _availableSchemes = ["http:", "https:", "magnet:"];
@@ -39,6 +43,17 @@ class CreateView extends GetView<CreateController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final appController = Get.find<AppController>();
if (_connectionsController.text.isEmpty) {
_connectionsController.text = appController
.downloaderConfig.value.protocolConfig.http.connections
.toString();
}
if (_pathController.text.isEmpty) {
_pathController.text = appController.downloaderConfig.value.downloadDir;
}
final String? filePath = Get.rootDelegate.arguments(); final String? filePath = Get.rootDelegate.arguments();
if (_urlController.text.isEmpty) { if (_urlController.text.isEmpty) {
if (filePath?.isNotEmpty ?? false) { if (filePath?.isNotEmpty ?? false) {
@@ -90,13 +105,15 @@ class CreateView extends GetView<CreateController> {
onTap: () { onTap: () {
FocusScope.of(context).requestFocus(FocusNode()); FocusScope.of(context).requestFocus(FocusNode());
}, },
child: Padding( child: SingleChildScrollView(
child: Padding(
padding: padding:
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0), const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
child: Form( child: Form(
key: _resolveFormKey, key: _confirmFormKey,
autovalidateMode: AutovalidateMode.onUserInteraction, autovalidateMode: AutovalidateMode.onUserInteraction,
child: Column(children: [ child: Column(
children: [
Row(children: [ Row(children: [
Expanded( Expanded(
child: TextFormField( child: TextFormField(
@@ -132,93 +149,209 @@ class CreateView extends GetView<CreateController> {
), ),
), ),
IconButton( IconButton(
icon: const Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: () async { onPressed: () async {
var pr = await FilePicker.platform.pickFiles( var pr = await FilePicker.platform.pickFiles(
type: FileType.custom, type: FileType.custom,
allowedExtensions: ["torrent"]); allowedExtensions: ["torrent"]);
if (pr != null) { if (pr != null) {
if (!Util.isWeb()) { if (!Util.isWeb()) {
_urlController.text = pr.files[0].path ?? ""; _urlController.text = pr.files[0].path ?? "";
return; return;
}
_urlController.text = pr.files[0].name;
controller.setFileDataUri(pr.files[0].bytes!);
} }
}), _urlController.text = pr.files[0].name;
] controller.setFileDataUri(pr.files[0].bytes!);
//.where((e) => e != null).map((e) => e!).toList(), }
), },
Obx(() => Visibility( ),
visible: controller.showAdvanced.value, IconButton(
child: Padding( icon: const Icon(Icons.history_rounded),
padding: const EdgeInsets.only(left: 40, top: 15), onPressed: () async {
child: Column( List<String> resultOfHistories =
children: [ await _historyController.getAllHistory();
TabBar( // reversing: display last entered history first
controller: controller.advancedTabController, List<String> reverseResultOfHistories =
tabs: const [ resultOfHistories.reversed.toList();
Tab( // show dialog box to list history
text: 'HTTP', if (context.mounted) {
showGeneralDialog(
barrierColor: Colors.black.withOpacity(0.5),
transitionBuilder: (context, a1, a2, widget) {
return Transform.scale(
scale: a1.value,
child: Opacity(
opacity: a1.value,
child: HistoryView(
isHistoryListEmpty:
reverseResultOfHistories.isEmpty,
historyList: ListView.builder(
itemCount:
reverseResultOfHistories.length,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () {
_urlController.text =
reverseResultOfHistories[
index];
Navigator.pop(context);
},
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
padding:
const EdgeInsets.symmetric(
horizontal: 8.0,
vertical: 8.0,
),
margin:
const EdgeInsets.symmetric(
horizontal: 10.0,
vertical: 8.0,
),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.background,
borderRadius:
BorderRadius.circular(
10.0),
),
child: Text(
reverseResultOfHistories[
index]
.toString(),
),
),
),
);
},
),
),
), ),
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( transitionDuration:
mainAxisSize: MainAxisSize.min, const Duration(milliseconds: 250),
barrierDismissible: true,
barrierLabel: '',
context: context,
pageBuilder: (context, animation1, animation2) {
return const Text('PAGE BUILDER');
},
);
}
},
),
]),
Padding(
padding: const EdgeInsets.only(left: 40),
child: Column(children: [
TextField(
controller: _renameController,
decoration: InputDecoration(labelText: 'rename'.tr),
),
TextField(
controller: _connectionsController,
decoration: InputDecoration(
labelText: 'connections'.tr,
),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 1, max: 256),
],
),
DirectorySelector(
controller: _pathController,
),
Obx(
() => Visibility(
visible: controller.showAdvanced.value,
child: Column(
children: [
TabBar(
controller: controller.advancedTabController,
tabs: const [
Tab(
text: 'HTTP',
),
Tab(
text: 'BitTorrent',
)
],
),
AutoScaleTabBarView(
controller: controller.advancedTabController,
children: [ 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,
)),
],
)
],
)
],
).paddingOnly(top: 16),
),
),
]),
),
Center(
child: Padding(
padding: const EdgeInsets.only(top: 15),
child: Column(
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
TextButton(
onPressed: () {
controller.directDownload.value =
!controller.directDownload.value;
},
child: Row(children: [
Obx(() => Checkbox(
value:
controller.directDownload.value,
onChanged: (bool? value) {
controller.directDownload.value =
value ?? false;
},
)),
Text('directDownload'.tr),
]),
),
TextButton(
onPressed: () {
controller.showAdvanced.value =
!controller.showAdvanced.value;
},
child: Row(children: [
Obx(() => Checkbox( Obx(() => Checkbox(
value: controller.showAdvanced.value, value: controller.showAdvanced.value,
onChanged: (bool? value) { onChanged: (bool? value) {
@@ -228,21 +361,27 @@ class CreateView extends GetView<CreateController> {
)), )),
Text('advancedOptions'.tr), Text('advancedOptions'.tr),
]), ]),
),
],
), ),
), SizedBox(
SizedBox( width: 150,
width: 150, child: RoundedLoadingButton(
child: RoundedLoadingButton( color: Get.theme.colorScheme.secondary,
color: Get.theme.colorScheme.secondary, onPressed: _doConfirm,
onPressed: _doResolve, controller: _confirmController,
controller: _confirmController, child: Text('confirm'.tr),
child: Text('confirm'.tr), ),
), ),
), ],
], ),
), ),
)), ),
]))), ],
),
),
),
),
), ),
), ),
); );
@@ -262,43 +401,54 @@ class CreateView extends GetView<CreateController> {
} }
} }
Future<void> _doResolve() async { Future<void> _doConfirm() async {
if (controller.isResolving.value) { if (controller.isConfirming.value) {
return; return;
} }
controller.isResolving.value = true; controller.isConfirming.value = true;
try { try {
_confirmController.start(); _confirmController.start();
if (_resolveFormKey.currentState!.validate()) { if (_confirmFormKey.currentState!.validate()) {
// check if is multi line urls final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
final urls = Util.textToLines(_urlController.text); ? controller.fileDataUri.value
ResolveResult rr; : _urlController.text;
bool isMuiltiLine = false;
if (urls.length > 1) { final urls = Util.textToLines(submitUrl);
isMuiltiLine = true; // Add url to the history
rr = ResolveResult( for (final url in urls) {
res: Resource( _historyController.addHistory(url);
files: urls }
.map((u) => FileInfo( /*
name: u, Check if is direct download, there has two ways to direct download
req: Request(url: u, extra: parseReqExtra(u)))) 1. Direct download option is checked
.toList())); 2. Muli line urls
*/
final isMultiLine = urls.length > 1;
final isDirect = controller.directDownload.value || isMultiLine;
if (isDirect) {
await Future.wait(urls.map((url) {
return createTask(CreateTask(
req: Request(url: url, extra: parseReqExtra(url)),
opt: Options(
name: isMultiLine ? "" : _renameController.text,
path: _pathController.text,
selectFiles: [],
extra: parseReqOpts())));
}));
Get.rootDelegate.offNamed(Routes.TASK);
} else { } else {
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty final rr = await resolve(Request(
? controller.fileDataUri.value
: _urlController.text;
rr = await resolve(Request(
url: submitUrl, url: submitUrl,
extra: parseReqExtra(_urlController.text), extra: parseReqExtra(_urlController.text),
)); ));
await _showResolveDialog(rr);
} }
await _showResolveDialog(rr, isMuiltiLine);
} }
} catch (e) { } catch (e) {
showErrorMessage(e); showErrorMessage(e);
} finally { } finally {
_confirmController.reset(); _confirmController.reset();
controller.isResolving.value = false; controller.isConfirming.value = false;
} }
} }
@@ -321,6 +471,13 @@ class CreateView extends GetView<CreateController> {
return reqExtra; return reqExtra;
} }
Object? parseReqOpts() {
return _connectionsController.text.isEmpty
? null
: (OptsExtraHttp()
..connections = int.parse(_connectionsController.text));
}
String _hitText() { String _hitText() {
return 'downloadLinkHit'.trParams({ return 'downloadLinkHit'.trParams({
'append': 'append':
@@ -328,14 +485,8 @@ class CreateView extends GetView<CreateController> {
}); });
} }
Future<void> _showResolveDialog(ResolveResult rr, bool isMuiltiLine) async { Future<void> _showResolveDialog(ResolveResult rr) async {
final appController = Get.find<AppController>();
final createFormKey = GlobalKey<FormState>(); final createFormKey = GlobalKey<FormState>();
final nameController = TextEditingController();
final connectionsController = TextEditingController();
final pathController = TextEditingController(
text: appController.downloaderConfig.value.downloadDir);
final downloadController = RoundedLoadingButtonController(); final downloadController = RoundedLoadingButtonController();
return showDialog<void>( return showDialog<void>(
context: Get.context!, context: Get.context!,
@@ -354,31 +505,7 @@ class CreateView extends GetView<CreateController> {
child: Form( child: Form(
key: createFormKey, key: createFormKey,
autovalidateMode: AutovalidateMode.always, autovalidateMode: AutovalidateMode.always,
child: Column( child: FileListView(files: rr.res.files)),
children: [
Expanded(child: FileListView(files: rr.res.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,
),
],
)),
); );
}, },
), ),
@@ -414,25 +541,19 @@ class CreateView extends GetView<CreateController> {
showMessage('tip'.tr, 'noFileSelected'.tr); showMessage('tip'.tr, 'noFileSelected'.tr);
return; return;
} }
Object? optExtra = connectionsController.text.isEmpty final optExtra = parseReqOpts();
? null
: (OptsExtraHttp()
..connections =
int.parse(connectionsController.text));
if (createFormKey.currentState!.validate()) { if (createFormKey.currentState!.validate()) {
if (rr.id.isEmpty) { if (rr.id.isEmpty) {
// create task batch, there has two ways to batch create task // from extension resolve result
// 1. from multi line urls
// 2. from extension resolve result
await Future.wait( await Future.wait(
controller.selectedIndexes.map((index) { controller.selectedIndexes.map((index) {
final file = rr.res.files[index]; final file = rr.res.files[index];
return createTask(CreateTask( return createTask(CreateTask(
req: file.req!, req: file.req!,
opt: Options( opt: Options(
name: isMuiltiLine ? "" : file.name, name: file.name,
path: path.join( path: path.join(_pathController.text,
pathController.text, rr.res.name), rr.res.name, file.path),
selectFiles: [], selectFiles: [],
extra: optExtra))); extra: optExtra)));
})); }));
@@ -440,8 +561,8 @@ class CreateView extends GetView<CreateController> {
await createTask(CreateTask( await createTask(CreateTask(
rid: rr.id, rid: rr.id,
opt: Options( opt: Options(
name: nameController.text, name: _renameController.text,
path: pathController.text, path: _pathController.text,
selectFiles: controller.selectedIndexes, selectFiles: controller.selectedIndexes,
extra: optExtra))); extra: optExtra)));
} }
@@ -21,6 +21,7 @@ import '../../../../api/model/install_extension.dart';
import '../../../../api/model/switch_extension.dart'; import '../../../../api/model/switch_extension.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../views/icon_button_loading.dart'; import '../../../views/icon_button_loading.dart';
import '../../../views/responsive_builder.dart';
import '../controllers/extension_controller.dart'; import '../controllers/extension_controller.dart';
class ExtensionView extends GetView<ExtensionController> { class ExtensionView extends GetView<ExtensionController> {
@@ -35,7 +36,31 @@ class ExtensionView extends GetView<ExtensionController> {
body: Column( body: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 20), Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextButton.icon(
onPressed: () {
launchUrl(
Uri.parse(
'https://github.com/search?q=topic%3Agopeed-extension&type=repositories'),
mode: LaunchMode.externalApplication);
},
icon: const Icon(Icons.search),
label: Text('extensionFind'.tr),
),
const SizedBox(width: 16),
TextButton.icon(
onPressed: () {
launchUrl(
Uri.parse('https://docs.gopeed.com/dev-extension.html'),
mode: LaunchMode.externalApplication);
},
icon: const Icon(Icons.edit),
label: Text('extensionDevelop'.tr),
),
],
),
Obx(() => Row( Obx(() => Row(
children: [ children: [
Expanded( Expanded(
@@ -89,14 +114,14 @@ class ExtensionView extends GetView<ExtensionController> {
: Container() : Container()
], ],
)), )),
const SizedBox(height: 32), const SizedBox(height: 16),
Expanded( Expanded(
child: Obx(() => ListView.builder( child: Obx(() => ListView.builder(
itemCount: controller.extensions.length, itemCount: controller.extensions.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final extension = controller.extensions[index]; final extension = controller.extensions[index];
return SizedBox( return SizedBox(
height: 112, height: ResponsiveBuilder.isNarrow(context) ? 152 : 112,
child: Card( child: Card(
elevation: 4.0, elevation: 4.0,
child: Column( child: Column(
@@ -145,23 +170,35 @@ class ExtensionView extends GetView<ExtensionController> {
title: Row( title: Row(
children: () { children: () {
final list = [ final list = [
Text(extension.title), ResponsiveBuilder.isNarrow(context)
? Expanded(
child: Text(
extension.title,
overflow:
TextOverflow.ellipsis,
),
)
: Text(extension.title),
const SizedBox(width: 8), const SizedBox(width: 8),
Chip( buildChip('v${extension.version}'),
label: Text('v${extension.version}'),
)
]; ];
if (extension.devMode) { if (extension.devMode) {
list.add(const SizedBox(width: 8)); list.add(const SizedBox(width: 8));
list.add(Chip( list.add(buildChip(
label: Text('dev'.tr), 'dev',
backgroundColor: Colors.redAccent, bgColor: Colors.blue.shade300,
)); ));
} }
return list; return list;
}(), }(),
), ),
subtitle: Text(extension.description), subtitle: ResponsiveBuilder.isNarrow(context)
? Text(
extension.description,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: Text(extension.description),
), ),
), ),
Row( Row(
@@ -233,6 +270,14 @@ class ExtensionView extends GetView<ExtensionController> {
); );
} }
Widget buildChip(String text, {Color? bgColor}) {
return Chip(
padding: const EdgeInsets.all(0),
backgroundColor: bgColor,
label: Text(text, style: const TextStyle(fontSize: 12)),
);
}
Future<void> _showSettingDialog(Extension extension) async { Future<void> _showSettingDialog(Extension extension) async {
final formKey = GlobalKey<FormBuilderState>(); final formKey = GlobalKey<FormBuilderState>();
final confrimController = RoundedLoadingButtonController(); final confrimController = RoundedLoadingButtonController();
@@ -256,9 +301,32 @@ class ExtensionView extends GetView<ExtensionController> {
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
child: Column( child: Column(
children: extension.settings! children: extension.settings!.map((e) {
.map((e) => _buildSettingItem(e)) final settingItem = _buildSettingItem(e);
.toList()),
return Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
SizedBox(
width: 20,
child: e.description.isEmpty
? null
: Tooltip(
message: e.description,
child: const CircleAvatar(
radius: 10,
backgroundColor:
Colors.grey,
child: Icon(
Icons.question_mark,
size: 10,
)),
))
.paddingOnly(right: 10),
Expanded(child: settingItem),
],
);
}).toList()),
), ),
), ),
]), ]),
@@ -324,8 +392,7 @@ class ExtensionView extends GetView<ExtensionController> {
FormFieldValidator<String>? validator, TextInputType? keyBoardType) { FormFieldValidator<String>? validator, TextInputType? keyBoardType) {
return FormBuilderTextField( return FormBuilderTextField(
name: setting.name, name: setting.name,
decoration: InputDecoration( decoration: InputDecoration(labelText: setting.title),
labelText: setting.title, hintText: setting.description),
initialValue: setting.value?.toString(), initialValue: setting.value?.toString(),
inputFormatters: inputFormatter != null ? [inputFormatter] : null, inputFormatters: inputFormatter != null ? [inputFormatter] : null,
keyboardType: keyBoardType, keyboardType: keyBoardType,
@@ -340,7 +407,8 @@ class ExtensionView extends GetView<ExtensionController> {
return FormBuilderDropdown<String>( return FormBuilderDropdown<String>(
name: setting.name, name: setting.name,
decoration: InputDecoration( decoration: InputDecoration(
labelText: setting.title, hintText: setting.description), labelText: setting.title,
),
initialValue: setting.value?.toString(), initialValue: setting.value?.toString(),
validator: FormBuilderValidators.compose([ validator: FormBuilderValidators.compose([
requiredValidator, requiredValidator,
@@ -0,0 +1,26 @@
import 'package:shared_preferences/shared_preferences.dart';
class HistoryController {
// Get History in List<String>
Future<List<String>> getAllHistory() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
List<String>? historyList = preferences.getStringList('history');
if(historyList == null) {
return [];
}
return historyList;
}
// Get Existing History & Add New History
void addHistory(String historyText) async {
SharedPreferences preferences = await SharedPreferences.getInstance();
List<String> existingHistoryData = await getAllHistory();
existingHistoryData.add(historyText);
preferences.setStringList('history', existingHistoryData);
}
void clearHistory() async {
SharedPreferences preferences = await SharedPreferences.getInstance();
preferences.remove('history');
}
}
@@ -0,0 +1,111 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
class HistoryView extends StatefulWidget {
const HistoryView({
super.key,
required this.isHistoryListEmpty,
required this.historyList,
});
final bool isHistoryListEmpty;
final Widget historyList;
@override
State<HistoryView> createState() => _HistoryViewState();
}
class _HistoryViewState extends State<HistoryView> {
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.sizeOf(context);
return Scaffold(
backgroundColor: Colors.transparent,
body: Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 0,
backgroundColor: Colors.transparent,
child: Container(
width: size.width * 0.8,
height: size.height * 0.8,
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(10.0),
),
child: Column(
children: <Widget>[
Padding(
padding: const EdgeInsets.all(8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Icon(
Icons.history_rounded,
size: Theme.of(context)
.textTheme
.headlineSmall
?.fontSize,
),
const SizedBox(width: 8.0),
Text(
'history'.tr,
style: Theme.of(context).textTheme.headlineSmall,
),
],
),
Row(
children: <Widget>[
IconButton(
onPressed: () {
HistoryController _controller = HistoryController();
_controller.clearHistory();
Navigator.pop(context);
},
tooltip: "clearHistory".tr,
icon: const Icon(
Icons.history_toggle_off_rounded,
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(
Icons.close_rounded,
),
),
],
),
],
),
),
Expanded(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: widget.isHistoryListEmpty
? <Widget>[
const Icon(
Icons.manage_history_rounded,
),
const SizedBox(height: 10.0),
Text(
'noHistoryFound'.tr,
),
]
: <Widget>[
Expanded(child: widget.historyList),
],
),
),
),
],
),
),
),
);
}
}
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
@@ -23,139 +24,141 @@ class HomeView extends GetView<HomeController> {
break; break;
} }
return Scaffold( return WithForegroundTask(
// extendBody: true, child: Scaffold(
body: Row( // extendBody: true,
// crossAxisAlignment: CrossAxisAlignment.start, body: Row(
children: [ // crossAxisAlignment: CrossAxisAlignment.start,
!ResponsiveBuilder.isNarrow(context) children: [
? NavigationRail( !ResponsiveBuilder.isNarrow(context)
extended: true, ? NavigationRail(
labelType: NavigationRailLabelType.none, extended: true,
minExtendedWidth: 170, labelType: NavigationRailLabelType.none,
groupAlignment: 0, minExtendedWidth: 170,
// useIndicator: false, groupAlignment: 0,
onDestinationSelected: (int index) { // useIndicator: false,
controller.currentIndex.value = index; onDestinationSelected: (int index) {
switch (index) { controller.currentIndex.value = index;
case 0: switch (index) {
delegate.offAndToNamed(Routes.TASK); case 0:
break; delegate.offAndToNamed(Routes.TASK);
case 1: break;
delegate.offAndToNamed(Routes.EXTENSION); case 1:
break; delegate.offAndToNamed(Routes.EXTENSION);
case 2: break;
delegate.offAndToNamed(Routes.SETTING); case 2:
break; delegate.offAndToNamed(Routes.SETTING);
} break;
}, }
destinations: [ },
NavigationRailDestination( destinations: [
icon: const Icon(Icons.task), NavigationRailDestination(
selectedIcon: const Icon(Icons.task), icon: const Icon(Icons.task),
label: Text('task'.tr), selectedIcon: const Icon(Icons.task),
), label: Text('task'.tr),
NavigationRailDestination( ),
icon: const Icon(Icons.extension), NavigationRailDestination(
selectedIcon: const Icon(Icons.extension), icon: const Icon(Icons.extension),
label: Text('extensions'.tr), selectedIcon: const Icon(Icons.extension),
), label: Text('extensions'.tr),
NavigationRailDestination( ),
icon: const Icon(Icons.settings), NavigationRailDestination(
selectedIcon: const Icon(Icons.settings), icon: const Icon(Icons.settings),
label: Text('setting'.tr), selectedIcon: const Icon(Icons.settings),
), label: Text('setting'.tr),
], ),
selectedIndex: controller.currentIndex.value, ],
leading: const Icon(Icons.menu), selectedIndex: controller.currentIndex.value,
// trailing: const Icon(Icons.info_outline), leading: const Icon(Icons.menu),
) // trailing: const Icon(Icons.info_outline),
: const SizedBox.shrink(), )
Expanded( : const SizedBox.shrink(),
child: GetRouterOutlet( Expanded(
initialRoute: Routes.TASK, child: GetRouterOutlet(
// anchorRoute: '/', initialRoute: Routes.TASK,
// filterPages: (afterAnchor) { // anchorRoute: '/',
// logger.w(afterAnchor); // filterPages: (afterAnchor) {
// logger.w(afterAnchor.take(1)); // logger.w(afterAnchor);
// return afterAnchor.take(1); // logger.w(afterAnchor.take(1));
// }, // return afterAnchor.take(1);
)) // },
]), ))
bottomNavigationBar: ResponsiveBuilder.isNarrow(context) ]),
? BottomNavigationBar( bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
items: <BottomNavigationBarItem>[ ? BottomNavigationBar(
BottomNavigationBarItem( items: <BottomNavigationBarItem>[
icon: const Icon(Icons.task), BottomNavigationBarItem(
label: 'task'.tr, icon: const Icon(Icons.task),
), label: 'task'.tr,
BottomNavigationBarItem( ),
icon: const Icon(Icons.extension), BottomNavigationBarItem(
label: 'extensions'.tr, icon: const Icon(Icons.extension),
), label: 'extensions'.tr,
BottomNavigationBarItem( ),
icon: const Icon(Icons.settings), BottomNavigationBarItem(
label: 'setting'.tr, icon: const Icon(Icons.settings),
), label: 'setting'.tr,
], ),
currentIndex: controller.currentIndex.value, ],
// selectedItemColor: Get.theme.highlightColor, currentIndex: controller.currentIndex.value,
onTap: (index) { // selectedItemColor: Get.theme.highlightColor,
controller.currentIndex.value = index; onTap: (index) {
switch (index) { controller.currentIndex.value = index;
case 0: switch (index) {
delegate.offAndToNamed(Routes.TASK); case 0:
break; delegate.offAndToNamed(Routes.TASK);
case 1: break;
delegate.offAndToNamed(Routes.EXTENSION); case 1:
break; delegate.offAndToNamed(Routes.EXTENSION);
case 2: break;
delegate.offAndToNamed(Routes.SETTING); case 2:
break; delegate.offAndToNamed(Routes.SETTING);
} break;
}, }
) },
// StylishBottomBar( )
// option: AnimatedBarOptions( // StylishBottomBar(
// iconSize: 32, // option: AnimatedBarOptions(
// barAnimation: BarAnimation.blink, // iconSize: 32,
// iconStyle: IconStyle.Default, // barAnimation: BarAnimation.blink,
// opacity: 0.3, // iconStyle: IconStyle.Default,
// ), // opacity: 0.3,
// items: [ // ),
// BottomBarItem( // items: [
// icon: const Icon(Icons.file_download), // BottomBarItem(
// selectedColor: Get.theme.primaryColor, // icon: const Icon(Icons.file_download),
// title: Text('downloading'.tr)), // selectedColor: Get.theme.primaryColor,
// BottomBarItem( // title: Text('downloading'.tr)),
// icon: const Icon(Icons.done), // BottomBarItem(
// selectedColor: Get.theme.primaryColor, // icon: const Icon(Icons.done),
// title: Text('downloaded'.tr)), // selectedColor: Get.theme.primaryColor,
// BottomBarItem( // title: Text('downloaded'.tr)),
// icon: const Icon(Icons.settings), // BottomBarItem(
// selectedColor: Get.theme.primaryColor, // icon: const Icon(Icons.settings),
// title: Text('setting'.tr)), // selectedColor: Get.theme.primaryColor,
// ], // title: Text('setting'.tr)),
// // hasNotch: true, // ],
// currentIndex: controller.currentIndex.value, // // hasNotch: true,
// onTap: (index) { // currentIndex: controller.currentIndex.value,
// switch (index) { // onTap: (index) {
// case 0: // switch (index) {
// delegate.toNamed(Routes.DOWNLOADING); // case 0:
// controller.currentIndex.value = 0; // delegate.toNamed(Routes.DOWNLOADING);
// break; // controller.currentIndex.value = 0;
// case 1: // break;
// delegate.toNamed(Routes.DOWNLOADED); // case 1:
// controller.currentIndex.value = 1; // delegate.toNamed(Routes.DOWNLOADED);
// break; // controller.currentIndex.value = 1;
// case 2: // break;
// delegate.toNamed(Routes.SETTING); // case 2:
// controller.currentIndex.value = 2; // delegate.toNamed(Routes.SETTING);
// break; // controller.currentIndex.value = 2;
// } // break;
// }, // }
// ) // },
: const SizedBox.shrink(), // )
: const SizedBox.shrink(),
),
); );
}); });
} }
@@ -96,6 +96,24 @@ class SettingView extends GetView<SettingController> {
// http config items start // http config items start
final httpConfig = downloaderCfg.value.protocolConfig.http; final httpConfig = downloaderCfg.value.protocolConfig.http;
final buildHttpUa =
_buildConfigItem('User-Agent', () => httpConfig.userAgent, (Key key) {
final uaController = TextEditingController(text: httpConfig.userAgent);
uaController.addListener(() async {
if (uaController.text.isNotEmpty &&
uaController.text != httpConfig.userAgent) {
httpConfig.userAgent = uaController.text;
await debounceSave();
}
});
return TextField(
key: key,
focusNode: FocusNode(),
controller: uaController,
);
});
final buildHttpConnections = _buildConfigItem( final buildHttpConnections = _buildConfigItem(
'connections', () => httpConfig.connections.toString(), (Key key) { 'connections', () => httpConfig.connections.toString(), (Key key) {
final connectionsController = final connectionsController =
@@ -122,8 +140,32 @@ class SettingView extends GetView<SettingController> {
}); });
// bt config items start // bt config items start
final btConfig = downloaderCfg.value.protocolConfig.bt;
final btExtConfig = downloaderCfg.value.extra.bt; final btExtConfig = downloaderCfg.value.extra.bt;
final buildBtListenPort = _buildConfigItem(
'port', () => btConfig.listenPort.toString(), (Key key) {
final listenPortController =
TextEditingController(text: btConfig.listenPort.toString());
listenPortController.addListener(() async {
if (listenPortController.text.isNotEmpty &&
listenPortController.text != btConfig.listenPort.toString()) {
btConfig.listenPort = int.parse(listenPortController.text);
await debounceSave();
}
});
return TextField(
key: key,
focusNode: FocusNode(),
controller: listenPortController,
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 0, max: 65535),
],
);
});
final buildBtTrackerSubscribeUrls = _buildConfigItem( final buildBtTrackerSubscribeUrls = _buildConfigItem(
'subscribeTracker', 'subscribeTracker',
() => 'items'.trParams( () => 'items'.trParams(
@@ -479,6 +521,7 @@ class SettingView extends GetView<SettingController> {
Card( Card(
child: Column( child: Column(
children: _addDivider([ children: _addDivider([
buildHttpUa(),
buildHttpConnections(), buildHttpConnections(),
]), ]),
)), )),
@@ -486,6 +529,7 @@ class SettingView extends GetView<SettingController> {
Card( Card(
child: Column( child: Column(
children: _addDivider([ children: _addDivider([
buildBtListenPort(),
buildBtTrackerSubscribeUrls(), buildBtTrackerSubscribeUrls(),
buildBtTrackers(), buildBtTrackers(),
]), ]),
@@ -1,8 +1,7 @@
import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart'; import 'package:gopeed/app/modules/task/controllers/task_list_controller.dart';
import '../../../../api/model/task.dart'; import '../../../../api/model/task.dart';
class TaskDownloadedController extends TaskListController { class TaskDownloadedController extends TaskListController {
TaskDownloadedController() : super([Status.done]); TaskDownloadedController() : super([Status.done], SortDirection.asc);
} }
@@ -9,5 +9,5 @@ class TaskDownloadingController extends TaskListController {
Status.pause, Status.pause,
Status.wait, Status.wait,
Status.error Status.error
]); ], SortDirection.desc);
} }
@@ -12,6 +12,12 @@ class FileItem {
FileItem(this.isDirectory, this.path, this.name, this.size); FileItem(this.isDirectory, this.path, this.name, this.size);
String get fullPath => "${path == "/" ? path : "$path/"}$name"; String get fullPath => "${path == "/" ? path : "$path/"}$name";
String filePath(String optName) {
return optName.isEmpty
? fullPath
: "${path == "/" ? path : "$path/"}$optName";
}
} }
class TaskFilesController extends GetxController { class TaskFilesController extends GetxController {
@@ -5,10 +5,13 @@ import 'package:get/get.dart';
import '../../../../api/api.dart'; import '../../../../api/api.dart';
import '../../../../api/model/task.dart'; import '../../../../api/model/task.dart';
enum SortDirection { asc, desc }
abstract class TaskListController extends GetxController { abstract class TaskListController extends GetxController {
List<Status> statuses; List<Status> statuses;
SortDirection sortDirection;
TaskListController(this.statuses); TaskListController(this.statuses, this.sortDirection);
final tasks = <Task>[].obs; final tasks = <Task>[].obs;
final isRunning = false.obs; final isRunning = false.obs;
@@ -43,6 +46,15 @@ abstract class TaskListController extends GetxController {
} }
getTasksState() async { getTasksState() async {
tasks.value = await getTasks(statuses); final tasks = await getTasks(statuses);
// sort tasks by create time
tasks.sort((a, b) {
if (sortDirection == SortDirection.asc) {
return a.createdAt.compareTo(b.createdAt);
} else {
return b.createdAt.compareTo(a.createdAt);
}
});
this.tasks.value = tasks;
} }
} }
@@ -1,8 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:open_file/open_file.dart'; import 'package:open_file/open_file.dart';
import 'package:path/path.dart';
import 'package:share_plus/share_plus.dart'; import 'package:share_plus/share_plus.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../api/api.dart' as api;
import '../../../../util/browser_download/browser_download.dart';
import '../../../../util/file_icon.dart'; import '../../../../util/file_icon.dart';
import '../../../../util/icons.dart'; import '../../../../util/icons.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
@@ -20,7 +24,7 @@ class TaskFilesView extends GetView<TaskFilesController> {
icon: const Icon(Icons.arrow_back), icon: const Icon(Icons.arrow_back),
onPressed: () => Get.rootDelegate.popRoute()), onPressed: () => Get.rootDelegate.popRoute()),
// actions: [], // actions: [],
title: Obx(() => Text(controller.task.value!.meta.res!.name)), title: Obx(() => Text(controller.task.value?.meta.res?.name ?? "")),
), ),
body: Obx(() { body: Obx(() {
final fileList = controller.fileList; final fileList = controller.fileList;
@@ -52,13 +56,18 @@ class TaskFilesView extends GetView<TaskFilesController> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final meta = controller.task.value!.meta; final meta = controller.task.value!.meta;
final file = fileList[index]; final file = fileList[index];
// if resource is single file, use opts.name as file name
final realFileName =
meta.res!.name.isEmpty ? file.name : "";
final fileRelativePath = file.filePath(realFileName);
final filePath = Util.safePathJoin( final filePath = Util.safePathJoin(
[meta.opts.path, meta.res!.name, file.fullPath]); [meta.opts.path, meta.res!.name, fileRelativePath]);
final fileName = basename(filePath);
return ListTile( return ListTile(
leading: file.isDirectory leading: file.isDirectory
? const Icon(Icons.folder) ? const Icon(Icons.folder)
: Icon(FaIcons.allIcons[findIcon(file.name)]), : Icon(FaIcons.allIcons[findIcon(file.name)]),
title: Text(fileList[index].name), title: Text(fileName),
subtitle: file.isDirectory subtitle: file.isDirectory
? Text('items'.trParams({ ? Text('items'.trParams({
'count': controller 'count': controller
@@ -72,19 +81,39 @@ class TaskFilesView extends GetView<TaskFilesController> {
width: 100, width: 100,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: Util.isWeb()
IconButton( ? () {
icon: const Icon(Icons.play_circle), final accessUrl = api.join(
onPressed: () { "/fs/tasks/${controller.task.value!.id}$fileRelativePath");
OpenFile.open(filePath); return [
}), IconButton(
IconButton( icon:
icon: const Icon(Icons.share), const Icon(Icons.play_circle),
onPressed: () { onPressed: () {
final xfile = XFile(filePath); launchUrl(Uri.parse(accessUrl),
Share.shareXFiles([xfile]); webOnlyWindowName:
}) "_blank");
], }),
IconButton(
icon: const Icon(Icons.download),
onPressed: () {
download(accessUrl, fileName);
})
];
}()
: [
IconButton(
icon: const Icon(Icons.play_circle),
onPressed: () {
OpenFile.open(filePath);
}),
IconButton(
icon: const Icon(Icons.share),
onPressed: () {
final xfile = XFile(filePath);
Share.shareXFiles([xfile]);
})
],
), ),
), ),
onTap: () { onTap: () {
@@ -1,11 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/app/modules/task/controllers/task_downloading_controller.dart';
import '../controllers/task_downloaded_controller.dart';
import 'task_downloading_view.dart';
import 'task_downloaded_view.dart';
import '../controllers/task_controller.dart'; import '../controllers/task_controller.dart';
import '../controllers/task_downloaded_controller.dart';
import '../controllers/task_downloading_controller.dart';
import 'task_downloaded_view.dart';
import 'task_downloading_view.dart';
class TaskView extends GetView<TaskController> { class TaskView extends GetView<TaskController> {
const TaskView({Key? key}) : super(key: key); const TaskView({Key? key}) : super(key: key);
@@ -1,10 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:gopeed/api/model/meta.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:styled_widget/styled_widget.dart'; import 'package:styled_widget/styled_widget.dart';
import '../../api/api.dart'; import '../../api/api.dart';
import '../../api/model/meta.dart';
import '../../api/model/task.dart'; import '../../api/model/task.dart';
import '../../util/file_explorer.dart'; import '../../util/file_explorer.dart';
import '../../util/file_icon.dart'; import '../../util/file_icon.dart';
@@ -110,19 +110,17 @@ class BuildTaskListView extends GetView {
List<Widget> buildActions() { List<Widget> buildActions() {
final list = <Widget>[]; final list = <Widget>[];
if (isDone()) { if (isDone()) {
if (Util.isDesktop() || Util.isMobile()) { list.add(IconButton(
list.add(IconButton( icon: const Icon(Icons.folder_open),
icon: const Icon(Icons.folder_open), onPressed: () {
onPressed: () { if (Util.isDesktop()) {
if (Util.isDesktop()) { FileExplorer.openAndSelectFile(buildExplorerUrl(task));
FileExplorer.openAndSelectFile(buildExplorerUrl(task)); } else {
} else { Get.rootDelegate
Get.rootDelegate .toNamed(Routes.TASK_FILES, parameters: {'id': task.id});
.toNamed(Routes.TASK_FILES, parameters: {'id': task.id}); }
} },
}, ));
));
}
} else { } else {
if (isRunning()) { if (isRunning()) {
list.add(IconButton( list.add(IconButton(
-2
View File
@@ -4,8 +4,6 @@ import "libgopeed_boot_stub.dart"
if (dart.library.io) 'entry/libgopeed_boot_native.dart'; if (dart.library.io) 'entry/libgopeed_boot_native.dart';
abstract class LibgopeedBoot { abstract class LibgopeedBoot {
static const unixSocketPath = 'gopeed.sock';
static LibgopeedBoot? _instance; static LibgopeedBoot? _instance;
static LibgopeedBoot get instance { static LibgopeedBoot get instance {
+61 -17
View File
@@ -28,6 +28,7 @@ class LocaleKeys {
static const donate = 'donate'; static const donate = 'donate';
static const exit = 'exit'; static const exit = 'exit';
static const create = 'create'; static const create = 'create';
static const directDownload = 'directDownload';
static const advancedOptions = 'advancedOptions'; static const advancedOptions = 'advancedOptions';
static const downloadLink = 'downloadLink'; static const downloadLink = 'downloadLink';
static const downloadLinkValid = 'downloadLinkValid'; static const downloadLinkValid = 'downloadLinkValid';
@@ -82,6 +83,13 @@ class LocaleKeys {
static const extensionUpdateSuccess = 'extensionUpdateSuccess'; static const extensionUpdateSuccess = 'extensionUpdateSuccess';
static const extensionDelete = 'extensionDelete'; static const extensionDelete = 'extensionDelete';
static const extensionAlreadyLatest = 'extensionAlreadyLatest'; static const extensionAlreadyLatest = 'extensionAlreadyLatest';
static const extensionFind = 'extensionFind';
static const extensionDevelop = 'extensionDevelop';
static const history = 'history';
static const clearHistory = 'clearHistory';
static const noHistoryFound = 'noHistoryFound';
static const serviceTitle = 'serviceTitle';
static const serviceText = 'serviceText';
} }
class Locales { class Locales {
@@ -92,13 +100,14 @@ class Locales {
'confirm': 'Confirm', 'confirm': 'Confirm',
'cancel': 'Cancel', 'cancel': 'Cancel',
'selectAll': 'Select All', 'selectAll': 'Select All',
'task': 'Task', 'task': 'Tasks',
'downloading': 'downloading', 'downloading': 'downloading',
'downloaded': 'downloaded', 'downloaded': 'downloaded',
'setting': 'Setting', 'setting': 'Settings',
'donate': 'Donate', 'donate': 'Donate',
'exit': 'Exit', 'exit': 'Exit',
'create': 'Create Task', 'create': 'Create Task',
'directDownload': 'Direct Download',
'advancedOptions': 'Advanced Options', 'advancedOptions': 'Advanced Options',
'downloadLink': 'Download Link', 'downloadLink': 'Download Link',
'downloadLinkValid': 'Please enter the download link', 'downloadLinkValid': 'Please enter the download link',
@@ -155,6 +164,13 @@ class Locales {
'extensionUpdateSuccess': 'Updated successfully', 'extensionUpdateSuccess': 'Updated successfully',
'extensionDelete': 'Delete Extension', 'extensionDelete': 'Delete Extension',
'extensionAlreadyLatest': 'It\'s already the latest version', 'extensionAlreadyLatest': 'It\'s already the latest version',
'extensionFind': 'Find Extensions',
'extensionDevelop': 'Develop Extensions',
'history': 'History',
'clearHistory': 'Clear History',
'noHistoryFound': 'No History Found',
'serviceTitle': 'Download Service',
'serviceText': 'Running',
}; };
static const fa_IR = { static const fa_IR = {
'label': 'فارسی', 'label': 'فارسی',
@@ -290,15 +306,17 @@ class Locales {
'donate': 'Пожертвовать', 'donate': 'Пожертвовать',
'exit': 'Выход', 'exit': 'Выход',
'create': 'Создать задачу', 'create': 'Создать задачу',
'downloadLink': 'Ссылка для скачивание', 'advancedOptions': 'Расширенные опции',
'downloadLink': 'Ссылка на скачивание',
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания', 'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
'downloadLinkHit': 'downloadLinkHit':
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append', 'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append',
'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл', 'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл',
'download': 'Скачать', 'download': 'Скачать',
'noFileSelected': 'Файл не выбран', 'noFileSelected': 'Файл не выбран',
'noStoragePermission': 'Требуется доступ к хранилищу', 'noStoragePermission': 'Требуется доступ к хранилищу',
'selectFile': 'Выберать файл', 'selectFile': 'Выбрать файл',
'rename': 'Переименовать',
'basic': 'Основные параметры', 'basic': 'Основные параметры',
'advanced': 'Расширенные', 'advanced': 'Расширенные',
'general': 'Общие', 'general': 'Общие',
@@ -338,6 +356,16 @@ class Locales {
'newVersionTitle': 'Обнаружена новая версия @version', 'newVersionTitle': 'Обнаружена новая версия @version',
'newVersionUpdate': 'Обновить', 'newVersionUpdate': 'Обновить',
'newVersionLater': 'позже', 'newVersionLater': 'позже',
'extensions': 'Расширения',
'extensionInstallSuccess': 'Установка завершена',
'extensionUpdateSuccess': 'Обновление завершено',
'extensionDelete': 'Удалить расширение',
'extensionAlreadyLatest': 'Это последняя версия',
'extensionFind': 'Найти расширения',
'extensionDevelop': 'Разработка расширений',
'history': 'История',
'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена',
}; };
static const zh_CN = { static const zh_CN = {
'label': '中文(简体)', 'label': '中文(简体)',
@@ -353,6 +381,7 @@ class Locales {
'donate': '打赏', 'donate': '打赏',
'exit': '退出', 'exit': '退出',
'create': '创建任务', 'create': '创建任务',
'directDownload': '直接下载',
'advancedOptions': '高级选项', 'advancedOptions': '高级选项',
'downloadLink': '下载链接', 'downloadLink': '下载链接',
'downloadLinkValid': '请输入下载链接', 'downloadLinkValid': '请输入下载链接',
@@ -407,9 +436,16 @@ class Locales {
'extensionUpdateSuccess': '更新成功', 'extensionUpdateSuccess': '更新成功',
'extensionDelete': '删除扩展', 'extensionDelete': '删除扩展',
'extensionAlreadyLatest': '已经是最新版本', 'extensionAlreadyLatest': '已经是最新版本',
'extensionFind': '获取扩展',
'extensionDevelop': '开发扩展',
'history': '历史记录',
'clearHistory': '清空历史记录',
'noHistoryFound': '暂无历史记录',
'serviceTitle': '下载服务',
'serviceText': '运行中',
}; };
static const zh_TW = { static const zh_TW = {
'label': '中文 (正體) ', 'label': '中文 (繁體) ',
'error': '錯誤', 'error': '錯誤',
'tip': '提示', 'tip': '提示',
'confirm': '確定', 'confirm': '確定',
@@ -422,6 +458,7 @@ class Locales {
'donate': '斗內', 'donate': '斗內',
'exit': '離開', 'exit': '離開',
'create': '新增任務', 'create': '新增任務',
'directDownload': '直接下載',
'advancedOptions': '進階選項', 'advancedOptions': '進階選項',
'downloadLink': '下載鏈接', 'downloadLink': '下載鏈接',
'downloadLinkValid': '請輸入下載鏈接', 'downloadLinkValid': '請輸入下載鏈接',
@@ -437,16 +474,16 @@ class Locales {
'general': '通用設定', 'general': '通用設定',
'downloadDir': '下載目録', 'downloadDir': '下載目録',
'downloadDirValid': '請選擇下載目録', 'downloadDirValid': '請選擇下載目録',
'connections': '同時連接數量', 'connections': '同時連接數',
'maxRunning': '最大同時下載數', 'maxRunning': '最大同時下載數',
'items': '@count 項', 'items': '@count 項',
'subscribeTracker': '已訂閲的 Tracker', 'subscribeTracker': '已訂閲的追蹤者列表',
'subscribeFail': '訂閲失敗', 'subscribeFail': '訂閲失敗',
'update': '更新', 'update': '更新',
'updateDaily': '每日自動更新', 'updateDaily': '每天自動更新',
'lastUpdate': '上次更新時間: @time', 'lastUpdate': '上次更新時間: @time',
'addTracker': '增加新的 Tracker', 'addTracker': '增加新的追蹤者',
'addTrackerHit': '請輸入 Tracker 服務器地址,每行一條', 'addTrackerHit': '請輸入追蹤者服務器地址,每行一條',
'ui': '介面', 'ui': '介面',
'theme': '佈景主題', 'theme': '佈景主題',
'themeSystem': '跟隨系統', 'themeSystem': '跟隨系統',
@@ -456,12 +493,12 @@ class Locales {
'about': '關於', 'about': '關於',
'homepage': '訪問主頁', 'homepage': '訪問主頁',
'version': '版本資訊', 'version': '版本資訊',
'protocol': '通訊協定', 'protocol': '端口設定',
'port': '端口', 'port': '端口',
'apiToken': '接口 Token', 'apiToken': '接口秘鑰',
'notSet': '尚未設定', 'notSet': '未設定',
'set': '已設定', 'set': '已設定',
'effectAfterRestart': '該選項將在重新啓動軟體後生效', 'effectAfterRestart': '重啟後生效',
'startAll': '全部開始', 'startAll': '全部開始',
'pauseAll': '全部暫停', 'pauseAll': '全部暫停',
'deleteTask': '刪除任務', 'deleteTask': '刪除任務',
@@ -471,10 +508,17 @@ class Locales {
'newVersionUpdate': '立刻更新', 'newVersionUpdate': '立刻更新',
'newVersionLater': '稍後更新', 'newVersionLater': '稍後更新',
'extensions': '外掛程序', 'extensions': '外掛程序',
'extensionInstallUrl': '安裝鏈接', 'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝完成', 'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成', 'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝', 'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本', 'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得外掛',
'extensionDevelop': '開發外掛',
'history': '歷史記錄',
'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄',
'serviceTitle': '下載服務',
'serviceText': '執行中',
}; };
} }
+5 -3
View File
@@ -37,11 +37,13 @@ Future<void> init() async {
}); });
} }
if (!Util.isWeb()) {
await Util.initStorageDir();
}
initLogger();
final controller = Get.put(AppController()); final controller = Get.put(AppController());
try { try {
if (!Util.isWeb()) {
await Util.initStorageDir();
}
await controller.loadStartConfig(); await controller.loadStartConfig();
final startCfg = controller.startConfig.value; final startCfg = controller.startConfig.value;
controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg); controller.runningPort.value = await LibgopeedBoot.instance.start(startCfg);
+2 -2
View File
@@ -26,14 +26,14 @@ class GopeedTheme {
}); });
static final _light = ThemeData( static final _light = ThemeData(
//TODO useMaterial3: true, useMaterial3: false,
brightness: Brightness.light, brightness: Brightness.light,
primarySwatch: _gopeedreen); primarySwatch: _gopeedreen);
static final light = _light.copyWith( static final light = _light.copyWith(
colorScheme: _light.colorScheme.copyWith(secondary: _gopeedreenAccent)); colorScheme: _light.colorScheme.copyWith(secondary: _gopeedreenAccent));
static final _dark = ThemeData( static final _dark = ThemeData(
//TODO useMaterial3: true, useMaterial3: false,
brightness: Brightness.dark, brightness: Brightness.dark,
primarySwatch: _gopeedreen); primarySwatch: _gopeedreen);
static final dark = _dark.copyWith( static final dark = _dark.copyWith(
@@ -0,0 +1,4 @@
import 'browser_download_stub.dart'
if (dart.library.html) 'entry/browser_download_browser.dart';
void download(String url, String name) => doDownload(url, name);
@@ -0,0 +1 @@
void doDownload(String url, String name) => throw UnimplementedError();
@@ -0,0 +1,9 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
void doDownload(String url, String name) {
final anchorElement = html.AnchorElement(href: url);
anchorElement.download = name;
anchorElement.target = '_blank';
anchorElement.click();
}
+13 -7
View File
@@ -1,18 +1,24 @@
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:logger/logger.dart'; import 'package:logger/logger.dart';
import 'package:path/path.dart' as path;
import 'util.dart'; import 'util.dart';
final logger = Logger( late final Logger logger;
filter: ProductionFilter(),
printer: SimplePrinter(printTime: true, colors: false),
output: buildOutput(),
);
buildOutput() { initLogger() {
// if is debug mode, don't log to file
logger = Logger(
filter: ProductionFilter(),
printer: SimplePrinter(printTime: true, colors: false),
output: _buildOutput(),
);
}
_buildOutput() {
// if is debug mode, don't log to file // if is debug mode, don't log to file
if (!kDebugMode && Util.isDesktop()) { if (!kDebugMode && Util.isDesktop()) {
const logDirPath = 'logs'; final logDirPath = path.join(Util.getStorageDir(), 'logs');
var logDir = Directory(logDirPath); var logDir = Directory(logDirPath);
if (!logDir.existsSync()) { if (!logDir.existsSync()) {
logDir.createSync(); logDir.createSync();
+15 -4
View File
@@ -29,6 +29,7 @@ class Util {
static String safePathJoin(List<String> paths) { static String safePathJoin(List<String> paths) {
return paths return paths
.where((e) => e.isNotEmpty)
.map((e) => safeDir(e)) .map((e) => safeDir(e))
.join("/") .join("/")
.replaceAll(RegExp(r'//'), "/"); .replaceAll(RegExp(r'//'), "/");
@@ -54,6 +55,16 @@ class Util {
storageDir = (await getExternalStorageDirectory())?.path ?? storageDir; storageDir = (await getExternalStorageDirectory())?.path ?? storageDir;
} else if (Platform.isIOS) { } else if (Platform.isIOS) {
storageDir = (await getLibraryDirectory()).path; storageDir = (await getLibraryDirectory()).path;
} else if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
// check has write permission, if not, fallback to application support dir
try {
final testFile = File(path.join(storageDir, ".test"));
await testFile.writeAsString("test");
await testFile.delete();
} catch (e) {
storageDir = (await getApplicationSupportDirectory()).path;
}
} }
_storageDir = storageDir; _storageDir = storageDir;
} }
@@ -62,10 +73,6 @@ class Util {
return _storageDir!; return _storageDir!;
} }
static String getStorageAsset(String asset) {
return path.join(getStorageDir(), asset);
}
static isAndroid() { static isAndroid() {
return !kIsWeb && Platform.isAndroid; return !kIsWeb && Platform.isAndroid;
} }
@@ -93,6 +100,10 @@ class Util {
return !kIsWeb && Platform.isMacOS; return !kIsWeb && Platform.isMacOS;
} }
static isLinux() {
return !kIsWeb && Platform.isLinux;
}
static isWeb() { static isWeb() {
return kIsWeb; return kIsWeb;
} }
@@ -0,0 +1,14 @@
[Desktop Entry]
Name=Gopeed
GenericName=Download Manager
GenericName[zh_CN]=下载器
GenericName[zh_TW]=下載器
Comment=High speed downloader that supports all platforms.
Comment[zh_CN]=支持全平台的高速下载器
Comment[zh_TW]=支持全平臺的高速下載器
Terminal=false
Exec=gopeed
Icon=com.gopeed.Gopeed
Type=Application
Categories=Utility;Internet
Keywords=Bittorrent;Downloader;
+148 -139
View File
@@ -5,34 +5,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: _fe_analyzer_shared name: _fe_analyzer_shared
sha256: "0816708f5fbcacca324d811297153fe3c8e047beb5c6752e12292d2974c17045" sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "62.0.0" version: "64.0.0"
analyzer: analyzer:
dependency: transitive dependency: transitive
description: description:
name: analyzer name: analyzer
sha256: "21862995c9932cd082f89d72ae5f5e2c110d1a0204ad06e4ebaee8307b76b834" sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.0.0" version: "6.2.0"
app_links: app_links:
dependency: "direct main" dependency: "direct main"
description: description:
name: app_links name: app_links
sha256: "16725e716afd0634a5441654b1dda2b6c5557aa230884b5e1f41a5aa546a4cb6" sha256: eb83c2b15b78a66db04e95132678e910fcdb8dc3a9b0aed0c138f50b2bef0dae
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.4.3" version: "3.4.5"
archive: archive:
dependency: transitive dependency: transitive
description: description:
name: archive name: archive
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.3.7" version: "3.4.6"
args: args:
dependency: transitive dependency: transitive
description: description:
@@ -61,10 +61,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: badges name: badges
sha256: "6e7f3ec561ec08f47f912cfe349d4a1707afdc8dda271e17b046aa6d42c89e77" sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.1" version: "3.1.2"
boolean_selector: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@@ -101,10 +101,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_resolvers name: build_resolvers
sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.1" version: "2.4.1"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -117,10 +117,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_runner_core name: build_runner_core
sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.2.10" version: "7.2.11"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@@ -133,10 +133,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: built_value name: built_value
sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "8.6.1" version: "8.6.3"
characters: characters:
dependency: transitive dependency: transitive
description: description:
@@ -173,18 +173,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: code_builder name: code_builder
sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.5.0" version: "4.7.0"
collection: collection:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.17.1" version: "1.18.0"
context_menus: context_menus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -205,10 +205,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: cross_file name: cross_file
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9" sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.3.3+4" version: "0.3.3+6"
crypto: crypto:
dependency: transitive dependency: transitive
description: description:
@@ -221,42 +221,34 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: cupertino_icons name: cupertino_icons
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.5" version: "1.0.6"
dart_internal:
dependency: transitive
description:
name: dart_internal
sha256: dae3976f383beddcfcd07ad5291a422df2c8c0a8a03c52cda63ac7b4f26e0f4e
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.2.8"
dart_style: dart_style:
dependency: transitive dependency: transitive
description: description:
name: dart_style name: dart_style
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.2" version: "2.3.3"
desktop_drop: desktop_drop:
dependency: "direct main" dependency: "direct main"
description: description:
name: desktop_drop name: desktop_drop
sha256: ebba9c9cb0b54385998a977d741cc06fd8324878c08d5a36e9da61cd56b04cc6 sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.4.3" version: "0.4.4"
dio: dio:
dependency: "direct main" dependency: "direct main"
description: description:
name: dio name: dio
sha256: a9d76e72985d7087eb7c5e7903224ae52b337131518d127c554b9405936752b8 sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "5.2.1+1" version: "5.3.3"
fake_async: fake_async:
dependency: transitive dependency: transitive
description: description:
@@ -269,10 +261,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: ffi name: ffi
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.2" version: "2.1.0"
ffigen: ffigen:
dependency: "direct dev" dependency: "direct dev"
description: description:
@@ -309,15 +301,23 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: fluent_ui name: fluent_ui
sha256: be237134ab215d4a1591307f6286f4aa53ed5a492c2ee56be5d86be344fd16db sha256: "43e95034b12a8285a78703b5995bb4d2e02a28750a1a363044c150533bc7f027"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.7.0" version: "4.7.6"
flutter: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" version: "0.0.0"
flutter_foreground_task:
dependency: "direct main"
description:
name: flutter_foreground_task
sha256: e48d2d810a2d643362e64de41146ed8e95d4dd282bae6abbb32309d9f0bf5d67
url: "https://pub.flutter-io.cn"
source: hosted
version: "6.1.2"
flutter_form_builder: flutter_form_builder:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -338,10 +338,10 @@ packages:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: flutter_lints name: flutter_lints
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4" sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.2" version: "2.0.3"
flutter_localizations: flutter_localizations:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@@ -351,10 +351,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: flutter_plugin_android_lifecycle name: flutter_plugin_android_lifecycle
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360" sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.15" version: "2.0.17"
flutter_test: flutter_test:
dependency: "direct dev" dependency: "direct dev"
description: flutter description: flutter
@@ -433,18 +433,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: image name: image
sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.0.17" version: "4.1.3"
intl: intl:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.18.0" version: "0.18.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -513,10 +513,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: logging_appenders name: logging_appenders
sha256: "69e6f077fa0f416dbb0d926fdb066dd686245ac5bd36e8fefce6ee4783d3c766" sha256: "1fb8a008c04246f4677a0d034d69779a5975e56e02573a5162240239b247e239"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.2.0+1"
lottie: lottie:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -537,18 +537,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.12.15" version: "0.12.16"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.2.0" version: "0.5.0"
math_expressions: math_expressions:
dependency: transitive dependency: transitive
description: description:
@@ -569,10 +569,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -601,10 +601,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: package_info_plus name: package_info_plus
sha256: ceb027f6bc6a60674a233b4a90a7658af1aebdea833da0b5b53c1e9821a78c7b sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "4.0.2" version: "4.2.0"
package_info_plus_platform_interface: package_info_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
@@ -625,50 +625,50 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: path_provider name: path_provider
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.15" version: "2.1.1"
path_provider_android: path_provider_android:
dependency: transitive dependency: transitive
description: description:
name: path_provider_android name: path_provider_android
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.27" version: "2.2.1"
path_provider_foundation: path_provider_foundation:
dependency: transitive dependency: transitive
description: description:
name: path_provider_foundation name: path_provider_foundation
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.3" version: "2.3.1"
path_provider_linux: path_provider_linux:
dependency: transitive dependency: transitive
description: description:
name: path_provider_linux name: path_provider_linux
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.11" version: "2.2.1"
path_provider_platform_interface: path_provider_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: path_provider_platform_interface name: path_provider_platform_interface
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.6" version: "2.1.1"
path_provider_windows: path_provider_windows:
dependency: transitive dependency: transitive
description: description:
name: path_provider_windows name: path_provider_windows
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.7" version: "2.2.1"
petitparser: petitparser:
dependency: transitive dependency: transitive
description: description:
@@ -681,18 +681,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: platform name: platform
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.1.0" version: "3.1.3"
plugin_platform_interface: plugin_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: plugin_platform_interface name: plugin_platform_interface
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.4" version: "2.1.6"
pointycastle: pointycastle:
dependency: transitive dependency: transitive
description: description:
@@ -709,14 +709,6 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.5.1" version: "1.5.1"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.flutter-io.cn"
source: hosted
version: "4.2.4"
pub_semver: pub_semver:
dependency: transitive dependency: transitive
description: description:
@@ -769,10 +761,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: screen_retriever name: screen_retriever
sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323" sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.1.6" version: "0.1.9"
scroll_pos: scroll_pos:
dependency: transitive dependency: transitive
description: description:
@@ -785,74 +777,74 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: share_plus name: share_plus
sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11" sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "7.1.0" version: "7.2.1"
share_plus_platform_interface: share_plus_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: share_plus_platform_interface name: share_plus_platform_interface
sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7" sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.3.0" version: "3.3.1"
shared_preferences: shared_preferences:
dependency: "direct main" dependency: "direct main"
description: description:
name: shared_preferences name: shared_preferences
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.0" version: "2.2.2"
shared_preferences_android: shared_preferences_android:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_android name: shared_preferences_android
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_foundation: shared_preferences_foundation:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_foundation name: shared_preferences_foundation
sha256: b046999bf0ff58f04c364491bb803dcfa8f42e47b19c75478f53d323684a8cc1 sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.1" version: "2.3.4"
shared_preferences_linux: shared_preferences_linux:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_linux name: shared_preferences_linux
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1" sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.2"
shared_preferences_platform_interface: shared_preferences_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_platform_interface name: shared_preferences_platform_interface
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.1"
shared_preferences_web: shared_preferences_web:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_web name: shared_preferences_web
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.2.0" version: "2.2.1"
shared_preferences_windows: shared_preferences_windows:
dependency: transitive dependency: transitive
description: description:
name: shared_preferences_windows name: shared_preferences_windows
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.3.0" version: "2.3.2"
shelf: shelf:
dependency: transitive dependency: transitive
description: description:
@@ -902,26 +894,34 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
sprintf:
dependency: transitive
description:
name: sprintf
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
url: "https://pub.flutter-io.cn"
source: hosted
version: "7.0.0"
stack_trace: stack_trace:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.11.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@@ -958,10 +958,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.5.1" version: "0.6.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -973,10 +973,11 @@ packages:
tray_manager: tray_manager:
dependency: "direct main" dependency: "direct main"
description: description:
name: tray_manager path: "."
sha256: b1975a05e0c6999e983cf9a58a6a098318c896040ccebac5398a3cc9e43b9c69 ref: "fix/libayatana-set-icon-deprecation"
url: "https://pub.flutter-io.cn" resolved-ref: "056da3fef20e75877e8d2a4ae73d93f51a0f77cc"
source: hosted url: "https://github.com/provokateurin/tray_manager.git"
source: git
version: "0.2.0" version: "0.2.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
@@ -998,74 +999,74 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: url_launcher name: url_launcher
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e" sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.1.12" version: "6.1.14"
url_launcher_android: url_launcher_android:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_android name: url_launcher_android
sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03" sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.0.36" version: "6.2.0"
url_launcher_ios: url_launcher_ios:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_ios name: url_launcher_ios
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.1.4" version: "6.2.0"
url_launcher_linux: url_launcher_linux:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_linux name: url_launcher_linux
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.5" version: "3.1.0"
url_launcher_macos: url_launcher_macos:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_macos name: url_launcher_macos
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.5" version: "3.1.0"
url_launcher_platform_interface: url_launcher_platform_interface:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_platform_interface name: url_launcher_platform_interface
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.3" version: "2.2.0"
url_launcher_web: url_launcher_web:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_web name: url_launcher_web
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4 sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.0.18" version: "2.0.19"
url_launcher_windows: url_launcher_windows:
dependency: transitive dependency: transitive
description: description:
name: url_launcher_windows name: url_launcher_windows
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422" sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.7" version: "3.1.0"
uuid: uuid:
dependency: transitive dependency: transitive
description: description:
name: uuid name: uuid
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "3.0.7" version: "4.1.0"
vector_math: vector_math:
dependency: transitive dependency: transitive
description: description:
@@ -1082,6 +1083,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.0"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@@ -1102,18 +1111,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: window_manager name: window_manager
sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88" sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.3.5" version: "0.3.7"
xdg_directories: xdg_directories:
dependency: transitive dependency: transitive
description: description:
name: xdg_directories name: xdg_directories
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.0.0" version: "1.0.3"
xml: xml:
dependency: transitive dependency: transitive
description: description:
@@ -1139,5 +1148,5 @@ packages:
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
sdks: sdks:
dart: ">=3.0.0 <3.2.0" dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.10.0" flutter: ">=3.10.0"
+7 -3
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.4.2+1 version: 1.5.0+1
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@@ -41,7 +41,7 @@ dependencies:
context_menus: ^1.0.2 context_menus: ^1.0.2
json_annotation: ^4.8.1 json_annotation: ^4.8.1
dio: ^5.2.1 dio: ^5.2.1
path_provider: ^2.0.11 path_provider: ^2.1.1
file_picker: 5.3.1 file_picker: 5.3.1
macos_secure_bookmarks: ^0.2.1 macos_secure_bookmarks: ^0.2.1
rounded_loading_button: ^2.1.0 rounded_loading_button: ^2.1.0
@@ -56,12 +56,16 @@ dependencies:
app_links: ^3.4.2 app_links: ^3.4.2
uri_to_file: ^0.2.0 uri_to_file: ^0.2.0
window_manager: ^0.3.4 window_manager: ^0.3.4
tray_manager: ^0.2.0 tray_manager:
git:
url: https://github.com/provokateurin/tray_manager.git
ref: fix/libayatana-set-icon-deprecation
autoscale_tabbarview: ^1.0.2 autoscale_tabbarview: ^1.0.2
share_plus: ^7.1.0 share_plus: ^7.1.0
open_file: ^3.3.2 open_file: ^3.3.2
flutter_form_builder: ^9.1.1 flutter_form_builder: ^9.1.1
form_builder_validators: ^9.0.0 form_builder_validators: ^9.0.0
flutter_foreground_task: ^6.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
+6 -1
View File
@@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake)
# https://github.com/flutter/flutter/issues/57146. # https://github.com/flutter/flutter/issues/57146.
set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper")
# Set fallback configurations for older versions of the flutter tool.
if (NOT DEFINED FLUTTER_TARGET_PLATFORM)
set(FLUTTER_TARGET_PLATFORM "windows-x64")
endif()
# === Flutter Library === # === Flutter Library ===
set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll")
@@ -92,7 +97,7 @@ add_custom_command(
COMMAND ${CMAKE_COMMAND} -E env COMMAND ${CMAKE_COMMAND} -E env
${FLUTTER_TOOL_ENVIRONMENT} ${FLUTTER_TOOL_ENVIRONMENT}
"${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat"
windows-x64 $<CONFIG> ${FLUTTER_TARGET_PLATFORM} $<CONFIG>
VERBATIM VERBATIM
) )
add_custom_target(flutter_assemble DEPENDS add_custom_target(flutter_assemble DEPENDS
@@ -31,6 +31,11 @@ bool FlutterWindow::OnCreate() {
this->Show(); this->Show();
}); });
// Flutter can complete the first frame before the "show window" callback is
// registered. The following call ensures a frame is pending to ensure the
// window is shown. It is a no-op if the first frame hasn't completed yet.
flutter_controller_->ForceRedraw();
return true; return true;
} }