diff --git a/ui/flutter/lib/app/modules/app/bindings/app_binding.dart b/ui/flutter/lib/app/modules/app/bindings/app_binding.dart deleted file mode 100644 index af092f1f5..000000000 --- a/ui/flutter/lib/app/modules/app/bindings/app_binding.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:get/get.dart'; - -import '../controllers/app_controller.dart'; - -class AppBinding extends Bindings { - @override - void dependencies() { - Get.lazyPut( - () => AppController(), - ); - } -} diff --git a/ui/flutter/lib/app/modules/root/bindings/root_binding.dart b/ui/flutter/lib/app/modules/root/bindings/root_binding.dart index 2a106a1d4..9ae0a49f0 100644 --- a/ui/flutter/lib/app/modules/root/bindings/root_binding.dart +++ b/ui/flutter/lib/app/modules/root/bindings/root_binding.dart @@ -1,10 +1,12 @@ 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(), fenix: true); + Get.put(NetworkMonitor(), permanent: true); } } diff --git a/ui/flutter/lib/app/modules/setting/controllers/setting_controller.dart b/ui/flutter/lib/app/modules/setting/controllers/setting_controller.dart index f9cedd026..fc724c66e 100644 --- a/ui/flutter/lib/app/modules/setting/controllers/setting_controller.dart +++ b/ui/flutter/lib/app/modules/setting/controllers/setting_controller.dart @@ -1,15 +1,19 @@ import 'package:get/get.dart'; +import '../../../../database/database.dart'; import '../../../../util/updater.dart'; class SettingController extends GetxController { final tapStatues = {}.obs; final latestVersion = Rxn(); + 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 @@ -27,4 +31,10 @@ 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); + } } diff --git a/ui/flutter/lib/app/modules/setting/views/setting_view.dart b/ui/flutter/lib/app/modules/setting/views/setting_view.dart index c1b64f2be..62d4a436d 100644 --- a/ui/flutter/lib/app/modules/setting/views/setting_view.dart +++ b/ui/flutter/lib/app/modules/setting/views/setting_view.dart @@ -11,6 +11,7 @@ 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'; @@ -543,6 +544,24 @@ class SettingView extends GetView { ); } + // 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( @@ -985,7 +1004,10 @@ class SettingView extends GetView { Text('network'.tr), Card( child: Column( - children: _addDivider([buildProxy()]), + children: _addDivider([ + buildNetworkAutoControl(), + buildProxy(), + ].where((e) => e != null).cast().toList()), )), const Text('API'), Card( diff --git a/ui/flutter/lib/database/database.dart b/ui/flutter/lib/database/database.dart index d939b048f..8db21cd3e 100644 --- a/ui/flutter/lib/database/database.dart +++ b/ui/flutter/lib/database/database.dart @@ -10,6 +10,7 @@ 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(); @@ -109,4 +110,12 @@ class Database { void clearCreateHistory() { clear(_createHistory); } + + void saveNetworkAutoControl(bool enabled) { + save(_networkAutoControl, enabled); + } + + bool getNetworkAutoControl() { + return get(_networkAutoControl, (json) => json as bool) ?? false; + } } diff --git a/ui/flutter/lib/i18n/langs/en_us.dart b/ui/flutter/lib/i18n/langs/en_us.dart index da6f55478..4f9273c31 100644 --- a/ui/flutter/lib/i18n/langs/en_us.dart +++ b/ui/flutter/lib/i18n/langs/en_us.dart @@ -125,5 +125,7 @@ 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', }, }; diff --git a/ui/flutter/lib/i18n/langs/es_es.dart b/ui/flutter/lib/i18n/langs/es_es.dart index 8fa896155..d687deb8b 100644 --- a/ui/flutter/lib/i18n/langs/es_es.dart +++ b/ui/flutter/lib/i18n/langs/es_es.dart @@ -104,5 +104,7 @@ 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', }, }; \ No newline at end of file diff --git a/ui/flutter/lib/i18n/langs/fr_fr.dart b/ui/flutter/lib/i18n/langs/fr_fr.dart index 75f630ff3..a2e027f7d 100644 --- a/ui/flutter/lib/i18n/langs/fr_fr.dart +++ b/ui/flutter/lib/i18n/langs/fr_fr.dart @@ -93,5 +93,7 @@ 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', }, }; diff --git a/ui/flutter/lib/i18n/langs/it_it.dart b/ui/flutter/lib/i18n/langs/it_it.dart index a6e9ffb58..4a47d5ef0 100644 --- a/ui/flutter/lib/i18n/langs/it_it.dart +++ b/ui/flutter/lib/i18n/langs/it_it.dart @@ -97,5 +97,7 @@ 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', }, }; diff --git a/ui/flutter/lib/i18n/langs/ja_jp.dart b/ui/flutter/lib/i18n/langs/ja_jp.dart index 26d999dae..b41d0668a 100644 --- a/ui/flutter/lib/i18n/langs/ja_jp.dart +++ b/ui/flutter/lib/i18n/langs/ja_jp.dart @@ -63,5 +63,7 @@ const jaJP = { 'thanksDesc': 'Gopeedコミュニティの建設に協力してくださったすべての貢献者の方々に感謝します!', 'browserExtension': 'ブラウザ拡張機能', 'skipVerifyCert': '証明書の検証をスキップ', + 'networkAutoControl': 'ネットワーク自動制御', + 'networkAutoControlTip': 'WiFiからモバイルデータに切り替わった時に自動的にダウンロードを一時停止し、WiFiに戻った時に自動的に再開します', } }; diff --git a/ui/flutter/lib/i18n/langs/ru_ru.dart b/ui/flutter/lib/i18n/langs/ru_ru.dart index 6bb59ce57..dfc13433a 100644 --- a/ui/flutter/lib/i18n/langs/ru_ru.dart +++ b/ui/flutter/lib/i18n/langs/ru_ru.dart @@ -119,5 +119,7 @@ const ruRU = { 'fileSelectedSize': 'Размер: ', 'httpHeaderName': 'Название заголовка HTTP', 'httpHeaderValue': 'Значение заголовка HTTP', + 'networkAutoControl': 'Автоматическое управление сетью', + 'networkAutoControlTip': 'Автоматически приостанавливать загрузки при переключении с WiFi на мобильные данные и возобновлять при возврате к WiFi', } }; diff --git a/ui/flutter/lib/i18n/langs/vi_vn.dart b/ui/flutter/lib/i18n/langs/vi_vn.dart index 309fdf69b..96054c654 100644 --- a/ui/flutter/lib/i18n/langs/vi_vn.dart +++ b/ui/flutter/lib/i18n/langs/vi_vn.dart @@ -90,5 +90,7 @@ 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', }, }; diff --git a/ui/flutter/lib/i18n/langs/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index acd437b63..f92afd94f 100644 --- a/ui/flutter/lib/i18n/langs/zh_cn.dart +++ b/ui/flutter/lib/i18n/langs/zh_cn.dart @@ -122,5 +122,7 @@ const zhCN = { 'login_success': '登录成功', 'login_failed': '登录失败,请检查用户名和密码', 'login_failed_network': '登录失败,请检查网络连接', + 'networkAutoControl': '网络状态自动控制', + 'networkAutoControlTip': '当网络从WiFi切换到移动数据时自动暂停下载,切换回WiFi时自动恢复下载', } }; diff --git a/ui/flutter/lib/i18n/langs/zh_tw.dart b/ui/flutter/lib/i18n/langs/zh_tw.dart index 35701c18e..d9497e841 100644 --- a/ui/flutter/lib/i18n/langs/zh_tw.dart +++ b/ui/flutter/lib/i18n/langs/zh_tw.dart @@ -116,5 +116,7 @@ const zhTW = { 'fileSelectedSize': '大小:', 'httpHeaderName': '標頭名稱', 'httpHeaderValue': '標頭值', + 'networkAutoControl': '網路狀態自動控制', + 'networkAutoControlTip': '當網路從WiFi切換到流動數據時自動暫停下載,切換回WiFi時自動恢復下載', } }; diff --git a/ui/flutter/lib/util/network_monitor.dart b/ui/flutter/lib/util/network_monitor.dart new file mode 100644 index 000000000..10417f219 --- /dev/null +++ b/ui/flutter/lib/util/network_monitor.dart @@ -0,0 +1,134 @@ +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> _connectivitySubscription; + + final _isWiFiConnected = false.obs; + bool get isWiFiConnected => _isWiFiConnected.value; + + NetworkType _previousNetworkType = NetworkType.none; + bool _isInitialized = false; + + @override + Future 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 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 result) { + final hasWiFi = result.contains(ConnectivityResult.wifi); + _isWiFiConnected.value = hasWiFi; + } + + void _onConnectivityChanged(List 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 _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 _resumeAllDownloads() async { + try { + await continueAllTasks(null); + logInfo('Successfully resumed all downloads due to network change'); + } catch (e) { + logError('Failed to resume all downloads: $e'); + } + } +} \ No newline at end of file diff --git a/ui/flutter/pubspec.yaml b/ui/flutter/pubspec.yaml index b7ceac55b..eb3e242cc 100644 --- a/ui/flutter/pubspec.yaml +++ b/ui/flutter/pubspec.yaml @@ -66,6 +66,7 @@ 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