diff --git a/ui/flutter/lib/app/modules/app/bindings/app_binding.dart b/ui/flutter/lib/app/modules/app/bindings/app_binding.dart index af092f1f..3f109299 100644 --- a/ui/flutter/lib/app/modules/app/bindings/app_binding.dart +++ b/ui/flutter/lib/app/modules/app/bindings/app_binding.dart @@ -1,5 +1,6 @@ import 'package:get/get.dart'; +import '../../../../util/network_monitor.dart'; import '../controllers/app_controller.dart'; class AppBinding extends Bindings { @@ -8,5 +9,6 @@ class AppBinding extends Bindings { Get.lazyPut( () => AppController(), ); + Get.put(NetworkMonitor(), permanent: true); } } 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 c1b64f2b..684d1112 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,26 @@ class SettingView extends GetView { ); } + // Network auto control setting (mobile only) + buildNetworkAutoControl() { + if (!Platform.isAndroid && !Platform.isIOS) { + return null; + } + + return ListTile( + title: Text('networkAutoControl'.tr), + subtitle: Text('networkAutoControlTip'.tr), + trailing: Switch( + value: Database.instance.getNetworkAutoControl(), + onChanged: (bool value) { + Database.instance.saveNetworkAutoControl(value); + // Trigger rebuild to update the display value + controller.update(); + }, + ), + ); + } + // advanced config proxy items start final proxy = downloaderCfg.value.proxy; final buildProxy = _buildConfigItem( @@ -985,7 +1006,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 d939b048..8db21cd3 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 da6f5547..4f9273c3 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/zh_cn.dart b/ui/flutter/lib/i18n/langs/zh_cn.dart index acd437b6..f92afd94 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/util/network_monitor.dart b/ui/flutter/lib/util/network_monitor.dart new file mode 100644 index 00000000..0ebebc0d --- /dev/null +++ b/ui/flutter/lib/util/network_monitor.dart @@ -0,0 +1,107 @@ +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'; + +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; + + bool _wasWiFiConnected = false; + bool _isInitialized = false; + + @override + Future onInit() async { + super.onInit(); + + // Only monitor network on mobile platforms + if (!Platform.isAndroid && !Platform.isIOS) { + return; + } + + _connectivity = Connectivity(); + + // Check initial connectivity state + final initialResult = await _connectivity.checkConnectivity(); + _updateWiFiStatus(initialResult); + _wasWiFiConnected = _isWiFiConnected.value; + _isInitialized = true; + + // Listen to connectivity changes + _connectivitySubscription = _connectivity.onConnectivityChanged.listen( + _onConnectivityChanged, + onError: (error) { + logError('Network monitoring error: $error'); + }, + ); + + logInfo('Network monitor initialized'); + } + + @override + void onClose() { + _connectivitySubscription.cancel(); + super.onClose(); + } + + void _updateWiFiStatus(List result) { + final hasWiFi = result.contains(ConnectivityResult.wifi); + _isWiFiConnected.value = hasWiFi; + } + + void _onConnectivityChanged(List result) { + if (!_isInitialized) return; + + // Check if network auto control is enabled + final isNetworkAutoControlEnabled = Database.instance.getNetworkAutoControl(); + if (!isNetworkAutoControlEnabled) { + return; + } + + _updateWiFiStatus(result); + final currentlyWiFi = _isWiFiConnected.value; + + logInfo('Network changed: wasWiFi=$_wasWiFiConnected, currentlyWiFi=$currentlyWiFi'); + + // WiFi to Mobile Data: pause all tasks + if (_wasWiFiConnected && !currentlyWiFi) { + _pauseAllDownloads(); + logInfo('Switched from WiFi to mobile data - pausing all downloads'); + } + // Mobile Data to WiFi: resume all tasks + else if (!_wasWiFiConnected && currentlyWiFi) { + _resumeAllDownloads(); + logInfo('Switched from mobile data to WiFi - resuming all downloads'); + } + + _wasWiFiConnected = currentlyWiFi; + } + + 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 b7ceac55..eb3e242c 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