diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1b63ca6..6c9556d 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -75,7 +75,7 @@ jobs: with: channel: 'stable' cache: true - flutter-version: 3.29.3 + # flutter-version: 3.29.3 - name: Get Flutter Dependency run: flutter pub get diff --git a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt index 0aa09de..10e0536 100644 --- a/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt +++ b/android/app/src/main/kotlin/com/follow/clash/extensions/Ext.kt @@ -104,7 +104,7 @@ fun ConnectivityManager.resolveDns(network: Network?): List { fun InetAddress.asSocketAddressText(port: Int): String { return when (this) { is Inet6Address -> - "[${numericToTextFormat(this.address)}]:$port" + "[${numericToTextFormat(this)}]:$port" is Inet4Address -> "${this.hostAddress}:$port" @@ -141,7 +141,8 @@ fun Context.getActionPendingIntent(action: String): PendingIntent { } } -private fun numericToTextFormat(src: ByteArray): String { +private fun numericToTextFormat(address: Inet6Address): String { + val src = address.address val sb = StringBuilder(39) for (i in 0 until 8) { sb.append( @@ -154,6 +155,10 @@ private fun numericToTextFormat(src: ByteArray): String { sb.append(":") } } + if (address.scopeId > 0) { + sb.append("%") + sb.append(address.scopeId) + } return sb.toString() } diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt index 9a73f77..3d3cc39 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/VpnPlugin.kt @@ -100,6 +100,7 @@ data object VpnPlugin : FlutterPlugin, MethodChannel.MethodCallHandler { } fun handleStart(options: VpnOptions): Boolean { + onUpdateNetwork(); if (options.enable != this.options?.enable) { this.flClashService = null } diff --git a/lib/common/request.dart b/lib/common/request.dart index d277ee5..9585596 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; @@ -90,37 +91,37 @@ class Request { "https://ipinfo.io/json/": IpInfo.fromIpInfoIoJson, }; - Future checkIp({CancelToken? cancelToken}) async { - for (final source in _ipInfoSources.entries) { - try { - final response = await Dio() - .get>( - source.key, - cancelToken: cancelToken, - options: Options( - responseType: ResponseType.json, - ), - ) - .timeout( - Duration( - seconds: 30, - ), - ); - if (response.statusCode != 200 || response.data == null) { - continue; + Future> checkIp({CancelToken? cancelToken}) async { + var failureCount = 0; + final futures = _ipInfoSources.entries.map((source) async { + final Completer> completer = Completer(); + final future = Dio().get>( + source.key, + cancelToken: cancelToken, + options: Options( + responseType: ResponseType.json, + ), + ); + future.then((res) { + if (res.statusCode == HttpStatus.ok && res.data != null) { + completer.complete(Result.success(source.value(res.data!))); + } else { + failureCount++; + if (failureCount == _ipInfoSources.length) { + completer.complete(Result.success(null)); + } } - if (response.data == null) { - continue; + }).catchError((e) { + failureCount++; + if (e == DioExceptionType.cancel) { + completer.complete(Result.error("cancelled")); } - return source.value(response.data!); - } catch (e) { - commonPrint.log("checkIp error ===> $e"); - if (e is DioException && e.type == DioExceptionType.cancel) { - throw "cancelled"; - } - } - } - return null; + }); + return completer.future; + }); + final res = await Future.any(futures); + cancelToken?.cancel(); + return res; } Future pingHelper() async { diff --git a/lib/main.dart b/lib/main.dart index 02dff41..17c8420 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -62,6 +62,7 @@ Future _service(List flags) async { vpn?.addListener( _VpnListenerWithService( onDnsChanged: (String dns) { + print("handle dns $dns"); clashLibHandler.updateDns(dns); }, ), diff --git a/lib/manager/message_manager.dart b/lib/manager/message_manager.dart index 6c67c5b..d5b53a3 100644 --- a/lib/manager/message_manager.dart +++ b/lib/manager/message_manager.dart @@ -91,7 +91,7 @@ class MessageManagerState extends State { key: Key(messages.last.id), builder: (_, constraints) { return Card( - shape: const RoundedRectangleBorder( + shape: const RoundedSuperellipseBorder( borderRadius: BorderRadius.all( Radius.circular(12.0), ), diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index f72700b..9f63160 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -200,6 +200,24 @@ class Tun with _$Tun { } } +extension TunExt on Tun { + Tun getRealTun(RouteMode routeMode) { + final mRouteAddress = routeMode == RouteMode.bypassPrivate + ? defaultBypassPrivateRouteAddress + : routeAddress; + return switch (system.isDesktop) { + true => copyWith( + autoRoute: true, + routeAddress: [], + ), + false => copyWith( + autoRoute: mRouteAddress.isEmpty ? true : false, + routeAddress: mRouteAddress, + ), + }; + } +} + @freezed class FallbackFilter with _$FallbackFilter { const factory FallbackFilter({ diff --git a/lib/providers/generated/state.g.dart b/lib/providers/generated/state.g.dart index 7b85e73..c8c59fb 100644 --- a/lib/providers/generated/state.g.dart +++ b/lib/providers/generated/state.g.dart @@ -94,7 +94,7 @@ final coreStateProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef CoreStateRef = AutoDisposeProviderRef; -String _$updateParamsHash() => r'79fd7a5a8650fabac3a2ca7ce903c1d9eb363ed2'; +String _$updateParamsHash() => r'012df72ab0e769a51c573f4692031506d7b1f1b4'; /// See also [updateParams]. @ProviderFor(updateParams) @@ -126,7 +126,7 @@ final proxyStateProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element typedef ProxyStateRef = AutoDisposeProviderRef; -String _$trayStateHash() => r'39ff84c50ad9c9cc666fa2538fe13ec0d7236b2e'; +String _$trayStateHash() => r'61c99bbae2cb7ed69dc9ee0f2149510eb6a87df4'; /// See also [trayState]. @ProviderFor(trayState) diff --git a/lib/providers/state.dart b/lib/providers/state.dart index 6daf2c3..469fdc3 100644 --- a/lib/providers/state.dart +++ b/lib/providers/state.dart @@ -105,10 +105,15 @@ CoreState coreState(Ref ref) { @riverpod UpdateParams updateParams(Ref ref) { + final routeMode = ref.watch( + networkSettingProvider.select( + (state) => state.routeMode, + ), + ); return ref.watch( patchClashConfigProvider.select( (state) => UpdateParams( - tun: state.tun, + tun: state.tun.getRealTun(routeMode), allowLan: state.allowLan, findProcessMode: state.findProcessMode, mode: state.mode, @@ -153,9 +158,11 @@ TrayState trayState(Ref ref) { final appSetting = ref.watch( appSettingProvider, ); - final groups = ref.watch( - groupsProvider, - ); + final groups = ref + .watch( + currentGroupsStateProvider, + ) + .value; final brightness = ref.watch( appBrightnessProvider, ); diff --git a/lib/state.dart b/lib/state.dart index 7d00055..953ea73 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -315,19 +315,9 @@ class GlobalState { final profileId = profile.id; final configMap = await getProfileConfig(profileId); final rawConfig = await handleEvaluate(configMap); - final routeAddress = - config.networkProps.routeMode == RouteMode.bypassPrivate - ? defaultBypassPrivateRouteAddress - : patchConfig.tun.routeAddress; - final realPatchConfig = !system.isDesktop - ? patchConfig.copyWith.tun( - autoRoute: routeAddress.isEmpty ? true : false, - routeAddress: routeAddress, - ) - : patchConfig.copyWith.tun( - autoRoute: true, - routeAddress: [], - ); + final realPatchConfig = patchConfig.copyWith( + tun: patchConfig.tun.getRealTun(config.networkProps.routeMode), + ); rawConfig["external-controller"] = realPatchConfig.externalController.value; rawConfig["external-ui"] = ""; rawConfig["interface-name"] = ""; @@ -411,21 +401,23 @@ class GlobalState { for (final host in realPatchConfig.hosts.entries) { rawConfig["hosts"][host.key] = host.value.splitByMultipleSeparators; } + if (rawConfig["dns"] == null) { + rawConfig["dns"] = {}; + } + final isEnableDns = rawConfig["dns"]["enable"] == true; final overrideDns = globalState.config.overrideDns; - if (overrideDns) { - rawConfig["dns"] = realPatchConfig.dns.toJson(); + if (overrideDns || !isEnableDns) { + final dns = switch (!isEnableDns) { + true => realPatchConfig.dns.copyWith( + nameserver: [...realPatchConfig.dns.nameserver, "system://"]), + false => realPatchConfig.dns, + }; + rawConfig["dns"] = dns.toJson(); rawConfig["dns"]["nameserver-policy"] = {}; - for (final entry in realPatchConfig.dns.nameserverPolicy.entries) { + for (final entry in dns.nameserverPolicy.entries) { rawConfig["dns"]["nameserver-policy"][entry.key] = entry.value.splitByMultipleSeparators; } - } else { - if (rawConfig["dns"] == null) { - rawConfig["dns"] = {}; - } - if (rawConfig["dns"]["enable"] != false) { - rawConfig["dns"]["enable"] = true; - } } var rules = []; if (rawConfig["rules"] != null) { @@ -509,6 +501,9 @@ class DetectionState { debouncer.call( FunctionTag.checkIp, _checkIp, + duration: Duration( + milliseconds: 1200, + ), ); } @@ -533,36 +528,35 @@ class DetectionState { cancelToken = null; } cancelToken = CancelToken(); - try { + state.value = state.value.copyWith( + isTesting: true, + ); + final res = await request.checkIp(cancelToken: cancelToken); + if (res.isError) { state.value = state.value.copyWith( - isTesting: true, + isLoading: true, + ipInfo: null, ); - final ipInfo = await request.checkIp(cancelToken: cancelToken); - state.value = state.value.copyWith( - isTesting: false, - ); - if (ipInfo != null) { - state.value = state.value.copyWith( - isLoading: false, - ipInfo: ipInfo, - ); - return; - } - _clearSetTimeoutTimer(); - _setTimeoutTimer = Timer(const Duration(milliseconds: 300), () { - state.value = state.value.copyWith( - isLoading: false, - ipInfo: null, - ); - }); - } catch (e) { - if (e.toString() == "cancelled") { - state.value = state.value.copyWith( - isLoading: true, - ipInfo: null, - ); - } + return; } + final ipInfo = res.data; + state.value = state.value.copyWith( + isTesting: false, + ); + if (ipInfo != null) { + state.value = state.value.copyWith( + isLoading: false, + ipInfo: ipInfo, + ); + return; + } + _clearSetTimeoutTimer(); + _setTimeoutTimer = Timer(const Duration(milliseconds: 300), () { + state.value = state.value.copyWith( + isLoading: false, + ipInfo: null, + ); + }); } _clearSetTimeoutTimer() { diff --git a/lib/views/dashboard/widgets/traffic_usage.dart b/lib/views/dashboard/widgets/traffic_usage.dart index 3b073d3..6fadb5e 100644 --- a/lib/views/dashboard/widgets/traffic_usage.dart +++ b/lib/views/dashboard/widgets/traffic_usage.dart @@ -135,10 +135,12 @@ class TrafficUsage extends StatelessWidget { Container( width: 20, height: 8, - decoration: BoxDecoration( + decoration: ShapeDecoration( color: primaryColor, - borderRadius: - BorderRadius.circular(2), + shape: RoundedSuperellipseBorder( + borderRadius: + BorderRadius.circular(3), + ), ), ), SizedBox( @@ -161,10 +163,12 @@ class TrafficUsage extends StatelessWidget { Container( width: 20, height: 8, - decoration: BoxDecoration( + decoration: ShapeDecoration( color: secondaryColor, - borderRadius: - BorderRadius.circular(2), + shape: RoundedSuperellipseBorder( + borderRadius: + BorderRadius.circular(3), + ), ), ), SizedBox( diff --git a/lib/views/proxies/list.dart b/lib/views/proxies/list.dart index 8b2bb0a..fd1cf47 100644 --- a/lib/views/proxies/list.dart +++ b/lib/views/proxies/list.dart @@ -418,9 +418,11 @@ class _ListHeaderState extends State { width: constraints.maxWidth, alignment: Alignment.center, padding: EdgeInsets.all(6.ap), - decoration: BoxDecoration( + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(12), + ), color: context.colorScheme.secondaryContainer, - borderRadius: BorderRadius.circular(12), ), clipBehavior: Clip.antiAlias, child: CommonTargetIcon( diff --git a/lib/widgets/card.dart b/lib/widgets/card.dart index a196640..f35ed70 100644 --- a/lib/widgets/card.dart +++ b/lib/widgets/card.dart @@ -185,7 +185,7 @@ class CommonCard extends StatelessWidget { style: ButtonStyle( padding: const WidgetStatePropertyAll(EdgeInsets.zero), shape: WidgetStatePropertyAll( - RoundedRectangleBorder( + RoundedSuperellipseBorder( borderRadius: BorderRadius.circular(radius), ), ), diff --git a/lib/widgets/color_scheme_box.dart b/lib/widgets/color_scheme_box.dart index 9f9ea63..8adfe1e 100644 --- a/lib/widgets/color_scheme_box.dart +++ b/lib/widgets/color_scheme_box.dart @@ -1,6 +1,7 @@ import 'package:fl_clash/providers/providers.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'card.dart'; import 'grid.dart'; diff --git a/lib/widgets/popup.dart b/lib/widgets/popup.dart index 4a8d7eb..e8305df 100644 --- a/lib/widgets/popup.dart +++ b/lib/widgets/popup.dart @@ -279,7 +279,7 @@ class CommonPopupMenu extends StatelessWidget { elevation: 12, color: context.colorScheme.surfaceContainer, clipBehavior: Clip.antiAlias, - shape: RoundedRectangleBorder( + shape: RoundedSuperellipseBorder( borderRadius: BorderRadius.circular(12), ), child: Column( diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index af67f30..f44a601 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -148,9 +148,11 @@ class _AdaptiveSheetScaffoldState extends State { final handleSize = Size(32, 4); return Container( clipBehavior: Clip.hardEdge, - decoration: BoxDecoration( - borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)), + decoration: ShapeDecoration( color: backgroundColor, + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.vertical(top: Radius.circular(28.0)), + ), ), child: Column( mainAxisSize: MainAxisSize.min, @@ -161,8 +163,10 @@ class _AdaptiveSheetScaffoldState extends State { alignment: Alignment.center, height: handleSize.height, width: handleSize.width, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(handleSize.height / 2), + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: BorderRadius.circular(handleSize.height / 2), + ), color: context.colorScheme.onSurfaceVariant, ), ), diff --git a/lib/widgets/side_sheet.dart b/lib/widgets/side_sheet.dart index f2a295d..adcdefa 100644 --- a/lib/widgets/side_sheet.dart +++ b/lib/widgets/side_sheet.dart @@ -83,7 +83,7 @@ class _SideSheetState extends State { final Color shadowColor = widget.shadowColor ?? Colors.transparent; final double elevation = widget.elevation ?? 0; final ShapeBorder shape = widget.shape ?? - RoundedRectangleBorder( + RoundedSuperellipseBorder( borderRadius: BorderRadius.circular(0), ); diff --git a/lib/widgets/tab.dart b/lib/widgets/tab.dart index 9b4fe79..c17bf30 100644 --- a/lib/widgets/tab.dart +++ b/lib/widgets/tab.dart @@ -373,8 +373,10 @@ class _CommonTabBarState extends State> child: Container( clipBehavior: Clip.antiAlias, padding: widget.padding.resolve(Directionality.of(context)), - decoration: BoxDecoration( - borderRadius: const BorderRadius.all(_kCornerRadius), + decoration: ShapeDecoration( + shape: RoundedSuperellipseBorder( + borderRadius: const BorderRadius.all(_kCornerRadius), + ), color: widget.backgroundColor, ), child: AnimatedBuilder( diff --git a/pubspec.lock b/pubspec.lock index 64fa721..2c53576 100755 --- a/pubspec.lock +++ b/pubspec.lock @@ -85,10 +85,10 @@ packages: dependency: transitive description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.13.0" boolean_selector: dependency: transitive description: @@ -373,10 +373,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.3" ffi: dependency: "direct main" description: @@ -706,10 +706,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" io: dependency: transitive description: @@ -770,10 +770,10 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.9" leak_tracker_flutter_testing: dependency: transitive description: @@ -1024,10 +1024,11 @@ packages: re_editor: dependency: "direct main" description: - name: re_editor - sha256: "17e430f0591dd361992ec2dd6f69191c1853fa46e05432e095310a8f82ee820e" - url: "https://pub.dev" - source: hosted + path: "." + ref: main + resolved-ref: "7cda330fc33d5ef9e00333048b70ce65a5f5d550" + url: "https://github.com/chen08209/re-editor.git" + source: git version: "0.7.0" re_highlight: dependency: "direct main" @@ -1494,10 +1495,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02 url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "15.0.0" watcher: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 76556ac..0243135 100755 --- 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.85+202506071 +version: 0.8.86+2025061501 environment: sdk: '>=3.1.0 <4.0.0' @@ -37,7 +37,10 @@ dependencies: dio: ^5.8.0+1 win32: ^5.5.1 ffi: ^2.1.2 - re_editor: ^0.7.0 + re_editor: + git: + url: https://github.com/chen08209/re-editor.git + ref: main re_highlight: ^0.0.3 archive: ^3.6.1 lpinyin: ^2.0.3