Compare commits

...
20 Commits
Author SHA1 Message Date
liwei 195dfac74b test 2024-01-19 14:31:03 +08:00
liwei 1c1fab2002 test 2024-01-19 11:54:36 +08:00
madoka773 abccbabc78 fix: wrong debian installed size (#335) 2024-01-19 11:17:50 +08:00
Levi c26eb26b8f fix: default download path on mobile (#337) 2024-01-19 11:12:27 +08:00
Levi 74beea6044 feat: auto create torrent task for http download (#334) 2024-01-18 18:38:44 +08:00
Levi e548d7e0c9 feat: save window size and resize on startup (#332) 2024-01-18 15:00:05 +08:00
madoka773 a996d57914 ci: fix debian 12 depends (#329) 2024-01-18 11:16:50 +08:00
Levi f7dc60ad50 fix: proxy setting only numeric input (#328) 2024-01-18 09:17:06 +08:00
Levi 8425a96ac9 feat: add mirror.ghproxy.com trackers subscribe cdn (#330) 2024-01-17 22:09:37 +08:00
Tracey Lesley 99f4e8c895 feat: add download stats info (#323) 2024-01-17 21:59:01 +08:00
Elliot cc0f61b527 feat: improve zh-TW translation (#327) 2024-01-17 18:16:40 +08:00
Levi 922ef969a6 feat: add thanks item for contributors (#325) 2024-01-16 19:02:20 +08:00
Tracey Lesley 3a542054fc fix: compatible with not follow the http range response standard 2024-01-14 09:58:14 +08:00
Levi 895a8d2ee4 fix: foreground task scope (#314) 2024-01-05 21:49:21 +08:00
madoka773 29d0f5ebcf ci: use flutter distributor (#313) 2024-01-04 12:46:16 +08:00
Levi 31228e3edf release: v1.5.3 2023-12-15 19:23:16 +08:00
Levi 31a32d7e14 fix: handle api panic (#302) 2023-12-15 19:20:56 +08:00
Levi 65fd6fb489 fix: http pause handle (#303) 2023-12-15 19:15:38 +08:00
Levi 953b878a89 fix: adapt mobile proxy settings ui (#301) 2023-12-15 19:09:48 +08:00
Elliot ae4ca3da58 feat: improve zh-TW translation (#300) 2023-12-14 18:06:49 +08:00
34 changed files with 579 additions and 409 deletions

No files matched your search

+16 -110
View File
@@ -223,126 +223,32 @@ jobs:
with: with:
flutter-version: ${{ env.FLUTTER_VERSION }} flutter-version: ${{ env.FLUTTER_VERSION }}
- run: | - run: |
sudo apt-get update -y sudo apt update -y
sudo apt-get install -y ninja-build libgtk-3-dev libappindicator3-dev # flutter build dependencies
sudo apt install -y ninja-build libgtk-3-dev libappindicator3-dev
# rpm build dependencies
sudo apt install -y rpm patchelf
# appimage build dependencies
sudo apt install -y libfuse2 locate
wget -O appimagetool "https://github.com/AppImage/AppImageKit/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x appimagetool
sudo mv appimagetool /usr/local/bin/
- name: Build - name: Build
env: env:
VERSION: ${{ needs.get-release.outputs.tag_name }} VERSION: ${{ needs.get-release.outputs.tag_name }}
run: | run: |
go build -tags nosqlite -ldflags="-w -s" -buildmode=c-shared -o ui/flutter/linux/bundle/lib/libgopeed.so github.com/GopeedLab/gopeed/bind/desktop 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 dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
mkdir -p build/dist flutter_distributor package --platform linux --targets appimage,deb
cd dist/*
# Build portable mv gopeed-*-linux.AppImage Gopeed-${VERSION}-linux-x86_64.AppImage
tar -C build/linux/x64/release/bundle -czaf build/dist/Gopeed-$VERSION-linux-amd64-portable.tar.gz ./ mv gopeed-*-linux.deb Gopeed-${VERSION}-linux-amd64.deb
# Build deb package
mkdir -p debian/gopeed/opt/gopeed
mkdir -p debian/gopeed/usr/share/applications
mkdir -p debian/gopeed/usr/share/icons/hicolor/scalable/apps
mkdir -p debian/gopeed/DEBIAN
cp -r build/linux/x64/release/bundle/* debian/gopeed/opt/gopeed/
cp assets/icon/icon.svg debian/gopeed/usr/share/icons/hicolor/scalable/apps/com.gopeed.Gopeed.svg
cat>debian/gopeed/DEBIAN/postinst<<EOF
#!/bin/bash
ln -sf /opt/gopeed/gopeed /usr/bin/gopeed
EOF
cat>debian/gopeed/DEBIAN/postrm<<EOF
#!/bin/bash
rm -rf /usr/bin/gopeed
EOF
chmod 0755 debian/gopeed/DEBIAN/postinst
chmod 0755 debian/gopeed/DEBIAN/postrm
cat>debian/gopeed/DEBIAN/control<<EOF
Package: gopeed
Version: ${VERSION:1}
Architecture: amd64
Essential: no
Priority: optional
Depends:
Maintainer: monkeyWie
Description: High speed downloader that supports all platforms.
EOF
cp linux/assets/com.gopeed.Gopeed.desktop debian/gopeed/usr/share/applications/gopeed.desktop
dpkg-deb --build debian/gopeed
mv debian/gopeed.deb build/dist/Gopeed-$VERSION-linux-amd64.deb
# Build AppImage
sudo apt-get -y install libfuse2
wget -O appimage-builder https://github.com/AppImageCrafters/appimage-builder/releases/download/v1.1.0/appimage-builder-1.1.0-x86_64.AppImage
chmod +x appimage-builder
mkdir AppDir
cp -r build/linux/x64/release/bundle/* AppDir
mkdir -p AppDir/usr/share/icons/hicolor/scalable/apps/
cp assets/icon/icon.svg AppDir/usr/share/icons/hicolor/scalable/apps/gopeed.svg
cat>AppImageBuilder.yml<<EOF
version: 1
AppDir:
path: ${PWD}/AppDir
app_info:
id: com.gopeed.gopeed
name: Gopeed
icon: gopeed
version: ${VERSION:1}
exec: gopeed
exec_args: \$@
apt:
arch: amd64
allow_unauthenticated: true
sources:
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy main restricted
universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-updates main
restricted universe multiverse
- sourceline: deb https://mirrors.tuna.tsinghua.edu.cn/ubuntu/ jammy-backports
main restricted universe multiverse
- sourceline: deb http://security.ubuntu.com/ubuntu/ jammy-security main restricted
universe multiverse
include: []
files:
include: []
exclude:
- usr/share/man
- usr/share/doc/*/README.*
- usr/share/doc/*/changelog.*
- usr/share/doc/*/NEWS.*
- usr/share/doc/*/TODO.*
test:
fedora-30:
image: appimagecrafters/tests-env:fedora-30
command: ./AppRun
debian-stable:
image: appimagecrafters/tests-env:debian-stable
command: ./AppRun
archlinux-latest:
image: appimagecrafters/tests-env:archlinux-latest
command: ./AppRun
centos-7:
image: appimagecrafters/tests-env:centos-7
command: ./AppRun
ubuntu-xenial:
image: appimagecrafters/tests-env:ubuntu-xenial
command: ./AppRun
AppImage:
arch: x86_64
update-information: guess
EOF
./appimage-builder --skip-tests --recipe AppImageBuilder.yml
mv Gopeed-*-x86_64.AppImage build/dist/Gopeed-$VERSION-linux-amd64.AppImage
- name: Upload - name: Upload
uses: shogo82148/actions-upload-release-asset@v1 uses: shogo82148/actions-upload-release-asset@v1
with: with:
upload_url: ${{ needs.get-release.outputs.upload_url }} upload_url: ${{ needs.get-release.outputs.upload_url }}
asset_path: ui/flutter/build/dist/* asset_path: ui/flutter/dist/*/*
overwrite: true overwrite: true
build-snap: build-snap:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }} if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'snap' }}
+1 -1
View File
@@ -4,7 +4,7 @@
*.dll *.dll
*.so *.so
*.dylib *.dylib
ui/flutter/dist/
# Test binary, built with `go test -c` # Test binary, built with `go test -c`
*.test *.test
+2
View File
@@ -21,6 +21,8 @@ type Fetcher interface {
Pause() error Pause() error
Close() error Close() error
// Stats refreshes health statistics and returns the latest information
Stats() any
// Meta returns the meta information of the download. // Meta returns the meta information of the download.
Meta() *FetcherMeta Meta() *FetcherMeta
// Progress returns the progress of the download. // Progress returns the progress of the download.
+11
View File
@@ -128,6 +128,7 @@ func (f *Fetcher) Start() (err error) {
file.Download() file.Download()
} }
} }
return return
} }
@@ -188,6 +189,16 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta return f.meta
} }
func (f *Fetcher) Stats() any {
stats := f.torrent.Stats()
baseStats := &bt.Stats{
TotalPeers: stats.TotalPeers,
ActivePeers: stats.ActivePeers,
ConnectedSeeders: stats.ConnectedSeeders,
}
return baseStats
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
if !f.torrentReady.Load() { if !f.torrentReady.Load() {
return f.progress return f.progress
+22 -18
View File
@@ -18,6 +18,7 @@ import (
"os" "os"
"path" "path"
"strconv" "strconv"
"strings"
"time" "time"
) )
@@ -104,8 +105,8 @@ func (f *Fetcher) Resolve(req *base.Request) error {
Range: false, Range: false,
Files: []*base.FileInfo{}, Files: []*base.FileInfo{},
} }
if base.HttpCodePartialContent == httpResp.StatusCode { if base.HttpCodePartialContent == httpResp.StatusCode || (base.HttpCodeOK == httpResp.StatusCode && httpResp.Header.Get(base.HttpHeaderAcceptRanges) == base.HttpHeaderBytes && strings.HasPrefix(httpResp.Header.Get(base.HttpHeaderContentRange), base.HttpHeaderBytes)) {
// 返回206响应码表示支持断点下载 // 1.返回206响应码表示支持断点下载 2.不返回206但是Accept-Ranges首部并且等于bytes也表示支持断点下载
res.Range = true res.Range = true
// 解析资源大小: bytes 0-1000/1001 => 1001 // 解析资源大小: bytes 0-1000/1001 => 1001
contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange)) contentTotal := path.Base(httpResp.Header.Get(base.HttpHeaderContentRange))
@@ -217,6 +218,11 @@ func (f *Fetcher) Meta() *fetcher.FetcherMeta {
return f.meta return f.meta
} }
func (f *Fetcher) Stats() any {
// todo http download health
return &fhttp.Stats{}
}
func (f *Fetcher) Progress() fetcher.Progress { func (f *Fetcher) Progress() fetcher.Progress {
p := make(fetcher.Progress, 0) p := make(fetcher.Progress, 0)
if len(f.chunks) > 0 { if len(f.chunks) > 0 {
@@ -306,26 +312,13 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
if err != nil { if err != nil {
return err return err
} }
defer resp.Body.Close()
if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent { if resp.StatusCode != base.HttpCodeOK && resp.StatusCode != base.HttpCodePartialContent {
err = NewRequestError(resp.StatusCode, resp.Status) err = NewRequestError(resp.StatusCode, resp.Status)
return err return err
} }
return nil // Http request success, reset retry times
}() chunk.retryTimes = 0
if err != nil {
// 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
}
// Http request success, reset retry times
chunk.retryTimes = 0
err = func() error {
defer resp.Body.Close()
for { for {
n, err := resp.Body.Read(buf) n, err := resp.Body.Read(buf)
if n > 0 { if n > 0 {
@@ -342,7 +335,18 @@ func (f *Fetcher) fetchChunk(index int, ctx context.Context) (err error) {
return err return err
} }
} }
return nil
}() }()
if err != nil {
// 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
}
// if chunk is completed, reset other not complete chunks retry times // if chunk is completed, reset other not complete chunks retry times
if err == nil { if err == nil {
for _, c := range f.chunks { for _, c := range f.chunks {
+2
View File
@@ -16,10 +16,12 @@ const (
HttpCodePartialContent = 206 HttpCodePartialContent = 206
HttpHeaderRange = "Range" HttpHeaderRange = "Range"
HttpHeaderAcceptRanges = "Accept-Ranges"
HttpHeaderContentLength = "Content-Length" HttpHeaderContentLength = "Content-Length"
HttpHeaderContentRange = "Content-Range" HttpHeaderContentRange = "Content-Range"
HttpHeaderContentDisposition = "Content-Disposition" HttpHeaderContentDisposition = "Content-Disposition"
HttpHeaderUserAgent = "Usr-Agent" HttpHeaderUserAgent = "Usr-Agent"
HttpHeaderBytes = "bytes"
HttpHeaderRangeFormat = "bytes=%d-%d" HttpHeaderRangeFormat = "bytes=%d-%d"
) )
+24
View File
@@ -6,8 +6,11 @@ import (
"github.com/GopeedLab/gopeed/internal/fetcher" "github.com/GopeedLab/gopeed/internal/fetcher"
"github.com/GopeedLab/gopeed/internal/logger" "github.com/GopeedLab/gopeed/internal/logger"
"github.com/GopeedLab/gopeed/pkg/base" "github.com/GopeedLab/gopeed/pkg/base"
"github.com/GopeedLab/gopeed/pkg/protocol/http"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
gonanoid "github.com/matoous/go-nanoid/v2" gonanoid "github.com/matoous/go-nanoid/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/pkgerrors"
"github.com/virtuald/go-paniclog" "github.com/virtuald/go-paniclog"
"os" "os"
"path/filepath" "path/filepath"
@@ -91,6 +94,7 @@ func NewDownloader(cfg *DownloaderConfig) *Downloader {
} }
} }
zerolog.ErrorStackMarshaler = pkgerrors.MarshalStack
d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log")) d.Logger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "core.log"))
d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log")) d.ExtensionLogger = logger.NewLogger(cfg.ProductionMode, filepath.Join(cfg.StorageDir, "logs", "extension.log"))
if cfg.ProductionMode { if cfg.ProductionMode {
@@ -591,6 +595,26 @@ func (d *Downloader) watch(task *Task) {
d.emit(EventKeyDone, task) d.emit(EventKeyDone, task)
d.emit(EventKeyFinally, task, err) d.emit(EventKeyFinally, task, err)
d.notifyRunning() d.notifyRunning()
if e, ok := task.Meta.Opts.Extra.(*http.OptsExtra); ok {
downloadFilePath := task.Meta.SingleFilepath()
if e.AutoTorrent && strings.HasSuffix(downloadFilePath, ".torrent") {
go func() {
_, err2 := d.CreateDirect(
&base.Request{
URL: downloadFilePath,
},
&base.Options{
Path: task.Meta.Opts.Path,
SelectFiles: make([]int, 0),
})
if err2 != nil {
d.Logger.Error().Err(err2).Msgf("auto create torrent task failed, task id: %s", task.ID)
}
}()
}
}
} }
func (d *Downloader) doOnError(task *Task, err error) { func (d *Downloader) doOnError(task *Task, err error) {
+4 -4
View File
@@ -254,12 +254,12 @@ func (d *Downloader) triggerOnResolve(req *base.Request) (res *base.Resource, er
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.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
for _, file := range ctx.Res.Files {
file.Name = util.ReplaceInvalidFilename(file.Name)
}
ctx.Res.CalcSize(nil) ctx.Res.CalcSize(nil)
} }
ctx.Res.Name = util.ReplaceInvalidFilename(ctx.Res.Name)
for _, file := range ctx.Res.Files {
file.Name = util.ReplaceInvalidFilename(file.Name)
}
res = ctx.Res res = ctx.Res
}, },
) )
+9
View File
@@ -3,3 +3,12 @@ package bt
type ReqExtra struct { type ReqExtra struct {
Trackers []string `json:"trackers"` Trackers []string `json:"trackers"`
} }
// Stats for download
type Stats struct {
// bt stats
// health indicators of torrents, from large to small, ConnectedSeeders are also the key to the health of seed resources
TotalPeers int `json:"totalPeers"`
ActivePeers int `json:"activePeers"`
ConnectedSeeders int `json:"connectedSeeders"`
}
+8
View File
@@ -8,4 +8,12 @@ type ReqExtra struct {
type OptsExtra struct { type OptsExtra struct {
Connections int `json:"connections"` Connections int `json:"connections"`
// AutoTorrent when task download complete, and it is a .torrent file, it will be auto create a new task for the torrent file
AutoTorrent bool `json:"autoTorrent"`
}
// Stats for download
type Stats struct {
// http stats
// health indicators of http
} }
+16
View File
@@ -3,11 +3,13 @@ package rest
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"github.com/GopeedLab/gopeed/pkg/download" "github.com/GopeedLab/gopeed/pkg/download"
"github.com/GopeedLab/gopeed/pkg/rest/model" "github.com/GopeedLab/gopeed/pkg/rest/model"
"github.com/GopeedLab/gopeed/pkg/util" "github.com/GopeedLab/gopeed/pkg/util"
"github.com/gorilla/handlers" "github.com/gorilla/handlers"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/pkg/errors"
"net" "net"
"net/http" "net/http"
"os" "os"
@@ -131,6 +133,20 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
}) })
} }
// recover panic
r.Use(func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if v := recover(); v != nil {
err := errors.WithStack(fmt.Errorf("%v", v))
Downloader.Logger.Error().Stack().Err(err).Msg("http server panic")
WriteJson(w, model.NewErrorResult(err.Error(), model.CodeError))
}
}()
h.ServeHTTP(w, r)
})
})
srv = &http.Server{Handler: handlers.CORS( srv = &http.Server{Handler: handlers.CORS(
handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}), handlers.AllowedHeaders([]string{"Content-Type", "X-Api-Token", "X-Target-Uri"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}), handlers.AllowedMethods([]string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"}),
+1
View File
@@ -0,0 +1 @@
output: dist/
+1
View File
@@ -25,6 +25,7 @@ class Options {
@JsonSerializable() @JsonSerializable()
class OptsExtraHttp { class OptsExtraHttp {
int connections = 0; int connections = 0;
bool autoTorrent = false;
OptsExtraHttp(); OptsExtraHttp();
+4 -1
View File
@@ -32,9 +32,12 @@ Map<String, dynamic> _$OptionsToJson(Options instance) {
} }
OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) => OptsExtraHttp _$OptsExtraHttpFromJson(Map<String, dynamic> json) =>
OptsExtraHttp()..connections = json['connections'] as int; OptsExtraHttp()
..connections = json['connections'] as int
..autoTorrent = json['autoTorrent'] as bool;
Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) => Map<String, dynamic> _$OptsExtraHttpToJson(OptsExtraHttp instance) =>
<String, dynamic>{ <String, dynamic>{
'connections': instance.connections, 'connections': instance.connections,
'autoTorrent': instance.autoTorrent,
}; };
@@ -6,6 +6,7 @@ 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:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
import 'package:lecle_downloads_path_provider/lecle_downloads_path_provider.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';
import 'package:tray_manager/tray_manager.dart'; import 'package:tray_manager/tray_manager.dart';
@@ -21,6 +22,7 @@ import '../../../../util/locale_manager.dart';
import '../../../../util/log_util.dart'; import '../../../../util/log_util.dart';
import '../../../../util/package_info.dart'; import '../../../../util/package_info.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
import '../../../../util/window_util.dart' as windows_util;
import '../../../routes/app_pages.dart'; import '../../../routes/app_pages.dart';
const _startConfigNetwork = "start.network"; const _startConfigNetwork = "start.network";
@@ -49,7 +51,12 @@ const allTrackerCdns = [
[ [
"https://ghproxy.com/https://raw.githubusercontent.com", "https://ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)" r".*github.com(/.*)/raw(/.*)"
] ],
// mirror.ghproxy.com: https://mirror.ghproxy.com/https://raw.githubusercontent.com/ngosang/trackerslist/master/trackers_all.txt
[
"https://mirror.ghproxy.com/https://raw.githubusercontent.com",
r".*github.com(/.*)/raw(/.*)"
],
]; ];
final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls, final allTrackerSubscribeUrlCdns = Map.fromIterable(allTrackerSubscribeUrls,
key: (v) => v as String, key: (v) => v as String,
@@ -118,6 +125,26 @@ class AppController extends GetxController with WindowListener, TrayListener {
trayManager.popUpContextMenu(); trayManager.popUpContextMenu();
} }
@override
void onWindowMaximize() {
windows_util.saveState(isMaximized: true);
}
@override
void onWindowUnmaximize() {
windows_util.saveState(isMaximized: false);
}
final _windowsResizeSave = Util.debounce(() async {
final size = await windowManager.getSize();
windows_util.saveState(width: size.width, height: size.height);
}, 500);
@override
void onWindowResize() {
_windowsResizeSave();
}
Future<void> _initDeepLinks() async { Future<void> _initDeepLinks() async {
// currently only support android // currently only support android
if (!Util.isAndroid()) { if (!Util.isAndroid()) {
@@ -391,9 +418,12 @@ class AppController extends GetxController with WindowListener, TrayListener {
if (Util.isDesktop()) { if (Util.isDesktop()) {
config.downloadDir = (await getDownloadsDirectory())?.path ?? "./"; config.downloadDir = (await getDownloadsDirectory())?.path ?? "./";
} else if (Util.isAndroid()) { } else if (Util.isAndroid()) {
config.downloadDir = (await getExternalStorageDirectory())?.path ?? final downloadDir = (await DownloadsPath.downloadsDirectory())?.path;
(await getApplicationDocumentsDirectory()).path; if (downloadDir != null) {
return; config.downloadDir = '$downloadDir/Gopeed';
} else {
config.downloadDir = (await getApplicationDocumentsDirectory()).path;
}
} else if (Util.isIOS()) { } else if (Util.isIOS()) {
config.downloadDir = (await getApplicationDocumentsDirectory()).path; config.downloadDir = (await getApplicationDocumentsDirectory()).path;
} else { } else {
@@ -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:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart'; import 'package:get/get.dart';
@@ -14,22 +15,24 @@ class AppView extends GetView<AppController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final config = controller.downloaderConfig.value; final config = controller.downloaderConfig.value;
return GetMaterialApp.router( return WithForegroundTask(
useInheritedMediaQuery: true, child: GetMaterialApp.router(
debugShowCheckedModeBanner: false, useInheritedMediaQuery: true,
theme: GopeedTheme.light, debugShowCheckedModeBanner: false,
darkTheme: GopeedTheme.dark, theme: GopeedTheme.light,
themeMode: ThemeMode.values.byName(config.extra.themeMode), darkTheme: GopeedTheme.dark,
translations: messages, themeMode: ThemeMode.values.byName(config.extra.themeMode),
locale: toLocale(config.extra.locale), translations: messages,
fallbackLocale: fallbackLocale, locale: toLocale(config.extra.locale),
localizationsDelegates: const [ fallbackLocale: fallbackLocale,
GlobalMaterialLocalizations.delegate, localizationsDelegates: const [
GlobalWidgetsLocalizations.delegate, GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate, GlobalWidgetsLocalizations.delegate,
], GlobalCupertinoLocalizations.delegate,
supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(), ],
getPages: AppPages.routes, supportedLocales: messages.keys.keys.map((e) => toLocale(e)).toList(),
getPages: AppPages.routes,
),
); );
} }
} }
@@ -4,16 +4,14 @@ 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';
import '../../../../api/model/request.dart'; import '../../../../api/model/request.dart';
import '../../../../api/model/resolve_result.dart'; import '../../../../api/model/resolve_result.dart';
import '../../../../api/model/resource.dart';
import '../../../../util/input_formatter.dart'; import '../../../../util/input_formatter.dart';
import '../../../../util/message.dart'; import '../../../../util/message.dart';
import '../../../../util/util.dart'; import '../../../../util/util.dart';
@@ -21,6 +19,8 @@ import '../../../routes/app_pages.dart';
import '../../../views/directory_selector.dart'; import '../../../views/directory_selector.dart';
import '../../../views/file_list_view.dart'; import '../../../views/file_list_view.dart';
import '../../app/controllers/app_controller.dart'; import '../../app/controllers/app_controller.dart';
import '../../history/controller/history_controller.dart';
import '../../history/views/history_view.dart';
import '../controllers/create_controller.dart'; import '../controllers/create_controller.dart';
class CreateView extends GetView<CreateController> { class CreateView extends GetView<CreateController> {
@@ -409,15 +409,20 @@ class CreateView extends GetView<CreateController> {
try { try {
_confirmController.start(); _confirmController.start();
if (_confirmFormKey.currentState!.validate()) { if (_confirmFormKey.currentState!.validate()) {
final submitUrl = Util.isWeb() && controller.fileDataUri.isNotEmpty final isWebFileChosen =
Util.isWeb() && controller.fileDataUri.isNotEmpty;
final submitUrl = isWebFileChosen
? controller.fileDataUri.value ? controller.fileDataUri.value
: _urlController.text; : _urlController.text;
final urls = Util.textToLines(submitUrl); final urls = Util.textToLines(submitUrl);
// Add url to the history // Add url to the history
for (final url in urls) { if (!isWebFileChosen) {
_historyController.addHistory(url); for (final url in urls) {
_historyController.addHistory(url);
}
} }
/* /*
Check if is direct download, there has two ways to direct download Check if is direct download, there has two ways to direct download
1. Direct download option is checked 1. Direct download option is checked
@@ -433,7 +438,7 @@ class CreateView extends GetView<CreateController> {
name: isMultiLine ? "" : _renameController.text, name: isMultiLine ? "" : _renameController.text,
path: _pathController.text, path: _pathController.text,
selectFiles: [], selectFiles: [],
extra: parseReqOpts()))); extra: parseReqOptsExtra())));
})); }));
Get.rootDelegate.offNamed(Routes.TASK); Get.rootDelegate.offNamed(Routes.TASK);
} else { } else {
@@ -471,11 +476,10 @@ class CreateView extends GetView<CreateController> {
return reqExtra; return reqExtra;
} }
Object? parseReqOpts() { Object? parseReqOptsExtra() {
return _connectionsController.text.isEmpty return OptsExtraHttp()
? null ..connections = int.tryParse(_connectionsController.text) ?? 0
: (OptsExtraHttp() ..autoTorrent = true;
..connections = int.parse(_connectionsController.text));
} }
String _hitText() { String _hitText() {
@@ -541,7 +545,7 @@ class CreateView extends GetView<CreateController> {
showMessage('tip'.tr, 'noFileSelected'.tr); showMessage('tip'.tr, 'noFileSelected'.tr);
return; return;
} }
final optExtra = parseReqOpts(); final optExtra = parseReqOptsExtra();
if (createFormKey.currentState!.validate()) { if (createFormKey.currentState!.validate()) {
if (rr.id.isEmpty) { if (rr.id.isEmpty) {
// from extension resolve result // from extension resolve result
@@ -1,5 +1,4 @@
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';
@@ -12,7 +11,7 @@ class HomeView extends GetView<HomeController> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) { return GetRouterOutlet.builder(builder: (context, delegate, currentRoute) {
switch (currentRoute?.location) { switch (currentRoute?.uri.path) {
case Routes.EXTENSION: case Routes.EXTENSION:
controller.currentIndex.value = 1; controller.currentIndex.value = 1;
break; break;
@@ -24,141 +23,139 @@ class HomeView extends GetView<HomeController> {
break; break;
} }
return WithForegroundTask( return Scaffold(
child: Scaffold( // extendBody: true,
// extendBody: true, body: Row(
body: Row( // crossAxisAlignment: CrossAxisAlignment.start,
// crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ !ResponsiveBuilder.isNarrow(context)
!ResponsiveBuilder.isNarrow(context) ? NavigationRail(
? NavigationRail( extended: true,
extended: true, labelType: NavigationRailLabelType.none,
labelType: NavigationRailLabelType.none, minExtendedWidth: 170,
minExtendedWidth: 170, groupAlignment: 0,
groupAlignment: 0, // useIndicator: false,
// useIndicator: false, onDestinationSelected: (int index) {
onDestinationSelected: (int index) { controller.currentIndex.value = index;
controller.currentIndex.value = index; switch (index) {
switch (index) { case 0:
case 0: delegate.offAndToNamed(Routes.TASK);
delegate.offAndToNamed(Routes.TASK); break;
break; case 1:
case 1: delegate.offAndToNamed(Routes.EXTENSION);
delegate.offAndToNamed(Routes.EXTENSION); break;
break; case 2:
case 2: delegate.offAndToNamed(Routes.SETTING);
delegate.offAndToNamed(Routes.SETTING); break;
break; }
} },
}, destinations: [
destinations: [ NavigationRailDestination(
NavigationRailDestination( icon: const Icon(Icons.task),
icon: const Icon(Icons.task), selectedIcon: const Icon(Icons.task),
selectedIcon: const Icon(Icons.task), label: Text('task'.tr),
label: Text('task'.tr), ),
), NavigationRailDestination(
NavigationRailDestination( icon: const Icon(Icons.extension),
icon: const Icon(Icons.extension), selectedIcon: const Icon(Icons.extension),
selectedIcon: const Icon(Icons.extension), label: Text('extensions'.tr),
label: Text('extensions'.tr), ),
), NavigationRailDestination(
NavigationRailDestination( icon: const Icon(Icons.settings),
icon: const Icon(Icons.settings), selectedIcon: const Icon(Icons.settings),
selectedIcon: const Icon(Icons.settings), label: Text('setting'.tr),
label: Text('setting'.tr), ),
), ],
], selectedIndex: controller.currentIndex.value,
selectedIndex: controller.currentIndex.value, leading: const Icon(Icons.menu),
leading: const Icon(Icons.menu), // trailing: const Icon(Icons.info_outline),
// trailing: const Icon(Icons.info_outline), )
) : const SizedBox.shrink(),
: const SizedBox.shrink(), Expanded(
Expanded( child: GetRouterOutlet(
child: GetRouterOutlet( initialRoute: Routes.TASK,
initialRoute: Routes.TASK, // anchorRoute: '/',
// anchorRoute: '/', // filterPages: (afterAnchor) {
// filterPages: (afterAnchor) { // logger.w(afterAnchor);
// logger.w(afterAnchor); // logger.w(afterAnchor.take(1));
// logger.w(afterAnchor.take(1)); // return afterAnchor.take(1);
// return afterAnchor.take(1); // },
// }, ))
)) ]),
]), bottomNavigationBar: ResponsiveBuilder.isNarrow(context)
bottomNavigationBar: ResponsiveBuilder.isNarrow(context) ? BottomNavigationBar(
? BottomNavigationBar( items: <BottomNavigationBarItem>[
items: <BottomNavigationBarItem>[ BottomNavigationBarItem(
BottomNavigationBarItem( icon: const Icon(Icons.task),
icon: const Icon(Icons.task), label: 'task'.tr,
label: 'task'.tr, ),
), BottomNavigationBarItem(
BottomNavigationBarItem( icon: const Icon(Icons.extension),
icon: const Icon(Icons.extension), label: 'extensions'.tr,
label: 'extensions'.tr, ),
), BottomNavigationBarItem(
BottomNavigationBarItem( icon: const Icon(Icons.settings),
icon: const Icon(Icons.settings), label: 'setting'.tr,
label: 'setting'.tr, ),
), ],
], currentIndex: controller.currentIndex.value,
currentIndex: controller.currentIndex.value, // selectedItemColor: Get.theme.highlightColor,
// selectedItemColor: Get.theme.highlightColor, onTap: (index) {
onTap: (index) { controller.currentIndex.value = index;
controller.currentIndex.value = index; switch (index) {
switch (index) { case 0:
case 0: delegate.offAndToNamed(Routes.TASK);
delegate.offAndToNamed(Routes.TASK); break;
break; case 1:
case 1: delegate.offAndToNamed(Routes.EXTENSION);
delegate.offAndToNamed(Routes.EXTENSION); break;
break; case 2:
case 2: delegate.offAndToNamed(Routes.SETTING);
delegate.offAndToNamed(Routes.SETTING); break;
break; }
} },
}, )
) // StylishBottomBar(
// StylishBottomBar( // option: AnimatedBarOptions(
// option: AnimatedBarOptions( // iconSize: 32,
// iconSize: 32, // barAnimation: BarAnimation.blink,
// barAnimation: BarAnimation.blink, // iconStyle: IconStyle.Default,
// iconStyle: IconStyle.Default, // opacity: 0.3,
// opacity: 0.3, // ),
// ), // items: [
// items: [ // BottomBarItem(
// BottomBarItem( // icon: const Icon(Icons.file_download),
// icon: const Icon(Icons.file_download), // selectedColor: Get.theme.primaryColor,
// selectedColor: Get.theme.primaryColor, // title: Text('downloading'.tr)),
// title: Text('downloading'.tr)), // BottomBarItem(
// BottomBarItem( // icon: const Icon(Icons.done),
// icon: const Icon(Icons.done), // selectedColor: Get.theme.primaryColor,
// selectedColor: Get.theme.primaryColor, // title: Text('downloaded'.tr)),
// title: Text('downloaded'.tr)), // BottomBarItem(
// BottomBarItem( // icon: const Icon(Icons.settings),
// icon: const Icon(Icons.settings), // selectedColor: Get.theme.primaryColor,
// selectedColor: Get.theme.primaryColor, // title: Text('setting'.tr)),
// title: Text('setting'.tr)), // ],
// ], // // hasNotch: true,
// // hasNotch: true, // currentIndex: controller.currentIndex.value,
// currentIndex: controller.currentIndex.value, // onTap: (index) {
// onTap: (index) { // switch (index) {
// switch (index) { // case 0:
// case 0: // delegate.toNamed(Routes.DOWNLOADING);
// delegate.toNamed(Routes.DOWNLOADING); // controller.currentIndex.value = 0;
// controller.currentIndex.value = 0; // break;
// break; // case 1:
// case 1: // delegate.toNamed(Routes.DOWNLOADED);
// delegate.toNamed(Routes.DOWNLOADED); // controller.currentIndex.value = 1;
// controller.currentIndex.value = 1; // break;
// break; // case 2:
// case 2: // delegate.toNamed(Routes.SETTING);
// delegate.toNamed(Routes.SETTING); // controller.currentIndex.value = 2;
// controller.currentIndex.value = 2; // break;
// break; // }
// } // },
// }, // )
// ) : const SizedBox.shrink(),
: const SizedBox.shrink(),
),
); );
}); });
} }
@@ -365,6 +365,18 @@ class SettingView extends GetView<SettingController> {
); );
} }
buildThanks() {
const thankPage =
'https://github.com/GopeedLab/gopeed/graphs/contributors';
return ListTile(
title: Text('thanks'.tr),
subtitle: Text('thanksDesc'.tr),
onTap: () {
launchUrl(Uri.parse(thankPage), mode: LaunchMode.externalApplication);
},
);
}
// advanced config proxy items start // advanced config proxy items start
final buildProxy = _buildConfigItem( final buildProxy = _buildConfigItem(
'proxy', 'proxy',
@@ -387,45 +399,43 @@ class SettingView extends GetView<SettingController> {
}, },
); );
final items = <Widget>[ final scheme = SizedBox(
SizedBox( width: 100,
width: 150, child: DropdownButtonFormField<String>(
child: DropdownButtonFormField<String>( value: proxy.scheme,
value: proxy.scheme, onChanged: (value) async {
onChanged: (value) async { if (value != null && value != proxy.scheme) {
if (value != null && value != proxy.scheme) { proxy.scheme = value;
proxy.scheme = value;
await debounceSave(); await debounceSave();
} }
}, },
items: const [ items: const [
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: 'http', value: 'http',
child: Text('HTTP'), child: Text('HTTP'),
), ),
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: 'https', value: 'https',
child: Text('HTTPS'), child: Text('HTTPS'),
), ),
DropdownMenuItem<String>( DropdownMenuItem<String>(
value: 'socks5', value: 'socks5',
child: Text('SOCKS5'), child: Text('SOCKS5'),
), ),
], ],
), ),
) );
];
final arr = proxy.host.split(':'); final arr = proxy.host.split(':');
var ip = ''; var host = '';
var port = ''; var port = '';
if (arr.length > 1) { if (arr.length > 1) {
ip = arr[0]; host = arr[0];
port = arr[1]; port = arr[1];
} }
final ipController = TextEditingController(text: ip); final ipController = TextEditingController(text: host);
final portController = TextEditingController(text: port); final portController = TextEditingController(text: port);
updateAddress() async { updateAddress() async {
final newAddress = '${ipController.text}:${portController.text}'; final newAddress = '${ipController.text}:${portController.text}';
@@ -438,25 +448,18 @@ class SettingView extends GetView<SettingController> {
ipController.addListener(updateAddress); ipController.addListener(updateAddress);
portController.addListener(updateAddress); portController.addListener(updateAddress);
items.addAll([ final server = [
const Padding(padding: EdgeInsets.only(left: 20)), Flexible(
SizedBox(
width: 150,
child: TextFormField( child: TextFormField(
controller: ipController, controller: ipController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'IP', labelText: 'server'.tr,
contentPadding: EdgeInsets.all(0.0), contentPadding: const EdgeInsets.all(0.0),
), ),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
), ),
), ),
const Padding(padding: EdgeInsets.only(left: 10)), const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: portController, controller: portController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -470,40 +473,29 @@ class SettingView extends GetView<SettingController> {
], ],
), ),
), ),
]); ];
final usrController = TextEditingController(text: proxy.usr); final usrController = TextEditingController(text: proxy.usr);
final pwdController = TextEditingController(text: proxy.pwd); final pwdController = TextEditingController(text: proxy.pwd);
final auth = [ final auth = [
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: usrController, controller: usrController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'username'.tr, labelText: 'username'.tr,
contentPadding: const EdgeInsets.all(0.0), contentPadding: const EdgeInsets.all(0.0),
), ),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.allow(RegExp('[0-9.]')),
],
), ),
), ),
const Padding(padding: EdgeInsets.only(left: 10)), const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: pwdController, controller: pwdController,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'password'.tr, labelText: 'password'.tr,
contentPadding: const EdgeInsets.all(0.0), contentPadding: const EdgeInsets.all(0.0),
), ),
keyboardType: TextInputType.number,
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
NumericalRangeFormatter(min: 0, max: 65535),
],
), ),
), ),
]; ];
@@ -513,8 +505,9 @@ class SettingView extends GetView<SettingController> {
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: _addPadding([ children: _addPadding([
switcher, switcher,
scheme,
Row( Row(
children: items, children: server,
), ),
Row( Row(
children: auth, children: auth,
@@ -534,7 +527,7 @@ class SettingView extends GetView<SettingController> {
(Key key) { (Key key) {
final items = <Widget>[ final items = <Widget>[
SizedBox( SizedBox(
width: 150, width: 80,
child: DropdownButtonFormField<String>( child: DropdownButtonFormField<String>(
value: startCfg.value.network, value: startCfg.value.network,
onChanged: Util.isDesktop() onChanged: Util.isDesktop()
@@ -585,8 +578,7 @@ class SettingView extends GetView<SettingController> {
portController.addListener(updateAddress); portController.addListener(updateAddress);
items.addAll([ items.addAll([
const Padding(padding: EdgeInsets.only(left: 20)), const Padding(padding: EdgeInsets.only(left: 20)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: ipController, controller: ipController,
decoration: const InputDecoration( decoration: const InputDecoration(
@@ -600,8 +592,7 @@ class SettingView extends GetView<SettingController> {
), ),
), ),
const Padding(padding: EdgeInsets.only(left: 10)), const Padding(padding: EdgeInsets.only(left: 10)),
SizedBox( Flexible(
width: 150,
child: TextFormField( child: TextFormField(
controller: portController, controller: portController,
decoration: InputDecoration( decoration: InputDecoration(
@@ -710,6 +701,7 @@ class SettingView extends GetView<SettingController> {
children: _addDivider([ children: _addDivider([
buildHomepage(), buildHomepage(),
buildVersion(), buildVersion(),
buildThanks(),
]), ]),
)), )),
]), ]),
@@ -35,7 +35,7 @@ class _DirectorySelectorState extends State<DirectorySelector> {
return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr; return v!.trim().isNotEmpty ? null : 'downloadDirValid'.tr;
}, },
)), )),
!Util.isWeb() && !Util.isIOS() !Util.isWeb() && !Util.isAndroid() && !Util.isIOS()
? IconButton( ? IconButton(
icon: const Icon(Icons.folder_open), icon: const Icon(Icons.folder_open),
onPressed: () async { onPressed: () async {
+4
View File
@@ -79,7 +79,11 @@ const enUS = {
'serviceText': 'Running', 'serviceText': 'Running',
'network': 'Network', 'network': 'Network',
'proxy': 'Proxy', 'proxy': 'Proxy',
'server': 'Server',
'username': 'Username', 'username': 'Username',
'password': 'Password', 'password': 'Password',
'thanks': 'Thanks',
'thanksDesc':
'Thanks to all the contributors who have helped build and develop the Gopeed community!',
}, },
}; };
+3
View File
@@ -57,5 +57,8 @@ const faIR = {
'newVersionTitle': '@version عنوان: کشف نسخه جدید', 'newVersionTitle': '@version عنوان: کشف نسخه جدید',
'newVersionUpdate': 'بروزرسانی', 'newVersionUpdate': 'بروزرسانی',
'newVersionLater': 'بعداً', 'newVersionLater': 'بعداً',
'thanks': 'تشکر',
'thanksDesc':
'از تمامی افرادی که در ساخت و توسعه‌ی جامعه‌ی Gopeed مشارکت کرده‌اند، سپاسگزاری می‌کنیم!',
} }
}; };
+2
View File
@@ -59,5 +59,7 @@ const jaJP = {
'newVersionTitle': '新しいバージョン @version を発見する', 'newVersionTitle': '新しいバージョン @version を発見する',
'newVersionUpdate': 'アップデート', 'newVersionUpdate': 'アップデート',
'newVersionLater': '後で', 'newVersionLater': '後で',
'thanks': '感謝',
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
} }
}; };
+3
View File
@@ -73,5 +73,8 @@ const ruRU = {
'history': 'История', 'history': 'История',
'clearHistory': 'Очистить историю', 'clearHistory': 'Очистить историю',
'noHistoryFound': 'История не найдена', 'noHistoryFound': 'История не найдена',
'thanks': 'Благодарности',
'thanksDesc':
'Благодарим всех участников за их вклад в строительство и развитие сообщества Gopeed!',
} }
}; };
+3
View File
@@ -77,7 +77,10 @@ const zhCN = {
'serviceText': '运行中', 'serviceText': '运行中',
'network': '网络', 'network': '网络',
'proxy': '代理', 'proxy': '代理',
'server': '服务器',
'username': '用户名', 'username': '用户名',
'password': '密码', 'password': '密码',
'thanks': '鸣谢',
'thanksDesc': '感谢所有为 Gopeed 社区建设添砖加瓦的贡献者们!',
} }
}; };
+10 -7
View File
@@ -24,7 +24,7 @@ const zhTW = {
'noStoragePermission': '沒有存儲權限', 'noStoragePermission': '沒有存儲權限',
'selectFile': '選擇文件', 'selectFile': '選擇文件',
'rename': '重命名', 'rename': '重命名',
'basic': '基礎', 'basic': '一般',
'advanced': '進階', 'advanced': '進階',
'general': '通用設定', 'general': '通用設定',
'downloadDir': '下載目録', 'downloadDir': '下載目録',
@@ -59,17 +59,17 @@ const zhTW = {
'deleteTask': '刪除任務', 'deleteTask': '刪除任務',
'deleteTaskTip': '保留已經下載的文件', 'deleteTaskTip': '保留已經下載的文件',
'delete': '刪除', 'delete': '刪除',
'newVersionTitle': '發現新版本 @version', 'newVersionTitle': '現新版本 @version 已發佈',
'newVersionUpdate': '立刻更新', 'newVersionUpdate': '下載',
'newVersionLater': '稍後更新', 'newVersionLater': '取消',
'extensions': '外掛程序', 'extensions': '擴充程序',
'extensionInstallUrl': '安裝URL', 'extensionInstallUrl': '安裝URL',
'extensionInstallSuccess': '安裝成功', 'extensionInstallSuccess': '安裝成功',
'extensionUpdateSuccess': '更新完成', 'extensionUpdateSuccess': '更新完成',
'extensionDelete': '解除安裝', 'extensionDelete': '解除安裝',
'extensionAlreadyLatest': '已是最新版本', 'extensionAlreadyLatest': '已是最新版本',
'extensionFind': '取得外掛', 'extensionFind': '取得擴充程序',
'extensionDevelop': '開發外掛', 'extensionDevelop': '開發擴充程序',
'history': '歷史記錄', 'history': '歷史記錄',
'clearHistory': '清空記錄', 'clearHistory': '清空記錄',
'noHistoryFound': '還沒有記錄', 'noHistoryFound': '還沒有記錄',
@@ -77,7 +77,10 @@ const zhTW = {
'serviceText': '執行中', 'serviceText': '執行中',
'network': '網路', 'network': '網路',
'proxy': '代理', 'proxy': '代理',
'server': '伺服器',
'username': '名稱', 'username': '名稱',
'password': '密碼', 'password': '密碼',
'thanks': '鳴謝',
'thanksDesc': '感謝所有為 Gopeed 社群建設添磚加瓦的貢獻者們!'
} }
}; };
+10 -3
View File
@@ -13,6 +13,7 @@ import 'util/log_util.dart';
import 'util/mac_secure_util.dart'; import 'util/mac_secure_util.dart';
import 'util/package_info.dart'; import 'util/package_info.dart';
import 'util/util.dart'; import 'util/util.dart';
import 'util/window_util.dart' as windows_util;
void main() async { void main() async {
await init(); await init();
@@ -25,15 +26,21 @@ Future<void> init() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
if (Util.isDesktop()) { if (Util.isDesktop()) {
await windowManager.ensureInitialized(); await windowManager.ensureInitialized();
const windowOptions = WindowOptions( final windowState = await windows_util.loadState();
size: Size(800, 600), final windowOptions = WindowOptions(
size: Size(windowState.width, windowState.height),
center: true, center: true,
skipTaskbar: false, skipTaskbar: false,
); );
windowManager.waitUntilReadyToShow(windowOptions, () async { await windowManager.waitUntilReadyToShow(windowOptions, () async {
await windowManager.show(); await windowManager.show();
await windowManager.focus(); await windowManager.focus();
await windowManager.setPreventClose(true); await windowManager.setPreventClose(true);
// windows_manager has a bug where when window to be maximized, it will be unmaximized immediately, so can't implement this feature currently.
// https://github.com/leanflutter/window_manager/issues/412
// if (windowState.isMaximized) {
// await windowManager.maximize();
// }
}); });
} }
+8
View File
@@ -143,4 +143,12 @@ class Util {
} }
return completer.future; return completer.future;
} }
static void Function() debounce(Function() fn, int ms) {
Timer? timer;
return () {
timer?.cancel();
timer = Timer(Duration(milliseconds: ms), fn);
};
}
} }
+39
View File
@@ -0,0 +1,39 @@
import 'dart:async';
import 'package:shared_preferences/shared_preferences.dart';
class WindowState {
late bool isMaximized;
late double width;
late double height;
WindowState(this.isMaximized, this.width, this.height);
}
const String _windowStateIsMaximized = "window.isMaximized";
const String _windowStateWidth = "window.width";
const String _windowStateHeight = "window.height";
/// Save window state to storage.
void saveState({bool? isMaximized, double? width, double? height}) async {
final prefs = await SharedPreferences.getInstance();
if (isMaximized != null) {
await prefs.setBool(_windowStateIsMaximized, isMaximized);
}
if (width != null) {
await prefs.setDouble(_windowStateWidth, width);
}
if (height != null) {
await prefs.setDouble(_windowStateHeight, height);
}
}
/// Load window state from storage.
Future<WindowState> loadState() async {
final prefs = await SharedPreferences.getInstance();
return WindowState(
prefs.getBool(_windowStateIsMaximized) ?? false,
prefs.getDouble(_windowStateWidth) ?? 800,
prefs.getDouble(_windowStateHeight) ?? 600,
);
}
@@ -0,0 +1,17 @@
display_name: Gopeed
icon: assets/icon/icon.svg
keywords:
- Application
- DownloadManager
- Network
- Utility
generic_name: Download Manager
categories:
- Network
- Utility
startup_notify: true
@@ -0,0 +1,31 @@
display_name: Gopeed
package_name: gopeed
maintainer:
name: monkeyWie
email: [email protected]
co_authors:
- name: madoka773
email: [email protected]
priority: optional
section: x11
installed_size: 52360
essential: false
icon: assets/icon/icon.svg
dependencies:
- libappindicator3-1 | libayatana-appindicator3-1
- gir1.2-appindicator3-0.1 | gir1.2-ayatanaappindicator3-0.1
keywords:
- Application
- DownloadManager
- Network
- Utility
generic_name: Download Manager
categories:
- Network
- Utility
startup_notify: true
@@ -0,0 +1,24 @@
display_name: Gopeed
icon: assets/icon/icon.svg
summary: A modern download manager that supports all platforms. Built with Golang and Flutter.
group: Applications/Internet
vendor: monkeyWie
packager: madoka773
packagerEmail: [email protected]
license: GPL-3.0-or-later
url: https://github.com/GopeedLab/gopeed
description: A modern download manager that supports all platforms. Built with Golang and Flutter.
keywords:
- Application
- DownloadManager
- Network
- Utility
generic_name: Download Manager
categories:
- Network
- Utility
startup_notify: true
+40 -25
View File
@@ -181,10 +181,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: collection name: collection
sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.17.1" version: "1.18.0"
context_menus: context_menus:
dependency: "direct main" dependency: "direct main"
description: description:
@@ -441,10 +441,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: intl name: intl
sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.18.0" version: "0.18.1"
io: io:
dependency: transitive dependency: transitive
description: description:
@@ -477,6 +477,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "6.7.1" version: "6.7.1"
lecle_downloads_path_provider:
dependency: "direct main"
description:
name: lecle_downloads_path_provider
sha256: "111f6d6d21e18acddda515eba8fd445658aa14defcbd59513d6d1b02b9ed47a0"
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.0.2+8"
lint: lint:
dependency: transitive dependency: transitive
description: description:
@@ -537,18 +545,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: matcher name: matcher
sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.12.15" version: "0.12.16"
material_color_utilities: material_color_utilities:
dependency: transitive dependency: transitive
description: description:
name: material_color_utilities name: material_color_utilities
sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.2.0" version: "0.5.0"
math_expressions: math_expressions:
dependency: transitive dependency: transitive
description: description:
@@ -569,10 +577,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: meta name: meta
sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" sha256: a6e590c838b18133bb482a2745ad77c5bb7715fb0451209e1a7567d416678b8e
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
mime: mime:
dependency: transitive dependency: transitive
description: description:
@@ -894,10 +902,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: source_span name: source_span
sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.9.1" version: "1.10.0"
sprintf: sprintf:
dependency: transitive dependency: transitive
description: description:
@@ -910,18 +918,18 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: stack_trace name: stack_trace
sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.11.0" version: "1.11.1"
stream_channel: stream_channel:
dependency: transitive dependency: transitive
description: description:
name: stream_channel name: stream_channel
sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "2.1.1" version: "2.1.2"
stream_transform: stream_transform:
dependency: transitive dependency: transitive
description: description:
@@ -958,10 +966,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: test_api name: test_api
sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b"
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "0.5.1" version: "0.6.1"
timing: timing:
dependency: transitive dependency: transitive
description: description:
@@ -973,12 +981,11 @@ packages:
tray_manager: tray_manager:
dependency: "direct main" dependency: "direct main"
description: description:
path: "." name: tray_manager
ref: "fix/libayatana-set-icon-deprecation" sha256: "4ab709d70a4374af172f8c39e018db33a4271265549c6fc9d269a65e5f4b0225"
resolved-ref: "056da3fef20e75877e8d2a4ae73d93f51a0f77cc" url: "https://pub.flutter-io.cn"
url: "https://github.com/provokateurin/tray_manager.git" source: hosted
source: git version: "0.2.1"
version: "0.2.0"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:
@@ -1083,6 +1090,14 @@ packages:
url: "https://pub.flutter-io.cn" url: "https://pub.flutter-io.cn"
source: hosted source: hosted
version: "1.1.0" version: "1.1.0"
web:
dependency: transitive
description:
name: web
sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152
url: "https://pub.flutter-io.cn"
source: hosted
version: "0.3.0"
web_socket_channel: web_socket_channel:
dependency: transitive dependency: transitive
description: description:
@@ -1140,5 +1155,5 @@ packages:
source: hosted source: hosted
version: "2.1.1" version: "2.1.1"
sdks: sdks:
dart: ">=3.0.0 <4.0.0" dart: ">=3.2.0-194.0.dev <4.0.0"
flutter: ">=3.10.0" flutter: ">=3.10.0"
+3 -5
View File
@@ -15,7 +15,7 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev
# In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion.
# Read more about iOS versioning at # Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.5.2+1 version: 1.5.3+1
environment: environment:
sdk: '>=3.0.0 <4.0.0' sdk: '>=3.0.0 <4.0.0'
@@ -56,16 +56,14 @@ 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:
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
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 flutter_foreground_task: ^6.1.2
open_filex: ^4.3.4 open_filex: ^4.3.4
tray_manager: ^0.2.1
lecle_downloads_path_provider: ^0.0.2+8
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter