From b20d9edec2d6bff218239806b41e9bc55e983e5d Mon Sep 17 00:00:00 2001 From: chen08209 Date: Sun, 7 Jul 2024 10:02:10 +0800 Subject: [PATCH] Fix url validate issues Fix check ip performance problem Optimize resources page --- .../clash/services/FlClashVpnService.kt | 4 - core/Clash.Meta | 2 +- core/common.go | 5 + core/hub.go | 2 +- lib/application.dart | 1 - lib/common/common.dart | 3 +- lib/common/iterable.dart | 13 + lib/common/picker.dart | 33 +- lib/common/string.dart | 9 +- lib/controller.dart | 4 + lib/fragments/backup_and_recovery.dart | 171 +++--- lib/fragments/config.dart | 575 +++++++++--------- lib/fragments/connections.dart | 23 +- lib/fragments/logs.dart | 27 +- lib/fragments/profiles/profiles.dart | 43 +- lib/fragments/proxies.dart | 145 +---- lib/fragments/requests.dart | 23 +- lib/fragments/resources.dart | 465 ++++++++++---- lib/fragments/tools.dart | 236 ++++--- lib/l10n/arb/intl_en.arb | 3 +- lib/l10n/arb/intl_zh_CN.arb | 3 +- lib/l10n/intl/messages_en.dart | 1 + lib/l10n/intl/messages_zh_CN.dart | 1 + lib/l10n/l10n.dart | 10 + lib/models/system_color_scheme.dart | 34 +- lib/state.dart | 2 +- lib/widgets/chip.dart | 4 +- lib/widgets/clash_message_container.dart | 3 +- lib/widgets/list.dart | 81 ++- lib/widgets/section.dart | 37 -- lib/widgets/widgets.dart | 3 +- pubspec.lock | 22 +- pubspec.yaml | 3 +- test/command_test.dart | 67 +- 34 files changed, 1068 insertions(+), 990 deletions(-) create mode 100644 lib/common/iterable.dart delete mode 100644 lib/widgets/section.dart diff --git a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt index c208237..5de2168 100644 --- a/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt +++ b/android/app/src/main/kotlin/com/follow/clash/services/FlClashVpnService.kt @@ -47,10 +47,6 @@ class FlClashVpnService : VpnService() { "192.168.*" ) - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { - return START_STICKY - } - fun start(port: Int, props: Props?) { fd = with(Builder()) { addAddress("172.16.0.1", 30) diff --git a/core/Clash.Meta b/core/Clash.Meta index 5766f0b..8a64960 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 5766f0b0264bc610650c7a5ac73e9c81bd4e82f2 +Subproject commit 8a6496026515dd973cd5d00efd83a56757573b33 diff --git a/core/common.go b/core/common.go index 245bc05..61f8a77 100644 --- a/core/common.go +++ b/core/common.go @@ -21,6 +21,7 @@ import ( "path/filepath" "runtime" "strings" + "sync" "syscall" "time" ) @@ -420,7 +421,11 @@ func patchSelectGroup() { } } +var applyLock sync.Mutex + func applyConfig() { + applyLock.Lock() + defer applyLock.Unlock() cfg, err := config.ParseRawConfig(currentConfig) if err != nil { cfg, _ = config.ParseRawConfig(config.DefaultRawConfig()) diff --git a/core/hub.go b/core/hub.go index eb4670f..98b5c49 100644 --- a/core/hub.go +++ b/core/hub.go @@ -94,11 +94,11 @@ func updateConfig(s *C.char, port C.longlong) { go func() { var params = &GenerateConfigParams{} err := json.Unmarshal([]byte(paramsString), params) - configParams = params.Params if err != nil { bridge.SendToPort(i, err.Error()) return } + configParams = params.Params prof := decorationConfig(params.ProfilePath, params.Config) currentConfig = prof applyConfig() diff --git a/lib/application.dart b/lib/application.dart index dcccc5a..66dc11a 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -115,7 +115,6 @@ class ApplicationState extends State { lightColorScheme: lightDynamic, darkColorScheme: darkDynamic, ); - WidgetsBinding.instance.addPostFrameCallback((_) { globalState.appController.updateSystemColorSchemes(systemColorSchemes); }); diff --git a/lib/common/common.dart b/lib/common/common.dart index 8d7f140..0e47945 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -23,4 +23,5 @@ export 'app_localizations.dart'; export 'function.dart'; export 'package.dart'; export 'measure.dart'; -export 'service.dart'; \ No newline at end of file +export 'service.dart'; +export 'iterable.dart'; \ No newline at end of file diff --git a/lib/common/iterable.dart b/lib/common/iterable.dart new file mode 100644 index 0000000..079a1c1 --- /dev/null +++ b/lib/common/iterable.dart @@ -0,0 +1,13 @@ +extension IterableExt on Iterable { + Iterable separated(T separator) sync* { + final iterator = this.iterator; + if (!iterator.moveNext()) return; + + yield iterator.current; + + while (iterator.moveNext()) { + yield separator; + yield iterator.current; + } + } +} diff --git a/lib/common/picker.dart b/lib/common/picker.dart index 2836b6f..417e567 100644 --- a/lib/common/picker.dart +++ b/lib/common/picker.dart @@ -1,29 +1,22 @@ -import 'dart:io'; - import 'package:file_picker/file_picker.dart'; import 'package:fl_clash/common/common.dart'; import 'package:image_picker/image_picker.dart'; class Picker { Future pickerConfigFile() async { - FilePickerResult? filePickerResult; - if (Platform.isAndroid) { - filePickerResult = await FilePicker.platform.pickFiles( - withData: true, - allowMultiple: false, - ); - } else { - filePickerResult = await FilePicker.platform.pickFiles( - withData: true, - type: FileType.custom, - allowedExtensions: ['yaml', 'txt', 'conf'], - ); - } - final file = filePickerResult?.files.first; - if (file == null) { - return null; - } - return file; + final filePickerResult = await FilePicker.platform.pickFiles( + withData: true, + allowMultiple: false, + ); + return filePickerResult?.files.first; + } + + Future pickerGeoDataFile() async { + final filePickerResult = await FilePicker.platform.pickFiles( + withData: true, + allowMultiple: false, + ); + return filePickerResult?.files.first; } Future pickerConfigQRCode() async { diff --git a/lib/common/string.dart b/lib/common/string.dart index 9088808..24d92d1 100644 --- a/lib/common/string.dart +++ b/lib/common/string.dart @@ -1,9 +1,14 @@ extension StringExtension on String { bool get isUrl { return RegExp( - r"^(http(s)?://)?(www\.)?[a-zA-Z0-9]+([\-.][a-zA-Z0-9]+)*\.[a-zA-Z]{2,5}(:[0-9]{1,5})?(/.*)?$", + r'^(https?:\/\/)?' + r'((([a-z\d]([a-z\d-]*[a-z\d])*)\.)+[a-z]{2,}|' + r'((\d{1,3}\.){3}\d{1,3}))' + r'(:\d+)?' + r'(\/[-a-z\d%_.~+]*)*' + r'(\?[;&a-z\d%_.~+=-]*)?' + r'(\#[-a-z\d_]*)?$', caseSensitive: false, - multiLine: false, ).hasMatch(this); } } diff --git a/lib/controller.dart b/lib/controller.dart index 1d948b8..683954d 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -17,6 +17,7 @@ class AppController { late ClashConfig clashConfig; late Measure measure; late Function updateClashConfigDebounce; + late Function addCheckIpNumDebounce; AppController(this.context) { appState = context.read(); @@ -25,6 +26,9 @@ class AppController { updateClashConfigDebounce = debounce(() async { await updateClashConfig(); }); + addCheckIpNumDebounce = debounce((){ + appState.checkIpNum++; + }); measure = Measure.of(context); } diff --git a/lib/fragments/backup_and_recovery.dart b/lib/fragments/backup_and_recovery.dart index 22b2426..9db13e0 100644 --- a/lib/fragments/backup_and_recovery.dart +++ b/lib/fragments/backup_and_recovery.dart @@ -6,7 +6,6 @@ import 'package:fl_clash/models/dav.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/list.dart'; -import 'package:fl_clash/widgets/section.dart'; import 'package:fl_clash/widgets/text.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -34,7 +33,7 @@ class _BackupAndRecoveryState extends State { final res = await commonScaffoldState?.loadingRun(() async { return await _client?.backup(); }); - if(res != true) return; + if (res != true) return; globalState.showMessage( title: appLocalizations.recovery, message: TextSpan(text: appLocalizations.backupSuccess), @@ -46,7 +45,7 @@ class _BackupAndRecoveryState extends State { final res = await commonScaffoldState?.loadingRun(() async { return await _client?.recovery(recoveryOption: recoveryOption); }); - if(res != true) return; + if (res != true) return; globalState.showMessage( title: appLocalizations.recovery, message: TextSpan(text: appLocalizations.recoverySuccess), @@ -69,26 +68,22 @@ class _BackupAndRecoveryState extends State { if (dav == null) { return ListView( children: [ - Section( + ListHeader( title: appLocalizations.account, - child: Builder( - builder: (_) { - return ListItem( - leading: const Icon(Icons.account_box), - title: Text(appLocalizations.noInfo), - subtitle: Text(appLocalizations.pleaseBindWebDAV), - trailing: FilledButton.tonal( - onPressed: () { - _showAddWebDAV(dav); - }, - child: Text( - appLocalizations.bind, - ), - ), - ); + ), + ListItem( + leading: const Icon(Icons.account_box), + title: Text(appLocalizations.noInfo), + subtitle: Text(appLocalizations.pleaseBindWebDAV), + trailing: FilledButton.tonal( + onPressed: () { + _showAddWebDAV(dav); }, + child: Text( + appLocalizations.bind, + ), ), - ) + ), ], ); } @@ -96,62 +91,60 @@ class _BackupAndRecoveryState extends State { final pingFuture = _client!.pingCompleter.future; return ListView( children: [ - Section( - title: appLocalizations.account, - child: ListItem( - leading: const Icon(Icons.account_box), - title: TooltipText( - text: Text( - dav.user, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + ListHeader(title: appLocalizations.account), + ListItem( + leading: const Icon(Icons.account_box), + title: TooltipText( + text: Text( + dav.user, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - subtitle: Padding( - padding: const EdgeInsets.symmetric(vertical: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text(appLocalizations.connectivity), - FutureBuilder( - future: pingFuture, - builder: (_, snapshot) { - return Center( - child: FadeBox( - key: const Key("fade_box_1"), - child: snapshot.connectionState == - ConnectionState.waiting - ? const SizedBox( - width: 12, - height: 12, - child: CircularProgressIndicator( - strokeWidth: 1, - ), - ) - : Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: snapshot.data == true - ? Colors.green - : Colors.red, - ), - width: 12, - height: 12, + ), + subtitle: Padding( + padding: const EdgeInsets.symmetric(vertical: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text(appLocalizations.connectivity), + FutureBuilder( + future: pingFuture, + builder: (_, snapshot) { + return Center( + child: FadeBox( + key: const Key("fade_box_1"), + child: snapshot.connectionState == + ConnectionState.waiting + ? const SizedBox( + width: 12, + height: 12, + child: CircularProgressIndicator( + strokeWidth: 1, ), - ), - ); - }, - ), - ], - ), + ) + : Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: snapshot.data == true + ? Colors.green + : Colors.red, + ), + width: 12, + height: 12, + ), + ), + ); + }, + ), + ], ), - trailing: FilledButton.tonal( - onPressed: () { - _showAddWebDAV(dav); - }, - child: Text( - appLocalizations.edit, - ), + ), + trailing: FilledButton.tonal( + onPressed: () { + _showAddWebDAV(dav); + }, + child: Text( + appLocalizations.edit, ), ), ), @@ -161,22 +154,21 @@ class _BackupAndRecoveryState extends State { return FadeBox( key: const Key("fade_box_2"), child: snapshot.data == true - ? Section( - title: appLocalizations.backupAndRecovery, - child: Column( - children: [ - ListItem( - onTab: _backup, - title: Text(appLocalizations.backup), - subtitle: Text(appLocalizations.backupDesc), - ), - ListItem( - onTab: _handleRecovery, - title: Text(appLocalizations.recovery), - subtitle: Text(appLocalizations.recoveryDesc), - ), - ], - ), + ? Column( + children: [ + ListHeader( + title: appLocalizations.backupAndRecovery), + ListItem( + onTab: _backup, + title: Text(appLocalizations.backup), + subtitle: Text(appLocalizations.backupDesc), + ), + ListItem( + onTab: _handleRecovery, + title: Text(appLocalizations.recovery), + subtitle: Text(appLocalizations.recoveryDesc), + ), + ], ) : Container(), ); @@ -228,7 +220,6 @@ class _WebDAVFormDialogState extends State { Navigator.pop(context); } - @override void dispose() { super.dispose(); diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index 8c49620..a3dab7b 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -137,335 +137,302 @@ class _ConfigFragmentState extends State { } } - Widget _buildAppSection() { - final items = [ - if (Platform.isAndroid) - Selector( - selector: (_, config) => config.allowBypass, - builder: (_, allowBypass, __) { - return ListItem.switchItem( - leading: const Icon(Icons.arrow_forward_outlined), - title: Text(appLocalizations.allowBypass), - subtitle: Text(appLocalizations.allowBypassDesc), - delegate: SwitchDelegate( - value: allowBypass, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.config.allowBypass = value; - }, - ), - ); - }, - ), - if (Platform.isAndroid) - Selector( - selector: (_, config) => config.systemProxy, - builder: (_, systemProxy, __) { - return ListItem.switchItem( - leading: const Icon(Icons.settings_ethernet), - title: Text(appLocalizations.systemProxy), - subtitle: Text(appLocalizations.systemProxyDesc), - delegate: SwitchDelegate( - value: systemProxy, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.config.systemProxy = value; - }, - ), - ); - }, - ), - Selector( - selector: (_, config) => config.isCompatible, - builder: (_, isCompatible, __) { - return ListItem.switchItem( - leading: const Icon(Icons.expand_outlined), - title: Text(appLocalizations.compatible), - subtitle: Text(appLocalizations.compatibleDesc), - delegate: SwitchDelegate( - value: isCompatible, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.config.isCompatible = value; - await appController.applyProfile(); - }, - ), - ); - }, - ), - ]; - return Section( + List _buildAppSection() { + return generateSection( title: appLocalizations.app, - child: Column( - children: [ - for (final item in items) ...[ - item, - if (items.last != item) - const Divider( - height: 0, - ) - ] - ], - ), + items: [ + if (Platform.isAndroid) + Selector( + selector: (_, config) => config.allowBypass, + builder: (_, allowBypass, __) { + return ListItem.switchItem( + leading: const Icon(Icons.arrow_forward_outlined), + title: Text(appLocalizations.allowBypass), + subtitle: Text(appLocalizations.allowBypassDesc), + delegate: SwitchDelegate( + value: allowBypass, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.config.allowBypass = value; + }, + ), + ); + }, + ), + if (Platform.isAndroid) + Selector( + selector: (_, config) => config.systemProxy, + builder: (_, systemProxy, __) { + return ListItem.switchItem( + leading: const Icon(Icons.settings_ethernet), + title: Text(appLocalizations.systemProxy), + subtitle: Text(appLocalizations.systemProxyDesc), + delegate: SwitchDelegate( + value: systemProxy, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.config.systemProxy = value; + }, + ), + ); + }, + ), + Selector( + selector: (_, config) => config.isCompatible, + builder: (_, isCompatible, __) { + return ListItem.switchItem( + leading: const Icon(Icons.expand_outlined), + title: Text(appLocalizations.compatible), + subtitle: Text(appLocalizations.compatibleDesc), + delegate: SwitchDelegate( + value: isCompatible, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.config.isCompatible = value; + await appController.applyProfile(); + }, + ), + ); + }, + ), + ], ); } - Widget _buildGeneralSection() { - final items = [ - Selector( - selector: (_, clashConfig) => clashConfig.logLevel, - builder: (_, value, __) { - return ListItem( - leading: const Icon(Icons.info_outline), - title: Text(appLocalizations.logLevel), - subtitle: Text(value.name), - onTab: () { - _showLogLevelDialog(value); - }, - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.globalRealUa, - builder: (_, value, __) { - return ListItem( - leading: const Icon(Icons.computer_outlined), - title: const Text("UA"), - subtitle: Text(value ?? appLocalizations.defaultText), - onTab: () { - _showUaDialog(value); - }, - ); - }, - ), - Selector( - selector: (_, config) => config.testUrl, - builder: (_, value, __) { - return ListItem( - leading: const Icon(Icons.timeline), - title: Text(appLocalizations.testUrl), - subtitle: Text(value), - onTab: () { - _modifyTestUrl(value); - }, - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.mixedPort, - builder: (_, mixedPort, __) { - return ListItem( - onTab: () { - _modifyMixedPort(mixedPort); - }, - leading: const Icon(Icons.adjust_outlined), - title: Text(appLocalizations.proxyPort), - subtitle: Text(appLocalizations.proxyPortDesc), - trailing: FilledButton.tonal( - onPressed: () { + List _buildGeneralSection() { + return generateSection( + title: appLocalizations.general, + items: [ + Selector( + selector: (_, clashConfig) => clashConfig.logLevel, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.info_outline), + title: Text(appLocalizations.logLevel), + subtitle: Text(value.name), + onTab: () { + _showLogLevelDialog(value); + }, + ); + }, + ), + Selector( + selector: (_, clashConfig) => clashConfig.globalRealUa, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.computer_outlined), + title: const Text("UA"), + subtitle: Text(value ?? appLocalizations.defaultText), + onTab: () { + _showUaDialog(value); + }, + ); + }, + ), + Selector( + selector: (_, config) => config.testUrl, + builder: (_, value, __) { + return ListItem( + leading: const Icon(Icons.timeline), + title: Text(appLocalizations.testUrl), + subtitle: Text(value), + onTab: () { + _modifyTestUrl(value); + }, + ); + }, + ), + Selector( + selector: (_, clashConfig) => clashConfig.mixedPort, + builder: (_, mixedPort, __) { + return ListItem( + onTab: () { _modifyMixedPort(mixedPort); }, - child: Text( - "$mixedPort", + leading: const Icon(Icons.adjust_outlined), + title: Text(appLocalizations.proxyPort), + subtitle: Text(appLocalizations.proxyPortDesc), + trailing: FilledButton.tonal( + onPressed: () { + _modifyMixedPort(mixedPort); + }, + child: Text( + "$mixedPort", + ), ), - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.ipv6, - builder: (_, ipv6, __) { - return ListItem.switchItem( - leading: const Icon(Icons.water_outlined), - title: const Text("IPv6"), - subtitle: Text(appLocalizations.ipv6Desc), - delegate: SwitchDelegate( - value: ipv6, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.clashConfig.ipv6 = value; - appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.allowLan, - builder: (_, allowLan, __) { - return ListItem.switchItem( - leading: const Icon(Icons.device_hub), - title: Text(appLocalizations.allowLan), - subtitle: Text(appLocalizations.allowLanDesc), - delegate: SwitchDelegate( - value: allowLan, - onChanged: (bool value) async { - final clashConfig = context.read(); - clashConfig.allowLan = value; - globalState.appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.unifiedDelay, - builder: (_, unifiedDelay, __) { - return ListItem.switchItem( - leading: const Icon(Icons.compress_outlined), - title: Text(appLocalizations.unifiedDelay), - subtitle: Text(appLocalizations.unifiedDelayDesc), - delegate: SwitchDelegate( - value: unifiedDelay, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.clashConfig.unifiedDelay = value; - appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => - clashConfig.findProcessMode == FindProcessMode.always, - builder: (_, findProcess, __) { - return ListItem.switchItem( - leading: const Icon(Icons.polymer_outlined), - title: Text(appLocalizations.findProcessMode), - subtitle: Text(appLocalizations.findProcessModeDesc), - delegate: SwitchDelegate( - value: findProcess, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.clashConfig.findProcessMode = - value ? FindProcessMode.always : FindProcessMode.off; - appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.tcpConcurrent, - builder: (_, tcpConcurrent, __) { - return ListItem.switchItem( - leading: const Icon(Icons.double_arrow_outlined), - title: Text(appLocalizations.tcpConcurrent), - subtitle: Text(appLocalizations.tcpConcurrentDesc), - delegate: SwitchDelegate( - value: tcpConcurrent, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.clashConfig.tcpConcurrent = value; - appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => - clashConfig.geodataLoader == geodataLoaderMemconservative, - builder: (_, memconservative, __) { - return ListItem.switchItem( - leading: const Icon(Icons.memory), - title: Text(appLocalizations.geodataLoader), - subtitle: Text(appLocalizations.geodataLoaderDesc), - delegate: SwitchDelegate( - value: memconservative, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.clashConfig.geodataLoader = value - ? geodataLoaderMemconservative - : geodataLoaderStandard; - appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - Selector( - selector: (_, clashConfig) => clashConfig.externalController.isNotEmpty, - builder: (_, hasExternalController, __) { - return ListItem.switchItem( - leading: const Icon(Icons.api_outlined), - title: Text(appLocalizations.externalController), - subtitle: Text(appLocalizations.externalControllerDesc), - delegate: SwitchDelegate( - value: hasExternalController, - onChanged: (bool value) async { - final appController = globalState.appController; - appController.clashConfig.externalController = - value ? defaultExternalController : ''; - appController.updateClashConfigDebounce(); - }, - ), - ); - }, - ), - ]; - return Section( - title: appLocalizations.general, - child: Column( - children: [ - for (final item in items) ...[ - item, - if (items.last != item) - const Divider( - height: 0, - ) - ] - ], - ), - ); - } - - Widget _buildMoreSection() { - final items = [ - if (system.isDesktop) + ); + }, + ), Selector( - selector: (_, clashConfig) => clashConfig.tun.enable, - builder: (_, tunEnable, __) { + selector: (_, clashConfig) => clashConfig.ipv6, + builder: (_, ipv6, __) { return ListItem.switchItem( - leading: const Icon(Icons.important_devices_outlined), - title: Text(appLocalizations.tun), - subtitle: Text(appLocalizations.tunDesc), + leading: const Icon(Icons.water_outlined), + title: const Text("IPv6"), + subtitle: Text(appLocalizations.ipv6Desc), delegate: SwitchDelegate( - value: tunEnable, + value: ipv6, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.clashConfig.ipv6 = value; + appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => clashConfig.allowLan, + builder: (_, allowLan, __) { + return ListItem.switchItem( + leading: const Icon(Icons.device_hub), + title: Text(appLocalizations.allowLan), + subtitle: Text(appLocalizations.allowLanDesc), + delegate: SwitchDelegate( + value: allowLan, onChanged: (bool value) async { final clashConfig = context.read(); - clashConfig.tun = Tun(enable: value); + clashConfig.allowLan = value; globalState.appController.updateClashConfigDebounce(); }, ), ); }, ), - ]; - if (items.isEmpty) return Container(); - return Section( + Selector( + selector: (_, clashConfig) => clashConfig.unifiedDelay, + builder: (_, unifiedDelay, __) { + return ListItem.switchItem( + leading: const Icon(Icons.compress_outlined), + title: Text(appLocalizations.unifiedDelay), + subtitle: Text(appLocalizations.unifiedDelayDesc), + delegate: SwitchDelegate( + value: unifiedDelay, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.clashConfig.unifiedDelay = value; + appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => + clashConfig.findProcessMode == FindProcessMode.always, + builder: (_, findProcess, __) { + return ListItem.switchItem( + leading: const Icon(Icons.polymer_outlined), + title: Text(appLocalizations.findProcessMode), + subtitle: Text(appLocalizations.findProcessModeDesc), + delegate: SwitchDelegate( + value: findProcess, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.clashConfig.findProcessMode = + value ? FindProcessMode.always : FindProcessMode.off; + appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => clashConfig.tcpConcurrent, + builder: (_, tcpConcurrent, __) { + return ListItem.switchItem( + leading: const Icon(Icons.double_arrow_outlined), + title: Text(appLocalizations.tcpConcurrent), + subtitle: Text(appLocalizations.tcpConcurrentDesc), + delegate: SwitchDelegate( + value: tcpConcurrent, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.clashConfig.tcpConcurrent = value; + appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => + clashConfig.geodataLoader == geodataLoaderMemconservative, + builder: (_, memconservative, __) { + return ListItem.switchItem( + leading: const Icon(Icons.memory), + title: Text(appLocalizations.geodataLoader), + subtitle: Text(appLocalizations.geodataLoaderDesc), + delegate: SwitchDelegate( + value: memconservative, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.clashConfig.geodataLoader = value + ? geodataLoaderMemconservative + : geodataLoaderStandard; + appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + Selector( + selector: (_, clashConfig) => + clashConfig.externalController.isNotEmpty, + builder: (_, hasExternalController, __) { + return ListItem.switchItem( + leading: const Icon(Icons.api_outlined), + title: Text(appLocalizations.externalController), + subtitle: Text(appLocalizations.externalControllerDesc), + delegate: SwitchDelegate( + value: hasExternalController, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.clashConfig.externalController = + value ? defaultExternalController : ''; + appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + ], + ); + } + + List _buildMoreSection() { + return generateSection( title: appLocalizations.more, - child: Column( - children: [ - for (final item in items) ...[ - item, - if (items.last != item) - const Divider( - height: 0, - ) - ] - ], - ), + items: [ + if (system.isDesktop) + Selector( + selector: (_, clashConfig) => clashConfig.tun.enable, + builder: (_, tunEnable, __) { + return ListItem.switchItem( + leading: const Icon(Icons.important_devices_outlined), + title: Text(appLocalizations.tun), + subtitle: Text(appLocalizations.tunDesc), + delegate: SwitchDelegate( + value: tunEnable, + onChanged: (bool value) async { + final clashConfig = context.read(); + clashConfig.tun = Tun(enable: value); + globalState.appController.updateClashConfigDebounce(); + }, + ), + ); + }, + ), + ], ); } @override Widget build(BuildContext context) { List items = [ - _buildAppSection(), - _buildGeneralSection(), - _buildMoreSection(), + ..._buildAppSection(), + ..._buildGeneralSection(), + ..._buildMoreSection(), ]; return ListView.builder( padding: const EdgeInsets.only(bottom: 32), diff --git a/lib/fragments/connections.dart b/lib/fragments/connections.dart index 27cf7cc..771faa3 100644 --- a/lib/fragments/connections.dart +++ b/lib/fragments/connections.dart @@ -139,8 +139,8 @@ class _ConnectionsFragmentState extends State { vertical: 16, ), child: Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final keyword in state.keywords) CommonChip( @@ -219,6 +219,10 @@ class ConnectionItem extends StatelessWidget { @override Widget build(BuildContext context) { return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), tileTitleAlignment: ListTileTitleAlignment.titleHeight, leading: Platform.isAndroid ? Container( @@ -249,17 +253,17 @@ class ConnectionItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( - height: 12, + height: 8, ), Text( _getSourceText(connection), ), const SizedBox( - height: 12, + height: 8, ), Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final chain in connection.chains) CommonChip( @@ -271,9 +275,6 @@ class ConnectionItem extends StatelessWidget { ), ], ), - const SizedBox( - height: 12, - ), ], ), trailing: IconButton( @@ -394,8 +395,8 @@ class ConnectionsSearchDelegate extends SearchDelegate { vertical: 16, ), child: Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final keyword in state.keywords) CommonChip( diff --git a/lib/fragments/logs.dart b/lib/fragments/logs.dart index c2e0dd4..bbe2d88 100644 --- a/lib/fragments/logs.dart +++ b/lib/fragments/logs.dart @@ -269,8 +269,8 @@ class LogsSearchDelegate extends SearchDelegate { vertical: 16, ), child: Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final keyword in state.keywords) CommonChip( @@ -328,26 +328,23 @@ class _LogItemState extends State { @override Widget build(BuildContext context) { final log = widget.log; - return ListTile( + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), title: SelectableText(log.payload ?? ''), subtitle: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only( - top: 8, - ), - child: SelectableText( - "${log.dateTime}", - style: context.textTheme.bodySmall - ?.copyWith(color: context.colorScheme.primary), - ), + SelectableText( + "${log.dateTime}", + style: context.textTheme.bodySmall + ?.copyWith(color: context.colorScheme.primary), ), + const SizedBox(height: 8,), Container( alignment: Alignment.centerLeft, - padding: const EdgeInsets.symmetric( - vertical: 8, - ), child: CommonChip( onPressed: () { if (widget.onClick == null) return; diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index 4df82b3..04e3275 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -3,7 +3,6 @@ import 'package:fl_clash/fragments/profiles/edit_profile.dart'; import 'package:fl_clash/fragments/profiles/view_profile.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; @@ -138,33 +137,30 @@ class _ProfilesFragmentState extends State { .map((profile) => GlobalObjectKey<_ProfileItemState>(profile.id)) .toList(); final columns = _getColumns(state.viewMode); - final isMobile = state.viewMode == ViewMode.mobile; return Align( alignment: Alignment.topCenter, child: NotificationListener( onNotification: (scrollNotification) { - WidgetsBinding.instance.addPostFrameCallback((_) { - hasPadding.value = - scrollNotification.metrics.maxScrollExtent > 0; - }); + WidgetsBinding.instance.addPostFrameCallback( + (_) { + hasPadding.value = + scrollNotification.metrics.maxScrollExtent > 0; + }, + ); return true; }, child: ValueListenableBuilder( valueListenable: hasPadding, builder: (_, hasPadding, __) { return SingleChildScrollView( - padding: !isMobile - ? EdgeInsets.only( - left: 16, - right: 16, - top: 16, - bottom: 16 + (hasPadding ? 56 : 0), - ) - : EdgeInsets.only( - bottom: 0 + (hasPadding ? 56 : 0), - ), + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 16, + bottom: 16 + (hasPadding ? 56 : 0), + ), child: Grid( - mainAxisSpacing: isMobile ? 8 : 16, + mainAxisSpacing: 16, crossAxisSpacing: 16, crossAxisCount: columns, children: [ @@ -225,6 +221,8 @@ class _ProfileItemState extends State { isUpdating.value = false; if (!isSingle) { return e.toString(); + } else { + rethrow; } } isUpdating.value = false; @@ -412,16 +410,7 @@ class _ProfileItemState extends State { final profile = widget.profile; final groupValue = widget.groupValue; final onChanged = widget.onChanged; - return Selector( - selector: (_, appState) => appState.viewMode, - builder: (_, viewMode, child) { - if (viewMode == ViewMode.mobile) { - return child!; - } - return CommonCard( - child: child!, - ); - }, + return CommonCard( child: ListItem.radio( key: Key(profile.id), horizontalTitleGap: 16, diff --git a/lib/fragments/proxies.dart b/lib/fragments/proxies.dart index ebb56bc..6c6fba4 100644 --- a/lib/fragments/proxies.dart +++ b/lib/fragments/proxies.dart @@ -1,4 +1,3 @@ -import 'dart:io'; import 'dart:math'; import 'package:collection/collection.dart'; @@ -280,7 +279,6 @@ class ProxyGroupView extends StatefulWidget { class _ProxyGroupViewState extends State { var isLock = false; - final isBoundaryNotifier = ValueNotifier(false); final scrollController = ScrollController(); var isEnd = false; @@ -374,53 +372,6 @@ class _ProxyGroupViewState extends State { ); } - Widget _androidExpansionHandle(Widget child) { - // return NotificationListener( - // onNotification: (ScrollNotification notification) { - // if (notification is ScrollEndNotification) { - // if (notification.metrics.atEdge) { - // isEnd = notification.metrics.pixels == - // notification.metrics.maxScrollExtent; - // isBoundaryNotifier.value = true; - // } - // } - // return false; - // }, - // child: Listener( - // onPointerMove: (details) { - // double yOffset = details.delta.dy; - // final isEnd = scrollController.position.maxScrollExtent == scrollController.position.pixels; - // final isTop = scrollController.position.minScrollExtent == scrollController.position.pixels; - // if(isEnd || isTop){ - // isBoundaryNotifier.value = true; - // } else if (yOffset > 0 && scrollController.position.maxScrollExtent == scrollController.position.pixels) { - // isBoundaryNotifier.value = false; - // } else if (yOffset < 0 && !isEnd) { - // isBoundaryNotifier.value = false; - // } - // }, - // child: child, - // ), - // ); - return Listener( - onPointerMove: (details) { - double yOffset = details.delta.dy; - final isEnd = scrollController.position.maxScrollExtent == - scrollController.position.pixels; - final isTop = scrollController.position.minScrollExtent == - scrollController.position.pixels; - if (isEnd && yOffset < 0) { - isBoundaryNotifier.value = true; - } else if (isTop && yOffset > 0) { - isBoundaryNotifier.value = true; - } else { - isBoundaryNotifier.value = false; - } - }, - child: child, - ); - } - Widget _buildExpansionGroupView({ required List proxies, required int columns, @@ -544,75 +495,32 @@ class _ProxyGroupViewState extends State { children: [ SizedBox( height: height, - child: Platform.isAndroid - ? _androidExpansionHandle( - ValueListenableBuilder( - valueListenable: isBoundaryNotifier, - builder: (_, isBoundary, child) { - return Scrollbar( - thickness: 6, - interactive: true, - radius: const Radius.circular(6), - child: GridView.builder( - key: widget.key, - controller: scrollController, - physics: isBoundary || !hasScrollable - ? const NeverScrollableScrollPhysics() - : const AlwaysScrollableScrollPhysics(), - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: _getItemHeight(proxyCardType), - ), - itemCount: sortedProxies.length, - itemBuilder: (_, index) { - final proxy = sortedProxies[index]; - return _currentProxyNameBuilder( - builder: (value) { - return ProxyCard( - style: CommonCardType.filled, - type: proxyCardType, - isSelected: value == proxy.name, - key: ValueKey('$groupName.${proxy.name}'), - proxy: proxy, - groupName: groupName, - ); - }); - }, - ), - ); - }, - ), - ) - : GridView.builder( - key: widget.key, - controller: scrollController, - physics: !hasScrollable - ? const NeverScrollableScrollPhysics() - : const AlwaysScrollableScrollPhysics(), - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: columns, - mainAxisSpacing: 8, - crossAxisSpacing: 8, - mainAxisExtent: _getItemHeight(proxyCardType), - ), - itemCount: sortedProxies.length, - itemBuilder: (_, index) { - final proxy = sortedProxies[index]; - return _currentProxyNameBuilder(builder: (value) { - return ProxyCard( - style: CommonCardType.filled, - type: proxyCardType, - isSelected: value == proxy.name, - key: ValueKey('$groupName.${proxy.name}'), - proxy: proxy, - groupName: groupName, - ); - }); - }, - ), + child: GridView.builder( + key: widget.key, + controller: scrollController, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: columns, + mainAxisSpacing: 8, + crossAxisSpacing: 8, + mainAxisExtent: _getItemHeight(proxyCardType), + ), + itemCount: sortedProxies.length, + itemBuilder: (_, index) { + final proxy = sortedProxies[index]; + return _currentProxyNameBuilder( + builder: (value) { + return ProxyCard( + style: CommonCardType.filled, + type: proxyCardType, + isSelected: value == proxy.name, + key: ValueKey('$groupName.${proxy.name}'), + proxy: proxy, + groupName: groupName, + ); + }, + ); + }, + ), ), ], ), @@ -624,7 +532,6 @@ class _ProxyGroupViewState extends State { @override void dispose() { super.dispose(); - isBoundaryNotifier.dispose(); scrollController.dispose(); } diff --git a/lib/fragments/requests.dart b/lib/fragments/requests.dart index 7ff038a..b80b707 100644 --- a/lib/fragments/requests.dart +++ b/lib/fragments/requests.dart @@ -137,8 +137,8 @@ class _RequestsFragmentState extends State { vertical: 16, ), child: Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final keyword in state.keywords) CommonChip( @@ -214,6 +214,10 @@ class RequestItem extends StatelessWidget { @override Widget build(BuildContext context) { return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), tileTitleAlignment: ListTileTitleAlignment.titleHeight, leading: Platform.isAndroid ? Container( @@ -244,17 +248,17 @@ class RequestItem extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox( - height: 12, + height: 8, ), Text( _getSourceText(connection), ), const SizedBox( - height: 12, + height: 8, ), Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final chain in connection.chains) CommonChip( @@ -266,9 +270,6 @@ class RequestItem extends StatelessWidget { ), ], ), - const SizedBox( - height: 12, - ), ], ), ); @@ -375,8 +376,8 @@ class RequestsSearchDelegate extends SearchDelegate { vertical: 16, ), child: Wrap( - runSpacing: 8, - spacing: 8, + runSpacing: 6, + spacing: 6, children: [ for (final keyword in state.keywords) CommonChip( diff --git a/lib/fragments/resources.dart b/lib/fragments/resources.dart index 4228684..6043d29 100644 --- a/lib/fragments/resources.dart +++ b/lib/fragments/resources.dart @@ -2,7 +2,8 @@ import 'dart:io'; import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/models/ffi.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart' hide context; @@ -18,6 +19,17 @@ class GeoItem { }); } +@immutable +class FileInfo { + final String size; + final DateTime lastModified; + + const FileInfo({ + required this.size, + required this.lastModified, + }); +} + class Resources extends StatefulWidget { const Resources({super.key}); @@ -26,147 +38,356 @@ class Resources extends StatefulWidget { } class _ResourcesState extends State { - _updateExternalProvider( - String providerName, - String providerType, - ) async { - final commonScaffoldState = context.commonScaffoldState; - await commonScaffoldState?.loadingRun(() async { - final message = await clashCore.updateExternalProvider( - providerName: providerName, - providerType: providerType, - ); - if (message.isNotEmpty) throw message; + List externalProviders = []; + + List> providerItemKeys = []; + + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _syncExternalProviders(); }); + } + + _syncExternalProviders() async { + externalProviders = await clashCore.getExternalProviders(); setState(() {}); } - Future _getGeoFileLastModified(String fileName) async { - final homePath = await appPath.getHomeDirPath(); - return await File(join(homePath, fileName)).lastModified(); - } - - Widget _buildExternalProviderSection() { - return FutureBuilder>( - future: () async { - await Future.delayed(const Duration(milliseconds: 200)); - return await clashCore.getExternalProviders(); - }(), - builder: (_, snapshot) { - return Center( - child: FadeBox( - key: const Key("external_providers"), - child: snapshot.data == null || snapshot.data!.isEmpty - ? Container() - : Section( - title: appLocalizations.externalResources, - child: Column( - children: [ - for (final externalProvider in snapshot.data!) - ListItem( - title: Text(externalProvider.name), - subtitle: Text( - "${externalProvider.type} (${externalProvider.vehicleType})", - ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - externalProvider.updateAt.lastUpdateTimeDesc, - style: context.textTheme.bodyMedium, - ), - const Padding( - padding: EdgeInsets.only(left: 12,right: 4), - child: VerticalDivider( - endIndent: 6, - width: 4, - indent: 6, - ), - ), - externalProvider.vehicleType == "HTTP" - ? IconButton( - icon: const Icon(Icons.sync), - onPressed: () { - _updateExternalProvider( - externalProvider.name, - externalProvider.type, - ); - }, - ) - : Container(), - ], - ), - ) - ], - ), - ), - ), - ); - }, + _updateProviders() async { + print(providerItemKeys); + final updateProviders = providerItemKeys.map( + (key) async => await key.currentState?.updateProvider(false), ); + await Future.wait(updateProviders); + _syncExternalProviders(); } - Widget _buildGeoDataSection() { + List _buildExternalProviderSection() { + List> keys = []; + final res = generateSection( + title: appLocalizations.externalResources, + actions: [ + IconButton( + onPressed: () { + _updateProviders(); + }, + icon: const Icon( + Icons.sync, + ), + ) + ], + items: externalProviders.map( + (externalProvider) { + final key = + GlobalObjectKey<_ProviderItemState>(externalProvider.name); + keys.add(key); + return ProviderItem( + key: key, + provider: externalProvider, + onUpdated: () { + _syncExternalProviders(); + }, + ); + }, + ), + ); + providerItemKeys = keys; + return res; + } + + List _buildGeoDataSection() { const geoItems = [ GeoItem(label: "GeoIp", fileName: mmdbFileName), GeoItem(label: "GeoSite", fileName: geoSiteFileName), GeoItem(label: "ASN", fileName: asnFileName), ]; - return Section( + return generateSection( title: appLocalizations.geoData, - child: Column( - children: [ - for (final geoItem in geoItems) - ListItem( - title: Text(geoItem.label), - subtitle: FutureBuilder( - future: () async { - await Future.delayed(const Duration(milliseconds: 200)); - return await _getGeoFileLastModified(geoItem.fileName); - }(), - builder: (_, snapshot) { - return Container( - alignment: Alignment.centerLeft, - height: 24, - child: FadeBox( - key: Key("fade_box_${geoItem.label}"), - child: snapshot.data == null - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - strokeWidth: 2, - ), - ) - : Text( - snapshot.data!.lastUpdateTimeDesc, - ), - ), - ); - }, - ), - trailing: IconButton( - icon: const Icon(Icons.sync), - onPressed: () { - _updateExternalProvider( - geoItem.fileName, - geoItem.label, - ); - }, - ), - ), - ], + items: geoItems.map( + (geoItem) => GeoDataListItem( + geoItem: geoItem, + ), ), ); } @override Widget build(BuildContext context) { - return ListView( - children: [ - _buildGeoDataSection(), - _buildExternalProviderSection(), + return generateListView( + [ + ..._buildGeoDataSection(), + ..._buildExternalProviderSection(), ], ); } } + +class GeoDataListItem extends StatefulWidget { + final GeoItem geoItem; + + const GeoDataListItem({ + super.key, + required this.geoItem, + }); + + @override + State createState() => _GeoDataListItemState(); +} + +class _GeoDataListItemState extends State { + final isUpdating = ValueNotifier(false); + + GeoItem get geoItem => widget.geoItem; + + Future _getGeoFileLastModified(String fileName) async { + final homePath = await appPath.getHomeDirPath(); + final file = File(join(homePath, fileName)); + final lastModified = await file.lastModified(); + final size = await file.length(); + return FileInfo( + size: TrafficValue(value: size).show, + lastModified: lastModified, + ); + } + + // _uploadGeoFile(String fileName) async { + // final res = await picker.pickerGeoDataFile(); + // if (res == null || res.bytes == null) return; + // final homePath = await appPath.getHomeDirPath(); + // final file = File(join(homePath, fileName)); + // await file.writeAsBytes( + // res.bytes!, + // flush: true, + // ); + // setState(() {}); + // } + + String _buildFileInfoDesc(FileInfo fileInfo) { + return "${fileInfo.size} · ${fileInfo.lastModified.lastUpdateTimeDesc}"; + } + + Widget _buildSubtitle() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + FutureBuilder( + future: _getGeoFileLastModified(geoItem.fileName), + builder: (_, snapshot) { + return SizedBox( + height: 24, + child: FadeBox( + key: Key("fade_box_${geoItem.label}"), + child: snapshot.data == null + ? const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator( + strokeWidth: 2, + ), + ) + : Text( + _buildFileInfoDesc(snapshot.data!), + ), + ), + ); + }, + ), + const SizedBox( + height: 8, + ), + Wrap( + runSpacing: 6, + spacing: 12, + children: [ + // CommonChip( + // avatar: const Icon(Icons.upload), + // label: "编辑", + // onPressed: () { + // _uploadGeoFile(geoItem.fileName); + // }, + // ), + CommonChip( + avatar: const Icon(Icons.sync), + label: appLocalizations.sync, + onPressed: () { + _handleUpdateGeoDataItem(); + }, + ), + ], + ), + ], + ); + } + + _handleUpdateGeoDataItem() async { + await globalState.safeRun(updateGeoDateItem); + setState(() {}); + } + + updateGeoDateItem() async { + isUpdating.value = true; + try { + final message = await clashCore.updateExternalProvider( + providerName: geoItem.fileName, + providerType: geoItem.label, + ); + if (message.isNotEmpty) throw message; + } catch (e) { + isUpdating.value = false; + rethrow; + } + isUpdating.value = false; + return null; + } + + @override + void dispose() { + super.dispose(); + isUpdating.dispose(); + } + + @override + Widget build(BuildContext context) { + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + title: Text(geoItem.label), + subtitle: _buildSubtitle(), + trailing: SizedBox( + height: 48, + width: 48, + child: ValueListenableBuilder( + valueListenable: isUpdating, + builder: (_, isUpdating, ___) { + return FadeBox( + child: isUpdating + ? const Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : const SizedBox(), + ); + }, + ), + ), + ); + } +} + +class ProviderItem extends StatefulWidget { + final ExternalProvider provider; + final Function onUpdated; + + const ProviderItem({ + super.key, + required this.provider, + required this.onUpdated, + }); + + @override + State createState() => _ProviderItemState(); +} + +class _ProviderItemState extends State { + final isUpdating = ValueNotifier(false); + + ExternalProvider get provider => widget.provider; + + _handleUpdateProfile() async { + await globalState.safeRun(updateProvider); + widget.onUpdated(); + } + + updateProvider([isSingle = true]) async { + if (provider.vehicleType != "HTTP") return; + isUpdating.value = true; + try { + final message = await clashCore.updateExternalProvider( + providerName: provider.name, + providerType: provider.type, + ); + if (message.isNotEmpty) throw message; + } catch (e) { + isUpdating.value = false; + if (!isSingle) { + return e.toString(); + } else { + rethrow; + } + } + isUpdating.value = false; + return null; + } + + String _buildProviderDesc() { + return "${provider.type} (${provider.vehicleType}) · ${provider.updateAt.lastUpdateTimeDesc}"; + } + + @override + void dispose() { + super.dispose(); + isUpdating.dispose(); + } + + Widget _buildSubtitle() { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox( + height: 4, + ), + Text( + _buildProviderDesc(), + ), + if (provider.vehicleType == "HTTP") ...[ + const SizedBox( + height: 8, + ), + CommonChip( + avatar: const Icon(Icons.sync), + label: appLocalizations.sync, + onPressed: () { + _handleUpdateProfile(); + }, + ), + ], + ], + ); + } + + @override + Widget build(BuildContext context) { + return ListItem( + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 4, + ), + title: Text(provider.name), + subtitle: _buildSubtitle(), + trailing: SizedBox( + height: 48, + width: 48, + child: ValueListenableBuilder( + valueListenable: isUpdating, + builder: (_, isUpdating, ___) { + return FadeBox( + child: isUpdating + ? const Padding( + padding: EdgeInsets.all(8), + child: CircularProgressIndicator(), + ) + : const SizedBox(), + ); + }, + ), + ), + ); + } +} diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart index 188f9f7..d2e7600 100644 --- a/lib/fragments/tools.dart +++ b/lib/fragments/tools.dart @@ -57,138 +57,120 @@ class _ToolboxFragmentState extends State { return Intl.message(locale.toString()); } - Widget _getOtherList() { - List items = [ - ListItem.open( - leading: const Icon(Icons.info), - title: Text(appLocalizations.about), - delegate: OpenDelegate( - title: appLocalizations.about, - widget: const AboutFragment(), + List _getOtherList() { + return generateSection( + title: appLocalizations.other, + items: [ + ListItem.open( + leading: const Icon(Icons.info), + title: Text(appLocalizations.about), + delegate: OpenDelegate( + title: appLocalizations.about, + widget: const AboutFragment(), + ), ), - ), - ]; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final item in items) ...[ - item, - if (item != items.last) - const Divider( - height: 0, - ), - ] ], ); } - Widget _getSettingList() { - List items = [ - Selector( - selector: (_, config) => config.locale, - builder: (_, localeString, __) { - final subTitle = localeString ?? appLocalizations.defaultText; - final currentLocale = other.getLocaleForString(localeString); - return ListTile( - leading: const Icon(Icons.language_outlined), - title: Text(appLocalizations.language), - subtitle: Text(Intl.message(subTitle)), - onTap: () { - globalState.showCommonDialog( - child: AlertDialog( - title: Text(appLocalizations.language), - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - for (final locale in [ - null, - ...AppLocalizations.delegate.supportedLocales - ]) - ListItem.radio( - delegate: RadioDelegate( - value: locale, - groupValue: currentLocale, - onChanged: (Locale? value) { - final config = context.read(); - config.locale = value?.toString(); - Navigator.of(context).pop(); - }, - ), - title: Text(_getLocaleString(locale)), - ) - ], + List _getSettingList() { + return generateSection( + title: appLocalizations.settings, + items: [ + Selector( + selector: (_, config) => config.locale, + builder: (_, localeString, __) { + final subTitle = localeString ?? appLocalizations.defaultText; + final currentLocale = other.getLocaleForString(localeString); + return ListTile( + leading: const Icon(Icons.language_outlined), + title: Text(appLocalizations.language), + subtitle: Text(Intl.message(subTitle)), + onTap: () { + globalState.showCommonDialog( + child: AlertDialog( + title: Text(appLocalizations.language), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + content: SizedBox( + width: 250, + child: Wrap( + children: [ + for (final locale in [ + null, + ...AppLocalizations.delegate.supportedLocales + ]) + ListItem.radio( + delegate: RadioDelegate( + value: locale, + groupValue: currentLocale, + onChanged: (Locale? value) { + final config = context.read(); + config.locale = value?.toString(); + Navigator.of(context).pop(); + }, + ), + title: Text(_getLocaleString(locale)), + ) + ], + ), ), ), - ), - ); - }, - ); - }, - ), - ListItem.open( - leading: const Icon(Icons.style), - title: Text(appLocalizations.theme), - subtitle: Text(appLocalizations.themeDesc), - delegate: OpenDelegate( - title: appLocalizations.theme, - widget: const ThemeFragment(), - extendPageWidth: 360, + ); + }, + ); + }, ), - ), - ListItem.open( - leading: const Icon(Icons.cloud_sync), - title: Text(appLocalizations.backupAndRecovery), - subtitle: Text(appLocalizations.backupAndRecoveryDesc), - delegate: OpenDelegate( - title: appLocalizations.backupAndRecovery, - widget: const BackupAndRecovery(), - ), - ), - if (Platform.isAndroid) ListItem.open( - leading: const Icon(Icons.view_list), - title: Text(appLocalizations.accessControl), - subtitle: Text(appLocalizations.accessControlDesc), + leading: const Icon(Icons.style), + title: Text(appLocalizations.theme), + subtitle: Text(appLocalizations.themeDesc), delegate: OpenDelegate( - title: appLocalizations.appAccessControl, - widget: const AccessFragment(), + title: appLocalizations.theme, + widget: const ThemeFragment(), + extendPageWidth: 360, ), ), - ListItem.open( - leading: const Icon(Icons.edit), - title: Text(appLocalizations.override), - subtitle: Text(appLocalizations.overrideDesc), - delegate: OpenDelegate( - title: appLocalizations.override, - widget: const ConfigFragment(), - extendPageWidth: 360, + ListItem.open( + leading: const Icon(Icons.cloud_sync), + title: Text(appLocalizations.backupAndRecovery), + subtitle: Text(appLocalizations.backupAndRecoveryDesc), + delegate: OpenDelegate( + title: appLocalizations.backupAndRecovery, + widget: const BackupAndRecovery(), + ), ), - ), - ListItem.open( - leading: const Icon(Icons.settings_applications), - title: Text(appLocalizations.application), - subtitle: Text(appLocalizations.applicationDesc), - delegate: OpenDelegate( - title: appLocalizations.application, - widget: const ApplicationSettingFragment(), - ), - ), - ]; - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - for (final item in items) ...[ - item, - if (item != items.last) - const Divider( - height: 0, + if (Platform.isAndroid) + ListItem.open( + leading: const Icon(Icons.view_list), + title: Text(appLocalizations.accessControl), + subtitle: Text(appLocalizations.accessControlDesc), + delegate: OpenDelegate( + title: appLocalizations.appAccessControl, + widget: const AccessFragment(), ), - ] + ), + ListItem.open( + leading: const Icon(Icons.edit), + title: Text(appLocalizations.override), + subtitle: Text(appLocalizations.overrideDesc), + delegate: OpenDelegate( + title: appLocalizations.override, + widget: const ConfigFragment(), + extendPageWidth: 360, + ), + ), + ListItem.open( + leading: const Icon(Icons.settings_applications), + title: Text(appLocalizations.application), + subtitle: Text(appLocalizations.applicationDesc), + delegate: OpenDelegate( + title: appLocalizations.application, + widget: const ApplicationSettingFragment(), + ), + ), ], ); } @@ -216,20 +198,16 @@ class _ToolboxFragmentState extends State { if (state.navigationItems.isEmpty) { return Container(); } - return Section( - title: appLocalizations.more, - child: _buildNavigationMenu(state.navigationItems), + return Column( + children: [ + ListHeader(title: appLocalizations.more), + _buildNavigationMenu(state.navigationItems) + ], ); }, ), - Section( - title: appLocalizations.settings, - child: _getSettingList(), - ), - Section( - title: appLocalizations.other, - child: _getOtherList(), - ), + ..._getSettingList(), + ..._getOtherList(), ]; return ListView.builder( itemCount: items.length, diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 5b0ec19..93ea016 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -192,5 +192,6 @@ "cut": "Cut", "copy": "Copy", "paste": "Paste", - "testUrl": "Test url" + "testUrl": "Test url", + "sync": "Sync" } \ No newline at end of file diff --git a/lib/l10n/arb/intl_zh_CN.arb b/lib/l10n/arb/intl_zh_CN.arb index 8fcfe79..cabcd3b 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -192,5 +192,6 @@ "cut": "剪切", "copy": "复制", "paste": "粘贴", - "testUrl": "测速链接" + "testUrl": "测速链接", + "sync": "同步" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index a571c8d..9c31a8e 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -260,6 +260,7 @@ class MessageLookup extends MessageLookupByLibrary { "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "submit": MessageLookupByLibrary.simpleMessage("Submit"), + "sync": MessageLookupByLibrary.simpleMessage("Sync"), "systemProxy": MessageLookupByLibrary.simpleMessage("SystemProxy"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage( "Attach HTTP proxy to VpnService"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 98bc31a..a51bd55 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -209,6 +209,7 @@ class MessageLookup extends MessageLookupByLibrary { "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "submit": MessageLookupByLibrary.simpleMessage("提交"), + "sync": MessageLookupByLibrary.simpleMessage("同步"), "systemProxy": MessageLookupByLibrary.simpleMessage("系统代理"), "systemProxyDesc": MessageLookupByLibrary.simpleMessage("为VpnService附加HTTP代理"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 3b5d2a2..228ed31 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1989,6 +1989,16 @@ class AppLocalizations { args: [], ); } + + /// `Sync` + String get sync { + return Intl.message( + 'Sync', + name: 'sync', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/models/system_color_scheme.dart b/lib/models/system_color_scheme.dart index eb13a2e..64ab2be 100644 --- a/lib/models/system_color_scheme.dart +++ b/lib/models/system_color_scheme.dart @@ -1,24 +1,30 @@ import 'package:fl_clash/common/constant.dart'; import 'package:flutter/material.dart'; +@immutable class SystemColorSchemes { - SystemColorSchemes({ - ColorScheme? lightColorScheme, - ColorScheme? darkColorScheme, - }) : lightColorScheme = lightColorScheme ?? - ColorScheme.fromSeed(seedColor: defaultPrimaryColor), - darkColorScheme = darkColorScheme ?? - ColorScheme.fromSeed( - seedColor: defaultPrimaryColor, - brightness: Brightness.dark, - ); - ColorScheme lightColorScheme; - ColorScheme darkColorScheme; + final ColorScheme? lightColorScheme; + final ColorScheme? darkColorScheme; + + const SystemColorSchemes({ + this.lightColorScheme, + this.darkColorScheme, + }); getSystemColorSchemeForBrightness(Brightness? brightness) { if (brightness != null && brightness == Brightness.dark) { - return darkColorScheme; + return darkColorScheme != null + ? ColorScheme.fromSeed( + seedColor: darkColorScheme!.primary, + brightness: brightness, + ) + : ColorScheme.fromSeed( + seedColor: defaultPrimaryColor, + brightness: brightness, + ); } - return lightColorScheme; + return lightColorScheme != null + ? ColorScheme.fromSeed(seedColor: darkColorScheme!.primary) + : ColorScheme.fromSeed(seedColor: defaultPrimaryColor); } } diff --git a/lib/state.dart b/lib/state.dart index 7eb6074..6c4ac9b 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -87,7 +87,7 @@ class GlobalState { config: config, clashConfig: clashConfig, ).then((_){ - appController.appState.checkIpNum++; + appController.addCheckIpNumDebounce(); }); } diff --git a/lib/widgets/chip.dart b/lib/widgets/chip.dart index ae564a2..6046c27 100644 --- a/lib/widgets/chip.dart +++ b/lib/widgets/chip.dart @@ -20,7 +20,7 @@ class CommonChip extends StatelessWidget { if (type == ChipType.delete) { return Chip( avatar: avatar, - padding: const EdgeInsets.symmetric( + labelPadding:const EdgeInsets.symmetric( vertical: 0, horizontal: 4, ), @@ -35,7 +35,7 @@ class CommonChip extends StatelessWidget { return ActionChip( materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, avatar: avatar, - padding: const EdgeInsets.symmetric( + labelPadding:const EdgeInsets.symmetric( vertical: 0, horizontal: 4, ), diff --git a/lib/widgets/clash_message_container.dart b/lib/widgets/clash_message_container.dart index 26fa475..d79618f 100644 --- a/lib/widgets/clash_message_container.dart +++ b/lib/widgets/clash_message_container.dart @@ -1,7 +1,6 @@ import 'package:fl_clash/clash/clash.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/models.dart'; -import 'package:fl_clash/plugins/proxy.dart'; import 'package:fl_clash/state.dart'; import 'package:flutter/material.dart'; import 'package:fl_clash/plugins/app.dart'; @@ -87,7 +86,7 @@ class _ClashMessageContainerState extends State proxyName: proxyName, ), ); - appController.appState.checkIpNum++; + appController.addCheckIpNumDebounce(); super.onLoaded(proxyName); } } diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 9641e60..62e54c5 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -1,3 +1,4 @@ +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/open_container.dart'; @@ -214,7 +215,8 @@ class ListItem extends StatelessWidget { return OpenContainer( closedBuilder: (_, action) { openAction() { - final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; + final isMobile = + globalState.appController.appState.viewMode == ViewMode.mobile; if (!isMobile) { showExtendPage( context, @@ -243,7 +245,8 @@ class ListItem extends StatelessWidget { final nextDelegate = delegate as NextDelegate; return _buildListTile( onTab: () { - final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; + final isMobile = + globalState.appController.appState.viewMode == ViewMode.mobile; if (!isMobile) { showExtendPage( context, @@ -319,3 +322,77 @@ class ListItem extends StatelessWidget { ); } } + +class ListHeader extends StatelessWidget { + final String title; + final List actions; + + const ListHeader({ + super.key, + required this.title, + List? actions, + }) : actions = actions ?? const []; + + @override + Widget build(BuildContext context) { + return Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 12, + ), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + title, + style: Theme.of(context).textTheme.labelLarge?.copyWith( + color: Theme.of(context).colorScheme.primary, + ), + ), + Expanded( + flex: 1, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ...actions, + ], + ), + ), + ], + ), + ); + } +} + +List generateSection({ + required String title, + required Iterable items, + List? actions, + bool separated = true, +}) { + final genItems = separated + ? items.separated( + const Divider( + height: 0, + ), + ) + : items; + return [ + if (items.isNotEmpty) + ListHeader( + title: title, + actions: actions, + ), + ...genItems, + ]; +} + +Widget generateListView(List items) { + return ListView.builder( + itemCount: items.length, + itemBuilder: (_, index) => items[index], + ); +} diff --git a/lib/widgets/section.dart b/lib/widgets/section.dart deleted file mode 100644 index 9c4d579..0000000 --- a/lib/widgets/section.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; - -class Section extends StatelessWidget { - final String title; - final Widget child; - - const Section({ - super.key, - required this.title, - required this.child, - }); - - @override - Widget build(BuildContext context) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Flexible( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), - child: Text( - title, - style: Theme.of(context).textTheme.labelLarge?.copyWith( - color: Theme.of(context).colorScheme.primary, - ), - ), - ), - ), - Expanded( - flex: 0, - child: child, - ) - ], - ); - } -} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index acc0ae9..0e6844f 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -22,5 +22,4 @@ export 'tile_container.dart'; export 'chip.dart'; export 'fade_box.dart'; export 'app_state_container.dart'; -export 'text.dart'; -export 'section.dart'; \ No newline at end of file +export 'text.dart'; \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 1622855..4679462 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -229,10 +229,18 @@ packages: dependency: "direct main" description: name: dio - sha256: "11e40df547d418cc0c4900a9318b26304e665da6fa4755399a9ff9efd09034b5" + sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714 url: "https://pub.dev" source: hosted - version: "5.4.3+1" + version: "5.5.0+1" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" + url: "https://pub.dev" + source: hosted + version: "1.0.1" dynamic_color: dependency: "direct main" description: @@ -277,10 +285,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "2ca051989f69d1b2ca012b2cf3ccf78c70d40144f0861ff2c063493f7c8c3d45" + sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" url: "https://pub.dev" source: hosted - version: "8.0.5" + version: "8.0.6" file_selector_linux: dependency: transitive description: @@ -369,10 +377,10 @@ packages: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: f54946fdb1fa7b01f780841937b1a80783a20b393485f3f6cdf336fd6f4705f2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.2" frontend_server_client: dependency: transitive description: @@ -630,7 +638,7 @@ packages: source: hosted version: "0.12.16+1" material_color_utilities: - dependency: transitive + dependency: "direct main" description: name: material_color_utilities sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" diff --git a/pubspec.yaml b/pubspec.yaml index e5a37e8..4045d3d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: fl_clash description: A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free. publish_to: 'none' -version: 0.8.34+202407041 +version: 0.8.35+202407071 environment: sdk: '>=3.1.0 <4.0.0' @@ -42,6 +42,7 @@ dependencies: re_highlight: ^0.0.3 win32: ^5.5.1 ffi: ^2.1.2 + material_color_utilities: ^0.8.0 dev_dependencies: flutter_test: sdk: flutter diff --git a/test/command_test.dart b/test/command_test.dart index 6baf655..1f2ad4e 100644 --- a/test/command_test.dart +++ b/test/command_test.dart @@ -1,67 +1,10 @@ // ignore_for_file: avoid_print +import 'package:fl_clash/common/common.dart'; + void main() async { - String input = """ -您 -
All changes from v0.8.5 to the latest commit: - -(unreleased) ------------- -- Fix submit error. [chen08209] -- Add WebDAV. [chen08209] - - add Auto check updates - - Optimize more details -- Optimize delayTest. [chen08209] -- Upgrade flutter version. [chen08209] -- Update kernel Add import profile via QR code image. [chen08209] -- Add compatibility mode and adapt clash scheme. [chen08209] -- Update Version. [chen08209] -- Reconstruction application proxy logic. [chen08209] -- Fix Tab destroy error. [chen08209] -- Optimize repeat healthcheck. [chen08209] -- Optimize Direct mode ui. [chen08209] -- Optimize Healthcheck. [chen08209] -- Remove proxies position animation, improve performance Add Telegram - Link. [chen08209] -- Update healthcheck policy. [chen08209] -- New Check URLTest. [chen08209] -- Fix the problem of invalid auto-selection. [chen08209] -- New Async UpdateConfig. [chen08209] -- Add changeProfileDebounce. [chen08209] -- Update Workflow. [chen08209] -- Fix ChangeProfile block. [chen08209] -- Fix Release Message Error. [chen08209] -- Update Selector 2. [chen08209] -- Update Version. [chen08209] -- Fix Proxies Select Error. [chen08209] -- Fix the problem that the proxy group is empty in global mode. - [chen08209] -- Fix the problem that the proxy group is empty in global mode. - [chen08209] -- Add ProxyProvider2. [chen08209] -- Add ProxyProvider. [chen08209] -- Update Version. [chen08209] -- Update ProxyGroup Sort. [chen08209] -- Fix Android quickStart VpnService some problems. [chen08209] -- Update version. [chen08209] -- Set Android notification low importance. [chen08209] -- Fix the issue that VpnService can't be closed correctly in special - cases. [chen08209] -- Fix the problem that TileService is not destroyed correctly in some - cases. [chen08209] - - Adjust tab animation defaults -- Add Telegram in README_zh_CN.md. [chen08209] -- Add Telegram. [chen08209] -"""; - const pattern = r'- (.+?)\. \[.+?\]'; - final regex = RegExp(pattern); - - for (final match in regex.allMatches(input)) { - final change = match.group(1); - print(change); - } + print("https://pqjc.site:10000/test.ymal".isUrl); + print("abcd".isUrl); + print("http://10.31.1.221:8848/cfa.yaml".isUrl); }