mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-10 02:20:28 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4840c4b8a4 | ||
|
|
e72b097aa2 | ||
|
|
af52c4ce7e | ||
|
|
aae79c2449 | ||
|
|
83838afce9 | ||
|
|
4690107e32 | ||
|
|
74b08b3631 | ||
|
|
953d348c0c | ||
|
|
00cea68b6e | ||
|
|
daa9d4c707 | ||
|
|
6bc587752a | ||
|
|
11befc09bd | ||
|
|
ff6c0da262 | ||
|
|
aab4a37a7e | ||
|
|
74c9491893 | ||
|
|
256faf2cbd | ||
|
|
32e3620f5e | ||
|
|
39202b44a1 | ||
|
|
457d9e85d6 | ||
|
|
858ad74339 | ||
|
|
755d7ce814 | ||
|
|
854f600d3f | ||
|
|
28da89f08c | ||
|
|
dddefb68a2 |
No files matched your search
@@ -11,6 +11,8 @@ categories:
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: '🧪 Tests'
|
||||
label: 'test'
|
||||
- title: '🧰 Maintenance'
|
||||
label: 'chore'
|
||||
- title: '📖 Document'
|
||||
@@ -30,6 +32,10 @@ version-resolver:
|
||||
- 'patch'
|
||||
default: patch
|
||||
template: |
|
||||
## Changes
|
||||
# Changes
|
||||
|
||||
$CHANGES
|
||||
|
||||
# 更新日志
|
||||
|
||||
$CHANGES
|
||||
+161
-6
@@ -2,6 +2,11 @@ name: build
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: 'Build platform'
|
||||
required: true
|
||||
default: 'all'
|
||||
|
||||
env:
|
||||
GO_VERSION: "1.19"
|
||||
@@ -24,6 +29,7 @@ jobs:
|
||||
myToken: ${{ github.token }}
|
||||
|
||||
build-windows:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||
runs-on: windows-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
@@ -44,7 +50,7 @@ jobs:
|
||||
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
|
||||
$mingw = "C:\ProgramData\Chocolatey\lib\mingw\tools\install\mingw64\bin"
|
||||
$mingw = "C:\Program Files\Git\mingw64\bin"
|
||||
$system = "C:\Windows\System32"
|
||||
$release = "build\windows\runner\Release\"
|
||||
cp $mingw\libstdc++-6.dll $release
|
||||
@@ -124,6 +130,7 @@ jobs:
|
||||
asset_path: ui/flutter/build/windows/Output/*
|
||||
overwrite: true
|
||||
build-macos-arm64-lib:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
@@ -134,13 +141,14 @@ jobs:
|
||||
- name: Build
|
||||
run: |
|
||||
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
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: macos-arm64-lib
|
||||
path: libgopeed.dylib
|
||||
build-macos:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-latest
|
||||
needs: [ get-release, build-macos-arm64-lib ]
|
||||
steps:
|
||||
@@ -155,7 +163,9 @@ jobs:
|
||||
with:
|
||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||
- name: Install appdmg
|
||||
run: npm install -g appdmg
|
||||
run: |
|
||||
python3 -m pip install setuptools
|
||||
npm install -g appdmg
|
||||
- uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: macos-arm64-lib
|
||||
@@ -197,6 +207,7 @@ jobs:
|
||||
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
|
||||
overwrite: true
|
||||
build-linux:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
@@ -210,6 +221,10 @@ jobs:
|
||||
- run: |
|
||||
sudo apt-get update -y
|
||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||
- name: Setup LXD
|
||||
uses: canonical/[email protected]
|
||||
with:
|
||||
channel: latest/stable
|
||||
- name: Build
|
||||
env:
|
||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
||||
@@ -217,13 +232,20 @@ 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
|
||||
cd ui/flutter
|
||||
flutter build linux
|
||||
mkdir -p build/dist
|
||||
|
||||
# Build portable
|
||||
tar -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz build/linux/x64/release/bundle
|
||||
|
||||
|
||||
# Build deb package
|
||||
mkdir -p debian/gui
|
||||
cp assets/icon/icon_1024.png debian/gui/gopeed.png
|
||||
cat>debian/debian.yaml<<EOF
|
||||
flutter_app:
|
||||
command: gopeed
|
||||
arch: x64
|
||||
parent: /usr/local/lib
|
||||
parent: /usr/local/bin
|
||||
control:
|
||||
Package: gopeed
|
||||
Version: ${VERSION:1}
|
||||
@@ -241,6 +263,8 @@ jobs:
|
||||
Name=Gopeed
|
||||
GenericName=Gopeed
|
||||
Comment=High speed downloader that supports all platforms.
|
||||
Exec=gopeed
|
||||
Icon=gopeed.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility
|
||||
@@ -249,14 +273,142 @@ jobs:
|
||||
|
||||
dart pub global activate flutter_to_debian
|
||||
flutter_to_debian
|
||||
mv debian/packages/gopeed*.deb debian/packages/Gopeed-$VERSION-linux-amd64.deb
|
||||
mv debian/packages/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
|
||||
cp assets/icon/icon_1024.png AppDir/usr/share/icons/gopeed.png
|
||||
cat>AppImageBuilder.yml<<EOF
|
||||
version: 1
|
||||
AppDir:
|
||||
path: ${PWD}/AppDir
|
||||
app_info:
|
||||
id: com.gopeed.gopeed
|
||||
name: Gopeed
|
||||
icon: gopeed.png
|
||||
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
|
||||
|
||||
# Build snap
|
||||
sudo snap install snapcraft --classic
|
||||
rm -rf build/linux
|
||||
mkdir -p snap/gui
|
||||
cp assets/icon/icon_1024.png snap/gui/gopeed.png
|
||||
|
||||
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:
|
||||
- libayatana-appindicator3-dev
|
||||
flutter-target: lib/main.dart
|
||||
EOF
|
||||
|
||||
cat>snap/gui/gopeed.desktop<<EOF
|
||||
[Desktop Entry]
|
||||
Version=${VERSION:1}
|
||||
Name=Gopeed
|
||||
GenericName=Gopeed
|
||||
Comment=High speed downloader that supports all platforms.
|
||||
Exec=gopeed
|
||||
Icon=\${SNAP}/meta/gui/gopeed.png
|
||||
Terminal=false
|
||||
Type=Application
|
||||
Categories=Utility
|
||||
Keywords=Flutter;Downloader;
|
||||
EOF
|
||||
|
||||
snapcraft --use-lxd
|
||||
|
||||
# Snapcraft login
|
||||
export SNAPCRAFT_STORE_CREDENTIALS=${{ secrets.SNAP_STORE_LOGIN }}
|
||||
snapcraft upload --release=stable gopeed_${VERSION:1}_amd64.snap
|
||||
- name: Upload
|
||||
uses: shogo82148/actions-upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||
asset_path: ui/flutter/debian/packages/*
|
||||
asset_path: ui/flutter/build/dist/*
|
||||
overwrite: true
|
||||
build-android:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
@@ -294,6 +446,7 @@ jobs:
|
||||
asset_path: ui/flutter/dist/*
|
||||
overwrite: true
|
||||
build-ios:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||
runs-on: macos-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
@@ -326,6 +479,7 @@ jobs:
|
||||
asset_path: ui/flutter/dist/*
|
||||
overwrite: true
|
||||
build-web:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
@@ -373,6 +527,7 @@ jobs:
|
||||
asset_path: dist/zip/*
|
||||
overwrite: true
|
||||
build-docker:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: [ get-release ]
|
||||
steps:
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
|
||||
## 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)
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
## はじめに
|
||||
|
||||
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)
|
||||
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
|
||||
## 介绍
|
||||
|
||||
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/)
|
||||
|
||||
|
||||
+14
-1
@@ -2,6 +2,7 @@ package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"github.com/GopeedLab/gopeed/pkg/download"
|
||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||
@@ -47,16 +48,28 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
printProgress(emptyTask, "downloading...")
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
var (
|
||||
lastLineLen = 0
|
||||
sb = new(strings.Builder)
|
||||
emptyTask = &download.Task{
|
||||
Progress: &download.Progress{},
|
||||
Meta: &fetcher.FetcherMeta{
|
||||
Res: &base.Resource{},
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
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)
|
||||
speed := util.ByteFmt(task.Progress.Speed)
|
||||
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
||||
|
||||
@@ -87,7 +87,6 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
||||
if f.meta.Res != nil {
|
||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||
}
|
||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -101,6 +100,14 @@ func (f *Fetcher) Start() (err error) {
|
||||
ft.setTorrentDir(f.meta.FolderPath())
|
||||
}
|
||||
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) {
|
||||
f.torrent.DownloadAll()
|
||||
} else {
|
||||
@@ -134,7 +141,7 @@ func (f *Fetcher) safeDrop() {
|
||||
|
||||
func (f *Fetcher) Wait() (err error) {
|
||||
for {
|
||||
if f.torrentReady.Load() {
|
||||
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||
done := true
|
||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||
file := f.torrent.Files()[selectIndex]
|
||||
@@ -154,7 +161,7 @@ func (f *Fetcher) Wait() (err error) {
|
||||
}
|
||||
}
|
||||
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
|
||||
@@ -195,7 +202,7 @@ func (f *Fetcher) updateRes() {
|
||||
Size: file.Length(),
|
||||
}
|
||||
}
|
||||
res.CalcSize()
|
||||
res.CalcSize(nil)
|
||||
f.meta.Res = res
|
||||
if f.meta.Opts != nil {
|
||||
f.meta.Opts.InitSelectFiles(len(res.Files))
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
type RequestError struct {
|
||||
@@ -37,6 +38,8 @@ type chunk struct {
|
||||
Begin int64
|
||||
End int64
|
||||
Downloaded int64
|
||||
|
||||
retryTimes int
|
||||
}
|
||||
|
||||
func newChunk(begin int64, end int64) *chunk {
|
||||
@@ -255,24 +258,43 @@ func (f *Fetcher) fetch() {
|
||||
|
||||
func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
chunk := f.chunks[index]
|
||||
chunk.retryTimes = 0
|
||||
|
||||
httpReq, err := buildRequest(ctx, f.meta.Req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var (
|
||||
client = buildClient()
|
||||
buf = make([]byte, 8192)
|
||||
client = buildClient()
|
||||
buf = make([]byte, 8192)
|
||||
maxRetries = 3
|
||||
)
|
||||
// 重试5次
|
||||
for i := 0; i < 5; i++ {
|
||||
// retry until all remain chunks failed
|
||||
for true {
|
||||
// if chunk is completed, return
|
||||
if f.meta.Res.Range && chunk.Downloaded >= chunk.End-chunk.Begin+1 {
|
||||
return
|
||||
}
|
||||
|
||||
if chunk.retryTimes >= maxRetries {
|
||||
if !f.meta.Res.Range {
|
||||
return
|
||||
}
|
||||
// check if all failed
|
||||
allFailed := true
|
||||
for _, c := range f.chunks {
|
||||
if chunk.Downloaded < chunk.End-chunk.Begin+1 && c.retryTimes < maxRetries {
|
||||
allFailed = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if allFailed {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
resp *http.Response
|
||||
retry bool
|
||||
resp *http.Response
|
||||
)
|
||||
if f.meta.Res.Range {
|
||||
httpReq.Header.Set(base.HttpHeaderRange,
|
||||
@@ -292,35 +314,45 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
// retry request
|
||||
// If canceled, do not retry
|
||||
if errors.Is(err, context.Canceled) {
|
||||
return
|
||||
}
|
||||
// retry request after 1 second
|
||||
chunk.retryTimes = chunk.retryTimes + 1
|
||||
time.Sleep(time.Second)
|
||||
continue
|
||||
}
|
||||
// 请求成功就重置错误次数,连续失败5次才终止
|
||||
i = 0
|
||||
retry, err = func() (bool, error) {
|
||||
// Http request success, reset retry times
|
||||
chunk.retryTimes = 0
|
||||
err = func() error {
|
||||
defer resp.Body.Close()
|
||||
for {
|
||||
n, err := resp.Body.Read(buf)
|
||||
if n > 0 {
|
||||
_, err := f.file.WriteAt(buf[:n], chunk.Begin+chunk.Downloaded)
|
||||
if err != nil {
|
||||
return false, err
|
||||
return err
|
||||
}
|
||||
chunk.Downloaded += int64(n)
|
||||
}
|
||||
if err != nil {
|
||||
if err != io.EOF {
|
||||
return true, err
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
break
|
||||
return err
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}()
|
||||
if !retry {
|
||||
// 下载成功,跳出重试
|
||||
break
|
||||
// if chunk is completed, reset other not complete chunks retry times
|
||||
if err == nil {
|
||||
for _, c := range f.chunks {
|
||||
if c != chunk && c.retryTimes > 0 {
|
||||
c.retryTimes = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -438,9 +470,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
||||
fetcher.meta = meta
|
||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||
if len(fd.Chunks) == 0 {
|
||||
fetcher.chunks = fetcher.splitChunk()
|
||||
} else {
|
||||
if len(fd.Chunks) > 0 {
|
||||
fetcher.chunks = fd.Chunks
|
||||
}
|
||||
return fetcher
|
||||
|
||||
@@ -76,7 +76,7 @@ func testResolve(startTestServer func() net.Listener, path string, want *base.Re
|
||||
func TestFetcher_DownloadNormal(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
// 正常下载
|
||||
|
||||
downloadNormal(listener, 1, t)
|
||||
downloadNormal(listener, 5, t)
|
||||
downloadNormal(listener, 8, t)
|
||||
@@ -86,7 +86,7 @@ func TestFetcher_DownloadNormal(t *testing.T) {
|
||||
func TestFetcher_DownloadContinue(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
// 暂停继续
|
||||
|
||||
downloadContinue(listener, 1, t)
|
||||
downloadContinue(listener, 5, t)
|
||||
downloadContinue(listener, 8, t)
|
||||
@@ -96,7 +96,7 @@ func TestFetcher_DownloadContinue(t *testing.T) {
|
||||
func TestFetcher_DownloadChunked(t *testing.T) {
|
||||
listener := test.StartTestCustomServer()
|
||||
defer listener.Close()
|
||||
// chunked编码下载
|
||||
|
||||
downloadNormal(listener, 1, t)
|
||||
downloadNormal(listener, 2, t)
|
||||
}
|
||||
@@ -104,24 +104,33 @@ func TestFetcher_DownloadChunked(t *testing.T) {
|
||||
func TestFetcher_DownloadPost(t *testing.T) {
|
||||
listener := test.StartTestPostServer()
|
||||
defer listener.Close()
|
||||
// post下载
|
||||
|
||||
downloadPost(listener, 1, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadRetry(t *testing.T) {
|
||||
listener := test.StartTestRetryServer()
|
||||
defer listener.Close()
|
||||
// chunked编码下载
|
||||
|
||||
downloadNormal(listener, 1, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadError(t *testing.T) {
|
||||
listener := test.StartTestErrorServer()
|
||||
defer listener.Close()
|
||||
// chunked编码下载
|
||||
|
||||
downloadError(listener, 1, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadLimit(t *testing.T) {
|
||||
listener := test.StartTestLimitServer()
|
||||
defer listener.Close()
|
||||
|
||||
downloadNormal(listener, 1, t)
|
||||
downloadNormal(listener, 2, t)
|
||||
downloadNormal(listener, 8, t)
|
||||
}
|
||||
|
||||
func TestFetcher_DownloadResume(t *testing.T) {
|
||||
listener := test.StartTestFileServer()
|
||||
defer listener.Close()
|
||||
|
||||
@@ -9,6 +9,9 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -81,7 +84,7 @@ func StartTestRetryServer() net.Listener {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
counter++
|
||||
if counter != 1 && counter < 5 {
|
||||
if counter != 1 && counter < 2 {
|
||||
writer.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
@@ -134,6 +137,79 @@ func StartTestErrorServer() net.Listener {
|
||||
})
|
||||
}
|
||||
|
||||
// StartTestLimitServer connections limit server
|
||||
func StartTestLimitServer() net.Listener {
|
||||
var connections atomic.Int32
|
||||
|
||||
return startTestServer(func() http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/"+BuildName, func(writer http.ResponseWriter, request *http.Request) {
|
||||
defer func() {
|
||||
connections.Add(-1)
|
||||
}()
|
||||
connections.Add(1)
|
||||
if connections.Load() > 4 {
|
||||
writer.WriteHeader(403)
|
||||
return
|
||||
}
|
||||
|
||||
r := request.Header.Get("Range")
|
||||
if r == "" {
|
||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", BuildSize))
|
||||
writer.WriteHeader(200)
|
||||
file, err := os.Open(BuildFile)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer file.Close()
|
||||
io.Copy(writer, file)
|
||||
} else {
|
||||
// split range
|
||||
s := strings.Split(r, "=")
|
||||
if len(s) != 2 {
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
s = strings.Split(s[1], "-")
|
||||
if len(s) != 2 {
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
start, err := strconv.ParseInt(s[0], 10, 64)
|
||||
if err != nil {
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
end, err := strconv.ParseInt(s[1], 10, 64)
|
||||
if err != nil {
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
if start < 0 || end < 0 || start > end {
|
||||
writer.WriteHeader(400)
|
||||
return
|
||||
}
|
||||
if end >= BuildSize {
|
||||
end = BuildSize - 1
|
||||
}
|
||||
writer.Header().Set("Content-Length", fmt.Sprintf("%d", end-start+1))
|
||||
writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, BuildSize))
|
||||
writer.Header().Set("Accept-Ranges", "bytes")
|
||||
writer.WriteHeader(206)
|
||||
file, err := os.Open(BuildFile)
|
||||
if err != nil {
|
||||
writer.WriteHeader(500)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
file.Seek(start, 0)
|
||||
io.CopyN(writer, file, end-start+1)
|
||||
}
|
||||
})
|
||||
return mux
|
||||
})
|
||||
}
|
||||
|
||||
func startTestServer(serverHandle func() http.Handler) net.Listener {
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
|
||||
+15
-4
@@ -3,12 +3,22 @@ package base
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
"golang.org/x/exp/slices"
|
||||
)
|
||||
|
||||
// Request download request
|
||||
type Request struct {
|
||||
URL string `json:"url"`
|
||||
Extra any `json:"extra"`
|
||||
// Labels is used to mark the download task
|
||||
Labels map[string]string `json:"labels"`
|
||||
}
|
||||
|
||||
func (r *Request) Validate() error {
|
||||
if r.URL == "" {
|
||||
return fmt.Errorf("invalid request url")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resource download resource
|
||||
@@ -36,13 +46,14 @@ func (r *Resource) Validate() error {
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *Resource) CalcSize() {
|
||||
func (r *Resource) CalcSize(selectFiles []int) {
|
||||
var size int64
|
||||
for _, file := range r.Files {
|
||||
size += file.Size
|
||||
for i, file := range r.Files {
|
||||
if len(selectFiles) == 0 || slices.Contains(selectFiles, i) {
|
||||
size += file.Size
|
||||
}
|
||||
}
|
||||
r.Size = size
|
||||
}
|
||||
|
||||
+294
-208
@@ -9,7 +9,6 @@ import (
|
||||
"github.com/GopeedLab/gopeed/pkg/util"
|
||||
gonanoid "github.com/matoous/go-nanoid/v2"
|
||||
"github.com/virtuald/go-paniclog"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
@@ -28,6 +27,8 @@ const (
|
||||
bucketConfig = "config"
|
||||
// downloader extension bucket
|
||||
bucketExtension = "extension"
|
||||
// downloader extension storage bucket
|
||||
bucketExtensionStorage = "extension_storage"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -54,11 +55,13 @@ type Downloader struct {
|
||||
fetcherCache map[string]fetcher.Fetcher
|
||||
storage Storage
|
||||
tasks []*Task
|
||||
waitTasks []*Task
|
||||
listener Listener
|
||||
|
||||
lock *sync.Mutex
|
||||
fetcherMapLock *sync.RWMutex
|
||||
closed atomic.Bool
|
||||
lock *sync.Mutex
|
||||
fetcherMapLock *sync.RWMutex
|
||||
checkDuplicateLock *sync.Mutex
|
||||
closed atomic.Bool
|
||||
|
||||
extensions []*Extension
|
||||
}
|
||||
@@ -70,14 +73,15 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
||||
cfg.Init()
|
||||
|
||||
d := &Downloader{
|
||||
cfg: cfg,
|
||||
|
||||
cfg: cfg,
|
||||
fetcherBuilders: make(map[string]fetcher.FetcherBuilder),
|
||||
fetcherCache: make(map[string]fetcher.Fetcher),
|
||||
waitTasks: make([]*Task, 0),
|
||||
storage: cfg.Storage,
|
||||
|
||||
storage: cfg.Storage,
|
||||
lock: &sync.Mutex{},
|
||||
fetcherMapLock: &sync.RWMutex{},
|
||||
lock: &sync.Mutex{},
|
||||
fetcherMapLock: &sync.RWMutex{},
|
||||
checkDuplicateLock: &sync.Mutex{},
|
||||
|
||||
extensions: make([]*Extension, 0),
|
||||
}
|
||||
@@ -97,7 +101,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
|
||||
|
||||
func (d *Downloader) Setup() error {
|
||||
// setup storage
|
||||
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension}); err != nil {
|
||||
if err := d.storage.Setup([]string{bucketTask, bucketSave, bucketConfig, bucketExtension, bucketExtensionStorage}); err != nil {
|
||||
return err
|
||||
}
|
||||
// load config from storage
|
||||
@@ -246,6 +250,33 @@ func (d *Downloader) Resolve(req *base.Request) (rr *ResolveResult, err error) {
|
||||
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) {
|
||||
var fetcher fetcher.Fetcher
|
||||
fetcher, err = d.buildFetcher(req.URL)
|
||||
@@ -279,10 +310,21 @@ func (d *Downloader) Pause(id string) (err error) {
|
||||
if task == nil {
|
||||
return ErrTaskNotFound
|
||||
}
|
||||
if err = d.doPause(task, base.DownloadStatusPause); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.notifyRunning()
|
||||
|
||||
func() {
|
||||
task.lock.Lock()
|
||||
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) {
|
||||
@@ -290,109 +332,164 @@ func (d *Downloader) Continue(id string) (err error) {
|
||||
if task == nil {
|
||||
return ErrTaskNotFound
|
||||
}
|
||||
d.tryRunning(func(remain int) {
|
||||
if remain == 0 {
|
||||
// no more task can be running, need to doPause a running task
|
||||
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
remainRunningCount := d.remainRunningCount()
|
||||
if remainRunningCount == 0 {
|
||||
for _, t := range d.tasks {
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
}()
|
||||
|
||||
func() {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
return
|
||||
}
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return d.doContinue(task)
|
||||
|
||||
if err = d.doStart(task); err != nil {
|
||||
return
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) PauseAll() (err error) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
return d.doPauseAll()
|
||||
}
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
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 {
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
runningCount++
|
||||
} else if task.Status != base.DownloadStatusDone {
|
||||
continueTasks = append(continueTasks, task)
|
||||
}
|
||||
}
|
||||
// calculate how many tasks can be continued, can't exceed maxRunning
|
||||
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 {
|
||||
err = func() error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
return d.doPause(task)
|
||||
}()
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (d *Downloader) Delete(id string, force bool) (err error) {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
task := d.GetTask(id)
|
||||
if task == nil {
|
||||
return ErrTaskNotFound
|
||||
}
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
if task.fetcher != nil {
|
||||
err = task.fetcher.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
for i, t := range d.tasks {
|
||||
if t.ID == id {
|
||||
d.tasks = append(d.tasks[:i], d.tasks[i+1:]...)
|
||||
}
|
||||
} 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
|
||||
}
|
||||
}
|
||||
}
|
||||
d.emit(EventKeyDelete, task)
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
if err = d.doNotifyRunning(); err != nil {
|
||||
if err := d.storage.Delete(bucketTask, task.ID); err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (d *Downloader) Close() error {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
d.closed.Store(true)
|
||||
if err := d.doPauseAll(); err != nil {
|
||||
if err := d.PauseAll(); err != nil {
|
||||
return err
|
||||
}
|
||||
return d.storage.Close()
|
||||
@@ -427,6 +524,9 @@ func (d *Downloader) emit(eventKey EventKey, task *Task, errs ...error) {
|
||||
Task: task,
|
||||
Err: err,
|
||||
})
|
||||
if eventKey == EventKeyError {
|
||||
d.emit(EventKeyFinally, task, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -440,11 +540,17 @@ func (d *Downloader) GetTask(id string) *Task {
|
||||
}
|
||||
|
||||
func (d *Downloader) GetTasks() []*Task {
|
||||
// reverse tasks, so that the latest task is in the front
|
||||
tasks := make([]*Task, len(d.tasks))
|
||||
for i := 0; i < len(d.tasks); i++ {
|
||||
tasks[i] = d.tasks[len(d.tasks)-i-1]
|
||||
tasks[i] = d.tasks[i]
|
||||
}
|
||||
// sort tasks by status, order by Status(if(running,1,0)) desc, CreatedAt desc
|
||||
sort.Slice(tasks, func(i, j int) bool {
|
||||
if tasks[i].Status != tasks[j].Status {
|
||||
return tasks[i].Status == base.DownloadStatusRunning
|
||||
}
|
||||
return tasks[i].CreatedAt.After(tasks[j].CreatedAt)
|
||||
})
|
||||
return tasks
|
||||
}
|
||||
|
||||
@@ -479,24 +585,22 @@ func (d *Downloader) watch(task *Task) {
|
||||
d.emit(EventKeyError, task, err)
|
||||
} else {
|
||||
task.Progress.Used = task.timer.Used()
|
||||
if task.Size == 0 {
|
||||
task.Size = task.fetcher.Progress().TotalDownloaded()
|
||||
task.Meta.Res.Size = task.Size
|
||||
if task.Meta.Res.Size == 0 {
|
||||
task.Meta.Res.Size = task.fetcher.Progress().TotalDownloaded()
|
||||
}
|
||||
used := task.Progress.Used / int64(time.Second)
|
||||
if used == 0 {
|
||||
used = 1
|
||||
}
|
||||
task.Progress.Speed = task.Size / used
|
||||
task.Progress.Downloaded = task.Size
|
||||
totalSize := task.Meta.Res.Size
|
||||
task.Progress.Speed = totalSize / used
|
||||
task.Progress.Downloaded = totalSize
|
||||
task.Status = base.DownloadStatusDone
|
||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||
d.emit(EventKeyDone, task)
|
||||
d.emit(EventKeyFinally, task, err)
|
||||
}
|
||||
d.emit(EventKeyFinally, task, err)
|
||||
if task.Status == EventKeyDone {
|
||||
d.notifyRunning()
|
||||
}
|
||||
d.notifyRunning()
|
||||
}
|
||||
|
||||
func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
@@ -524,6 +628,12 @@ func (d *Downloader) restoreFetcher(task *Task) error {
|
||||
task.fetcher = fb.Build()
|
||||
}
|
||||
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)
|
||||
}
|
||||
task.fetcher.Create(task.Meta.Opts)
|
||||
@@ -568,145 +678,122 @@ func (d *Downloader) doCreate(fetcher fetcher.Fetcher, opts *base.Options) (task
|
||||
func() {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
|
||||
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)) {
|
||||
runningCount := 0
|
||||
for _, task := range d.tasks {
|
||||
if task.Status == base.DownloadStatusRunning {
|
||||
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 {
|
||||
remainRunningCount := d.remainRunningCount()
|
||||
if remainRunningCount == 0 {
|
||||
task.Status = base.DownloadStatusWait
|
||||
d.waitTasks = append(d.waitTasks, task)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
d.doStart(task)
|
||||
}()
|
||||
|
||||
go d.watch(task)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) start(task *Task) error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
isCreate := task.Status == base.DownloadStatusReady
|
||||
task.Status = base.DownloadStatusRunning
|
||||
if task.Meta.Res == nil {
|
||||
err := task.fetcher.Resolve(task.Meta.Req)
|
||||
func (d *Downloader) doStart(task *Task) (err error) {
|
||||
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
|
||||
err := d.restoreFetcher(task)
|
||||
if err != nil {
|
||||
task.Status = base.DownloadStatusError
|
||||
d.emit(EventKeyError, task, err)
|
||||
d.Logger.Error().Stack().Err(err).Msgf("restore fetcher failed, task id: %s", task.ID)
|
||||
return err
|
||||
}
|
||||
isCreate = true
|
||||
}
|
||||
|
||||
if isCreate {
|
||||
d.lock.Lock()
|
||||
defer d.lock.Unlock()
|
||||
// check if the download file is duplicated and rename it automatically.
|
||||
if task.Meta.Res.Name != "" {
|
||||
fullDirPath := task.Meta.FolderPath()
|
||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||
if err != nil {
|
||||
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
|
||||
cloneTask := task.clone()
|
||||
isCreate := task.Status == base.DownloadStatusReady
|
||||
task.Status = base.DownloadStatusRunning
|
||||
|
||||
doStart := func() error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
|
||||
req := d.triggerOnStart(cloneTask)
|
||||
if req != nil {
|
||||
task.Meta.Req = req
|
||||
task.fetcher.Meta().Req = req
|
||||
}
|
||||
}
|
||||
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 {
|
||||
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
|
||||
}
|
||||
if task.Meta.Res == nil {
|
||||
err := task.fetcher.Resolve(task.Meta.Req)
|
||||
if err != nil {
|
||||
task.Status = base.DownloadStatusError
|
||||
d.emit(EventKeyError, task, err)
|
||||
return err
|
||||
}
|
||||
task.Meta.Res = task.fetcher.Meta().Res
|
||||
}
|
||||
|
||||
func (d *Downloader) doPause(task *Task, status base.Status) (err error) {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
if task.Status != base.DownloadStatusDone {
|
||||
task.Status = status
|
||||
task.timer.Pause()
|
||||
task.fetcher.Pause()
|
||||
d.storage.Put(bucketTask, task.ID, task.clone())
|
||||
d.emit(EventKeyPause, task)
|
||||
if isCreate {
|
||||
d.checkDuplicateLock.Lock()
|
||||
defer d.checkDuplicateLock.Unlock()
|
||||
// check if the download file is duplicated and rename it automatically.
|
||||
if task.Meta.Res.Name != "" {
|
||||
fullDirPath := task.Meta.FolderPath()
|
||||
newName, err := util.CheckDuplicateAndRename(fullDirPath)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (d *Downloader) doContinue(task *Task) (err error) {
|
||||
func (d *Downloader) doPause(task *Task) (err error) {
|
||||
err = func() error {
|
||||
task.lock.Lock()
|
||||
defer task.lock.Unlock()
|
||||
if task.Status != base.DownloadStatusRunning && task.Status != base.DownloadStatusDone {
|
||||
err := d.restoreFetcher(task)
|
||||
if err != nil {
|
||||
if task.Status != base.DownloadStatusDone {
|
||||
task.Status = base.DownloadStatusPause
|
||||
task.timer.Pause()
|
||||
if task.fetcher != nil {
|
||||
if err := task.fetcher.Pause(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := d.storage.Put(bucketTask, task.ID, task.clone()); err != nil {
|
||||
return err
|
||||
}
|
||||
d.emit(EventKeyPause, task)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
if err != nil {
|
||||
d.Logger.Error().Stack().Err(err).Msgf("pause task failed, task id: %s", task.ID)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -769,12 +856,11 @@ func (b *boot) Listener(listener Listener) *boot {
|
||||
}
|
||||
|
||||
func (b *boot) Create(opts *base.Options) (string, error) {
|
||||
rr, err := b.Resolve()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
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 {
|
||||
|
||||
+208
-32
@@ -33,6 +33,7 @@ type ActivationEvent string
|
||||
|
||||
const (
|
||||
EventOnResolve ActivationEvent = "onResolve"
|
||||
EventOnStart ActivationEvent = "onStart"
|
||||
//EventOnError ActivationEvent = "onError"
|
||||
//EventOnDone ActivationEvent = "onDone"
|
||||
)
|
||||
@@ -47,7 +48,7 @@ func (d *Downloader) InstallExtensionByFolder(path string, devMode bool) (*Exten
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// if dev mode, don't copy the extension to the extensions' directory
|
||||
// if dev mode, don't copy to the extensions' directory
|
||||
if devMode {
|
||||
ext.DevMode = true
|
||||
ext.DevPath, _ = filepath.Abs(path)
|
||||
@@ -229,23 +230,70 @@ func (d *Downloader) parseExtensionByPath(path string) (*Extension, error) {
|
||||
}
|
||||
|
||||
func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
||||
doTrigger(d,
|
||||
EventOnResolve,
|
||||
req,
|
||||
&OnResolveContext{
|
||||
Req: req,
|
||||
},
|
||||
func(ext *Extension, gopeed *Instance, ctx *OnResolveContext) {
|
||||
// Validate resource structure
|
||||
if ctx.Res != nil && len(ctx.Res.Files) > 0 {
|
||||
if err := ctx.Res.Validate(); err != nil {
|
||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
||||
return
|
||||
}
|
||||
ctx.Res.CalcSize(nil)
|
||||
}
|
||||
res = ctx.Res
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
|
||||
doTrigger(d,
|
||||
EventOnStart,
|
||||
task.Meta.Req,
|
||||
&OnStartContext{
|
||||
Task: task,
|
||||
},
|
||||
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
||||
// Validate request structure
|
||||
if ctx.Task.Meta.Req != nil {
|
||||
if err := ctx.Task.Meta.Req.Validate(); err != nil {
|
||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
||||
return
|
||||
}
|
||||
req = ctx.Task.Meta.Req
|
||||
}
|
||||
},
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
func doTrigger[T any](d *Downloader, event ActivationEvent, req *base.Request, ctx T, handler func(ext *Extension, gopeed *Instance, ctx T)) {
|
||||
// init extension global object
|
||||
gopeed := &Instance{
|
||||
Events: make(InstanceEvents),
|
||||
}
|
||||
ctx := &Context{
|
||||
Req: req,
|
||||
}
|
||||
var err error
|
||||
for _, ext := range d.extensions {
|
||||
if ext.Disabled {
|
||||
continue
|
||||
}
|
||||
for _, script := range ext.Scripts {
|
||||
if script.match(EventOnResolve, req.URL) {
|
||||
if script.match(event, req) {
|
||||
gopeed.Info = NewExtensionInfo(ext)
|
||||
gopeed.Logger = newInstanceLogger(ext, d.ExtensionLogger)
|
||||
gopeed.Settings = parseSettings(ext.Settings)
|
||||
gopeed.Storage = &ContextStorage{
|
||||
storage: d.storage,
|
||||
identity: ext.buildIdentity(),
|
||||
}
|
||||
scriptFilePath := filepath.Join(d.ExtensionPath(ext), script.Entry)
|
||||
if _, err = os.Stat(scriptFilePath); os.IsNotExist(err) {
|
||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] script file not exist", ext.buildIdentity())
|
||||
continue
|
||||
}
|
||||
func() {
|
||||
@@ -256,13 +304,16 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
||||
return
|
||||
}
|
||||
defer scriptFile.Close()
|
||||
ctx.Settings = parseSettings(ext.Settings)
|
||||
var scriptBuf []byte
|
||||
scriptBuf, err = io.ReadAll(scriptFile)
|
||||
if err != nil {
|
||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] read script file failed", ext.buildIdentity())
|
||||
return
|
||||
}
|
||||
// Init request labels
|
||||
if req.Labels == nil {
|
||||
req.Labels = make(map[string]string)
|
||||
}
|
||||
engine := engine.NewEngine()
|
||||
defer engine.Close()
|
||||
err = engine.Runtime.Set("gopeed", gopeed)
|
||||
@@ -275,21 +326,13 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] run script failed", ext.buildIdentity())
|
||||
return
|
||||
}
|
||||
if fn, ok := gopeed.Events[EventOnResolve]; ok {
|
||||
if fn, ok := gopeed.Events[event]; ok {
|
||||
_, err = engine.CallFunction(fn, ctx)
|
||||
if err != nil {
|
||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", EventOnResolve, ext.buildIdentity())
|
||||
gopeed.Logger.logger.Error().Err(err).Msgf("[%s] call function failed: %s", ext.buildIdentity(), event)
|
||||
return
|
||||
}
|
||||
// calculate resource total size
|
||||
if ctx.Res != nil && len(ctx.Res.Files) > 0 {
|
||||
if err = ctx.Res.Validate(); err != nil {
|
||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
||||
return
|
||||
}
|
||||
ctx.Res.CalcSize()
|
||||
}
|
||||
res = ctx.Res
|
||||
handler(ext, gopeed, ctx)
|
||||
}
|
||||
}()
|
||||
}
|
||||
@@ -378,8 +421,50 @@ func (e *Extension) update(newExt *Extension) error {
|
||||
e.Homepage = newExt.Homepage
|
||||
e.Repository = newExt.Repository
|
||||
e.Scripts = newExt.Scripts
|
||||
// don't override settings
|
||||
//e.Settings = newExt.Settings
|
||||
// merge 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()
|
||||
return nil
|
||||
}
|
||||
@@ -392,30 +477,46 @@ type Repository struct {
|
||||
type Script struct {
|
||||
// Event active event name
|
||||
Event string `json:"event"`
|
||||
// Matches match request url pattern list
|
||||
Matches []string `json:"matches"`
|
||||
// Match rules
|
||||
Match *Match `json:"match"`
|
||||
// Entry js script file path
|
||||
Entry string `json:"entry"`
|
||||
}
|
||||
|
||||
func (s *Script) match(event ActivationEvent, url string) bool {
|
||||
func (s *Script) match(event ActivationEvent, req *base.Request) bool {
|
||||
if s.Event == "" {
|
||||
return false
|
||||
}
|
||||
if s.Event != string(event) {
|
||||
return false
|
||||
}
|
||||
if len(s.Matches) == 0 {
|
||||
if s.Match == nil || (len(s.Match.Urls) == 0 && len(s.Match.Labels) == 0) {
|
||||
return false
|
||||
}
|
||||
for _, match := range s.Matches {
|
||||
if util.Match(match, url) {
|
||||
|
||||
// match url
|
||||
for _, url := range s.Match.Urls {
|
||||
if util.Match(url, req.URL) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// match label
|
||||
for _, label := range s.Match.Labels {
|
||||
if _, ok := req.Labels[label]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Match struct {
|
||||
// Urls match expression, refer to https://developer.chrome.com/docs/extensions/mv3/match_patterns/
|
||||
Urls []string `json:"urls"`
|
||||
// Labels match request labels
|
||||
Labels []string `json:"labels"`
|
||||
}
|
||||
|
||||
type SettingType string
|
||||
|
||||
const (
|
||||
@@ -442,10 +543,13 @@ type Option struct {
|
||||
Value any `json:"value"`
|
||||
}
|
||||
|
||||
// Instance inject to js context
|
||||
// Instance inject to js context when extension script is activated
|
||||
type Instance struct {
|
||||
Events InstanceEvents `json:"events"`
|
||||
Logger *InstanceLogger `json:"logger"`
|
||||
Events InstanceEvents `json:"events"`
|
||||
Info *ExtensionInfo `json:"info"`
|
||||
Logger *InstanceLogger `json:"logger"`
|
||||
Settings map[string]any `json:"settings"`
|
||||
Storage *ContextStorage `json:"storage"`
|
||||
}
|
||||
|
||||
type InstanceEvents map[ActivationEvent]goja.Callable
|
||||
@@ -458,6 +562,10 @@ func (h InstanceEvents) OnResolve(fn goja.Callable) {
|
||||
h.register(EventOnResolve, fn)
|
||||
}
|
||||
|
||||
func (h InstanceEvents) OnStart(fn goja.Callable) {
|
||||
h.register(EventOnStart, fn)
|
||||
}
|
||||
|
||||
//func (h InstanceEvents) OnError(fn goja.Callable) {
|
||||
// h.register(HookEventOnError, fn)
|
||||
//}
|
||||
@@ -466,6 +574,24 @@ func (h InstanceEvents) OnResolve(fn goja.Callable) {
|
||||
// h.register(HookEventOnDone, fn)
|
||||
//}
|
||||
|
||||
type ExtensionInfo struct {
|
||||
Identity string `json:"identity"`
|
||||
Name string `json:"name"`
|
||||
Author string `json:"author"`
|
||||
Title string `json:"title"`
|
||||
Version string `json:"version"`
|
||||
}
|
||||
|
||||
func NewExtensionInfo(ext *Extension) *ExtensionInfo {
|
||||
return &ExtensionInfo{
|
||||
Identity: ext.buildIdentity(),
|
||||
Name: ext.Name,
|
||||
Author: ext.Author,
|
||||
Title: ext.Title,
|
||||
Version: ext.Version,
|
||||
}
|
||||
}
|
||||
|
||||
type InstanceLogger struct {
|
||||
identity string
|
||||
devMode bool
|
||||
@@ -506,10 +632,13 @@ func newInstanceLogger(extension *Extension, logger *logger.Logger) *InstanceLog
|
||||
}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Req *base.Request `json:"req"`
|
||||
Res *base.Resource `json:"res"`
|
||||
Settings map[string]any `json:"settings"`
|
||||
type OnResolveContext struct {
|
||||
Req *base.Request `json:"req"`
|
||||
Res *base.Resource `json:"res"`
|
||||
}
|
||||
|
||||
type OnStartContext struct {
|
||||
Task *Task `json:"task"`
|
||||
}
|
||||
|
||||
func parseSettings(settings []*Setting) map[string]any {
|
||||
@@ -547,3 +676,50 @@ func tryParse(val any, settingType SettingType) any {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
type ContextStorage struct {
|
||||
storage Storage
|
||||
identity string
|
||||
}
|
||||
|
||||
func (s *ContextStorage) Get(key string) any {
|
||||
raw := s.getRawData()
|
||||
if v, ok := raw[key]; ok {
|
||||
return v
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ContextStorage) Set(key string, value string) {
|
||||
raw := s.getRawData()
|
||||
raw[key] = value
|
||||
s.storage.Put(bucketExtensionStorage, s.identity, raw)
|
||||
}
|
||||
|
||||
func (s *ContextStorage) Remove(key string) {
|
||||
raw := s.getRawData()
|
||||
delete(raw, key)
|
||||
s.storage.Put(bucketExtensionStorage, s.identity, raw)
|
||||
}
|
||||
|
||||
func (s *ContextStorage) Keys() []string {
|
||||
raw := s.getRawData()
|
||||
keys := make([]string, 0)
|
||||
for k := range raw {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func (s *ContextStorage) Clear() {
|
||||
s.storage.Delete(bucketExtensionStorage, s.identity)
|
||||
}
|
||||
|
||||
func (s *ContextStorage) getRawData() map[string]string {
|
||||
var data map[string]string
|
||||
s.storage.Get(bucketExtensionStorage, s.identity, &data)
|
||||
if data == nil {
|
||||
data = make(map[string]string)
|
||||
}
|
||||
return data
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/GopeedLab/gopeed/internal/logger"
|
||||
"github.com/GopeedLab/gopeed/pkg/base"
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestDownloader_InstallExtensionByFolder(t *testing.T) {
|
||||
@@ -93,6 +95,15 @@ func TestDownloader_InstallExtensionByGitFull(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) {
|
||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false)
|
||||
if err != nil {
|
||||
@@ -120,14 +131,116 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
|
||||
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{
|
||||
URL: "https://github.com/GopeedLab/gopeed/releases",
|
||||
URL: "https://test.com",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rr.Res.Files) == 1 {
|
||||
t.Fatal("resolve error")
|
||||
if rr.Res.Name != "test" {
|
||||
t.Fatal("script update fail")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_ExtensionByOnStart(t *testing.T) {
|
||||
downloadAndCheck := func(req *base.Request) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/on_start", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
errCh := make(chan error, 1)
|
||||
downloader.Listener(func(event *Event) {
|
||||
if event.Key == EventKeyFinally {
|
||||
errCh <- event.Err
|
||||
}
|
||||
})
|
||||
id, err := downloader.CreateDirect(req, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
select {
|
||||
case err = <-errCh:
|
||||
break
|
||||
case <-time.After(time.Second * 10):
|
||||
err = errors.New("timeout")
|
||||
}
|
||||
if err != nil {
|
||||
panic("extension on start download error: " + err.Error())
|
||||
}
|
||||
task := downloader.GetTask(id)
|
||||
if task.Meta.Req.URL != "https://github.com" {
|
||||
panic("extension on start modify url error")
|
||||
}
|
||||
if task.Meta.Req.Labels["modified"] != "true" {
|
||||
panic("extension on start modify label error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// url match
|
||||
downloadAndCheck(&base.Request{
|
||||
URL: "https://github.com/gopeed/test/404",
|
||||
})
|
||||
|
||||
// label match
|
||||
downloadAndCheck(&base.Request{
|
||||
URL: "https://xxx.com",
|
||||
Labels: map[string]string{
|
||||
"test": "true",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_Extension_Errors(t *testing.T) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/script_error", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr, err := downloader.Resolve(&base.Request{
|
||||
URL: "https://github.com/test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rr.Res.Files) == 2 {
|
||||
t.Fatal("script error catch failed")
|
||||
}
|
||||
})
|
||||
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/function_error", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr, err := downloader.Resolve(&base.Request{
|
||||
URL: "https://github.com/test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rr.Res.Files) == 2 {
|
||||
t.Fatal("function error catch failed")
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -170,6 +283,23 @@ func TestDownloader_Extension_Settings(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_ExtensionStorage(t *testing.T) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
if _, err := downloader.InstallExtensionByFolder("./testdata/extensions/storage", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rr, err := downloader.Resolve(&base.Request{
|
||||
URL: "https://github.com/test",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(rr.Res.Files) == 1 {
|
||||
t.Fatal("resolve error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDownloader_SwitchExtension(t *testing.T) {
|
||||
setupDownloader(func(downloader *Downloader) {
|
||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/basic", false)
|
||||
|
||||
@@ -22,7 +22,6 @@ type Task struct {
|
||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||
Status base.Status `json:"status"`
|
||||
Progress *Progress `json:"progress"`
|
||||
Size int64 `json:"size"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
|
||||
fetcherBuilder fetcher.FetcherBuilder
|
||||
@@ -50,7 +49,6 @@ func (t *Task) clone() *Task {
|
||||
Meta: t.Meta,
|
||||
Status: t.Status,
|
||||
Progress: t.Progress,
|
||||
Size: t.Size,
|
||||
CreatedAt: t.CreatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,9 @@ func (b *BoltStorage) Pop(bucket string, key string, v any) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
return json.Unmarshal(data, v)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,9 @@ gopeed.events.onResolve(async function (ctx) {
|
||||
size: 1024,
|
||||
req: {
|
||||
url: ctx.req.url + "/" + i,
|
||||
labels:{
|
||||
"from": gopeed.info.name,
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
|
||||
+5
-3
@@ -5,9 +5,11 @@
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"matches": [
|
||||
"*://github.com/*"
|
||||
],
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
],
|
||||
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
ctx.res = {
|
||||
name: "test",
|
||||
files: Array(2).fill(true).map((_, i) => ({
|
||||
name: `test-${i}.txt`,
|
||||
size: 1024,
|
||||
req: {
|
||||
url: ctx.req.url + "/" + i,
|
||||
extra: {
|
||||
headers: {
|
||||
'User-Agent': ctx.settings.ua,
|
||||
},
|
||||
}
|
||||
}
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "extra",
|
||||
"title": "gopeed extension extra test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"matches": [
|
||||
"*://github.com/*"
|
||||
],
|
||||
"entry": "index.js"
|
||||
}
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"name": "ua",
|
||||
"title": "User-Agent",
|
||||
"type": "string",
|
||||
"value": "gopeed"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
const aaa = {};
|
||||
gopeed.logger.info(aaa.bbb.ccc);
|
||||
|
||||
ctx.res = {
|
||||
name: "test",
|
||||
files: Array(2).fill(true).map((_, i) => ({
|
||||
name: `test-${i}.txt`,
|
||||
size: 1024,
|
||||
req: {
|
||||
url: ctx.req.url + "/" + i,
|
||||
}
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "function-error",
|
||||
"title": "gopeed extension function error test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"name": "ua",
|
||||
"title": "User-Agent",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
gopeed.events.onStart(async function (ctx) {
|
||||
ctx.task.meta.req.url = "https://github.com";
|
||||
ctx.task.meta.req.labels['modified'] = 'true';
|
||||
});
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "on-start",
|
||||
"title": "gopeed extension on start event test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onStart",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
],
|
||||
"labels": [
|
||||
"test"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
const aaa = {};
|
||||
gopeed.logger.info(aaa.bbb.ccc);
|
||||
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
ctx.res = {
|
||||
name: "test",
|
||||
files: Array(2).fill(true).map((_, i) => ({
|
||||
name: `test-${i}.txt`,
|
||||
size: 1024,
|
||||
req: {
|
||||
url: ctx.req.url + "/" + i,
|
||||
}
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "script-error",
|
||||
"title": "gopeed extension script error test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://github.com/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"name": "ua",
|
||||
"title": "User-Agent",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
+9
-9
@@ -1,31 +1,31 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
if (ctx.settings.string != null) {
|
||||
if (gopeed.settings.string != null) {
|
||||
throw new Error("string is not null");
|
||||
}
|
||||
if (ctx.settings.number != null) {
|
||||
if (gopeed.settings.number != null) {
|
||||
throw new Error("number is not null");
|
||||
}
|
||||
if (ctx.settings.boolean != null) {
|
||||
if (gopeed.settings.boolean != null) {
|
||||
throw new Error("boolean is not null");
|
||||
}
|
||||
|
||||
if (ctx.settings.stringDefault !== "default") {
|
||||
if (gopeed.settings.stringDefault !== "default") {
|
||||
throw new Error("string default value is incorrect");
|
||||
}
|
||||
if (ctx.settings.numberDefault !== 1) {
|
||||
if (gopeed.settings.numberDefault !== 1) {
|
||||
throw new Error("number default value is incorrect");
|
||||
}
|
||||
if (ctx.settings.booleanDefault !== true) {
|
||||
if (gopeed.settings.booleanDefault !== true) {
|
||||
throw new Error("boolean default value is incorrect");
|
||||
}
|
||||
|
||||
if (ctx.settings.stringValued !== "valued") {
|
||||
if (gopeed.settings.stringValued !== "valued") {
|
||||
throw new Error("string value is incorrect");
|
||||
}
|
||||
if (ctx.settings.numberValued !== 1.1) {
|
||||
if (gopeed.settings.numberValued !== 1.1) {
|
||||
throw new Error("number value is incorrect");
|
||||
}
|
||||
if (ctx.settings.booleanValued !== true) {
|
||||
if (gopeed.settings.booleanValued !== true) {
|
||||
throw new Error("boolean value is incorrect");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"matches": [
|
||||
"*://*/*"
|
||||
],
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://*/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
if (Object.keys(ctx.settings).length > 0){
|
||||
if (Object.keys(gopeed.settings).length > 0){
|
||||
throw new Error("settings is not empty");
|
||||
}
|
||||
|
||||
|
||||
@@ -5,9 +5,11 @@
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"matches": [
|
||||
"*://*/*"
|
||||
],
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://*/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
]
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
gopeed.events.onResolve(async function (ctx) {
|
||||
const key = "key"
|
||||
const value1 = "value1", value2 = JSON.stringify({a: 1, b: "2"})
|
||||
|
||||
if (gopeed.storage.get(key) !== null) {
|
||||
throw new Error("storage get null error")
|
||||
}
|
||||
gopeed.storage.remove(key)
|
||||
if(gopeed.storage.keys().length !== 0) {
|
||||
throw new Error("storage keys null error")
|
||||
}
|
||||
|
||||
gopeed.storage.set(key, value1)
|
||||
if (gopeed.storage.get(key) !== value1) {
|
||||
throw new Error("storage put1 error")
|
||||
}
|
||||
|
||||
gopeed.storage.set(key, value2)
|
||||
if (gopeed.storage.get(key) !== value2) {
|
||||
throw new Error("storage put2 error")
|
||||
}
|
||||
|
||||
if(gopeed.storage.keys().length !== 1) {
|
||||
throw new Error("storage keys error")
|
||||
}
|
||||
|
||||
gopeed.storage.remove(key)
|
||||
if (gopeed.storage.get(key) !== null) {
|
||||
throw new Error("storage delete error")
|
||||
}
|
||||
|
||||
gopeed.storage.set(key, value1)
|
||||
gopeed.storage.clear()
|
||||
if (gopeed.storage.get(key) !== null) {
|
||||
throw new Error("storage clear error")
|
||||
}
|
||||
|
||||
ctx.res = {
|
||||
name: "test",
|
||||
files: Array(2).fill(true).map((_, i) => ({
|
||||
name: `test-${i}.txt`,
|
||||
size: 1024,
|
||||
req: {
|
||||
url: ctx.req.url + "/" + i,
|
||||
}
|
||||
}),
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"name": "storage",
|
||||
"title": "gopeed extension storage test",
|
||||
"version": "0.0.1",
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://*/*"
|
||||
]
|
||||
},
|
||||
"entry": "index.js"
|
||||
}
|
||||
],
|
||||
"settings": [
|
||||
{
|
||||
"name": "ua",
|
||||
"title": "User-Agent",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
+33
-7
@@ -1,21 +1,47 @@
|
||||
{
|
||||
"name": "github-release",
|
||||
"name": "extension-test",
|
||||
"author": "gopeed",
|
||||
"title": "Github Release",
|
||||
"description": "Download github release assets",
|
||||
"title": "Gopeed Extension Test",
|
||||
"description": "Test extension settings and upgrade",
|
||||
"version": "0.0.1",
|
||||
"homepage": "https://gopeed.com",
|
||||
"repository": {
|
||||
"url": "https://github.com/GopeedLab/gopeed-extension-samples",
|
||||
"directory": "github-release-sample"
|
||||
"directory": "extension-test"
|
||||
},
|
||||
"scripts": [
|
||||
{
|
||||
"event": "onResolve",
|
||||
"matches": [
|
||||
"*://github.com/*/releases/*"
|
||||
],
|
||||
"match": {
|
||||
"urls": [
|
||||
"*://*/*"
|
||||
]
|
||||
},
|
||||
"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
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -204,19 +204,16 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
||||
|
||||
// continue all
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
|
||||
if len(tasks) != cfg.MaxRunning {
|
||||
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
|
||||
}
|
||||
// pause all
|
||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
|
||||
time.Sleep(time.Millisecond * 500)
|
||||
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
|
||||
if len(tasks) != total {
|
||||
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
|
||||
}
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -47,7 +47,7 @@ android {
|
||||
applicationId "com.gopeed"
|
||||
// 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.
|
||||
minSdkVersion 19
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 29
|
||||
versionCode flutterVersionCode.toInteger()
|
||||
versionName flutterVersionName
|
||||
|
||||
@@ -72,6 +72,12 @@
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</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.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"selectAll": "Select All",
|
||||
"task": "Task",
|
||||
"task": "Tasks",
|
||||
"downloading": "downloading",
|
||||
"downloaded": "downloaded",
|
||||
"setting": "Setting",
|
||||
"setting": "Settings",
|
||||
"donate": "Donate",
|
||||
"exit": "Exit",
|
||||
"create": "Create Task",
|
||||
"directDownload": "Direct Download",
|
||||
"advancedOptions": "Advanced Options",
|
||||
"downloadLink": "Download Link",
|
||||
"downloadLinkValid": "Please enter the download link",
|
||||
@@ -65,6 +66,12 @@
|
||||
"extensionInstallSuccess": "Installed successfully",
|
||||
"extensionUpdateSuccess": "Updated successfully",
|
||||
"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"
|
||||
}
|
||||
@@ -12,14 +12,16 @@
|
||||
"donate": "Пожертвовать",
|
||||
"exit": "Выход",
|
||||
"create": "Создать задачу",
|
||||
"downloadLink": "Ссылка для скачивание",
|
||||
"advancedOptions": "Расширенные опции",
|
||||
"downloadLink": "Ссылка на скачивание",
|
||||
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
|
||||
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append",
|
||||
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append",
|
||||
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
|
||||
"download": "Скачать",
|
||||
"noFileSelected": "Файл не выбран",
|
||||
"noStoragePermission": "Требуется доступ к хранилищу",
|
||||
"selectFile": "Выберать файл",
|
||||
"selectFile": "Выбрать файл",
|
||||
"rename": "Переименовать",
|
||||
"basic": "Основные параметры",
|
||||
"advanced": "Расширенные",
|
||||
"general": "Общие",
|
||||
@@ -57,5 +59,15 @@
|
||||
"delete": "Удалить",
|
||||
"newVersionTitle": "Обнаружена новая версия @version",
|
||||
"newVersionUpdate": "Обновить",
|
||||
"newVersionLater": "позже"
|
||||
}
|
||||
"newVersionLater": "позже",
|
||||
"extensions": "Расширения",
|
||||
"extensionInstallSuccess": "Установка завершена",
|
||||
"extensionUpdateSuccess": "Обновление завершено",
|
||||
"extensionDelete": "Удалить расширение",
|
||||
"extensionAlreadyLatest": "Это последняя версия",
|
||||
"extensionFind": "Найти расширения",
|
||||
"extensionDevelop": "Разработка расширений",
|
||||
"history": "История",
|
||||
"clearHistory": "Очистить историю",
|
||||
"noHistoryFound": "История не найдена"
|
||||
}
|
||||
@@ -12,6 +12,7 @@
|
||||
"donate": "打赏",
|
||||
"exit": "退出",
|
||||
"create": "创建任务",
|
||||
"directDownload": "直接下载",
|
||||
"advancedOptions": "高级选项",
|
||||
"downloadLink": "下载链接",
|
||||
"downloadLinkValid": "请输入下载链接",
|
||||
@@ -65,5 +66,12 @@
|
||||
"extensionInstallSuccess": "安装成功",
|
||||
"extensionUpdateSuccess": "更新成功",
|
||||
"extensionDelete": "删除扩展",
|
||||
"extensionAlreadyLatest": "已经是最新版本"
|
||||
}
|
||||
"extensionAlreadyLatest": "已经是最新版本",
|
||||
"extensionFind": "获取扩展",
|
||||
"extensionDevelop": "开发扩展",
|
||||
"history": "历史记录",
|
||||
"clearHistory": "清空历史记录",
|
||||
"noHistoryFound": "暂无历史记录",
|
||||
"serviceTitle": "下载服务",
|
||||
"serviceText": "运行中"
|
||||
}
|
||||
@@ -33,6 +33,15 @@ import Libgopeed
|
||||
})
|
||||
|
||||
GeneratedPluginRegistrant.register(with: self)
|
||||
|
||||
SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback(registerPlugins)
|
||||
if #available(iOS 10.0, *) {
|
||||
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||
}
|
||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||
}
|
||||
}
|
||||
|
||||
func registerPlugins(registry: FlutterPluginRegistry) {
|
||||
GeneratedPluginRegistrant.register(with: registry)
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
#import "GeneratedPluginRegistrant.h"
|
||||
#import <flutter_foreground_task/FlutterForegroundTaskPlugin.h>
|
||||
@@ -16,6 +16,7 @@ class Extension {
|
||||
List<Setting>? settings;
|
||||
bool disabled;
|
||||
bool devMode;
|
||||
String devPath;
|
||||
|
||||
Extension({
|
||||
required this.identity,
|
||||
@@ -29,6 +30,7 @@ class Extension {
|
||||
required this.repository,
|
||||
required this.disabled,
|
||||
required this.devMode,
|
||||
required this.devPath,
|
||||
});
|
||||
|
||||
factory Extension.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -20,6 +20,7 @@ Extension _$ExtensionFromJson(Map<String, dynamic> json) => Extension(
|
||||
: Repository.fromJson(json['repository'] as Map<String, dynamic>),
|
||||
disabled: json['disabled'] as bool,
|
||||
devMode: json['devMode'] as bool,
|
||||
devPath: json['devPath'] as String,
|
||||
)..settings = (json['settings'] as List<dynamic>?)
|
||||
?.map((e) => Setting.fromJson(e as Map<String, dynamic>))
|
||||
.toList();
|
||||
@@ -46,6 +47,7 @@ Map<String, dynamic> _$ExtensionToJson(Extension instance) {
|
||||
writeNotNull('settings', instance.settings?.map((e) => e.toJson()).toList());
|
||||
val['disabled'] = instance.disabled;
|
||||
val['devMode'] = instance.devMode;
|
||||
val['devPath'] = instance.devPath;
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,10 +6,12 @@ part 'request.g.dart';
|
||||
class Request {
|
||||
String url;
|
||||
Object? extra;
|
||||
Map<String, String>? labels = {};
|
||||
|
||||
Request({
|
||||
required this.url,
|
||||
this.extra,
|
||||
this.labels,
|
||||
});
|
||||
|
||||
factory Request.fromJson(Map<String, dynamic> json) =>
|
||||
|
||||
@@ -9,6 +9,9 @@ part of 'request.dart';
|
||||
Request _$RequestFromJson(Map<String, dynamic> json) => Request(
|
||||
url: json['url'] as String,
|
||||
extra: json['extra'],
|
||||
labels: (json['labels'] as Map<String, dynamic>?)?.map(
|
||||
(k, e) => MapEntry(k, e as String),
|
||||
),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
@@ -23,6 +26,7 @@ Map<String, dynamic> _$RequestToJson(Request instance) {
|
||||
}
|
||||
|
||||
writeNotNull('extra', instance.extra);
|
||||
writeNotNull('labels', instance.labels);
|
||||
return val;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ class Task {
|
||||
Meta meta;
|
||||
Status status;
|
||||
Progress progress;
|
||||
int size;
|
||||
DateTime createdAt;
|
||||
|
||||
Task({
|
||||
@@ -20,7 +19,6 @@ class Task {
|
||||
required this.meta,
|
||||
required this.status,
|
||||
required this.progress,
|
||||
required this.size,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||
size: json['size'] as int,
|
||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||
);
|
||||
|
||||
@@ -20,7 +19,6 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
||||
'meta': instance.meta.toJson(),
|
||||
'status': _$StatusEnumMap[instance.status]!,
|
||||
'progress': instance.progress.toJson(),
|
||||
'size': instance.size,
|
||||
'createdAt': instance.createdAt.toIso8601String(),
|
||||
};
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import 'dart:ui';
|
||||
|
||||
import 'package:app_links/app_links.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -79,21 +80,18 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
@override
|
||||
void onReady() {
|
||||
super.onReady();
|
||||
try {
|
||||
_initDeepLinks();
|
||||
} catch (e) {
|
||||
logger.w("initDeepLinks error", e);
|
||||
}
|
||||
try {
|
||||
_initWindows();
|
||||
} catch (e) {
|
||||
logger.w("initWindows error", e);
|
||||
}
|
||||
try {
|
||||
_initTray();
|
||||
} catch (e) {
|
||||
logger.w("initTray error", e);
|
||||
}
|
||||
|
||||
_initDeepLinks().onError((error, stackTrace) =>
|
||||
logger.w("initDeepLinks error", error, stackTrace));
|
||||
|
||||
_initWindows().onError((error, stackTrace) =>
|
||||
logger.w("initWindows error", error, stackTrace));
|
||||
|
||||
_initTray().onError(
|
||||
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
||||
|
||||
_initForegroundTask().onError((error, stackTrace) =>
|
||||
logger.w("initForegroundTask error", error, stackTrace));
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -202,10 +200,50 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
||||
onClick: (menuItem) => {windowManager.destroy()},
|
||||
),
|
||||
]);
|
||||
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);
|
||||
trayManager.addListener(this);
|
||||
}
|
||||
|
||||
Future<void> _initForegroundTask() async {
|
||||
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 {
|
||||
final path = uri.scheme == "magnet"
|
||||
? uri.toString()
|
||||
|
||||
@@ -10,8 +10,9 @@ class CreateController extends GetxController
|
||||
final RxList fileInfos = [].obs;
|
||||
final RxList openedFolders = [].obs;
|
||||
final selectedIndexes = <int>[].obs;
|
||||
final isResolving = false.obs;
|
||||
final isConfirming = false.obs;
|
||||
final showAdvanced = false.obs;
|
||||
final directDownload = false.obs;
|
||||
late TabController advancedTabController;
|
||||
final oldUrl = "".obs;
|
||||
final fileDataUri = "".obs;
|
||||
|
||||
@@ -4,10 +4,10 @@ import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.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:rounded_loading_button/rounded_loading_button.dart';
|
||||
|
||||
import '../../../../api/api.dart';
|
||||
import '../../../../api/model/create_task.dart';
|
||||
import '../../../../api/model/options.dart';
|
||||
@@ -24,14 +24,18 @@ import '../../app/controllers/app_controller.dart';
|
||||
import '../controllers/create_controller.dart';
|
||||
|
||||
class CreateView extends GetView<CreateController> {
|
||||
final _resolveFormKey = GlobalKey<FormState>();
|
||||
final _confirmFormKey = GlobalKey<FormState>();
|
||||
|
||||
final _urlController = TextEditingController();
|
||||
final _renameController = TextEditingController();
|
||||
final _connectionsController = TextEditingController();
|
||||
final _pathController = TextEditingController();
|
||||
final _confirmController = RoundedLoadingButtonController();
|
||||
final _httpUaController = TextEditingController();
|
||||
final _httpCookieController = TextEditingController();
|
||||
final _httpRefererController = TextEditingController();
|
||||
final _btTrackerController = TextEditingController();
|
||||
final _historyController = HistoryController();
|
||||
|
||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||
|
||||
@@ -39,6 +43,17 @@ class CreateView extends GetView<CreateController> {
|
||||
|
||||
@override
|
||||
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();
|
||||
if (_urlController.text.isEmpty) {
|
||||
if (filePath?.isNotEmpty ?? false) {
|
||||
@@ -90,13 +105,15 @@ class CreateView extends GetView<CreateController> {
|
||||
onTap: () {
|
||||
FocusScope.of(context).requestFocus(FocusNode());
|
||||
},
|
||||
child: Padding(
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding:
|
||||
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||
child: Form(
|
||||
key: _resolveFormKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(children: [
|
||||
key: _confirmFormKey,
|
||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||
child: Column(
|
||||
children: [
|
||||
Row(children: [
|
||||
Expanded(
|
||||
child: TextFormField(
|
||||
@@ -132,93 +149,209 @@ class CreateView extends GetView<CreateController> {
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var pr = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["torrent"]);
|
||||
if (pr != null) {
|
||||
if (!Util.isWeb()) {
|
||||
_urlController.text = pr.files[0].path ?? "";
|
||||
return;
|
||||
}
|
||||
_urlController.text = pr.files[0].name;
|
||||
controller.setFileDataUri(pr.files[0].bytes!);
|
||||
icon: const Icon(Icons.folder_open),
|
||||
onPressed: () async {
|
||||
var pr = await FilePicker.platform.pickFiles(
|
||||
type: FileType.custom,
|
||||
allowedExtensions: ["torrent"]);
|
||||
if (pr != null) {
|
||||
if (!Util.isWeb()) {
|
||||
_urlController.text = pr.files[0].path ?? "";
|
||||
return;
|
||||
}
|
||||
}),
|
||||
]
|
||||
//.where((e) => e != null).map((e) => e!).toList(),
|
||||
),
|
||||
Obx(() => Visibility(
|
||||
visible: controller.showAdvanced.value,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 40, top: 15),
|
||||
child: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: controller.advancedTabController,
|
||||
tabs: const [
|
||||
Tab(
|
||||
text: 'HTTP',
|
||||
_urlController.text = pr.files[0].name;
|
||||
controller.setFileDataUri(pr.files[0].bytes!);
|
||||
}
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.history_rounded),
|
||||
onPressed: () async {
|
||||
List<String> resultOfHistories =
|
||||
await _historyController.getAllHistory();
|
||||
// reversing: display last entered history first
|
||||
List<String> reverseResultOfHistories =
|
||||
resultOfHistories.reversed.toList();
|
||||
// show dialog box to list history
|
||||
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(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
transitionDuration:
|
||||
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: [
|
||||
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(
|
||||
value: controller.showAdvanced.value,
|
||||
onChanged: (bool? value) {
|
||||
@@ -228,21 +361,27 @@ class CreateView extends GetView<CreateController> {
|
||||
)),
|
||||
Text('advancedOptions'.tr),
|
||||
]),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: RoundedLoadingButton(
|
||||
color: Get.theme.colorScheme.secondary,
|
||||
onPressed: _doResolve,
|
||||
controller: _confirmController,
|
||||
child: Text('confirm'.tr),
|
||||
SizedBox(
|
||||
width: 150,
|
||||
child: RoundedLoadingButton(
|
||||
color: Get.theme.colorScheme.secondary,
|
||||
onPressed: _doConfirm,
|
||||
controller: _confirmController,
|
||||
child: Text('confirm'.tr),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
)),
|
||||
]))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -262,43 +401,54 @@ class CreateView extends GetView<CreateController> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _doResolve() async {
|
||||
if (controller.isResolving.value) {
|
||||
Future<void> _doConfirm() async {
|
||||
if (controller.isConfirming.value) {
|
||||
return;
|
||||
}
|
||||
controller.isResolving.value = true;
|
||||
controller.isConfirming.value = true;
|
||||
try {
|
||||
_confirmController.start();
|
||||
if (_resolveFormKey.currentState!.validate()) {
|
||||
// check if is multi line urls
|
||||
final urls = Util.textToLines(_urlController.text);
|
||||
ResolveResult rr;
|
||||
bool isMuiltiLine = false;
|
||||
if (urls.length > 1) {
|
||||
isMuiltiLine = true;
|
||||
rr = ResolveResult(
|
||||
res: Resource(
|
||||
files: urls
|
||||
.map((u) => FileInfo(
|
||||
name: u,
|
||||
req: Request(url: u, extra: parseReqExtra(u))))
|
||||
.toList()));
|
||||
if (_confirmFormKey.currentState!.validate()) {
|
||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
||||
? controller.fileDataUri.value
|
||||
: _urlController.text;
|
||||
|
||||
final urls = Util.textToLines(submitUrl);
|
||||
// Add url to the history
|
||||
for (final url in urls) {
|
||||
_historyController.addHistory(url);
|
||||
}
|
||||
/*
|
||||
Check if is direct download, there has two ways to direct download
|
||||
1. Direct download option is checked
|
||||
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 {
|
||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
||||
? controller.fileDataUri.value
|
||||
: _urlController.text;
|
||||
rr = await resolve(Request(
|
||||
final rr = await resolve(Request(
|
||||
url: submitUrl,
|
||||
extra: parseReqExtra(_urlController.text),
|
||||
));
|
||||
await _showResolveDialog(rr);
|
||||
}
|
||||
await _showResolveDialog(rr, isMuiltiLine);
|
||||
}
|
||||
} catch (e) {
|
||||
showErrorMessage(e);
|
||||
} finally {
|
||||
_confirmController.reset();
|
||||
controller.isResolving.value = false;
|
||||
controller.isConfirming.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -321,6 +471,13 @@ class CreateView extends GetView<CreateController> {
|
||||
return reqExtra;
|
||||
}
|
||||
|
||||
Object? parseReqOpts() {
|
||||
return _connectionsController.text.isEmpty
|
||||
? null
|
||||
: (OptsExtraHttp()
|
||||
..connections = int.parse(_connectionsController.text));
|
||||
}
|
||||
|
||||
String _hitText() {
|
||||
return 'downloadLinkHit'.trParams({
|
||||
'append':
|
||||
@@ -328,14 +485,8 @@ class CreateView extends GetView<CreateController> {
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _showResolveDialog(ResolveResult rr, bool isMuiltiLine) async {
|
||||
final appController = Get.find<AppController>();
|
||||
|
||||
Future<void> _showResolveDialog(ResolveResult rr) async {
|
||||
final createFormKey = GlobalKey<FormState>();
|
||||
final nameController = TextEditingController();
|
||||
final connectionsController = TextEditingController();
|
||||
final pathController = TextEditingController(
|
||||
text: appController.downloaderConfig.value.downloadDir);
|
||||
final downloadController = RoundedLoadingButtonController();
|
||||
return showDialog<void>(
|
||||
context: Get.context!,
|
||||
@@ -354,31 +505,7 @@ class CreateView extends GetView<CreateController> {
|
||||
child: Form(
|
||||
key: createFormKey,
|
||||
autovalidateMode: AutovalidateMode.always,
|
||||
child: Column(
|
||||
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,
|
||||
),
|
||||
],
|
||||
)),
|
||||
child: FileListView(files: rr.res.files)),
|
||||
);
|
||||
},
|
||||
),
|
||||
@@ -414,25 +541,19 @@ class CreateView extends GetView<CreateController> {
|
||||
showMessage('tip'.tr, 'noFileSelected'.tr);
|
||||
return;
|
||||
}
|
||||
Object? optExtra = connectionsController.text.isEmpty
|
||||
? null
|
||||
: (OptsExtraHttp()
|
||||
..connections =
|
||||
int.parse(connectionsController.text));
|
||||
final optExtra = parseReqOpts();
|
||||
if (createFormKey.currentState!.validate()) {
|
||||
if (rr.id.isEmpty) {
|
||||
// create task batch, there has two ways to batch create task
|
||||
// 1. from multi line urls
|
||||
// 2. from extension resolve result
|
||||
// from extension resolve result
|
||||
await Future.wait(
|
||||
controller.selectedIndexes.map((index) {
|
||||
final file = rr.res.files[index];
|
||||
return createTask(CreateTask(
|
||||
req: file.req!,
|
||||
opt: Options(
|
||||
name: isMuiltiLine ? "" : file.name,
|
||||
name: file.name,
|
||||
path: path.join(
|
||||
pathController.text, rr.res.name),
|
||||
_pathController.text, rr.res.name),
|
||||
selectFiles: [],
|
||||
extra: optExtra)));
|
||||
}));
|
||||
@@ -440,8 +561,8 @@ class CreateView extends GetView<CreateController> {
|
||||
await createTask(CreateTask(
|
||||
rid: rr.id,
|
||||
opt: Options(
|
||||
name: nameController.text,
|
||||
path: pathController.text,
|
||||
name: _renameController.text,
|
||||
path: _pathController.text,
|
||||
selectFiles: controller.selectedIndexes,
|
||||
extra: optExtra)));
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ import '../../../../api/model/install_extension.dart';
|
||||
import '../../../../api/model/switch_extension.dart';
|
||||
import '../../../../util/util.dart';
|
||||
import '../../../views/icon_button_loading.dart';
|
||||
import '../../../views/responsive_builder.dart';
|
||||
import '../controllers/extension_controller.dart';
|
||||
|
||||
class ExtensionView extends GetView<ExtensionController> {
|
||||
@@ -35,7 +36,31 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
children: [
|
||||
Expanded(
|
||||
@@ -89,14 +114,14 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
: Container()
|
||||
],
|
||||
)),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 16),
|
||||
Expanded(
|
||||
child: Obx(() => ListView.builder(
|
||||
itemCount: controller.extensions.length,
|
||||
itemBuilder: (context, index) {
|
||||
final extension = controller.extensions[index];
|
||||
return SizedBox(
|
||||
height: 112,
|
||||
height: ResponsiveBuilder.isNarrow(context) ? 152 : 112,
|
||||
child: Card(
|
||||
elevation: 4.0,
|
||||
child: Column(
|
||||
@@ -110,14 +135,22 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
height: 48,
|
||||
)
|
||||
: Util.isWeb()
|
||||
? Image.network(join(
|
||||
'/fs/extensions/${extension.identity}/${extension.icon}'))
|
||||
? Image.network(
|
||||
join(
|
||||
'/fs/extensions/${extension.identity}/${extension.icon}'),
|
||||
width: 48,
|
||||
height: 48,
|
||||
)
|
||||
: Image.file(
|
||||
File(path.join(
|
||||
Util.getStorageDir(),
|
||||
"extensions",
|
||||
extension.identity,
|
||||
extension.icon)),
|
||||
extension.devMode
|
||||
? File(path.join(
|
||||
extension.devPath,
|
||||
extension.icon))
|
||||
: File(path.join(
|
||||
Util.getStorageDir(),
|
||||
"extensions",
|
||||
extension.identity,
|
||||
extension.icon)),
|
||||
width: 48,
|
||||
height: 48,
|
||||
),
|
||||
@@ -137,23 +170,35 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
title: Row(
|
||||
children: () {
|
||||
final list = [
|
||||
Text(extension.title),
|
||||
ResponsiveBuilder.isNarrow(context)
|
||||
? Expanded(
|
||||
child: Text(
|
||||
extension.title,
|
||||
overflow:
|
||||
TextOverflow.ellipsis,
|
||||
),
|
||||
)
|
||||
: Text(extension.title),
|
||||
const SizedBox(width: 8),
|
||||
Chip(
|
||||
label: Text('v${extension.version}'),
|
||||
)
|
||||
buildChip('v${extension.version}'),
|
||||
];
|
||||
if (extension.devMode) {
|
||||
list.add(const SizedBox(width: 8));
|
||||
list.add(Chip(
|
||||
label: Text('dev'.tr),
|
||||
backgroundColor: Colors.redAccent,
|
||||
list.add(buildChip(
|
||||
'dev',
|
||||
bgColor: Colors.blue.shade300,
|
||||
));
|
||||
}
|
||||
return list;
|
||||
}(),
|
||||
),
|
||||
subtitle: Text(extension.description),
|
||||
subtitle: ResponsiveBuilder.isNarrow(context)
|
||||
? Text(
|
||||
extension.description,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: Text(extension.description),
|
||||
),
|
||||
),
|
||||
Row(
|
||||
@@ -225,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 {
|
||||
final formKey = GlobalKey<FormBuilderState>();
|
||||
final confrimController = RoundedLoadingButtonController();
|
||||
@@ -248,9 +301,32 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: extension.settings!
|
||||
.map((e) => _buildSettingItem(e))
|
||||
.toList()),
|
||||
children: extension.settings!.map((e) {
|
||||
final settingItem = _buildSettingItem(e);
|
||||
|
||||
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()),
|
||||
),
|
||||
),
|
||||
]),
|
||||
@@ -316,8 +392,7 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
FormFieldValidator<String>? validator, TextInputType? keyBoardType) {
|
||||
return FormBuilderTextField(
|
||||
name: setting.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: setting.title, hintText: setting.description),
|
||||
decoration: InputDecoration(labelText: setting.title),
|
||||
initialValue: setting.value?.toString(),
|
||||
inputFormatters: inputFormatter != null ? [inputFormatter] : null,
|
||||
keyboardType: keyBoardType,
|
||||
@@ -332,7 +407,8 @@ class ExtensionView extends GetView<ExtensionController> {
|
||||
return FormBuilderDropdown<String>(
|
||||
name: setting.name,
|
||||
decoration: InputDecoration(
|
||||
labelText: setting.title, hintText: setting.description),
|
||||
labelText: setting.title,
|
||||
),
|
||||
initialValue: setting.value?.toString(),
|
||||
validator: FormBuilderValidators.compose([
|
||||
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_foreground_task/flutter_foreground_task.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../routes/app_pages.dart';
|
||||
@@ -23,139 +24,141 @@ class HomeView extends GetView<HomeController> {
|
||||
break;
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
// extendBody: true,
|
||||
body: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
!ResponsiveBuilder.isNarrow(context)
|
||||
? NavigationRail(
|
||||
extended: true,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
minExtendedWidth: 170,
|
||||
groupAlignment: 0,
|
||||
// useIndicator: false,
|
||||
onDestinationSelected: (int index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.task),
|
||||
selectedIcon: const Icon(Icons.task),
|
||||
label: Text('task'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.extension),
|
||||
selectedIcon: const Icon(Icons.extension),
|
||||
label: Text('extensions'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
label: Text('setting'.tr),
|
||||
),
|
||||
],
|
||||
selectedIndex: controller.currentIndex.value,
|
||||
leading: const Icon(Icons.menu),
|
||||
// trailing: const Icon(Icons.info_outline),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: GetRouterOutlet(
|
||||
initialRoute: Routes.TASK,
|
||||
// anchorRoute: '/',
|
||||
// filterPages: (afterAnchor) {
|
||||
// logger.w(afterAnchor);
|
||||
// logger.w(afterAnchor.take(1));
|
||||
// return afterAnchor.take(1);
|
||||
// },
|
||||
))
|
||||
]),
|
||||
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
|
||||
? BottomNavigationBar(
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.task),
|
||||
label: 'task'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.extension),
|
||||
label: 'extensions'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: 'setting'.tr,
|
||||
),
|
||||
],
|
||||
currentIndex: controller.currentIndex.value,
|
||||
// selectedItemColor: Get.theme.highlightColor,
|
||||
onTap: (index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
)
|
||||
// StylishBottomBar(
|
||||
// option: AnimatedBarOptions(
|
||||
// iconSize: 32,
|
||||
// barAnimation: BarAnimation.blink,
|
||||
// iconStyle: IconStyle.Default,
|
||||
// opacity: 0.3,
|
||||
// ),
|
||||
// items: [
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.file_download),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloading'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.done),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloaded'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('setting'.tr)),
|
||||
// ],
|
||||
// // hasNotch: true,
|
||||
// currentIndex: controller.currentIndex.value,
|
||||
// onTap: (index) {
|
||||
// switch (index) {
|
||||
// case 0:
|
||||
// delegate.toNamed(Routes.DOWNLOADING);
|
||||
// controller.currentIndex.value = 0;
|
||||
// break;
|
||||
// case 1:
|
||||
// delegate.toNamed(Routes.DOWNLOADED);
|
||||
// controller.currentIndex.value = 1;
|
||||
// break;
|
||||
// case 2:
|
||||
// delegate.toNamed(Routes.SETTING);
|
||||
// controller.currentIndex.value = 2;
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
: const SizedBox.shrink(),
|
||||
return WithForegroundTask(
|
||||
child: Scaffold(
|
||||
// extendBody: true,
|
||||
body: Row(
|
||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
!ResponsiveBuilder.isNarrow(context)
|
||||
? NavigationRail(
|
||||
extended: true,
|
||||
labelType: NavigationRailLabelType.none,
|
||||
minExtendedWidth: 170,
|
||||
groupAlignment: 0,
|
||||
// useIndicator: false,
|
||||
onDestinationSelected: (int index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
destinations: [
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.task),
|
||||
selectedIcon: const Icon(Icons.task),
|
||||
label: Text('task'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.extension),
|
||||
selectedIcon: const Icon(Icons.extension),
|
||||
label: Text('extensions'.tr),
|
||||
),
|
||||
NavigationRailDestination(
|
||||
icon: const Icon(Icons.settings),
|
||||
selectedIcon: const Icon(Icons.settings),
|
||||
label: Text('setting'.tr),
|
||||
),
|
||||
],
|
||||
selectedIndex: controller.currentIndex.value,
|
||||
leading: const Icon(Icons.menu),
|
||||
// trailing: const Icon(Icons.info_outline),
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
Expanded(
|
||||
child: GetRouterOutlet(
|
||||
initialRoute: Routes.TASK,
|
||||
// anchorRoute: '/',
|
||||
// filterPages: (afterAnchor) {
|
||||
// logger.w(afterAnchor);
|
||||
// logger.w(afterAnchor.take(1));
|
||||
// return afterAnchor.take(1);
|
||||
// },
|
||||
))
|
||||
]),
|
||||
bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
|
||||
? BottomNavigationBar(
|
||||
items: <BottomNavigationBarItem>[
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.task),
|
||||
label: 'task'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.extension),
|
||||
label: 'extensions'.tr,
|
||||
),
|
||||
BottomNavigationBarItem(
|
||||
icon: const Icon(Icons.settings),
|
||||
label: 'setting'.tr,
|
||||
),
|
||||
],
|
||||
currentIndex: controller.currentIndex.value,
|
||||
// selectedItemColor: Get.theme.highlightColor,
|
||||
onTap: (index) {
|
||||
controller.currentIndex.value = index;
|
||||
switch (index) {
|
||||
case 0:
|
||||
delegate.offAndToNamed(Routes.TASK);
|
||||
break;
|
||||
case 1:
|
||||
delegate.offAndToNamed(Routes.EXTENSION);
|
||||
break;
|
||||
case 2:
|
||||
delegate.offAndToNamed(Routes.SETTING);
|
||||
break;
|
||||
}
|
||||
},
|
||||
)
|
||||
// StylishBottomBar(
|
||||
// option: AnimatedBarOptions(
|
||||
// iconSize: 32,
|
||||
// barAnimation: BarAnimation.blink,
|
||||
// iconStyle: IconStyle.Default,
|
||||
// opacity: 0.3,
|
||||
// ),
|
||||
// items: [
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.file_download),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloading'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.done),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('downloaded'.tr)),
|
||||
// BottomBarItem(
|
||||
// icon: const Icon(Icons.settings),
|
||||
// selectedColor: Get.theme.primaryColor,
|
||||
// title: Text('setting'.tr)),
|
||||
// ],
|
||||
// // hasNotch: true,
|
||||
// currentIndex: controller.currentIndex.value,
|
||||
// onTap: (index) {
|
||||
// switch (index) {
|
||||
// case 0:
|
||||
// delegate.toNamed(Routes.DOWNLOADING);
|
||||
// controller.currentIndex.value = 0;
|
||||
// break;
|
||||
// case 1:
|
||||
// delegate.toNamed(Routes.DOWNLOADED);
|
||||
// controller.currentIndex.value = 1;
|
||||
// break;
|
||||
// case 2:
|
||||
// delegate.toNamed(Routes.SETTING);
|
||||
// controller.currentIndex.value = 2;
|
||||
// break;
|
||||
// }
|
||||
// },
|
||||
// )
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -12,6 +12,12 @@ class FileItem {
|
||||
FileItem(this.isDirectory, this.path, this.name, this.size);
|
||||
|
||||
String get fullPath => "${path == "/" ? path : "$path/"}$name";
|
||||
|
||||
String filePath(String optName) {
|
||||
return optName.isEmpty
|
||||
? fullPath
|
||||
: "${path == "/" ? path : "$path/"}$optName";
|
||||
}
|
||||
}
|
||||
|
||||
class TaskFilesController extends GetxController {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:open_file/open_file.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../../util/file_icon.dart';
|
||||
@@ -52,13 +53,17 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
||||
itemBuilder: (context, index) {
|
||||
final meta = controller.task.value!.meta;
|
||||
final file = fileList[index];
|
||||
final filePath = Util.safePathJoin(
|
||||
[meta.opts.path, meta.res!.name, file.fullPath]);
|
||||
final filePath = Util.safePathJoin([
|
||||
meta.opts.path,
|
||||
meta.res!.name,
|
||||
file.filePath(meta.opts.name)
|
||||
]);
|
||||
final fileName = basename(filePath);
|
||||
return ListTile(
|
||||
leading: file.isDirectory
|
||||
? const Icon(Icons.folder)
|
||||
: Icon(FaIcons.allIcons[findIcon(file.name)]),
|
||||
title: Text(fileList[index].name),
|
||||
title: Text(fileName),
|
||||
subtitle: file.isDirectory
|
||||
? Text('items'.trParams({
|
||||
'count': controller
|
||||
|
||||
@@ -28,6 +28,7 @@ class LocaleKeys {
|
||||
static const donate = 'donate';
|
||||
static const exit = 'exit';
|
||||
static const create = 'create';
|
||||
static const directDownload = 'directDownload';
|
||||
static const advancedOptions = 'advancedOptions';
|
||||
static const downloadLink = 'downloadLink';
|
||||
static const downloadLinkValid = 'downloadLinkValid';
|
||||
@@ -82,6 +83,13 @@ class LocaleKeys {
|
||||
static const extensionUpdateSuccess = 'extensionUpdateSuccess';
|
||||
static const extensionDelete = 'extensionDelete';
|
||||
static const extensionAlreadyLatest = 'extensionAlreadyLatest';
|
||||
static const extensionFind = 'extensionFind';
|
||||
static const extensionDevelop = 'extensionDevelop';
|
||||
static const history = 'history';
|
||||
static const clearHistory = 'clearHistory';
|
||||
static const noHistoryFound = 'noHistoryFound';
|
||||
static const serviceTitle = 'serviceTitle';
|
||||
static const serviceText = 'serviceText';
|
||||
}
|
||||
|
||||
class Locales {
|
||||
@@ -92,13 +100,14 @@ class Locales {
|
||||
'confirm': 'Confirm',
|
||||
'cancel': 'Cancel',
|
||||
'selectAll': 'Select All',
|
||||
'task': 'Task',
|
||||
'task': 'Tasks',
|
||||
'downloading': 'downloading',
|
||||
'downloaded': 'downloaded',
|
||||
'setting': 'Setting',
|
||||
'setting': 'Settings',
|
||||
'donate': 'Donate',
|
||||
'exit': 'Exit',
|
||||
'create': 'Create Task',
|
||||
'directDownload': 'Direct Download',
|
||||
'advancedOptions': 'Advanced Options',
|
||||
'downloadLink': 'Download Link',
|
||||
'downloadLinkValid': 'Please enter the download link',
|
||||
@@ -155,6 +164,13 @@ class Locales {
|
||||
'extensionUpdateSuccess': 'Updated successfully',
|
||||
'extensionDelete': 'Delete Extension',
|
||||
'extensionAlreadyLatest': 'It\'s already the latest version',
|
||||
'extensionFind': 'Find Extensions',
|
||||
'extensionDevelop': 'Develop Extensions',
|
||||
'history': 'History',
|
||||
'clearHistory': 'Clear History',
|
||||
'noHistoryFound': 'No History Found',
|
||||
'serviceTitle': 'Download Service',
|
||||
'serviceText': 'Running',
|
||||
};
|
||||
static const fa_IR = {
|
||||
'label': 'فارسی',
|
||||
@@ -290,15 +306,17 @@ class Locales {
|
||||
'donate': 'Пожертвовать',
|
||||
'exit': 'Выход',
|
||||
'create': 'Создать задачу',
|
||||
'downloadLink': 'Ссылка для скачивание',
|
||||
'advancedOptions': 'Расширенные опции',
|
||||
'downloadLink': 'Ссылка на скачивание',
|
||||
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
||||
'downloadLinkHit':
|
||||
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append',
|
||||
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append',
|
||||
'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл',
|
||||
'download': 'Скачать',
|
||||
'noFileSelected': 'Файл не выбран',
|
||||
'noStoragePermission': 'Требуется доступ к хранилищу',
|
||||
'selectFile': 'Выберать файл',
|
||||
'selectFile': 'Выбрать файл',
|
||||
'rename': 'Переименовать',
|
||||
'basic': 'Основные параметры',
|
||||
'advanced': 'Расширенные',
|
||||
'general': 'Общие',
|
||||
@@ -338,6 +356,16 @@ class Locales {
|
||||
'newVersionTitle': 'Обнаружена новая версия @version',
|
||||
'newVersionUpdate': 'Обновить',
|
||||
'newVersionLater': 'позже',
|
||||
'extensions': 'Расширения',
|
||||
'extensionInstallSuccess': 'Установка завершена',
|
||||
'extensionUpdateSuccess': 'Обновление завершено',
|
||||
'extensionDelete': 'Удалить расширение',
|
||||
'extensionAlreadyLatest': 'Это последняя версия',
|
||||
'extensionFind': 'Найти расширения',
|
||||
'extensionDevelop': 'Разработка расширений',
|
||||
'history': 'История',
|
||||
'clearHistory': 'Очистить историю',
|
||||
'noHistoryFound': 'История не найдена',
|
||||
};
|
||||
static const zh_CN = {
|
||||
'label': '中文(简体)',
|
||||
@@ -353,6 +381,7 @@ class Locales {
|
||||
'donate': '打赏',
|
||||
'exit': '退出',
|
||||
'create': '创建任务',
|
||||
'directDownload': '直接下载',
|
||||
'advancedOptions': '高级选项',
|
||||
'downloadLink': '下载链接',
|
||||
'downloadLinkValid': '请输入下载链接',
|
||||
@@ -407,6 +436,13 @@ class Locales {
|
||||
'extensionUpdateSuccess': '更新成功',
|
||||
'extensionDelete': '删除扩展',
|
||||
'extensionAlreadyLatest': '已经是最新版本',
|
||||
'extensionFind': '获取扩展',
|
||||
'extensionDevelop': '开发扩展',
|
||||
'history': '历史记录',
|
||||
'clearHistory': '清空历史记录',
|
||||
'noHistoryFound': '暂无历史记录',
|
||||
'serviceTitle': '下载服务',
|
||||
'serviceText': '运行中',
|
||||
};
|
||||
static const zh_TW = {
|
||||
'label': '中文 (正體) ',
|
||||
|
||||
@@ -29,6 +29,7 @@ class Util {
|
||||
|
||||
static String safePathJoin(List<String> paths) {
|
||||
return paths
|
||||
.where((e) => e.isNotEmpty)
|
||||
.map((e) => safeDir(e))
|
||||
.join("/")
|
||||
.replaceAll(RegExp(r'//'), "/");
|
||||
@@ -93,6 +94,10 @@ class Util {
|
||||
return !kIsWeb && Platform.isMacOS;
|
||||
}
|
||||
|
||||
static isLinux() {
|
||||
return !kIsWeb && Platform.isLinux;
|
||||
}
|
||||
|
||||
static isWeb() {
|
||||
return kIsWeb;
|
||||
}
|
||||
|
||||
+122
-121
@@ -5,34 +5,34 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _fe_analyzer_shared
|
||||
sha256: "0816708f5fbcacca324d811297153fe3c8e047beb5c6752e12292d2974c17045"
|
||||
sha256: eb376e9acf6938204f90eb3b1f00b578640d3188b4c8a8ec054f9f479af8d051
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "62.0.0"
|
||||
version: "64.0.0"
|
||||
analyzer:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: analyzer
|
||||
sha256: "21862995c9932cd082f89d72ae5f5e2c110d1a0204ad06e4ebaee8307b76b834"
|
||||
sha256: "69f54f967773f6c26c7dcb13e93d7ccee8b17a641689da39e878d5cf13b06893"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.0"
|
||||
version: "6.2.0"
|
||||
app_links:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: app_links
|
||||
sha256: "16725e716afd0634a5441654b1dda2b6c5557aa230884b5e1f41a5aa546a4cb6"
|
||||
sha256: eb83c2b15b78a66db04e95132678e910fcdb8dc3a9b0aed0c138f50b2bef0dae
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.4.3"
|
||||
version: "3.4.5"
|
||||
archive:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: archive
|
||||
sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a"
|
||||
sha256: "7e0d52067d05f2e0324268097ba723b71cb41ac8a6a2b24d1edf9c536b987b03"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.3.7"
|
||||
version: "3.4.6"
|
||||
args:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -61,10 +61,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: badges
|
||||
sha256: "6e7f3ec561ec08f47f912cfe349d4a1707afdc8dda271e17b046aa6d42c89e77"
|
||||
sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
version: "3.1.2"
|
||||
boolean_selector:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -101,10 +101,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_resolvers
|
||||
sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
|
||||
sha256: "64e12b0521812d1684b1917bc80945625391cb9bdd4312536b1d69dcb6133ed8"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.1"
|
||||
version: "2.4.1"
|
||||
build_runner:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -117,10 +117,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: build_runner_core
|
||||
sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41"
|
||||
sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.2.10"
|
||||
version: "7.2.11"
|
||||
built_collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -133,10 +133,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: built_value
|
||||
sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166"
|
||||
sha256: a8de5955205b4d1dbbbc267daddf2178bd737e4bab8987c04a500478c9651e74
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "8.6.1"
|
||||
version: "8.6.3"
|
||||
characters:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -173,10 +173,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: code_builder
|
||||
sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189"
|
||||
sha256: "1be9be30396d7e4c0db42c35ea6ccd7cc6a1e19916b5dc64d6ac216b5544d677"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.5.0"
|
||||
version: "4.7.0"
|
||||
collection:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -205,10 +205,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
|
||||
sha256: "445db18de832dba8d851e287aff8ccf169bed30d2e94243cb54c7d2f1ed2142c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.3+4"
|
||||
version: "0.3.3+6"
|
||||
crypto:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -221,42 +221,34 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: cupertino_icons
|
||||
sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
|
||||
sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.5"
|
||||
dart_internal:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_internal
|
||||
sha256: dae3976f383beddcfcd07ad5291a422df2c8c0a8a03c52cda63ac7b4f26e0f4e
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.2.8"
|
||||
version: "1.0.6"
|
||||
dart_style:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: dart_style
|
||||
sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
|
||||
sha256: abd7625e16f51f554ea244d090292945ec4d4be7bfbaf2ec8cccea568919d334
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.2"
|
||||
version: "2.3.3"
|
||||
desktop_drop:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: desktop_drop
|
||||
sha256: ebba9c9cb0b54385998a977d741cc06fd8324878c08d5a36e9da61cd56b04cc6
|
||||
sha256: d55a010fe46c8e8fcff4ea4b451a9ff84a162217bdb3b2a0aa1479776205e15d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.4.3"
|
||||
version: "0.4.4"
|
||||
dio:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: dio
|
||||
sha256: a9d76e72985d7087eb7c5e7903224ae52b337131518d127c554b9405936752b8
|
||||
sha256: "417e2a6f9d83ab396ec38ff4ea5da6c254da71e4db765ad737a42af6930140b7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "5.2.1+1"
|
||||
version: "5.3.3"
|
||||
fake_async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -269,10 +261,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: ffi
|
||||
sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99
|
||||
sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.1.0"
|
||||
ffigen:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
@@ -309,15 +301,23 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: fluent_ui
|
||||
sha256: be237134ab215d4a1591307f6286f4aa53ed5a492c2ee56be5d86be344fd16db
|
||||
sha256: "43e95034b12a8285a78703b5995bb4d2e02a28750a1a363044c150533bc7f027"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.7.0"
|
||||
version: "4.7.6"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
source: sdk
|
||||
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:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -338,10 +338,10 @@ packages:
|
||||
dependency: "direct dev"
|
||||
description:
|
||||
name: flutter_lints
|
||||
sha256: "2118df84ef0c3ca93f96123a616ae8540879991b8b57af2f81b76a7ada49b2a4"
|
||||
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.2"
|
||||
version: "2.0.3"
|
||||
flutter_localizations:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@@ -351,10 +351,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
|
||||
sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.0.17"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@@ -433,10 +433,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image
|
||||
sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf
|
||||
sha256: "028f61960d56f26414eb616b48b04eb37d700cbe477b7fb09bf1d7ce57fd9271"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.17"
|
||||
version: "4.1.3"
|
||||
intl:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -513,10 +513,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: logging_appenders
|
||||
sha256: "69e6f077fa0f416dbb0d926fdb066dd686245ac5bd36e8fefce6ee4783d3c766"
|
||||
sha256: "1fb8a008c04246f4677a0d034d69779a5975e56e02573a5162240239b247e239"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
version: "1.2.0+1"
|
||||
lottie:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
@@ -601,10 +601,10 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: package_info_plus
|
||||
sha256: ceb027f6bc6a60674a233b4a90a7658af1aebdea833da0b5b53c1e9821a78c7b
|
||||
sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "4.0.2"
|
||||
version: "4.2.0"
|
||||
package_info_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -625,50 +625,50 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: path_provider
|
||||
sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2"
|
||||
sha256: a1aa8aaa2542a6bc57e381f132af822420216c80d4781f7aa085ca3229208aaa
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.15"
|
||||
version: "2.1.1"
|
||||
path_provider_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_android
|
||||
sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86"
|
||||
sha256: e595b98692943b4881b219f0a9e3945118d3c16bd7e2813f98ec6e532d905f72
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.27"
|
||||
version: "2.2.1"
|
||||
path_provider_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_foundation
|
||||
sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3"
|
||||
sha256: "19314d595120f82aca0ba62787d58dde2cc6b5df7d2f0daf72489e38d1b57f2d"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.3"
|
||||
version: "2.3.1"
|
||||
path_provider_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_linux
|
||||
sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57
|
||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.11"
|
||||
version: "2.2.1"
|
||||
path_provider_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_platform_interface
|
||||
sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec"
|
||||
sha256: "94b1e0dd80970c1ce43d5d4e050a9918fce4f4a775e6142424c30a29a363265c"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.6"
|
||||
version: "2.1.1"
|
||||
path_provider_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: path_provider_windows
|
||||
sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96"
|
||||
sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.7"
|
||||
version: "2.2.1"
|
||||
petitparser:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -681,18 +681,18 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: platform
|
||||
sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76"
|
||||
sha256: "0a279f0707af40c890e80b1e9df8bb761694c074ba7e1d4ab1bc4b728e200b59"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.1.0"
|
||||
version: "3.1.3"
|
||||
plugin_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: plugin_platform_interface
|
||||
sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc"
|
||||
sha256: da3fdfeccc4d4ff2da8f8c556704c08f912542c5fb3cf2233ed75372384a034d
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.4"
|
||||
version: "2.1.6"
|
||||
pointycastle:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -709,14 +709,6 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
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:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -769,10 +761,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: screen_retriever
|
||||
sha256: "4931f226ca158123ccd765325e9fbf360bfed0af9b460a10f960f9bb13d58323"
|
||||
sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.1.6"
|
||||
version: "0.1.9"
|
||||
scroll_pos:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -785,74 +777,74 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: share_plus
|
||||
sha256: "6cec740fa0943a826951223e76218df002804adb588235a8910dc3d6b0654e11"
|
||||
sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.1.0"
|
||||
version: "7.2.1"
|
||||
share_plus_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: share_plus_platform_interface
|
||||
sha256: "357412af4178d8e11d14f41723f80f12caea54cf0d5cd29af9dcdab85d58aea7"
|
||||
sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.3.0"
|
||||
version: "3.3.1"
|
||||
shared_preferences:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
|
||||
sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.2"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_android
|
||||
sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
|
||||
sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_foundation:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_foundation
|
||||
sha256: b046999bf0ff58f04c364491bb803dcfa8f42e47b19c75478f53d323684a8cc1
|
||||
sha256: "7bf53a9f2d007329ee6f3df7268fd498f8373602f943c975598bbb34649b62a7"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.1"
|
||||
version: "2.3.4"
|
||||
shared_preferences_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_linux
|
||||
sha256: "71d6806d1449b0a9d4e85e0c7a917771e672a3d5dc61149cc9fac871115018e1"
|
||||
sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
shared_preferences_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_platform_interface
|
||||
sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
|
||||
sha256: d4ec5fc9ebb2f2e056c617112aa75dcf92fc2e4faaf2ae999caa297473f75d8a
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.1"
|
||||
shared_preferences_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_web
|
||||
sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
|
||||
sha256: d762709c2bbe80626ecc819143013cc820fa49ca5e363620ee20a8b15a3e3daf
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
version: "2.2.1"
|
||||
shared_preferences_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: shared_preferences_windows
|
||||
sha256: f95e6a43162bce43c9c3405f3eb6f39e5b5d11f65fab19196cf8225e2777624d
|
||||
sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.3.0"
|
||||
version: "2.3.2"
|
||||
shelf:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -906,6 +898,14 @@ packages:
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.9.1"
|
||||
sprintf:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: sprintf
|
||||
sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "7.0.0"
|
||||
stack_trace:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -973,10 +973,11 @@ packages:
|
||||
tray_manager:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: tray_manager
|
||||
sha256: b1975a05e0c6999e983cf9a58a6a098318c896040ccebac5398a3cc9e43b9c69
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
path: "."
|
||||
ref: "fix/libayatana-set-icon-deprecation"
|
||||
resolved-ref: "056da3fef20e75877e8d2a4ae73d93f51a0f77cc"
|
||||
url: "https://github.com/provokateurin/tray_manager.git"
|
||||
source: git
|
||||
version: "0.2.0"
|
||||
typed_data:
|
||||
dependency: transitive
|
||||
@@ -998,74 +999,74 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: url_launcher
|
||||
sha256: "781bd58a1eb16069412365c98597726cd8810ae27435f04b3b4d3a470bacd61e"
|
||||
sha256: "47e208a6711459d813ba18af120d9663c20bdf6985d6ad39fe165d2538378d27"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.12"
|
||||
version: "6.1.14"
|
||||
url_launcher_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_android
|
||||
sha256: "15f5acbf0dce90146a0f5a2c4a002b1814a6303c4c5c075aa2623b2d16156f03"
|
||||
sha256: "31222ffb0063171b526d3e569079cf1f8b294075ba323443fdc690842bfd4def"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.0.36"
|
||||
version: "6.2.0"
|
||||
url_launcher_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_ios
|
||||
sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2"
|
||||
sha256: "4ac97281cf60e2e8c5cc703b2b28528f9b50c8f7cebc71df6bdf0845f647268a"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "6.1.4"
|
||||
version: "6.2.0"
|
||||
url_launcher_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_linux
|
||||
sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5"
|
||||
sha256: "9f2d390e096fdbe1e6e6256f97851e51afc2d9c423d3432f1d6a02a8a9a8b9fd"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.1.0"
|
||||
url_launcher_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_macos
|
||||
sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e"
|
||||
sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.5"
|
||||
version: "3.1.0"
|
||||
url_launcher_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_platform_interface
|
||||
sha256: bfdfa402f1f3298637d71ca8ecfe840b4696698213d5346e9d12d4ab647ee2ea
|
||||
sha256: "980e8d9af422f477be6948bdfb68df8433be71f5743a188968b0c1b887807e50"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.1.3"
|
||||
version: "2.2.0"
|
||||
url_launcher_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_web
|
||||
sha256: cc26720eefe98c1b71d85f9dc7ef0cada5132617046369d9dc296b3ecaa5cbb4
|
||||
sha256: ba140138558fcc3eead51a1c42e92a9fb074a1b1149ed3c73e66035b2ccd94f2
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "2.0.18"
|
||||
version: "2.0.19"
|
||||
url_launcher_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: url_launcher_windows
|
||||
sha256: "7967065dd2b5fccc18c653b97958fdf839c5478c28e767c61ee879f4e7882422"
|
||||
sha256: "7754a1ad30ee896b265f8d14078b0513a4dba28d358eabb9d5f339886f4a1adc"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "3.1.0"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
|
||||
sha256: b715b8d3858b6fa9f68f87d20d98830283628014750c2b09b6f516c1da4af2a7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "3.0.7"
|
||||
version: "4.1.0"
|
||||
vector_math:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1102,18 +1103,18 @@ packages:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: window_manager
|
||||
sha256: "9eef00e393e7f9308309ce9a8b2398c9ee3ca78b50c96e8b4f9873945693ac88"
|
||||
sha256: dcc865277f26a7dad263a47d0e405d77e21f12cb71f30333a52710a408690bd7
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "0.3.5"
|
||||
version: "0.3.7"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: xdg_directories
|
||||
sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1
|
||||
sha256: "589ada45ba9e39405c198fe34eb0f607cddb2108527e658136120892beac46d2"
|
||||
url: "https://pub.flutter-io.cn"
|
||||
source: hosted
|
||||
version: "1.0.0"
|
||||
version: "1.0.3"
|
||||
xml:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1139,5 +1140,5 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.1"
|
||||
sdks:
|
||||
dart: ">=3.0.0 <3.2.0"
|
||||
dart: ">=3.0.0 <4.0.0"
|
||||
flutter: ">=3.10.0"
|
||||
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||
# Read more about iOS versioning at
|
||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||
version: 1.4.0+1
|
||||
version: 1.4.4+1
|
||||
|
||||
environment:
|
||||
sdk: '>=3.0.0 <4.0.0'
|
||||
@@ -41,7 +41,7 @@ dependencies:
|
||||
context_menus: ^1.0.2
|
||||
json_annotation: ^4.8.1
|
||||
dio: ^5.2.1
|
||||
path_provider: ^2.0.11
|
||||
path_provider: ^2.1.1
|
||||
file_picker: 5.3.1
|
||||
macos_secure_bookmarks: ^0.2.1
|
||||
rounded_loading_button: ^2.1.0
|
||||
@@ -56,12 +56,16 @@ dependencies:
|
||||
app_links: ^3.4.2
|
||||
uri_to_file: ^0.2.0
|
||||
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
|
||||
share_plus: ^7.1.0
|
||||
open_file: ^3.3.2
|
||||
flutter_form_builder: ^9.1.1
|
||||
form_builder_validators: ^9.0.0
|
||||
flutter_foreground_task: ^6.1.2
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
Reference in new issue
Block a user