Compare commits

..
28 changed files with 1056 additions and 282 deletions

No files matched your search

+1 -1
View File
@@ -508,7 +508,7 @@ jobs:
overwrite: true
build-ios:
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'ios' }}
runs-on: macos-latest
runs-on: macos-14
needs: [get-release]
steps:
- uses: actions/checkout@v3
+13 -7
View File
@@ -53,7 +53,13 @@ jobs:
with:
go-version: ${{ env.GO_VERSION }}
- name: Run Unit tests.
run: go test -v -coverpkg=$(go list ./... | grep -v /cmd/ | grep -v /bind/ | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $(go list ./... | grep -v /cmd/ | grep -v /bind/)
run: |
# Get packages for testing and coverage (including cmd/web for flags_test.go)
PACKAGES=$(go list ./... | grep -v /bind/ | grep -v -E '/cmd/(?!web)')
go test -v -coverpkg=$(echo "$PACKAGES" | paste -sd "," -) -covermode=count -coverprofile=coverage.txt $PACKAGES
# Filter out main.go from coverage report to avoid lowering coverage percentage
grep -v "main.go" coverage.txt > coverage_filtered.txt || true
mv coverage_filtered.txt coverage.txt
- uses: codecov/codecov-action@v4
with:
files: ./coverage.txt
@@ -63,8 +69,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm ]
needs: [ test ]
os: [windows-latest, macos-latest, ubuntu-22.04, ubuntu-22.04-arm]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -96,8 +102,8 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ macos-latest, ubuntu-latest ]
needs: [ test ]
os: [macos-14, ubuntu-latest]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -129,7 +135,7 @@ jobs:
build-web:
name: Build web check
runs-on: ubuntu-latest
needs: [ test ]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
@@ -149,7 +155,7 @@ jobs:
build-docker:
name: Build docker check
runs-on: ubuntu-latest
needs: [ test ]
needs: [test]
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v4
+170 -51
View File
@@ -3,19 +3,23 @@ package main
import (
"encoding/json"
"flag"
"fmt"
"github.com/GopeedLab/gopeed/pkg/base"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"github.com/GopeedLab/gopeed/pkg/base"
)
type args struct {
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"`
Address *string `json:"address"`
Port *int `json:"port"`
Username *string `json:"username"`
Password *string `json:"password"`
ApiToken *string `json:"apiToken"`
StorageDir *string `json:"storageDir"`
WhiteDownloadDirs []string `json:"whiteDownloadDirs"`
// DownloadConfig when the first time to start the server, it will be configured as initial value
DownloadConfig *base.DownloaderStoreConfig `json:"downloadConfig"`
@@ -23,62 +27,177 @@ type args struct {
}
func parse() *args {
var cliArgs args
cliArgs.Address = flag.String("A", "0.0.0.0", "Bind Address")
cliArgs.Port = flag.Int("P", 9999, "Bind Port")
cliArgs.Username = flag.String("u", "gopeed", "Web Authentication Username")
cliArgs.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cliArgs.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
cliArgs.StorageDir = flag.String("d", "", "Storage directory")
cliArgs.configPath = flag.String("c", "./config.json", "Config file path")
cfg := &args{}
cliConfig := loadCliArgs()
loadConfigFile(cfg, *cliConfig.configPath)
loadEnvVars(cfg)
// override with non-default command line arguments
overrideWithCliArgs(cfg, cliConfig)
// set default values for any unset fields
setDefaults(cfg)
return cfg
}
// loadCliArgs parses command line arguments and returns initial config
func loadCliArgs() *args {
cfg := &args{}
cfg.Address = flag.String("A", "", "Bind Address")
cfg.Port = flag.Int("P", 0, "Bind Port")
cfg.Username = flag.String("u", "", "Web Authentication Username")
cfg.Password = flag.String("p", "", "Web Authentication Password, if no password is set, web authentication will not be enabled")
cfg.ApiToken = flag.String("T", "", "API token, it must be configured when using HTTP API in the case of enabling web authentication")
cfg.StorageDir = flag.String("d", "", "Storage directory")
whiteDownloadDirs := flag.String("w", "", "White download directories, comma-separated")
cfg.configPath = flag.String("c", "./config.json", "Config file path")
flag.Parse()
// args priority: config file > cli args
cfgArgs := loadConfig(*cliArgs.configPath)
if cfgArgs.Address == nil {
cfgArgs.Address = cliArgs.Address
// Parse white download directories from comma-separated string
if whiteDownloadDirs != nil && *whiteDownloadDirs != "" {
dirs := strings.Split(*whiteDownloadDirs, ",")
for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
cfg.WhiteDownloadDirs = dirs
}
if cfgArgs.Port == nil {
cfgArgs.Port = cliArgs.Port
}
if cfgArgs.Username == nil {
cfgArgs.Username = cliArgs.Username
}
if cfgArgs.Password == nil {
cfgArgs.Password = cliArgs.Password
}
if cfgArgs.ApiToken == nil {
cfgArgs.ApiToken = cliArgs.ApiToken
}
if cfgArgs.StorageDir == nil {
cfgArgs.StorageDir = cliArgs.StorageDir
}
return cfgArgs
return cfg
}
func loadConfig(path string) *args {
var args args
// overrideWithCliArgs overrides config with non-empty command line arguments
func overrideWithCliArgs(cfg *args, cliConfig *args) {
// Only override if the cli value is not empty/zero
if cliConfig.Address != nil && *cliConfig.Address != "" {
cfg.Address = cliConfig.Address
}
if !filepath.IsAbs(path) {
if cliConfig.Port != nil && *cliConfig.Port != 0 {
cfg.Port = cliConfig.Port
}
if cliConfig.Username != nil && *cliConfig.Username != "" {
cfg.Username = cliConfig.Username
}
if cliConfig.Password != nil && *cliConfig.Password != "" {
cfg.Password = cliConfig.Password
}
if cliConfig.ApiToken != nil && *cliConfig.ApiToken != "" {
cfg.ApiToken = cliConfig.ApiToken
}
if cliConfig.StorageDir != nil && *cliConfig.StorageDir != "" {
cfg.StorageDir = cliConfig.StorageDir
}
if cliConfig.WhiteDownloadDirs != nil {
cfg.WhiteDownloadDirs = cliConfig.WhiteDownloadDirs
}
}
// setDefaults sets default values for any unset configuration fields
func setDefaults(cfg *args) {
if cfg.Address == nil {
address := "0.0.0.0"
cfg.Address = &address
}
if cfg.Port == nil {
port := 9999
cfg.Port = &port
}
if cfg.Username == nil {
username := "gopeed"
cfg.Username = &username
}
}
// loadConfigFile loads configuration from file
func loadConfigFile(cfg *args, configPath string) {
if !filepath.IsAbs(configPath) {
dir, err := os.Getwd()
if err != nil {
fmt.Println("config dir get failed, reason:" + err.Error())
return &args
return
}
path = filepath.Join(dir, path)
configPath = filepath.Join(dir, configPath)
}
file, err := os.ReadFile(path)
file, err := os.ReadFile(configPath)
if err != nil {
if os.IsNotExist(err) {
return &args
return
}
fmt.Println("config file read failed, reason:" + err.Error())
return &args
return
}
if err = json.Unmarshal(file, &args); err != nil {
fmt.Println("config file parse failed, reason:" + err.Error())
return &args
if err = json.Unmarshal(file, cfg); err != nil {
return
}
}
// loadEnvVars loads configuration from environment variables with prefix GOPEED_
func loadEnvVars(cfg *args) {
v := reflect.ValueOf(cfg).Elem()
t := reflect.TypeOf(cfg).Elem()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
fieldType := t.Field(i)
// Get json tag as environment variable suffix
jsonTag := fieldType.Tag.Get("json")
if jsonTag == "" || jsonTag == "-" {
continue
}
// Remove options like omitempty
if commaIdx := strings.Index(jsonTag, ","); commaIdx != -1 {
jsonTag = jsonTag[:commaIdx]
}
// Convert to uppercase and add GOPEED_ prefix
envKey := "GOPEED_" + strings.ToUpper(jsonTag)
envValue := os.Getenv(envKey)
if envValue == "" {
continue
}
// Set value based on field type
if field.Kind() == reflect.Ptr {
if field.IsNil() {
// Create new pointer instance
newVal := reflect.New(field.Type().Elem())
field.Set(newVal)
}
switch field.Type().Elem().Kind() {
case reflect.String:
field.Elem().SetString(envValue)
case reflect.Int:
if intVal, err := strconv.Atoi(envValue); err == nil {
field.Elem().SetInt(int64(intVal))
}
default:
// For complex types like DownloadConfig, try JSON unmarshaling
if field.Type().Elem() == reflect.TypeOf(base.DownloaderStoreConfig{}) {
var config base.DownloaderStoreConfig
if err := json.Unmarshal([]byte(envValue), &config); err == nil {
field.Set(reflect.ValueOf(&config))
}
}
}
} else if field.Kind() == reflect.Slice {
// Handle non-pointer slice types (like []string for WhiteDownloadDirs)
if field.Type().Elem().Kind() == reflect.String {
dirs := strings.Split(envValue, ",")
for i := range dirs {
dirs[i] = strings.TrimSpace(dirs[i])
}
field.Set(reflect.ValueOf(dirs))
}
}
}
fmt.Printf("config file loaded: %s\n", path)
return &args
}
File diff suppressed because it is too large. Load diff
+17 -15
View File
@@ -6,11 +6,12 @@ package main
import (
"embed"
"fmt"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
"io/fs"
"os"
"path/filepath"
"github.com/GopeedLab/gopeed/cmd"
"github.com/GopeedLab/gopeed/pkg/rest/model"
)
//go:embed dist/*
@@ -31,28 +32,29 @@ func main() {
}
}
var dir string
var storageDir string
if args.StorageDir != nil && *args.StorageDir != "" {
dir = *args.StorageDir
storageDir = *args.StorageDir
} else {
exe, err := os.Executable()
if err != nil {
panic(err)
}
dir = filepath.Dir(exe)
storageDir = filepath.Join(filepath.Dir(exe), "storage")
}
cfg := &model.StartConfig{
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: filepath.Join(dir, "storage"),
ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true,
WebEnable: true,
WebFS: sub,
WebAuth: webAuth,
Network: "tcp",
Address: fmt.Sprintf("%s:%d", *args.Address, *args.Port),
Storage: model.StorageBolt,
StorageDir: storageDir,
WhiteDownloadDirs: args.WhiteDownloadDirs,
ApiToken: *args.ApiToken,
DownloadConfig: args.DownloadConfig,
ProductionMode: true,
WebEnable: true,
WebFS: sub,
WebAuth: webAuth,
}
cmd.Start(cfg)
}
+2 -2
View File
@@ -941,9 +941,9 @@ func (d *Downloader) doCreate(f fetcher.Fetcher, opts *base.Options) (taskId str
}
// if enable white download directory, check if the download directory is in the white list
if len(d.cfg.DownloadDirWhiteList) > 0 {
if len(d.cfg.WhiteDownloadDirs) > 0 {
inWhiteList := false
for _, dir := range d.cfg.DownloadDirWhiteList {
for _, dir := range d.cfg.WhiteDownloadDirs {
if match, err := filepath.Match(dir, opts.Path); match && err == nil {
inWhiteList = true
break
+1 -1
View File
@@ -91,7 +91,7 @@ func TestDownloader_CreateNotInWhite(t *testing.T) {
defer listener.Close()
downloader := NewDownloader(&DownloaderConfig{
DownloadDirWhiteList: []string{"./downloads"},
WhiteDownloadDirs: []string{"./downloads"},
})
if err := downloader.Setup(); err != nil {
t.Fatal(err)
+4 -4
View File
@@ -137,10 +137,10 @@ type DownloaderConfig struct {
Controller *controller.Controller
FetchManagers []fetcher.FetcherManager
RefreshInterval int `json:"refreshInterval"` // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
StorageDir string
DownloadDirWhiteList []string `json:"downloadDirWhiteList"`
RefreshInterval int // RefreshInterval time duration to refresh task progress(ms)
Storage Storage
StorageDir string
WhiteDownloadDirs []string
ProductionMode bool
+3 -2
View File
@@ -83,8 +83,9 @@ func BuildServer(startCfg *model.StartConfig) (*http.Server, net.Listener, error
startCfg.Init()
downloadCfg := &download.DownloaderConfig{
ProductionMode: startCfg.ProductionMode,
RefreshInterval: startCfg.RefreshInterval,
ProductionMode: startCfg.ProductionMode,
RefreshInterval: startCfg.RefreshInterval,
WhiteDownloadDirs: startCfg.WhiteDownloadDirs,
}
if startCfg.Storage == model.StorageBolt {
downloadCfg.Storage = download.NewBoltStorage(startCfg.StorageDir)
@@ -0,0 +1,12 @@
import 'package:get/get.dart';
import '../controllers/app_controller.dart';
class AppBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<AppController>(
() => AppController(),
);
}
}
@@ -1,12 +1,10 @@
import 'package:get/get.dart';
import '../../../../util/network_monitor.dart';
import '../controllers/root_controller.dart';
class RootBinding extends Bindings {
@override
void dependencies() {
Get.lazyPut<RootController>(() => RootController(), fenix: true);
Get.put<NetworkMonitor>(NetworkMonitor(), permanent: true);
}
}
@@ -1,19 +1,15 @@
import 'package:get/get.dart';
import '../../../../database/database.dart';
import '../../../../util/updater.dart';
class SettingController extends GetxController {
final tapStatues = <String, bool>{}.obs;
final latestVersion = Rxn<VersionInfo>();
final networkAutoControl = false.obs;
@override
void onInit() {
super.onInit();
fetchLatestVersion();
// Initialize network auto control setting
networkAutoControl.value = Database.instance.getNetworkAutoControl();
}
// set all tap status to false
@@ -31,10 +27,4 @@ class SettingController extends GetxController {
void fetchLatestVersion() async {
latestVersion.value = await checkUpdate();
}
// update network auto control setting
void updateNetworkAutoControl(bool value) {
networkAutoControl.value = value;
Database.instance.saveNetworkAutoControl(value);
}
}
@@ -11,7 +11,6 @@ import 'package:launch_at_startup/launch_at_startup.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../../api/model/downloader_config.dart';
import '../../../../database/database.dart';
import '../../../../i18n/message.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/locale_manager.dart';
@@ -544,24 +543,6 @@ class SettingView extends GetView<SettingController> {
);
}
// Network auto control setting (mobile only)
buildNetworkAutoControl() {
if (!Platform.isAndroid && !Platform.isIOS) {
return null;
}
return Obx(() => ListTile(
title: Text('networkAutoControl'.tr),
subtitle: Text('networkAutoControlTip'.tr),
trailing: Switch(
value: controller.networkAutoControl.value,
onChanged: (bool value) {
controller.updateNetworkAutoControl(value);
},
),
));
}
// advanced config proxy items start
final proxy = downloaderCfg.value.proxy;
final buildProxy = _buildConfigItem(
@@ -1004,10 +985,7 @@ class SettingView extends GetView<SettingController> {
Text('network'.tr),
Card(
child: Column(
children: _addDivider([
buildNetworkAutoControl(),
buildProxy(),
].where((e) => e != null).cast<Widget>().toList()),
children: _addDivider([buildProxy()]),
)),
const Text('API'),
Card(
-9
View File
@@ -10,7 +10,6 @@ const String _windowState = 'windowState';
const String _bookmark = 'bookmark';
const String _createHistory = 'createHistory';
const String _webToken = 'webToken';
const String _networkAutoControl = 'networkAutoControl';
class Database {
static final Database _instance = Database._internal();
@@ -110,12 +109,4 @@ class Database {
void clearCreateHistory() {
clear(_createHistory);
}
void saveNetworkAutoControl(bool enabled) {
save<bool>(_networkAutoControl, enabled);
}
bool getNetworkAutoControl() {
return get<bool>(_networkAutoControl, (json) => json as bool) ?? false;
}
}
+129
View File
@@ -0,0 +1,129 @@
const deDE = {
'de_DE': {
'label': 'Deutsch',
'error': 'Fehler',
'tip': 'Tipp',
'confirm': 'Bestätigen',
'cancel': 'Abbrechen',
'on': 'An',
'off': 'Aus',
'selectAll': 'Alle Auswählen',
'select': 'Auswählen',
'task': 'Aufgaben',
'downloading': 'Wird Heruntergeladen',
'downloaded': 'Heruntergeladen',
'setting': 'Einstellungen',
'donate': 'Spenden',
'exit': 'Beenden',
'create': 'Aufgabe Erstellen',
'directDownload': 'Direkter Download',
'advancedOptions': 'Erweiterte Einstellungen',
'followSettings': 'Einstellungen Folgen',
'downloadLink': 'Download Link',
'downloadLinkValid': 'Bitte geben Sie den Download-Link ein',
'downloadLinkHit': 'Bitte geben Sie den Download-Link ein, einen pro Zeile@append',
'downloadLinkHitDesktop': ', oder ziehen Sie die Torrent-Datei direkt hierher',
'download': 'Download',
'noFileSelected': 'Bitte wählen Sie mindestens eine Datei aus, um fortzufahren.',
'noStoragePermission': 'Speicherberechtigung erforderlich',
'selectFile': 'Datei auswählen',
'rename': 'Umbenennen',
'basic': 'Basic',
'advanced': 'Fortschrittlich',
'general': 'Allgemein',
'downloadDir': 'Download-Verzeichnis',
'downloadDirValid': 'Bitte wählen Sie das Download-Verzeichnis',
'connections': 'Anschlüsse',
'useServerCtime': 'Serverzeit für die Dateierstellung verwenden',
'maxRunning': 'Maximale Anzahl ausgeführter Aufgaben',
'defaultDirectDownload': 'Direkten Download standardmäßig aktivieren',
'items': '@count Artikel',
'subscribeTracker': 'Abonnement-Tracker',
'subscribeFail':
'Anmeldung fehlgeschlagen. Bitte überprüfen Sie das Netzwerk oder versuchen Sie es später erneut.',
'update': 'Aktualisieren',
'updateDaily': 'Tägliche Aktualisierung',
'lastUpdate': 'Letzte Aktualisierung: @time',
'addTracker': 'Tracker hinzufügen',
'addTrackerHit': 'Bitte geben Sie die URL des Tracker-Servers ein, eine pro Zeile',
'ui': 'Benutzeroberfläche',
'theme': 'Thema',
'themeSystem': 'System',
'themeLight': 'Lichtmodus',
'themeDark': 'Dunkler Modus',
'locale': 'Sprache',
'about': 'Über',
'homepage': 'Homepage',
'version': 'Version',
'protocol': 'Protokoll',
'port': 'Port',
'apiToken': 'API Token',
'notSet': 'NS',
'set': 'SET',
'portInUse': 'Port [@port] wird verwendet, bitte ändern Sie den Port',
'effectAfterRestart': 'Wirkung nach Neustart',
'developer': 'Entwickler-in',
'logDirectory': 'Protokollverzeichnis',
'show': 'Zeigen',
'continue': 'Weitermachen',
'pause': 'Pause',
'startAll': 'Alles starten',
'pauseAll': 'Alle pausieren',
'deleteTask': '@count Aufgaben löschen',
'deleteTaskTip': 'Heruntergeladene Dateien behalten',
'delete': 'Löschen',
'newVersionTitle': 'Entdecken Sie die neue Version @version',
'newVersionUpdate': 'Jetzt aktualisieren',
'newVersionLater': 'Später',
'extensions': 'Erweiterungen',
'extensionInstallUrl': 'Installations-URL',
'extensionInstallSuccess': 'Erfolgreich installiert',
'extensionUpdateSuccess': 'Erfolgreich aktualisiert',
'extensionDelete': 'Erweiterung löschen',
'extensionAlreadyLatest': 'Es ist bereits die neueste Version',
'extensionFind': 'Erweiterungen suchen',
'extensionDevelop': 'Erweiterungen entwickeln',
'history': 'Verlauf',
'clearHistory': 'Verlauf löschen',
'noHistoryFound': 'Kein Verlauf gefunden',
'serviceTitle': 'Download-Service',
'serviceText': 'Läuft',
'network': 'Netzwerk',
'proxy': 'Proxy',
'noProxy': 'Kein Proxy',
'systemProxy': 'System-Proxy',
'customProxy': 'Benutzerdefinierter Proxy',
'server': 'Server',
'username': 'Benutzername',
'password': 'Passwort',
'thanks': 'Danke',
'thanksDesc':
'Vielen Dank an alle Mitwirkenden, die beim Aufbau und der Entwicklung der Gopeed-Community geholfen haben!',
'browserExtension': 'Browsererweiterung',
'launchAtStartup': 'Beim Start starten',
'seedConfig': 'Seed-Konfiguration',
'seedKeep': 'Weiter-seeden bis manuell gestoppt',
'seedRatio': 'Seed Verhältniss',
'seedTime': 'Seed-Zeit (Minuten)',
'setAsDefaultBtClient': 'Als Standard-BT-Client festlegen',
'taskDetail': 'Aufgabendetails',
'taskName': 'Aufgabenname',
'taskUrl': 'Aufgaben-URL',
'downloadPath': 'Download-Pfad',
'skipVerifyCert': 'Zertifikatsüberprüfung überspringen',
'name': 'Name',
'size': 'Größe',
'unknown': 'Unbekannt',
'fileSelectedCount': 'Dateien: ',
'fileSelectedSize': 'Größe: ',
'httpHeaderName': 'Header-Name',
'httpHeaderValue': 'Header-Wert',
'login': 'Login',
'username_required': 'Bitte geben Sie Ihren Benutzernamen ein',
'password_required': 'Bitte geben Sie Ihr Passwort ein',
'login_success': 'Anmeldung erfolgreich',
'login_failed': 'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihren Benutzernamen und Ihr Passwort',
'login_failed_network':
'Anmeldung fehlgeschlagen, bitte überprüfen Sie Ihre Netzwerkverbindung',
},
};
-2
View File
@@ -125,7 +125,5 @@ const enUS = {
'login_failed': 'Login failed, please check your username and password',
'login_failed_network':
'Login failed, please check your network connection',
'networkAutoControl': 'Network Auto Control',
'networkAutoControlTip': 'Automatically pause downloads when switching from WiFi to mobile data, and resume when switching back to WiFi',
},
};
-2
View File
@@ -104,7 +104,5 @@ const esES = {
'taskUrl': 'URL de la Tarea',
'downloadPath': 'Ruta de Descarga',
'skipVerifyCert': 'Omitir Verificación de Certificado',
'networkAutoControl': 'Control Automático de Red',
'networkAutoControlTip': 'Pausar automáticamente las descargas al cambiar de WiFi a datos móviles y reanudar al volver a WiFi',
},
};
-2
View File
@@ -93,7 +93,5 @@ const frFR = {
'browserExtension': 'Extension du navigateur',
'launchAtStartup': 'Lancer au démarrage',
'skipVerifyCert': 'Bypass Verifikasi Sertifikat',
'networkAutoControl': 'Contrôle automatique du réseau',
'networkAutoControlTip': 'Mettre automatiquement en pause les téléchargements lors du passage du WiFi aux données mobiles et reprendre lors du retour au WiFi',
},
};
-2
View File
@@ -97,7 +97,5 @@ const itIT = {
'browserExtension': 'Estensione del browser',
'launchAtStartup': "Lancia all'avvio",
'skipVerifyCert': 'Salta la verifica del certificato',
'networkAutoControl': 'Controllo automatico della rete',
'networkAutoControlTip': 'Mette automaticamente in pausa i download quando si passa dal WiFi ai dati mobili e riprende quando si torna al WiFi',
},
};
-2
View File
@@ -63,7 +63,5 @@ const jaJP = {
'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!',
'browserExtension': 'ブラウザ拡張機能',
'skipVerifyCert': '証明書の検証をスキップ',
'networkAutoControl': 'ネットワーク自動制御',
'networkAutoControlTip': 'WiFiからモバイルデータに切り替わった時に自動的にダウンロードを一時停止し、WiFiに戻った時に自動的に再開します',
}
};
-2
View File
@@ -119,7 +119,5 @@ const ruRU = {
'fileSelectedSize': 'Размер: ',
'httpHeaderName': 'Название заголовка HTTP',
'httpHeaderValue': 'Значение заголовка HTTP',
'networkAutoControl': 'Автоматическое управление сетью',
'networkAutoControlTip': 'Автоматически приостанавливать загрузки при переключении с WiFi на мобильные данные и возобновлять при возврате к WiFi',
}
};
-2
View File
@@ -90,7 +90,5 @@ const viVN = {
'Cảm ơn tất cả những người đóng góp đã giúp xây dựng và phát triển cộng đồng Gopeed!',
'browserExtension': 'Tiện ích mở rộng trình duyệt',
'skipVerifyCert': 'Bỏ qua xác thực chứng chỉ',
'networkAutoControl': 'Điều khiển mạng tự động',
'networkAutoControlTip': 'Tự động tạm dừng tải xuống khi chuyển từ WiFi sang dữ liệu di động và tiếp tục khi quay lại WiFi',
},
};
-2
View File
@@ -122,7 +122,5 @@ const zhCN = {
'login_success': '登录成功',
'login_failed': '登录失败,请检查用户名和密码',
'login_failed_network': '登录失败,请检查网络连接',
'networkAutoControl': '网络状态自动控制',
'networkAutoControlTip': '当网络从WiFi切换到移动数据时自动暂停下载,切换回WiFi时自动恢复下载',
}
};
-2
View File
@@ -116,7 +116,5 @@ const zhTW = {
'fileSelectedSize': '大小:',
'httpHeaderName': '標頭名稱',
'httpHeaderValue': '標頭值',
'networkAutoControl': '網路狀態自動控制',
'networkAutoControlTip': '當網路從WiFi切換到流動數據時自動暫停下載,切換回WiFi時自動恢復下載',
}
};
+2
View File
@@ -1,5 +1,6 @@
import 'package:get/get.dart';
import 'langs/de_de.dart';
import 'langs/en_us.dart';
import 'langs/fa_ir.dart';
import 'langs/fr_fr.dart';
@@ -41,5 +42,6 @@ class _Messages extends Translations {
...ukUA,
...huHU,
...ptBR,
...deDE
};
}
-134
View File
@@ -1,134 +0,0 @@
import 'dart:async';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:get/get.dart';
import '../api/api.dart';
import '../database/database.dart';
import '../util/log_util.dart';
enum NetworkType { wifi, mobile, other, none }
class NetworkMonitor extends GetxService {
static NetworkMonitor get to => Get.find();
late final Connectivity _connectivity;
late StreamSubscription<List<ConnectivityResult>> _connectivitySubscription;
final _isWiFiConnected = false.obs;
bool get isWiFiConnected => _isWiFiConnected.value;
NetworkType _previousNetworkType = NetworkType.none;
bool _isInitialized = false;
@override
Future<void> onInit() async {
super.onInit();
logInfo('NetworkMonitor.onInit() called');
// Only monitor network on mobile platforms
if (!Platform.isAndroid && !Platform.isIOS) {
logInfo('NetworkMonitor: Not on mobile platform, skipping initialization');
return;
}
_connectivity = Connectivity();
// Check initial connectivity state
final initialResult = await _connectivity.checkConnectivity();
logInfo('NetworkMonitor: Initial connectivity result: $initialResult');
_updateNetworkStatus(initialResult);
_previousNetworkType = _getNetworkType(initialResult);
logInfo('NetworkMonitor: Initial network type: $_previousNetworkType');
_isInitialized = true;
// Listen to connectivity changes
_connectivitySubscription = _connectivity.onConnectivityChanged.listen(
_onConnectivityChanged,
onError: (error) {
logError('Network monitoring error: $error');
},
);
logInfo('Network monitor initialized successfully');
}
@override
void onClose() {
_connectivitySubscription.cancel();
super.onClose();
}
NetworkType _getNetworkType(List<ConnectivityResult> result) {
if (result.contains(ConnectivityResult.wifi)) {
return NetworkType.wifi;
} else if (result.contains(ConnectivityResult.mobile)) {
return NetworkType.mobile;
} else if (result.contains(ConnectivityResult.none)) {
return NetworkType.none;
} else {
// Ethernet, Bluetooth, VPN, or other connection types
return NetworkType.other;
}
}
void _updateNetworkStatus(List<ConnectivityResult> result) {
final hasWiFi = result.contains(ConnectivityResult.wifi);
_isWiFiConnected.value = hasWiFi;
}
void _onConnectivityChanged(List<ConnectivityResult> result) {
logInfo('NetworkMonitor: Connectivity changed event received: $result');
if (!_isInitialized) {
logInfo('NetworkMonitor: Not initialized yet, ignoring connectivity change');
return;
}
// Check if network auto control is enabled
final isNetworkAutoControlEnabled = Database.instance.getNetworkAutoControl();
logInfo('NetworkMonitor: Network auto control enabled: $isNetworkAutoControlEnabled');
if (!isNetworkAutoControlEnabled) {
logInfo('NetworkMonitor: Network auto control disabled, ignoring connectivity change');
return;
}
_updateNetworkStatus(result);
final currentNetworkType = _getNetworkType(result);
logInfo('Network changed: previous=$_previousNetworkType, current=$currentNetworkType');
// Only handle WiFi ↔ Mobile transitions, ignore other connection types
if (_previousNetworkType == NetworkType.wifi && currentNetworkType == NetworkType.mobile) {
_pauseAllDownloads();
logInfo('Switched from WiFi to mobile data - pausing all downloads');
} else if (_previousNetworkType == NetworkType.mobile && currentNetworkType == NetworkType.wifi) {
_resumeAllDownloads();
logInfo('Switched from mobile data to WiFi - resuming all downloads');
} else {
logInfo('Network change ignored: not a WiFi ↔ Mobile transition');
}
_previousNetworkType = currentNetworkType;
}
Future<void> _pauseAllDownloads() async {
try {
await pauseAllTasks(null);
logInfo('Successfully paused all downloads due to network change');
} catch (e) {
logError('Failed to pause all downloads: $e');
}
}
Future<void> _resumeAllDownloads() async {
try {
await continueAllTasks(null);
logInfo('Successfully resumed all downloads due to network change');
} catch (e) {
logError('Failed to resume all downloads: $e');
}
}
}
+2 -1
View File
@@ -54,7 +54,8 @@ class Util {
static Future<void> initStorageDir() async {
var storageDir = "";
if (Util.isWindows()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
storageDir =
path.join(File(Platform.resolvedExecutable).parent.path, "storage");
} else if (!Util.isWeb()) {
if (Util.isLinux()) {
storageDir = File(Platform.resolvedExecutable).parent.path;
+1 -2
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.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
version: 1.7.1+1
version: 1.8.0+1
environment:
sdk: ">=3.0.0 <4.0.0"
@@ -66,7 +66,6 @@ dependencies:
toggle_switch: ^2.3.0
permission_handler: ^11.3.1
device_info_plus: ^11.1.0
connectivity_plus: ^6.1.0
checkable_treeview: ^1.3.1
contextmenu_plus: ^1.0.1
contentsize_tabbarview: ^0.0.2