mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-13 11:53:41 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4840c4b8a4 | ||
|
|
e72b097aa2 | ||
|
|
af52c4ce7e | ||
|
|
aae79c2449 | ||
|
|
83838afce9 | ||
|
|
4690107e32 | ||
|
|
74b08b3631 | ||
|
|
953d348c0c | ||
|
|
00cea68b6e | ||
|
|
daa9d4c707 | ||
|
|
6bc587752a | ||
|
|
11befc09bd | ||
|
|
ff6c0da262 | ||
|
|
aab4a37a7e | ||
|
|
74c9491893 | ||
|
|
256faf2cbd | ||
|
|
32e3620f5e |
No files matched your search
@@ -32,6 +32,10 @@ version-resolver:
|
|||||||
- 'patch'
|
- 'patch'
|
||||||
default: patch
|
default: patch
|
||||||
template: |
|
template: |
|
||||||
## Changes
|
# Changes
|
||||||
|
|
||||||
|
$CHANGES
|
||||||
|
|
||||||
|
# 更新日志
|
||||||
|
|
||||||
$CHANGES
|
$CHANGES
|
||||||
+160
-5
@@ -2,6 +2,11 @@ name: build
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
platform:
|
||||||
|
description: 'Build platform'
|
||||||
|
required: true
|
||||||
|
default: 'all'
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.19"
|
GO_VERSION: "1.19"
|
||||||
@@ -24,6 +29,7 @@ jobs:
|
|||||||
myToken: ${{ github.token }}
|
myToken: ${{ github.token }}
|
||||||
|
|
||||||
build-windows:
|
build-windows:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows' }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -124,6 +130,7 @@ jobs:
|
|||||||
asset_path: ui/flutter/build/windows/Output/*
|
asset_path: ui/flutter/build/windows/Output/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-macos-arm64-lib:
|
build-macos-arm64-lib:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -134,13 +141,14 @@ jobs:
|
|||||||
- name: Build
|
- name: Build
|
||||||
run: |
|
run: |
|
||||||
go install src.techknowlogick.com/xgo@latest
|
go install src.techknowlogick.com/xgo@latest
|
||||||
xgo -go go-1.19.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
xgo -go go-$GO_VERSION.x --targets=darwin/arm64 -tags="nosqlite" -ldflags="-w -s" -buildmode=c-shared -pkg=bind/desktop -out=libgopeed .
|
||||||
mv libgopeed-*.dylib libgopeed.dylib
|
mv libgopeed-*.dylib libgopeed.dylib
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-arm64-lib
|
||||||
path: libgopeed.dylib
|
path: libgopeed.dylib
|
||||||
build-macos:
|
build-macos:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
needs: [ get-release, build-macos-arm64-lib ]
|
needs: [ get-release, build-macos-arm64-lib ]
|
||||||
steps:
|
steps:
|
||||||
@@ -155,7 +163,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
flutter-version: ${{ env.FLUTTER_VERSION }}
|
flutter-version: ${{ env.FLUTTER_VERSION }}
|
||||||
- name: Install appdmg
|
- name: Install appdmg
|
||||||
run: npm install -g appdmg
|
run: |
|
||||||
|
python3 -m pip install setuptools
|
||||||
|
npm install -g appdmg
|
||||||
- uses: actions/download-artifact@v3
|
- uses: actions/download-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: macos-arm64-lib
|
name: macos-arm64-lib
|
||||||
@@ -197,6 +207,7 @@ jobs:
|
|||||||
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
|
asset_path: ui/flutter/build/macos/Build/Products/Release/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-linux:
|
build-linux:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -210,6 +221,10 @@ jobs:
|
|||||||
- run: |
|
- run: |
|
||||||
sudo apt-get update -y
|
sudo apt-get update -y
|
||||||
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev
|
||||||
|
- name: Setup LXD
|
||||||
|
uses: canonical/[email protected]
|
||||||
|
with:
|
||||||
|
channel: latest/stable
|
||||||
- name: Build
|
- name: Build
|
||||||
env:
|
env:
|
||||||
VERSION: ${{ needs.get-release.outputs.tag_name }}
|
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
|
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop
|
||||||
cd ui/flutter
|
cd ui/flutter
|
||||||
flutter build linux
|
flutter build linux
|
||||||
|
mkdir -p 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
|
mkdir -p debian/gui
|
||||||
cp assets/icon/icon_1024.png debian/gui/gopeed.png
|
cp assets/icon/icon_1024.png debian/gui/gopeed.png
|
||||||
cat>debian/debian.yaml<<EOF
|
cat>debian/debian.yaml<<EOF
|
||||||
flutter_app:
|
flutter_app:
|
||||||
command: gopeed
|
command: gopeed
|
||||||
arch: x64
|
arch: x64
|
||||||
parent: /usr/local/lib
|
parent: /usr/local/bin
|
||||||
control:
|
control:
|
||||||
Package: gopeed
|
Package: gopeed
|
||||||
Version: ${VERSION:1}
|
Version: ${VERSION:1}
|
||||||
@@ -241,6 +263,8 @@ jobs:
|
|||||||
Name=Gopeed
|
Name=Gopeed
|
||||||
GenericName=Gopeed
|
GenericName=Gopeed
|
||||||
Comment=High speed downloader that supports all platforms.
|
Comment=High speed downloader that supports all platforms.
|
||||||
|
Exec=gopeed
|
||||||
|
Icon=gopeed.png
|
||||||
Terminal=false
|
Terminal=false
|
||||||
Type=Application
|
Type=Application
|
||||||
Categories=Utility
|
Categories=Utility
|
||||||
@@ -249,14 +273,142 @@ jobs:
|
|||||||
|
|
||||||
dart pub global activate flutter_to_debian
|
dart pub global activate flutter_to_debian
|
||||||
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
|
- name: Upload
|
||||||
uses: shogo82148/actions-upload-release-asset@v1
|
uses: shogo82148/actions-upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
upload_url: ${{ needs.get-release.outputs.upload_url }}
|
||||||
asset_path: ui/flutter/debian/packages/*
|
asset_path: ui/flutter/build/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-android:
|
build-android:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -294,6 +446,7 @@ jobs:
|
|||||||
asset_path: ui/flutter/dist/*
|
asset_path: ui/flutter/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-ios:
|
build-ios:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -326,6 +479,7 @@ jobs:
|
|||||||
asset_path: ui/flutter/dist/*
|
asset_path: ui/flutter/dist/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-web:
|
build-web:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'web' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
@@ -373,6 +527,7 @@ jobs:
|
|||||||
asset_path: dist/zip/*
|
asset_path: dist/zip/*
|
||||||
overwrite: true
|
overwrite: true
|
||||||
build-docker:
|
build-docker:
|
||||||
|
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'docker' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: [ get-release ]
|
needs: [ get-release ]
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -13,7 +13,8 @@
|
|||||||
|
|
||||||
## Introduction
|
## Introduction
|
||||||
|
|
||||||
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms.
|
Gopeed (full name Go Speed), a high-speed downloader developed by `Golang` + `Flutter`, supports (HTTP, BitTorrent, Magnet) protocol, and supports all platforms. In addition to basic download functions, Gopeed is also a highly customizable downloader that supports implementing more features through integration with [APIs](https://docs.gopeed.com/dev-api.html) or installation and development of [extensions](https://docs.gopeed.com/dev-extension.html).
|
||||||
|
|
||||||
|
|
||||||
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
|
Visit ✈ [Official Website](https://gopeed.com) | 📖 [Develop Docs](https://docs.gopeed.com)
|
||||||
|
|
||||||
|
|||||||
+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)
|
見て下さい ✈ [公式ウェブサイト](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/)
|
访问 ✈ [官方网站](https://gopeed.com/zh-CN) | 📖 [开发文档](https://docs.gopeed.com/zh/)
|
||||||
|
|
||||||
|
|||||||
+14
-1
@@ -2,6 +2,7 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/GopeedLab/gopeed/internal/fetcher"
|
||||||
"github.com/GopeedLab/gopeed/pkg/base"
|
"github.com/GopeedLab/gopeed/pkg/base"
|
||||||
"github.com/GopeedLab/gopeed/pkg/download"
|
"github.com/GopeedLab/gopeed/pkg/download"
|
||||||
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
"github.com/GopeedLab/gopeed/pkg/protocol/http"
|
||||||
@@ -47,16 +48,28 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
printProgress(emptyTask, "downloading...")
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lastLineLen = 0
|
lastLineLen = 0
|
||||||
sb = new(strings.Builder)
|
sb = new(strings.Builder)
|
||||||
|
emptyTask = &download.Task{
|
||||||
|
Progress: &download.Progress{},
|
||||||
|
Meta: &fetcher.FetcherMeta{
|
||||||
|
Res: &base.Resource{},
|
||||||
|
},
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func printProgress(task *download.Task, title string) {
|
func printProgress(task *download.Task, title string) {
|
||||||
rate := float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
var rate float64
|
||||||
|
if task.Meta.Res.Size <= 0 {
|
||||||
|
rate = 0
|
||||||
|
} else {
|
||||||
|
rate = float64(task.Progress.Downloaded) / float64(task.Meta.Res.Size)
|
||||||
|
}
|
||||||
completeWidth := int(progressWidth * rate)
|
completeWidth := int(progressWidth * rate)
|
||||||
speed := util.ByteFmt(task.Progress.Speed)
|
speed := util.ByteFmt(task.Progress.Speed)
|
||||||
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
totalSize := util.ByteFmt(task.Meta.Res.Size)
|
||||||
|
|||||||
@@ -87,7 +87,6 @@ func (f *Fetcher) Create(opts *base.Options) (err error) {
|
|||||||
if f.meta.Res != nil {
|
if f.meta.Res != nil {
|
||||||
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
torrentDirMap[f.meta.Res.Hash] = f.meta.FolderPath()
|
||||||
}
|
}
|
||||||
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +100,14 @@ func (f *Fetcher) Start() (err error) {
|
|||||||
ft.setTorrentDir(f.meta.FolderPath())
|
ft.setTorrentDir(f.meta.FolderPath())
|
||||||
}
|
}
|
||||||
files := f.torrent.Files()
|
files := f.torrent.Files()
|
||||||
|
// If the user does not specify the file to download, all files will be downloaded by default
|
||||||
|
if len(f.meta.Opts.SelectFiles) == 0 {
|
||||||
|
f.meta.Opts.SelectFiles = make([]int, len(files))
|
||||||
|
for i := range files {
|
||||||
|
f.meta.Opts.SelectFiles[i] = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
f.progress = make(fetcher.Progress, len(f.meta.Opts.SelectFiles))
|
||||||
if len(f.meta.Opts.SelectFiles) == len(files) {
|
if len(f.meta.Opts.SelectFiles) == len(files) {
|
||||||
f.torrent.DownloadAll()
|
f.torrent.DownloadAll()
|
||||||
} else {
|
} else {
|
||||||
@@ -134,7 +141,7 @@ func (f *Fetcher) safeDrop() {
|
|||||||
|
|
||||||
func (f *Fetcher) Wait() (err error) {
|
func (f *Fetcher) Wait() (err error) {
|
||||||
for {
|
for {
|
||||||
if f.torrentReady.Load() {
|
if f.torrentReady.Load() && len(f.meta.Opts.SelectFiles) > 0 {
|
||||||
done := true
|
done := true
|
||||||
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
for _, selectIndex := range f.meta.Opts.SelectFiles {
|
||||||
file := f.torrent.Files()[selectIndex]
|
file := f.torrent.Files()[selectIndex]
|
||||||
@@ -154,7 +161,7 @@ func (f *Fetcher) Wait() (err error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !selected {
|
if !selected {
|
||||||
util.SafeRemove(filepath.Join(f.meta.Opts.Path, file.Path()))
|
util.SafeRemove(filepath.Join(f.meta.Opts.Path, f.meta.Res.Name, file.Path()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
@@ -195,7 +202,7 @@ func (f *Fetcher) updateRes() {
|
|||||||
Size: file.Length(),
|
Size: file.Length(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
res.CalcSize()
|
res.CalcSize(nil)
|
||||||
f.meta.Res = res
|
f.meta.Res = res
|
||||||
if f.meta.Opts != nil {
|
if f.meta.Opts != nil {
|
||||||
f.meta.Opts.InitSelectFiles(len(res.Files))
|
f.meta.Opts.InitSelectFiles(len(res.Files))
|
||||||
|
|||||||
@@ -314,6 +314,10 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
// If canceled, do not retry
|
||||||
|
if errors.Is(err, context.Canceled) {
|
||||||
|
return
|
||||||
|
}
|
||||||
// retry request after 1 second
|
// retry request after 1 second
|
||||||
chunk.retryTimes = chunk.retryTimes + 1
|
chunk.retryTimes = chunk.retryTimes + 1
|
||||||
time.Sleep(time.Second)
|
time.Sleep(time.Second)
|
||||||
@@ -466,9 +470,7 @@ func (fb *FetcherBuilder) Restore() (v any, f func(meta *fetcher.FetcherMeta, v
|
|||||||
fetcher.meta = meta
|
fetcher.meta = meta
|
||||||
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
base.ParseReqExtra[fhttp.ReqExtra](fetcher.meta.Req)
|
||||||
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
base.ParseOptsExtra[fhttp.OptsExtra](fetcher.meta.Opts)
|
||||||
if len(fd.Chunks) == 0 {
|
if len(fd.Chunks) > 0 {
|
||||||
fetcher.chunks = fetcher.splitChunk()
|
|
||||||
} else {
|
|
||||||
fetcher.chunks = fd.Chunks
|
fetcher.chunks = fd.Chunks
|
||||||
}
|
}
|
||||||
return fetcher
|
return fetcher
|
||||||
|
|||||||
+5
-2
@@ -3,6 +3,7 @@ package base
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/GopeedLab/gopeed/pkg/util"
|
"github.com/GopeedLab/gopeed/pkg/util"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Request download request
|
// Request download request
|
||||||
@@ -47,11 +48,13 @@ func (r *Resource) Validate() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Resource) CalcSize() {
|
func (r *Resource) CalcSize(selectFiles []int) {
|
||||||
var size int64
|
var size int64
|
||||||
for _, file := range r.Files {
|
for i, file := range r.Files {
|
||||||
|
if len(selectFiles) == 0 || slices.Contains(selectFiles, i) {
|
||||||
size += file.Size
|
size += file.Size
|
||||||
}
|
}
|
||||||
|
}
|
||||||
r.Size = size
|
r.Size = size
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+227
-148
File diff suppressed because it is too large.
Load diff
@@ -243,7 +243,7 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
|||||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] resource invalid", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Res.CalcSize()
|
ctx.Res.CalcSize(nil)
|
||||||
}
|
}
|
||||||
res = ctx.Res
|
res = ctx.Res
|
||||||
},
|
},
|
||||||
@@ -251,12 +251,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Downloader) triggerOnStart(task *Task) {
|
func (d *Downloader) triggerOnStart(task *Task) (req *base.Request) {
|
||||||
doTrigger(d,
|
doTrigger(d,
|
||||||
EventOnStart,
|
EventOnStart,
|
||||||
task.Meta.Req,
|
task.Meta.Req,
|
||||||
&OnStartContext{
|
&OnStartContext{
|
||||||
Task: task.clone(),
|
Task: task,
|
||||||
},
|
},
|
||||||
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
func(ext *Extension, gopeed *Instance, ctx *OnStartContext) {
|
||||||
// Validate request structure
|
// Validate request structure
|
||||||
@@ -265,8 +265,7 @@ func (d *Downloader) triggerOnStart(task *Task) {
|
|||||||
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
gopeed.Logger.logger.Warn().Err(err).Msgf("[%s] request invalid", ext.buildIdentity())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Modify real task request
|
req = ctx.Task.Meta.Req
|
||||||
task.Meta.Req = ctx.Task.Meta.Req
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -422,8 +421,50 @@ func (e *Extension) update(newExt *Extension) error {
|
|||||||
e.Homepage = newExt.Homepage
|
e.Homepage = newExt.Homepage
|
||||||
e.Repository = newExt.Repository
|
e.Repository = newExt.Repository
|
||||||
e.Scripts = newExt.Scripts
|
e.Scripts = newExt.Scripts
|
||||||
// don't override settings
|
// merge settings
|
||||||
//e.Settings = newExt.Settings
|
// if new setting not exist in old settings, append it
|
||||||
|
for _, newSetting := range newExt.Settings {
|
||||||
|
var exist bool
|
||||||
|
for _, setting := range e.Settings {
|
||||||
|
if setting.Name == newSetting.Name {
|
||||||
|
exist = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
e.Settings = append(e.Settings, newSetting)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if old setting not exist in new settings, remove it
|
||||||
|
for i := 0; i < len(e.Settings); i++ {
|
||||||
|
var exist bool
|
||||||
|
for _, setting := range newExt.Settings {
|
||||||
|
if setting.Name == e.Settings[i].Name {
|
||||||
|
exist = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exist {
|
||||||
|
e.Settings = append(e.Settings[:i], e.Settings[i+1:]...)
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if new setting exist in old settings, update it
|
||||||
|
for _, newSetting := range newExt.Settings {
|
||||||
|
for _, setting := range e.Settings {
|
||||||
|
if setting.Name == newSetting.Name {
|
||||||
|
setting.Title = newSetting.Title
|
||||||
|
setting.Description = newSetting.Description
|
||||||
|
setting.Options = newSetting.Options
|
||||||
|
// if type changed, reset value
|
||||||
|
if setting.Type != newSetting.Type {
|
||||||
|
setting.Type = newSetting.Type
|
||||||
|
setting.Value = newSetting.Value
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
e.UpdatedAt = time.Now()
|
e.UpdatedAt = time.Now()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ func TestDownloader_InstallExtensionByGitFull(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestDownloader_UpgradeExtension(t *testing.T) {
|
func TestDownloader_UpgradeExtension(t *testing.T) {
|
||||||
|
getSetting := func(settings []*Setting, name string) *Setting {
|
||||||
|
for _, setting := range settings {
|
||||||
|
if setting.Name == name {
|
||||||
|
return setting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
setupDownloader(func(downloader *Downloader) {
|
setupDownloader(func(downloader *Downloader) {
|
||||||
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false)
|
installedExt, err := downloader.InstallExtensionByFolder("./testdata/extensions/update", false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,14 +131,35 @@ func TestDownloader_UpgradeExtension(t *testing.T) {
|
|||||||
t.Fatal("extension update fail")
|
t.Fatal("extension update fail")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check setting update
|
||||||
|
s1 := getSetting(upgradeExt.Settings, "s1")
|
||||||
|
if s1.Title == "S1 old" {
|
||||||
|
t.Fatal("setting update fail")
|
||||||
|
}
|
||||||
|
// check setting type update
|
||||||
|
s2 := getSetting(upgradeExt.Settings, "s2")
|
||||||
|
if s2.Type == "number" {
|
||||||
|
t.Fatal("setting type update fail")
|
||||||
|
}
|
||||||
|
// check setting remove
|
||||||
|
d1 := getSetting(upgradeExt.Settings, "d1")
|
||||||
|
if d1 != nil {
|
||||||
|
t.Fatal("setting remove fail")
|
||||||
|
}
|
||||||
|
// check setting add
|
||||||
|
s3 := getSetting(upgradeExt.Settings, "s3")
|
||||||
|
if s3 == nil {
|
||||||
|
t.Fatal("setting add fail")
|
||||||
|
}
|
||||||
|
|
||||||
rr, err := downloader.Resolve(&base.Request{
|
rr, err := downloader.Resolve(&base.Request{
|
||||||
URL: "https://github.com/GopeedLab/gopeed/releases",
|
URL: "https://test.com",
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if len(rr.Res.Files) == 1 {
|
if rr.Res.Name != "test" {
|
||||||
t.Fatal("resolve error")
|
t.Fatal("script update fail")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ type Task struct {
|
|||||||
Meta *fetcher.FetcherMeta `json:"meta"`
|
Meta *fetcher.FetcherMeta `json:"meta"`
|
||||||
Status base.Status `json:"status"`
|
Status base.Status `json:"status"`
|
||||||
Progress *Progress `json:"progress"`
|
Progress *Progress `json:"progress"`
|
||||||
Size int64 `json:"size"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
|
||||||
fetcherBuilder fetcher.FetcherBuilder
|
fetcherBuilder fetcher.FetcherBuilder
|
||||||
@@ -50,7 +49,6 @@ func (t *Task) clone() *Task {
|
|||||||
Meta: t.Meta,
|
Meta: t.Meta,
|
||||||
Status: t.Status,
|
Status: t.Status,
|
||||||
Progress: t.Progress,
|
Progress: t.Progress,
|
||||||
Size: t.Size,
|
|
||||||
CreatedAt: t.CreatedAt,
|
CreatedAt: t.CreatedAt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -222,6 +222,9 @@ func (b *BoltStorage) Pop(bucket string, key string, v any) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return json.Unmarshal(data, v)
|
return json.Unmarshal(data, v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+28
-4
@@ -1,13 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "github-release",
|
"name": "extension-test",
|
||||||
"author": "gopeed",
|
"author": "gopeed",
|
||||||
"title": "Github Release",
|
"title": "Gopeed Extension Test",
|
||||||
"description": "Download github release assets",
|
"description": "Test extension settings and upgrade",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"homepage": "https://gopeed.com",
|
"homepage": "https://gopeed.com",
|
||||||
"repository": {
|
"repository": {
|
||||||
"url": "https://github.com/GopeedLab/gopeed-extension-samples",
|
"url": "https://github.com/GopeedLab/gopeed-extension-samples",
|
||||||
"directory": "github-release-sample"
|
"directory": "extension-test"
|
||||||
},
|
},
|
||||||
"scripts": [
|
"scripts": [
|
||||||
{
|
{
|
||||||
@@ -19,5 +19,29 @@
|
|||||||
},
|
},
|
||||||
"entry": "index.js"
|
"entry": "index.js"
|
||||||
}
|
}
|
||||||
|
],
|
||||||
|
"settings": [
|
||||||
|
{
|
||||||
|
"name": "s1",
|
||||||
|
"title": "S1 old",
|
||||||
|
"description": "Test setting update",
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "s2",
|
||||||
|
"title": "s2 number old",
|
||||||
|
"description": "Test setting type update",
|
||||||
|
"type": "number",
|
||||||
|
"required": true,
|
||||||
|
"value": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "d1",
|
||||||
|
"title": "Delete test",
|
||||||
|
"description": "Test setting delete",
|
||||||
|
"type": "string",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -204,19 +204,16 @@ func TestPauseAllAndContinueALLTasks(t *testing.T) {
|
|||||||
|
|
||||||
// continue all
|
// continue all
|
||||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
|
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/continue", nil)
|
||||||
time.Sleep(time.Millisecond * 100)
|
|
||||||
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
|
tasks := httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusRunning), nil)
|
||||||
if len(tasks) != cfg.MaxRunning {
|
if len(tasks) != cfg.MaxRunning {
|
||||||
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
|
t.Errorf("ContinueAllTasks() got = %v, want %v", len(tasks), cfg.MaxRunning)
|
||||||
}
|
}
|
||||||
// pause all
|
// pause all
|
||||||
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
|
httpRequestCheckOk[any](http.MethodPut, "/api/v1/tasks/pause", nil)
|
||||||
time.Sleep(time.Millisecond * 500)
|
|
||||||
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
|
tasks = httpRequestCheckOk[[]*download.Task](http.MethodGet, fmt.Sprintf("/api/v1/tasks?status=%s", base.DownloadStatusPause), nil)
|
||||||
if len(tasks) != total {
|
if len(tasks) != total {
|
||||||
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
|
t.Errorf("PauseAllTasks() got = %v, want %v", len(tasks), total)
|
||||||
}
|
}
|
||||||
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ android {
|
|||||||
applicationId "com.gopeed"
|
applicationId "com.gopeed"
|
||||||
// You can update the following values to match your application needs.
|
// You can update the following values to match your application needs.
|
||||||
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
// For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration.
|
||||||
minSdkVersion 19
|
minSdkVersion 21
|
||||||
targetSdkVersion 29
|
targetSdkVersion 29
|
||||||
versionCode flutterVersionCode.toInteger()
|
versionCode flutterVersionCode.toInteger()
|
||||||
versionName flutterVersionName
|
versionName flutterVersionName
|
||||||
|
|||||||
@@ -72,6 +72,12 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<!-- Add android:stopWithTask option only when necessary. -->
|
||||||
|
<service
|
||||||
|
android:name="com.pravera.flutter_foreground_task.service.ForegroundService"
|
||||||
|
android:stopWithTask="true" />
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
|
|||||||
@@ -5,13 +5,14 @@
|
|||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"selectAll": "Select All",
|
"selectAll": "Select All",
|
||||||
"task": "Task",
|
"task": "Tasks",
|
||||||
"downloading": "downloading",
|
"downloading": "downloading",
|
||||||
"downloaded": "downloaded",
|
"downloaded": "downloaded",
|
||||||
"setting": "Setting",
|
"setting": "Settings",
|
||||||
"donate": "Donate",
|
"donate": "Donate",
|
||||||
"exit": "Exit",
|
"exit": "Exit",
|
||||||
"create": "Create Task",
|
"create": "Create Task",
|
||||||
|
"directDownload": "Direct Download",
|
||||||
"advancedOptions": "Advanced Options",
|
"advancedOptions": "Advanced Options",
|
||||||
"downloadLink": "Download Link",
|
"downloadLink": "Download Link",
|
||||||
"downloadLinkValid": "Please enter the download link",
|
"downloadLinkValid": "Please enter the download link",
|
||||||
@@ -65,6 +66,12 @@
|
|||||||
"extensionInstallSuccess": "Installed successfully",
|
"extensionInstallSuccess": "Installed successfully",
|
||||||
"extensionUpdateSuccess": "Updated successfully",
|
"extensionUpdateSuccess": "Updated successfully",
|
||||||
"extensionDelete": "Delete Extension",
|
"extensionDelete": "Delete Extension",
|
||||||
"extensionAlreadyLatest": "It's already the latest version"
|
"extensionAlreadyLatest": "It's already the latest version",
|
||||||
|
"extensionFind": "Find Extensions",
|
||||||
|
"extensionDevelop": "Develop Extensions",
|
||||||
|
"history": "History",
|
||||||
|
"clearHistory": "Clear History",
|
||||||
|
"noHistoryFound": "No History Found",
|
||||||
|
"serviceTitle": "Download Service",
|
||||||
|
"serviceText": "Running"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -12,14 +12,16 @@
|
|||||||
"donate": "Пожертвовать",
|
"donate": "Пожертвовать",
|
||||||
"exit": "Выход",
|
"exit": "Выход",
|
||||||
"create": "Создать задачу",
|
"create": "Создать задачу",
|
||||||
"downloadLink": "Ссылка для скачивание",
|
"advancedOptions": "Расширенные опции",
|
||||||
|
"downloadLink": "Ссылка на скачивание",
|
||||||
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
|
"downloadLinkValid": "Пожалуйста, введите ссылку для скачивания",
|
||||||
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append",
|
"downloadLinkHit": "Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append",
|
||||||
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
|
"downloadLinkHitDesktop": ", или перетащите сюда торрент-файл",
|
||||||
"download": "Скачать",
|
"download": "Скачать",
|
||||||
"noFileSelected": "Файл не выбран",
|
"noFileSelected": "Файл не выбран",
|
||||||
"noStoragePermission": "Требуется доступ к хранилищу",
|
"noStoragePermission": "Требуется доступ к хранилищу",
|
||||||
"selectFile": "Выберать файл",
|
"selectFile": "Выбрать файл",
|
||||||
|
"rename": "Переименовать",
|
||||||
"basic": "Основные параметры",
|
"basic": "Основные параметры",
|
||||||
"advanced": "Расширенные",
|
"advanced": "Расширенные",
|
||||||
"general": "Общие",
|
"general": "Общие",
|
||||||
@@ -57,5 +59,15 @@
|
|||||||
"delete": "Удалить",
|
"delete": "Удалить",
|
||||||
"newVersionTitle": "Обнаружена новая версия @version",
|
"newVersionTitle": "Обнаружена новая версия @version",
|
||||||
"newVersionUpdate": "Обновить",
|
"newVersionUpdate": "Обновить",
|
||||||
"newVersionLater": "позже"
|
"newVersionLater": "позже",
|
||||||
|
"extensions": "Расширения",
|
||||||
|
"extensionInstallSuccess": "Установка завершена",
|
||||||
|
"extensionUpdateSuccess": "Обновление завершено",
|
||||||
|
"extensionDelete": "Удалить расширение",
|
||||||
|
"extensionAlreadyLatest": "Это последняя версия",
|
||||||
|
"extensionFind": "Найти расширения",
|
||||||
|
"extensionDevelop": "Разработка расширений",
|
||||||
|
"history": "История",
|
||||||
|
"clearHistory": "Очистить историю",
|
||||||
|
"noHistoryFound": "История не найдена"
|
||||||
}
|
}
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
"donate": "打赏",
|
"donate": "打赏",
|
||||||
"exit": "退出",
|
"exit": "退出",
|
||||||
"create": "创建任务",
|
"create": "创建任务",
|
||||||
|
"directDownload": "直接下载",
|
||||||
"advancedOptions": "高级选项",
|
"advancedOptions": "高级选项",
|
||||||
"downloadLink": "下载链接",
|
"downloadLink": "下载链接",
|
||||||
"downloadLinkValid": "请输入下载链接",
|
"downloadLinkValid": "请输入下载链接",
|
||||||
@@ -65,5 +66,12 @@
|
|||||||
"extensionInstallSuccess": "安装成功",
|
"extensionInstallSuccess": "安装成功",
|
||||||
"extensionUpdateSuccess": "更新成功",
|
"extensionUpdateSuccess": "更新成功",
|
||||||
"extensionDelete": "删除扩展",
|
"extensionDelete": "删除扩展",
|
||||||
"extensionAlreadyLatest": "已经是最新版本"
|
"extensionAlreadyLatest": "已经是最新版本",
|
||||||
|
"extensionFind": "获取扩展",
|
||||||
|
"extensionDevelop": "开发扩展",
|
||||||
|
"history": "历史记录",
|
||||||
|
"clearHistory": "清空历史记录",
|
||||||
|
"noHistoryFound": "暂无历史记录",
|
||||||
|
"serviceTitle": "下载服务",
|
||||||
|
"serviceText": "运行中"
|
||||||
}
|
}
|
||||||
@@ -33,6 +33,15 @@ import Libgopeed
|
|||||||
})
|
})
|
||||||
|
|
||||||
GeneratedPluginRegistrant.register(with: self)
|
GeneratedPluginRegistrant.register(with: self)
|
||||||
|
|
||||||
|
SwiftFlutterForegroundTaskPlugin.setPluginRegistrantCallback(registerPlugins)
|
||||||
|
if #available(iOS 10.0, *) {
|
||||||
|
UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate
|
||||||
|
}
|
||||||
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func registerPlugins(registry: FlutterPluginRegistry) {
|
||||||
|
GeneratedPluginRegistrant.register(with: registry)
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
#import "GeneratedPluginRegistrant.h"
|
#import "GeneratedPluginRegistrant.h"
|
||||||
|
#import <flutter_foreground_task/FlutterForegroundTaskPlugin.h>
|
||||||
@@ -12,7 +12,6 @@ class Task {
|
|||||||
Meta meta;
|
Meta meta;
|
||||||
Status status;
|
Status status;
|
||||||
Progress progress;
|
Progress progress;
|
||||||
int size;
|
|
||||||
DateTime createdAt;
|
DateTime createdAt;
|
||||||
|
|
||||||
Task({
|
Task({
|
||||||
@@ -20,7 +19,6 @@ class Task {
|
|||||||
required this.meta,
|
required this.meta,
|
||||||
required this.status,
|
required this.status,
|
||||||
required this.progress,
|
required this.progress,
|
||||||
required this.size,
|
|
||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ Task _$TaskFromJson(Map<String, dynamic> json) => Task(
|
|||||||
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
meta: Meta.fromJson(json['meta'] as Map<String, dynamic>),
|
||||||
status: $enumDecode(_$StatusEnumMap, json['status']),
|
status: $enumDecode(_$StatusEnumMap, json['status']),
|
||||||
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
progress: Progress.fromJson(json['progress'] as Map<String, dynamic>),
|
||||||
size: json['size'] as int,
|
|
||||||
createdAt: DateTime.parse(json['createdAt'] as String),
|
createdAt: DateTime.parse(json['createdAt'] as String),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -20,7 +19,6 @@ Map<String, dynamic> _$TaskToJson(Task instance) => <String, dynamic>{
|
|||||||
'meta': instance.meta.toJson(),
|
'meta': instance.meta.toJson(),
|
||||||
'status': _$StatusEnumMap[instance.status]!,
|
'status': _$StatusEnumMap[instance.status]!,
|
||||||
'progress': instance.progress.toJson(),
|
'progress': instance.progress.toJson(),
|
||||||
'size': instance.size,
|
|
||||||
'createdAt': instance.createdAt.toIso8601String(),
|
'createdAt': instance.createdAt.toIso8601String(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:ui';
|
|||||||
|
|
||||||
import 'package:app_links/app_links.dart';
|
import 'package:app_links/app_links.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -79,21 +80,18 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
@override
|
@override
|
||||||
void onReady() {
|
void onReady() {
|
||||||
super.onReady();
|
super.onReady();
|
||||||
try {
|
|
||||||
_initDeepLinks();
|
_initDeepLinks().onError((error, stackTrace) =>
|
||||||
} catch (e) {
|
logger.w("initDeepLinks error", error, stackTrace));
|
||||||
logger.w("initDeepLinks error", e);
|
|
||||||
}
|
_initWindows().onError((error, stackTrace) =>
|
||||||
try {
|
logger.w("initWindows error", error, stackTrace));
|
||||||
_initWindows();
|
|
||||||
} catch (e) {
|
_initTray().onError(
|
||||||
logger.w("initWindows error", e);
|
(error, stackTrace) => logger.w("initTray error", error, stackTrace));
|
||||||
}
|
|
||||||
try {
|
_initForegroundTask().onError((error, stackTrace) =>
|
||||||
_initTray();
|
logger.w("initForegroundTask error", error, stackTrace));
|
||||||
} catch (e) {
|
|
||||||
logger.w("initTray error", e);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -202,11 +200,50 @@ class AppController extends GetxController with WindowListener, TrayListener {
|
|||||||
onClick: (menuItem) => {windowManager.destroy()},
|
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.setToolTip('Gopeed');
|
||||||
|
}
|
||||||
await trayManager.setContextMenu(menu);
|
await trayManager.setContextMenu(menu);
|
||||||
trayManager.addListener(this);
|
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 {
|
Future<void> _toCreate(Uri uri) async {
|
||||||
final path = uri.scheme == "magnet"
|
final path = uri.scheme == "magnet"
|
||||||
? uri.toString()
|
? uri.toString()
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ class CreateController extends GetxController
|
|||||||
final RxList fileInfos = [].obs;
|
final RxList fileInfos = [].obs;
|
||||||
final RxList openedFolders = [].obs;
|
final RxList openedFolders = [].obs;
|
||||||
final selectedIndexes = <int>[].obs;
|
final selectedIndexes = <int>[].obs;
|
||||||
final isResolving = false.obs;
|
final isConfirming = false.obs;
|
||||||
final showAdvanced = false.obs;
|
final showAdvanced = false.obs;
|
||||||
|
final directDownload = false.obs;
|
||||||
late TabController advancedTabController;
|
late TabController advancedTabController;
|
||||||
final oldUrl = "".obs;
|
final oldUrl = "".obs;
|
||||||
final fileDataUri = "".obs;
|
final fileDataUri = "".obs;
|
||||||
|
|||||||
@@ -4,10 +4,10 @@ import 'package:file_picker/file_picker.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
|
||||||
|
import 'package:gopeed/app/modules/history/views/history_view.dart';
|
||||||
import 'package:path/path.dart' as path;
|
import 'package:path/path.dart' as path;
|
||||||
|
|
||||||
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
import 'package:rounded_loading_button/rounded_loading_button.dart';
|
||||||
|
|
||||||
import '../../../../api/api.dart';
|
import '../../../../api/api.dart';
|
||||||
import '../../../../api/model/create_task.dart';
|
import '../../../../api/model/create_task.dart';
|
||||||
import '../../../../api/model/options.dart';
|
import '../../../../api/model/options.dart';
|
||||||
@@ -24,14 +24,18 @@ import '../../app/controllers/app_controller.dart';
|
|||||||
import '../controllers/create_controller.dart';
|
import '../controllers/create_controller.dart';
|
||||||
|
|
||||||
class CreateView extends GetView<CreateController> {
|
class CreateView extends GetView<CreateController> {
|
||||||
final _resolveFormKey = GlobalKey<FormState>();
|
final _confirmFormKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
final _urlController = TextEditingController();
|
final _urlController = TextEditingController();
|
||||||
|
final _renameController = TextEditingController();
|
||||||
|
final _connectionsController = TextEditingController();
|
||||||
|
final _pathController = TextEditingController();
|
||||||
final _confirmController = RoundedLoadingButtonController();
|
final _confirmController = RoundedLoadingButtonController();
|
||||||
final _httpUaController = TextEditingController();
|
final _httpUaController = TextEditingController();
|
||||||
final _httpCookieController = TextEditingController();
|
final _httpCookieController = TextEditingController();
|
||||||
final _httpRefererController = TextEditingController();
|
final _httpRefererController = TextEditingController();
|
||||||
final _btTrackerController = TextEditingController();
|
final _btTrackerController = TextEditingController();
|
||||||
|
final _historyController = HistoryController();
|
||||||
|
|
||||||
final _availableSchemes = ["http:", "https:", "magnet:"];
|
final _availableSchemes = ["http:", "https:", "magnet:"];
|
||||||
|
|
||||||
@@ -39,6 +43,17 @@ class CreateView extends GetView<CreateController> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final appController = Get.find<AppController>();
|
||||||
|
|
||||||
|
if (_connectionsController.text.isEmpty) {
|
||||||
|
_connectionsController.text = appController
|
||||||
|
.downloaderConfig.value.protocolConfig.http.connections
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
if (_pathController.text.isEmpty) {
|
||||||
|
_pathController.text = appController.downloaderConfig.value.downloadDir;
|
||||||
|
}
|
||||||
|
|
||||||
final String? filePath = Get.rootDelegate.arguments();
|
final String? filePath = Get.rootDelegate.arguments();
|
||||||
if (_urlController.text.isEmpty) {
|
if (_urlController.text.isEmpty) {
|
||||||
if (filePath?.isNotEmpty ?? false) {
|
if (filePath?.isNotEmpty ?? false) {
|
||||||
@@ -90,13 +105,15 @@ class CreateView extends GetView<CreateController> {
|
|||||||
onTap: () {
|
onTap: () {
|
||||||
FocusScope.of(context).requestFocus(FocusNode());
|
FocusScope.of(context).requestFocus(FocusNode());
|
||||||
},
|
},
|
||||||
|
child: SingleChildScrollView(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding:
|
padding:
|
||||||
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
const EdgeInsets.symmetric(vertical: 16.0, horizontal: 24.0),
|
||||||
child: Form(
|
child: Form(
|
||||||
key: _resolveFormKey,
|
key: _confirmFormKey,
|
||||||
autovalidateMode: AutovalidateMode.onUserInteraction,
|
autovalidateMode: AutovalidateMode.onUserInteraction,
|
||||||
child: Column(children: [
|
child: Column(
|
||||||
|
children: [
|
||||||
Row(children: [
|
Row(children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
@@ -145,14 +162,111 @@ class CreateView extends GetView<CreateController> {
|
|||||||
_urlController.text = pr.files[0].name;
|
_urlController.text = pr.files[0].name;
|
||||||
controller.setFileDataUri(pr.files[0].bytes!);
|
controller.setFileDataUri(pr.files[0].bytes!);
|
||||||
}
|
}
|
||||||
}),
|
},
|
||||||
]
|
|
||||||
//.where((e) => e != null).map((e) => e!).toList(),
|
|
||||||
),
|
),
|
||||||
Obx(() => Visibility(
|
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(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
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,
|
visible: controller.showAdvanced.value,
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(left: 40, top: 15),
|
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
TabBar(
|
TabBar(
|
||||||
@@ -202,23 +316,42 @@ class CreateView extends GetView<CreateController> {
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
],
|
],
|
||||||
|
).paddingOnly(top: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
))),
|
|
||||||
Center(
|
Center(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.only(top: 15),
|
padding: const EdgeInsets.only(top: 15),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Row(
|
||||||
padding: const EdgeInsets.only(right: 10),
|
mainAxisSize: MainAxisSize.min,
|
||||||
child: TextButton(
|
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: () {
|
onPressed: () {
|
||||||
controller.showAdvanced.value =
|
controller.showAdvanced.value =
|
||||||
!controller.showAdvanced.value;
|
!controller.showAdvanced.value;
|
||||||
},
|
},
|
||||||
child: Row(
|
child: Row(children: [
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
Obx(() => Checkbox(
|
Obx(() => Checkbox(
|
||||||
value: controller.showAdvanced.value,
|
value: controller.showAdvanced.value,
|
||||||
onChanged: (bool? value) {
|
onChanged: (bool? value) {
|
||||||
@@ -229,20 +362,26 @@ class CreateView extends GetView<CreateController> {
|
|||||||
Text('advancedOptions'.tr),
|
Text('advancedOptions'.tr),
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 150,
|
width: 150,
|
||||||
child: RoundedLoadingButton(
|
child: RoundedLoadingButton(
|
||||||
color: Get.theme.colorScheme.secondary,
|
color: Get.theme.colorScheme.secondary,
|
||||||
onPressed: _doResolve,
|
onPressed: _doConfirm,
|
||||||
controller: _confirmController,
|
controller: _confirmController,
|
||||||
child: Text('confirm'.tr),
|
child: Text('confirm'.tr),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
)),
|
),
|
||||||
]))),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -262,43 +401,54 @@ class CreateView extends GetView<CreateController> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _doResolve() async {
|
Future<void> _doConfirm() async {
|
||||||
if (controller.isResolving.value) {
|
if (controller.isConfirming.value) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
controller.isResolving.value = true;
|
controller.isConfirming.value = true;
|
||||||
try {
|
try {
|
||||||
_confirmController.start();
|
_confirmController.start();
|
||||||
if (_resolveFormKey.currentState!.validate()) {
|
if (_confirmFormKey.currentState!.validate()) {
|
||||||
// check if is multi line urls
|
|
||||||
final 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()));
|
|
||||||
} else {
|
|
||||||
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty
|
||||||
? controller.fileDataUri.value
|
? controller.fileDataUri.value
|
||||||
: _urlController.text;
|
: _urlController.text;
|
||||||
rr = await resolve(Request(
|
|
||||||
|
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 rr = await resolve(Request(
|
||||||
url: submitUrl,
|
url: submitUrl,
|
||||||
extra: parseReqExtra(_urlController.text),
|
extra: parseReqExtra(_urlController.text),
|
||||||
));
|
));
|
||||||
|
await _showResolveDialog(rr);
|
||||||
}
|
}
|
||||||
await _showResolveDialog(rr, isMuiltiLine);
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
showErrorMessage(e);
|
showErrorMessage(e);
|
||||||
} finally {
|
} finally {
|
||||||
_confirmController.reset();
|
_confirmController.reset();
|
||||||
controller.isResolving.value = false;
|
controller.isConfirming.value = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -321,6 +471,13 @@ class CreateView extends GetView<CreateController> {
|
|||||||
return reqExtra;
|
return reqExtra;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object? parseReqOpts() {
|
||||||
|
return _connectionsController.text.isEmpty
|
||||||
|
? null
|
||||||
|
: (OptsExtraHttp()
|
||||||
|
..connections = int.parse(_connectionsController.text));
|
||||||
|
}
|
||||||
|
|
||||||
String _hitText() {
|
String _hitText() {
|
||||||
return 'downloadLinkHit'.trParams({
|
return 'downloadLinkHit'.trParams({
|
||||||
'append':
|
'append':
|
||||||
@@ -328,14 +485,8 @@ class CreateView extends GetView<CreateController> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _showResolveDialog(ResolveResult rr, bool isMuiltiLine) async {
|
Future<void> _showResolveDialog(ResolveResult rr) async {
|
||||||
final appController = Get.find<AppController>();
|
|
||||||
|
|
||||||
final createFormKey = GlobalKey<FormState>();
|
final createFormKey = GlobalKey<FormState>();
|
||||||
final nameController = TextEditingController();
|
|
||||||
final connectionsController = TextEditingController();
|
|
||||||
final pathController = TextEditingController(
|
|
||||||
text: appController.downloaderConfig.value.downloadDir);
|
|
||||||
final downloadController = RoundedLoadingButtonController();
|
final downloadController = RoundedLoadingButtonController();
|
||||||
return showDialog<void>(
|
return showDialog<void>(
|
||||||
context: Get.context!,
|
context: Get.context!,
|
||||||
@@ -354,31 +505,7 @@ class CreateView extends GetView<CreateController> {
|
|||||||
child: Form(
|
child: Form(
|
||||||
key: createFormKey,
|
key: createFormKey,
|
||||||
autovalidateMode: AutovalidateMode.always,
|
autovalidateMode: AutovalidateMode.always,
|
||||||
child: Column(
|
child: FileListView(files: rr.res.files)),
|
||||||
children: [
|
|
||||||
Expanded(child: FileListView(files: rr.res.files)),
|
|
||||||
TextFormField(
|
|
||||||
controller: nameController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'rename'.tr,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
TextFormField(
|
|
||||||
controller: connectionsController,
|
|
||||||
decoration: InputDecoration(
|
|
||||||
labelText: 'connections'.tr,
|
|
||||||
),
|
|
||||||
keyboardType: TextInputType.number,
|
|
||||||
inputFormatters: [
|
|
||||||
FilteringTextInputFormatter.digitsOnly,
|
|
||||||
NumericalRangeFormatter(min: 1, max: 256),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
DirectorySelector(
|
|
||||||
controller: pathController,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)),
|
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -414,25 +541,19 @@ class CreateView extends GetView<CreateController> {
|
|||||||
showMessage('tip'.tr, 'noFileSelected'.tr);
|
showMessage('tip'.tr, 'noFileSelected'.tr);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Object? optExtra = connectionsController.text.isEmpty
|
final optExtra = parseReqOpts();
|
||||||
? null
|
|
||||||
: (OptsExtraHttp()
|
|
||||||
..connections =
|
|
||||||
int.parse(connectionsController.text));
|
|
||||||
if (createFormKey.currentState!.validate()) {
|
if (createFormKey.currentState!.validate()) {
|
||||||
if (rr.id.isEmpty) {
|
if (rr.id.isEmpty) {
|
||||||
// create task batch, there has two ways to batch create task
|
// from extension resolve result
|
||||||
// 1. from multi line urls
|
|
||||||
// 2. from extension resolve result
|
|
||||||
await Future.wait(
|
await Future.wait(
|
||||||
controller.selectedIndexes.map((index) {
|
controller.selectedIndexes.map((index) {
|
||||||
final file = rr.res.files[index];
|
final file = rr.res.files[index];
|
||||||
return createTask(CreateTask(
|
return createTask(CreateTask(
|
||||||
req: file.req!,
|
req: file.req!,
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: isMuiltiLine ? "" : file.name,
|
name: file.name,
|
||||||
path: path.join(
|
path: path.join(
|
||||||
pathController.text, rr.res.name),
|
_pathController.text, rr.res.name),
|
||||||
selectFiles: [],
|
selectFiles: [],
|
||||||
extra: optExtra)));
|
extra: optExtra)));
|
||||||
}));
|
}));
|
||||||
@@ -440,8 +561,8 @@ class CreateView extends GetView<CreateController> {
|
|||||||
await createTask(CreateTask(
|
await createTask(CreateTask(
|
||||||
rid: rr.id,
|
rid: rr.id,
|
||||||
opt: Options(
|
opt: Options(
|
||||||
name: nameController.text,
|
name: _renameController.text,
|
||||||
path: pathController.text,
|
path: _pathController.text,
|
||||||
selectFiles: controller.selectedIndexes,
|
selectFiles: controller.selectedIndexes,
|
||||||
extra: optExtra)));
|
extra: optExtra)));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import '../../../../api/model/install_extension.dart';
|
|||||||
import '../../../../api/model/switch_extension.dart';
|
import '../../../../api/model/switch_extension.dart';
|
||||||
import '../../../../util/util.dart';
|
import '../../../../util/util.dart';
|
||||||
import '../../../views/icon_button_loading.dart';
|
import '../../../views/icon_button_loading.dart';
|
||||||
|
import '../../../views/responsive_builder.dart';
|
||||||
import '../controllers/extension_controller.dart';
|
import '../controllers/extension_controller.dart';
|
||||||
|
|
||||||
class ExtensionView extends GetView<ExtensionController> {
|
class ExtensionView extends GetView<ExtensionController> {
|
||||||
@@ -35,7 +36,31 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
body: Column(
|
body: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const SizedBox(height: 20),
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse(
|
||||||
|
'https://github.com/search?q=topic%3Agopeed-extension&type=repositories'),
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.search),
|
||||||
|
label: Text('extensionFind'.tr),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
TextButton.icon(
|
||||||
|
onPressed: () {
|
||||||
|
launchUrl(
|
||||||
|
Uri.parse('https://docs.gopeed.com/dev-extension.html'),
|
||||||
|
mode: LaunchMode.externalApplication);
|
||||||
|
},
|
||||||
|
icon: const Icon(Icons.edit),
|
||||||
|
label: Text('extensionDevelop'.tr),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
Obx(() => Row(
|
Obx(() => Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -89,14 +114,14 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
: Container()
|
: Container()
|
||||||
],
|
],
|
||||||
)),
|
)),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Obx(() => ListView.builder(
|
child: Obx(() => ListView.builder(
|
||||||
itemCount: controller.extensions.length,
|
itemCount: controller.extensions.length,
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final extension = controller.extensions[index];
|
final extension = controller.extensions[index];
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 112,
|
height: ResponsiveBuilder.isNarrow(context) ? 152 : 112,
|
||||||
child: Card(
|
child: Card(
|
||||||
elevation: 4.0,
|
elevation: 4.0,
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -145,23 +170,35 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
title: Row(
|
title: Row(
|
||||||
children: () {
|
children: () {
|
||||||
final list = [
|
final list = [
|
||||||
Text(extension.title),
|
ResponsiveBuilder.isNarrow(context)
|
||||||
const SizedBox(width: 8),
|
? Expanded(
|
||||||
Chip(
|
child: Text(
|
||||||
label: Text('v${extension.version}'),
|
extension.title,
|
||||||
|
overflow:
|
||||||
|
TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
: Text(extension.title),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
buildChip('v${extension.version}'),
|
||||||
];
|
];
|
||||||
if (extension.devMode) {
|
if (extension.devMode) {
|
||||||
list.add(const SizedBox(width: 8));
|
list.add(const SizedBox(width: 8));
|
||||||
list.add(Chip(
|
list.add(buildChip(
|
||||||
label: Text('dev'.tr),
|
'dev',
|
||||||
backgroundColor: Colors.redAccent,
|
bgColor: Colors.blue.shade300,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}(),
|
}(),
|
||||||
),
|
),
|
||||||
subtitle: Text(extension.description),
|
subtitle: ResponsiveBuilder.isNarrow(context)
|
||||||
|
? Text(
|
||||||
|
extension.description,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
)
|
||||||
|
: Text(extension.description),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
@@ -233,6 +270,14 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget buildChip(String text, {Color? bgColor}) {
|
||||||
|
return Chip(
|
||||||
|
padding: const EdgeInsets.all(0),
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
label: Text(text, style: const TextStyle(fontSize: 12)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _showSettingDialog(Extension extension) async {
|
Future<void> _showSettingDialog(Extension extension) async {
|
||||||
final formKey = GlobalKey<FormBuilderState>();
|
final formKey = GlobalKey<FormBuilderState>();
|
||||||
final confrimController = RoundedLoadingButtonController();
|
final confrimController = RoundedLoadingButtonController();
|
||||||
@@ -256,9 +301,32 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: extension.settings!
|
children: extension.settings!.map((e) {
|
||||||
.map((e) => _buildSettingItem(e))
|
final settingItem = _buildSettingItem(e);
|
||||||
.toList()),
|
|
||||||
|
return Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 20,
|
||||||
|
child: e.description.isEmpty
|
||||||
|
? null
|
||||||
|
: Tooltip(
|
||||||
|
message: e.description,
|
||||||
|
child: const CircleAvatar(
|
||||||
|
radius: 10,
|
||||||
|
backgroundColor:
|
||||||
|
Colors.grey,
|
||||||
|
child: Icon(
|
||||||
|
Icons.question_mark,
|
||||||
|
size: 10,
|
||||||
|
)),
|
||||||
|
))
|
||||||
|
.paddingOnly(right: 10),
|
||||||
|
Expanded(child: settingItem),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}).toList()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
]),
|
]),
|
||||||
@@ -324,8 +392,7 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
FormFieldValidator<String>? validator, TextInputType? keyBoardType) {
|
FormFieldValidator<String>? validator, TextInputType? keyBoardType) {
|
||||||
return FormBuilderTextField(
|
return FormBuilderTextField(
|
||||||
name: setting.name,
|
name: setting.name,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(labelText: setting.title),
|
||||||
labelText: setting.title, hintText: setting.description),
|
|
||||||
initialValue: setting.value?.toString(),
|
initialValue: setting.value?.toString(),
|
||||||
inputFormatters: inputFormatter != null ? [inputFormatter] : null,
|
inputFormatters: inputFormatter != null ? [inputFormatter] : null,
|
||||||
keyboardType: keyBoardType,
|
keyboardType: keyBoardType,
|
||||||
@@ -340,7 +407,8 @@ class ExtensionView extends GetView<ExtensionController> {
|
|||||||
return FormBuilderDropdown<String>(
|
return FormBuilderDropdown<String>(
|
||||||
name: setting.name,
|
name: setting.name,
|
||||||
decoration: InputDecoration(
|
decoration: InputDecoration(
|
||||||
labelText: setting.title, hintText: setting.description),
|
labelText: setting.title,
|
||||||
|
),
|
||||||
initialValue: setting.value?.toString(),
|
initialValue: setting.value?.toString(),
|
||||||
validator: FormBuilderValidators.compose([
|
validator: FormBuilderValidators.compose([
|
||||||
requiredValidator,
|
requiredValidator,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
|
class HistoryController {
|
||||||
|
// Get History in List<String>
|
||||||
|
Future<List<String>> getAllHistory() async {
|
||||||
|
SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||||
|
List<String>? historyList = preferences.getStringList('history');
|
||||||
|
if(historyList == null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return historyList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get Existing History & Add New History
|
||||||
|
void addHistory(String historyText) async {
|
||||||
|
SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||||
|
List<String> existingHistoryData = await getAllHistory();
|
||||||
|
existingHistoryData.add(historyText);
|
||||||
|
preferences.setStringList('history', existingHistoryData);
|
||||||
|
}
|
||||||
|
|
||||||
|
void clearHistory() async {
|
||||||
|
SharedPreferences preferences = await SharedPreferences.getInstance();
|
||||||
|
preferences.remove('history');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:get/get.dart';
|
||||||
|
import 'package:gopeed/app/modules/history/controller/history_controller.dart';
|
||||||
|
|
||||||
|
class HistoryView extends StatefulWidget {
|
||||||
|
const HistoryView({
|
||||||
|
super.key,
|
||||||
|
required this.isHistoryListEmpty,
|
||||||
|
required this.historyList,
|
||||||
|
});
|
||||||
|
|
||||||
|
final bool isHistoryListEmpty;
|
||||||
|
final Widget historyList;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HistoryView> createState() => _HistoryViewState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HistoryViewState extends State<HistoryView> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final Size size = MediaQuery.sizeOf(context);
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
body: Dialog(
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||||
|
elevation: 0,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
child: Container(
|
||||||
|
width: size.width * 0.8,
|
||||||
|
height: size.height * 0.8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Theme.of(context).colorScheme.surface,
|
||||||
|
borderRadius: BorderRadius.circular(10.0),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
|
children: <Widget>[
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
|
children: <Widget>[
|
||||||
|
Icon(
|
||||||
|
Icons.history_rounded,
|
||||||
|
size: Theme.of(context)
|
||||||
|
.textTheme
|
||||||
|
.headlineSmall
|
||||||
|
?.fontSize,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8.0),
|
||||||
|
Text(
|
||||||
|
'history'.tr,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: <Widget>[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {
|
||||||
|
HistoryController _controller = HistoryController();
|
||||||
|
_controller.clearHistory();
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
tooltip: "clearHistory".tr,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.history_toggle_off_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close_rounded,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: widget.isHistoryListEmpty
|
||||||
|
? <Widget>[
|
||||||
|
const Icon(
|
||||||
|
Icons.manage_history_rounded,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10.0),
|
||||||
|
Text(
|
||||||
|
'noHistoryFound'.tr,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
: <Widget>[
|
||||||
|
Expanded(child: widget.historyList),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
|
|
||||||
import '../../../routes/app_pages.dart';
|
import '../../../routes/app_pages.dart';
|
||||||
@@ -23,7 +24,8 @@ class HomeView extends GetView<HomeController> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Scaffold(
|
return WithForegroundTask(
|
||||||
|
child: Scaffold(
|
||||||
// extendBody: true,
|
// extendBody: true,
|
||||||
body: Row(
|
body: Row(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
// crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
@@ -156,6 +158,7 @@ class HomeView extends GetView<HomeController> {
|
|||||||
// },
|
// },
|
||||||
// )
|
// )
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ class FileItem {
|
|||||||
FileItem(this.isDirectory, this.path, this.name, this.size);
|
FileItem(this.isDirectory, this.path, this.name, this.size);
|
||||||
|
|
||||||
String get fullPath => "${path == "/" ? path : "$path/"}$name";
|
String get fullPath => "${path == "/" ? path : "$path/"}$name";
|
||||||
|
|
||||||
|
String filePath(String optName) {
|
||||||
|
return optName.isEmpty
|
||||||
|
? fullPath
|
||||||
|
: "${path == "/" ? path : "$path/"}$optName";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class TaskFilesController extends GetxController {
|
class TaskFilesController extends GetxController {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:get/get.dart';
|
import 'package:get/get.dart';
|
||||||
import 'package:open_file/open_file.dart';
|
import 'package:open_file/open_file.dart';
|
||||||
|
import 'package:path/path.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
import '../../../../util/file_icon.dart';
|
import '../../../../util/file_icon.dart';
|
||||||
@@ -52,13 +53,17 @@ class TaskFilesView extends GetView<TaskFilesController> {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final meta = controller.task.value!.meta;
|
final meta = controller.task.value!.meta;
|
||||||
final file = fileList[index];
|
final file = fileList[index];
|
||||||
final filePath = Util.safePathJoin(
|
final filePath = Util.safePathJoin([
|
||||||
[meta.opts.path, meta.res!.name, file.fullPath]);
|
meta.opts.path,
|
||||||
|
meta.res!.name,
|
||||||
|
file.filePath(meta.opts.name)
|
||||||
|
]);
|
||||||
|
final fileName = basename(filePath);
|
||||||
return ListTile(
|
return ListTile(
|
||||||
leading: file.isDirectory
|
leading: file.isDirectory
|
||||||
? const Icon(Icons.folder)
|
? const Icon(Icons.folder)
|
||||||
: Icon(FaIcons.allIcons[findIcon(file.name)]),
|
: Icon(FaIcons.allIcons[findIcon(file.name)]),
|
||||||
title: Text(fileList[index].name),
|
title: Text(fileName),
|
||||||
subtitle: file.isDirectory
|
subtitle: file.isDirectory
|
||||||
? Text('items'.trParams({
|
? Text('items'.trParams({
|
||||||
'count': controller
|
'count': controller
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class LocaleKeys {
|
|||||||
static const donate = 'donate';
|
static const donate = 'donate';
|
||||||
static const exit = 'exit';
|
static const exit = 'exit';
|
||||||
static const create = 'create';
|
static const create = 'create';
|
||||||
|
static const directDownload = 'directDownload';
|
||||||
static const advancedOptions = 'advancedOptions';
|
static const advancedOptions = 'advancedOptions';
|
||||||
static const downloadLink = 'downloadLink';
|
static const downloadLink = 'downloadLink';
|
||||||
static const downloadLinkValid = 'downloadLinkValid';
|
static const downloadLinkValid = 'downloadLinkValid';
|
||||||
@@ -82,6 +83,13 @@ class LocaleKeys {
|
|||||||
static const extensionUpdateSuccess = 'extensionUpdateSuccess';
|
static const extensionUpdateSuccess = 'extensionUpdateSuccess';
|
||||||
static const extensionDelete = 'extensionDelete';
|
static const extensionDelete = 'extensionDelete';
|
||||||
static const extensionAlreadyLatest = 'extensionAlreadyLatest';
|
static const extensionAlreadyLatest = 'extensionAlreadyLatest';
|
||||||
|
static const extensionFind = 'extensionFind';
|
||||||
|
static const extensionDevelop = 'extensionDevelop';
|
||||||
|
static const history = 'history';
|
||||||
|
static const clearHistory = 'clearHistory';
|
||||||
|
static const noHistoryFound = 'noHistoryFound';
|
||||||
|
static const serviceTitle = 'serviceTitle';
|
||||||
|
static const serviceText = 'serviceText';
|
||||||
}
|
}
|
||||||
|
|
||||||
class Locales {
|
class Locales {
|
||||||
@@ -92,13 +100,14 @@ class Locales {
|
|||||||
'confirm': 'Confirm',
|
'confirm': 'Confirm',
|
||||||
'cancel': 'Cancel',
|
'cancel': 'Cancel',
|
||||||
'selectAll': 'Select All',
|
'selectAll': 'Select All',
|
||||||
'task': 'Task',
|
'task': 'Tasks',
|
||||||
'downloading': 'downloading',
|
'downloading': 'downloading',
|
||||||
'downloaded': 'downloaded',
|
'downloaded': 'downloaded',
|
||||||
'setting': 'Setting',
|
'setting': 'Settings',
|
||||||
'donate': 'Donate',
|
'donate': 'Donate',
|
||||||
'exit': 'Exit',
|
'exit': 'Exit',
|
||||||
'create': 'Create Task',
|
'create': 'Create Task',
|
||||||
|
'directDownload': 'Direct Download',
|
||||||
'advancedOptions': 'Advanced Options',
|
'advancedOptions': 'Advanced Options',
|
||||||
'downloadLink': 'Download Link',
|
'downloadLink': 'Download Link',
|
||||||
'downloadLinkValid': 'Please enter the download link',
|
'downloadLinkValid': 'Please enter the download link',
|
||||||
@@ -155,6 +164,13 @@ class Locales {
|
|||||||
'extensionUpdateSuccess': 'Updated successfully',
|
'extensionUpdateSuccess': 'Updated successfully',
|
||||||
'extensionDelete': 'Delete Extension',
|
'extensionDelete': 'Delete Extension',
|
||||||
'extensionAlreadyLatest': 'It\'s already the latest version',
|
'extensionAlreadyLatest': 'It\'s already the latest version',
|
||||||
|
'extensionFind': 'Find Extensions',
|
||||||
|
'extensionDevelop': 'Develop Extensions',
|
||||||
|
'history': 'History',
|
||||||
|
'clearHistory': 'Clear History',
|
||||||
|
'noHistoryFound': 'No History Found',
|
||||||
|
'serviceTitle': 'Download Service',
|
||||||
|
'serviceText': 'Running',
|
||||||
};
|
};
|
||||||
static const fa_IR = {
|
static const fa_IR = {
|
||||||
'label': 'فارسی',
|
'label': 'فارسی',
|
||||||
@@ -290,15 +306,17 @@ class Locales {
|
|||||||
'donate': 'Пожертвовать',
|
'donate': 'Пожертвовать',
|
||||||
'exit': 'Выход',
|
'exit': 'Выход',
|
||||||
'create': 'Создать задачу',
|
'create': 'Создать задачу',
|
||||||
'downloadLink': 'Ссылка для скачивание',
|
'advancedOptions': 'Расширенные опции',
|
||||||
|
'downloadLink': 'Ссылка на скачивание',
|
||||||
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
'downloadLinkValid': 'Пожалуйста, введите ссылку для скачивания',
|
||||||
'downloadLinkHit':
|
'downloadLinkHit':
|
||||||
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET supported@append',
|
'Пожалуйста, введите ссылку для скачивания, HTTP/HTTPS/MAGNET поддерживаются@append',
|
||||||
'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл',
|
'downloadLinkHitDesktop': ', или перетащите сюда торрент-файл',
|
||||||
'download': 'Скачать',
|
'download': 'Скачать',
|
||||||
'noFileSelected': 'Файл не выбран',
|
'noFileSelected': 'Файл не выбран',
|
||||||
'noStoragePermission': 'Требуется доступ к хранилищу',
|
'noStoragePermission': 'Требуется доступ к хранилищу',
|
||||||
'selectFile': 'Выберать файл',
|
'selectFile': 'Выбрать файл',
|
||||||
|
'rename': 'Переименовать',
|
||||||
'basic': 'Основные параметры',
|
'basic': 'Основные параметры',
|
||||||
'advanced': 'Расширенные',
|
'advanced': 'Расширенные',
|
||||||
'general': 'Общие',
|
'general': 'Общие',
|
||||||
@@ -338,6 +356,16 @@ class Locales {
|
|||||||
'newVersionTitle': 'Обнаружена новая версия @version',
|
'newVersionTitle': 'Обнаружена новая версия @version',
|
||||||
'newVersionUpdate': 'Обновить',
|
'newVersionUpdate': 'Обновить',
|
||||||
'newVersionLater': 'позже',
|
'newVersionLater': 'позже',
|
||||||
|
'extensions': 'Расширения',
|
||||||
|
'extensionInstallSuccess': 'Установка завершена',
|
||||||
|
'extensionUpdateSuccess': 'Обновление завершено',
|
||||||
|
'extensionDelete': 'Удалить расширение',
|
||||||
|
'extensionAlreadyLatest': 'Это последняя версия',
|
||||||
|
'extensionFind': 'Найти расширения',
|
||||||
|
'extensionDevelop': 'Разработка расширений',
|
||||||
|
'history': 'История',
|
||||||
|
'clearHistory': 'Очистить историю',
|
||||||
|
'noHistoryFound': 'История не найдена',
|
||||||
};
|
};
|
||||||
static const zh_CN = {
|
static const zh_CN = {
|
||||||
'label': '中文(简体)',
|
'label': '中文(简体)',
|
||||||
@@ -353,6 +381,7 @@ class Locales {
|
|||||||
'donate': '打赏',
|
'donate': '打赏',
|
||||||
'exit': '退出',
|
'exit': '退出',
|
||||||
'create': '创建任务',
|
'create': '创建任务',
|
||||||
|
'directDownload': '直接下载',
|
||||||
'advancedOptions': '高级选项',
|
'advancedOptions': '高级选项',
|
||||||
'downloadLink': '下载链接',
|
'downloadLink': '下载链接',
|
||||||
'downloadLinkValid': '请输入下载链接',
|
'downloadLinkValid': '请输入下载链接',
|
||||||
@@ -407,6 +436,13 @@ class Locales {
|
|||||||
'extensionUpdateSuccess': '更新成功',
|
'extensionUpdateSuccess': '更新成功',
|
||||||
'extensionDelete': '删除扩展',
|
'extensionDelete': '删除扩展',
|
||||||
'extensionAlreadyLatest': '已经是最新版本',
|
'extensionAlreadyLatest': '已经是最新版本',
|
||||||
|
'extensionFind': '获取扩展',
|
||||||
|
'extensionDevelop': '开发扩展',
|
||||||
|
'history': '历史记录',
|
||||||
|
'clearHistory': '清空历史记录',
|
||||||
|
'noHistoryFound': '暂无历史记录',
|
||||||
|
'serviceTitle': '下载服务',
|
||||||
|
'serviceText': '运行中',
|
||||||
};
|
};
|
||||||
static const zh_TW = {
|
static const zh_TW = {
|
||||||
'label': '中文 (正體) ',
|
'label': '中文 (正體) ',
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ class Util {
|
|||||||
|
|
||||||
static String safePathJoin(List<String> paths) {
|
static String safePathJoin(List<String> paths) {
|
||||||
return paths
|
return paths
|
||||||
|
.where((e) => e.isNotEmpty)
|
||||||
.map((e) => safeDir(e))
|
.map((e) => safeDir(e))
|
||||||
.join("/")
|
.join("/")
|
||||||
.replaceAll(RegExp(r'//'), "/");
|
.replaceAll(RegExp(r'//'), "/");
|
||||||
@@ -93,6 +94,10 @@ class Util {
|
|||||||
return !kIsWeb && Platform.isMacOS;
|
return !kIsWeb && Platform.isMacOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static isLinux() {
|
||||||
|
return !kIsWeb && Platform.isLinux;
|
||||||
|
}
|
||||||
|
|
||||||
static isWeb() {
|
static isWeb() {
|
||||||
return kIsWeb;
|
return kIsWeb;
|
||||||
}
|
}
|
||||||
|
|||||||
+122
-121
File diff suppressed because it is too large.
Load diff
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
|||||||
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
|
||||||
# Read more about iOS versioning at
|
# Read more about iOS versioning at
|
||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
version: 1.4.2+1
|
version: 1.4.4+1
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.0.0 <4.0.0'
|
sdk: '>=3.0.0 <4.0.0'
|
||||||
@@ -41,7 +41,7 @@ dependencies:
|
|||||||
context_menus: ^1.0.2
|
context_menus: ^1.0.2
|
||||||
json_annotation: ^4.8.1
|
json_annotation: ^4.8.1
|
||||||
dio: ^5.2.1
|
dio: ^5.2.1
|
||||||
path_provider: ^2.0.11
|
path_provider: ^2.1.1
|
||||||
file_picker: 5.3.1
|
file_picker: 5.3.1
|
||||||
macos_secure_bookmarks: ^0.2.1
|
macos_secure_bookmarks: ^0.2.1
|
||||||
rounded_loading_button: ^2.1.0
|
rounded_loading_button: ^2.1.0
|
||||||
@@ -56,12 +56,16 @@ dependencies:
|
|||||||
app_links: ^3.4.2
|
app_links: ^3.4.2
|
||||||
uri_to_file: ^0.2.0
|
uri_to_file: ^0.2.0
|
||||||
window_manager: ^0.3.4
|
window_manager: ^0.3.4
|
||||||
tray_manager: ^0.2.0
|
tray_manager:
|
||||||
|
git:
|
||||||
|
url: https://github.com/provokateurin/tray_manager.git
|
||||||
|
ref: fix/libayatana-set-icon-deprecation
|
||||||
autoscale_tabbarview: ^1.0.2
|
autoscale_tabbarview: ^1.0.2
|
||||||
share_plus: ^7.1.0
|
share_plus: ^7.1.0
|
||||||
open_file: ^3.3.2
|
open_file: ^3.3.2
|
||||||
flutter_form_builder: ^9.1.1
|
flutter_form_builder: ^9.1.1
|
||||||
form_builder_validators: ^9.0.0
|
form_builder_validators: ^9.0.0
|
||||||
|
flutter_foreground_task: ^6.1.2
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
|||||||
Reference in new issue
Block a user