diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index be29d26..ade65ab 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,10 +17,16 @@ jobs: os: windows-latest - platform: linux os: ubuntu-latest -# - platform: macos -# os: macos-13 + - platform: macos + os: macos-13 steps: + - name: Check Matrix + run: | + echo "Running on ${{ matrix.os }}" + echo "Arch: ${{ runner.arch }}" + gcc --version + - name: Checkout uses: actions/checkout@v4 with: diff --git a/.gitmodules b/.gitmodules index 7ad77e0..ecedb42 100644 --- a/.gitmodules +++ b/.gitmodules @@ -5,4 +5,4 @@ [submodule "plugins/flutter_distributor"] path = plugins/flutter_distributor url = git@github.com:chen08209/flutter_distributor.git - branch = main + branch = FlClash diff --git a/android/app/src/main/kotlin/com/follow/clash/models/AccessControl.kt b/android/app/src/main/kotlin/com/follow/clash/models/Props.kt similarity index 71% rename from android/app/src/main/kotlin/com/follow/clash/models/AccessControl.kt rename to android/app/src/main/kotlin/com/follow/clash/models/Props.kt index 60d72f5..8215e39 100644 --- a/android/app/src/main/kotlin/com/follow/clash/models/AccessControl.kt +++ b/android/app/src/main/kotlin/com/follow/clash/models/Props.kt @@ -10,3 +10,8 @@ data class AccessControl( val acceptList: List, val rejectList: List, ) + +data class Props ( + val accessControl: AccessControl?, + val allowBypass: Boolean?, +) diff --git a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt index 83ccf4d..eaeb40a 100644 --- a/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt +++ b/android/app/src/main/kotlin/com/follow/clash/plugins/ProxyPlugin.kt @@ -17,6 +17,7 @@ import androidx.core.content.ContextCompat import com.follow.clash.GlobalState import com.follow.clash.RunState import com.follow.clash.models.AccessControl +import com.follow.clash.models.Props import com.follow.clash.services.FlClashVpnService import com.google.gson.Gson import io.flutter.embedding.android.FlutterActivity @@ -41,7 +42,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar private var flClashVpnService: FlClashVpnService? = null private var isBound = false private var port: Int? = null - private var accessControl: AccessControl? = null + private var props: Props? = null private lateinit var title: String private lateinit var content: String @@ -73,8 +74,8 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar "StartProxy" -> { port = call.argument("port") val args = call.argument("args") - accessControl = - if (args != null) Gson().fromJson(args, AccessControl::class.java) else null + props = + if (args != null) Gson().fromJson(args, Props::class.java) else null handleStartVpn() result.success(true) } @@ -121,7 +122,7 @@ class ProxyPlugin : FlutterPlugin, MethodChannel.MethodCallHandler, ActivityAwar private fun startVpn(port: Int) { if (GlobalState.runState.value == RunState.START) return; - flClashVpnService?.start(port, accessControl) + flClashVpnService?.start(port, props) GlobalState.runState.value = RunState.START GlobalState.runTime = Date() startAfter() 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 ff89451..c2100fd 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 @@ -17,6 +17,7 @@ import com.follow.clash.MainActivity import com.follow.clash.R import com.follow.clash.models.AccessControl import com.follow.clash.models.AccessControlMode +import com.follow.clash.models.Props class FlClashVpnService : VpnService() { @@ -51,12 +52,12 @@ class FlClashVpnService : VpnService() { return START_STICKY } - fun start(port: Int, accessControl: AccessControl?) { + fun start(port: Int, props: Props?) { fd = with(Builder()) { addAddress("172.16.0.1", 30) setMtu(9000) addRoute("0.0.0.0", 0) - if (accessControl != null) { + props?.accessControl?.let { accessControl -> when (accessControl.mode) { AccessControlMode.acceptSelected -> { (accessControl.acceptList + packageName).forEach { @@ -77,7 +78,9 @@ class FlClashVpnService : VpnService() { if (Build.VERSION.SDK_INT >= 29) { setMetered(false) } - allowBypass() + if (props?.allowBypass == true) { + allowBypass() + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { setHttpProxy( ProxyInfo.buildDirectProxy( @@ -144,8 +147,8 @@ class FlClashVpnService : VpnService() { val notification = notificationBuilder.setContentTitle(title).setContentText(content).build() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) - }else{ + startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE) + } else { startForeground(notificationId, notification) } } diff --git a/core/go.mod b/core/go.mod index 76b7e0e..ee3a5b5 100644 --- a/core/go.mod +++ b/core/go.mod @@ -1,6 +1,6 @@ module core -go 1.20 +go 1.21.0 replace github.com/metacubex/mihomo => ./Clash.Meta diff --git a/lib/common/picker.dart b/lib/common/picker.dart index afde0e8..2836b6f 100644 --- a/lib/common/picker.dart +++ b/lib/common/picker.dart @@ -10,8 +10,7 @@ class Picker { if (Platform.isAndroid) { filePickerResult = await FilePicker.platform.pickFiles( withData: true, - type: FileType.custom, - allowedExtensions: ['txt', 'conf'], + allowMultiple: false, ); } else { filePickerResult = await FilePicker.platform.pickFiles( diff --git a/lib/fragments/access.dart b/lib/fragments/access.dart index 1302597..9347265 100644 --- a/lib/fragments/access.dart +++ b/lib/fragments/access.dart @@ -530,12 +530,11 @@ class AccessControlSearchDelegate extends SearchDelegate { @override Widget buildResults(BuildContext context) { - return _packageList(packages); + return buildSuggestions(context); } @override Widget buildSuggestions(BuildContext context) { - final packages = _results; - return _packageList(packages); + return _packageList(_results); } } diff --git a/lib/fragments/config.dart b/lib/fragments/config.dart index d1976fe..c631621 100644 --- a/lib/fragments/config.dart +++ b/lib/fragments/config.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; @@ -54,7 +56,7 @@ class _ConfigFragmentState extends State { onTab: () { _modifyMixedPort(mixedPort); }, - padding: const EdgeInsets.symmetric(horizontal: 16,vertical: 4), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), leading: const Icon(Icons.adjust), title: Text(appLocalizations.proxyPort), trailing: FilledButton.tonal( @@ -105,6 +107,24 @@ class _ConfigFragmentState extends State { // ); // }, // ), + if (Platform.isAndroid) + Selector( + selector: (_, config) => config.allowBypass, + builder: (_, allowBypass, __) { + return ListItem.switchItem( + leading: const Icon(Icons.double_arrow), + title: Text(appLocalizations.allowBypass), + subtitle: Text(appLocalizations.allowBypassDesc), + delegate: SwitchDelegate( + value: allowBypass, + onChanged: (bool value) async { + final appController = globalState.appController; + appController.config.allowBypass = value; + }, + ), + ); + }, + ), Selector( selector: (_, config) => config.isCompatible, builder: (_, isCompatible, __) { diff --git a/lib/fragments/profiles/profiles.dart b/lib/fragments/profiles/profiles.dart index ed68aec..001389a 100644 --- a/lib/fragments/profiles/profiles.dart +++ b/lib/fragments/profiles/profiles.dart @@ -62,7 +62,7 @@ class _ProfilesFragmentState extends State { }, ); }, - icon: const Icon(Icons.download), + icon: const Icon(Icons.sync), ), const SizedBox( width: 8, diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 62b060f..f54f7e9 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -161,5 +161,7 @@ "country": "Country", "checkError": "Check error", "ipCheckTimeout": "Ip check timeout", - "search": "Search" + "search": "Search", + "allowBypass": "Allow applications to bypass VPN", + "allowBypassDesc": "After opening, some applications can bypass VPN" } \ 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 9b1599d..058793a 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -161,5 +161,7 @@ "country": "区域", "checkError": "检测失败", "ipCheckTimeout": "Ip检测超时", - "search": "搜索" + "search": "搜索", + "allowBypass": "允许应用绕过vpn", + "allowBypassDesc": "开启后部分应用可绕过VPN" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 832545a..a045ff1 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -40,6 +40,10 @@ class MessageLookup extends MessageLookupByLibrary { "addressTip": MessageLookupByLibrary.simpleMessage( "Please enter a valid WebDAV address"), "ago": MessageLookupByLibrary.simpleMessage(" Ago"), + "allowBypass": MessageLookupByLibrary.simpleMessage( + "Allow applications to bypass VPN"), + "allowBypassDesc": MessageLookupByLibrary.simpleMessage( + "After opening, some applications can bypass VPN"), "allowLan": MessageLookupByLibrary.simpleMessage("AllowLan"), "allowLanDesc": MessageLookupByLibrary.simpleMessage( "Allow access proxy through the LAN"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 6fc1e03..a8861b2 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -36,6 +36,9 @@ class MessageLookup extends MessageLookupByLibrary { "addressHelp": MessageLookupByLibrary.simpleMessage("WebDAV服务器地址"), "addressTip": MessageLookupByLibrary.simpleMessage("请输入有效的WebDAV地址"), "ago": MessageLookupByLibrary.simpleMessage("前"), + "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过vpn"), + "allowBypassDesc": + MessageLookupByLibrary.simpleMessage("开启后部分应用可绕过VPN"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), "allowLanDesc": MessageLookupByLibrary.simpleMessage("允许通过局域网访问代理"), "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index 61b875d..100bc00 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -1679,6 +1679,26 @@ class AppLocalizations { args: [], ); } + + /// `Allow applications to bypass VPN` + String get allowBypass { + return Intl.message( + 'Allow applications to bypass VPN', + name: 'allowBypass', + desc: '', + args: [], + ); + } + + /// `After opening, some applications can bypass VPN` + String get allowBypassDesc { + return Intl.message( + 'After opening, some applications can bypass VPN', + name: 'allowBypassDesc', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/models/config.dart b/lib/models/config.dart index 7874a30..fe1bd4f 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -24,6 +24,17 @@ class AccessControl with _$AccessControl { _$AccessControlFromJson(json); } +@freezed +class Props with _$Props { + const factory Props({ + AccessControl? accessControl, + bool? allowBypass, + }) = _Props; + + factory Props.fromJson(Map json) => + _$PropsFromJson(json); +} + @JsonSerializable() class Config extends ChangeNotifier { List _profiles; @@ -42,6 +53,7 @@ class Config extends ChangeNotifier { AccessControl _accessControl; bool _isAnimateToPage; bool _autoCheckUpdate; + bool _allowBypass; DAV? _dav; Config() @@ -58,7 +70,8 @@ class Config extends ChangeNotifier { _isAccessControl = false, _autoCheckUpdate = true, _accessControl = const AccessControl(), - _isAnimateToPage = true; + _isAnimateToPage = true, + _allowBypass = true; deleteProfileById(String id) { _profiles = profiles.where((element) => element.id != id).toList(); @@ -306,8 +319,22 @@ class Config extends ChangeNotifier { } } - update( - [Config? config, RecoveryOption recoveryOptions = RecoveryOption.all]) { + @JsonKey(defaultValue: true) + bool get allowBypass { + return _allowBypass; + } + + set allowBypass(bool value) { + if (_allowBypass != value) { + _allowBypass = value; + notifyListeners(); + } + } + + update([ + Config? config, + RecoveryOption recoveryOptions = RecoveryOption.all, + ]) { if (config != null) { _profiles = config._profiles; for (final profile in config._profiles) { @@ -326,6 +353,7 @@ class Config extends ChangeNotifier { _openLog = config._openLog; _themeMode = config._themeMode; _locale = config._locale; + _allowBypass = config._allowBypass; _primaryColor = config._primaryColor; _proxiesSortType = config._proxiesSortType; _isMinimizeOnExit = config._isMinimizeOnExit; diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index b9ee139..0c22ee4 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -239,3 +239,172 @@ abstract class _AccessControl implements AccessControl { _$$AccessControlImplCopyWith<_$AccessControlImpl> get copyWith => throw _privateConstructorUsedError; } + +Props _$PropsFromJson(Map json) { + return _Props.fromJson(json); +} + +/// @nodoc +mixin _$Props { + AccessControl? get accessControl => throw _privateConstructorUsedError; + bool? get allowBypass => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $PropsCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PropsCopyWith<$Res> { + factory $PropsCopyWith(Props value, $Res Function(Props) then) = + _$PropsCopyWithImpl<$Res, Props>; + @useResult + $Res call({AccessControl? accessControl, bool? allowBypass}); + + $AccessControlCopyWith<$Res>? get accessControl; +} + +/// @nodoc +class _$PropsCopyWithImpl<$Res, $Val extends Props> + implements $PropsCopyWith<$Res> { + _$PropsCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessControl = freezed, + Object? allowBypass = freezed, + }) { + return _then(_value.copyWith( + accessControl: freezed == accessControl + ? _value.accessControl + : accessControl // ignore: cast_nullable_to_non_nullable + as AccessControl?, + allowBypass: freezed == allowBypass + ? _value.allowBypass + : allowBypass // ignore: cast_nullable_to_non_nullable + as bool?, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $AccessControlCopyWith<$Res>? get accessControl { + if (_value.accessControl == null) { + return null; + } + + return $AccessControlCopyWith<$Res>(_value.accessControl!, (value) { + return _then(_value.copyWith(accessControl: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$PropsImplCopyWith<$Res> implements $PropsCopyWith<$Res> { + factory _$$PropsImplCopyWith( + _$PropsImpl value, $Res Function(_$PropsImpl) then) = + __$$PropsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({AccessControl? accessControl, bool? allowBypass}); + + @override + $AccessControlCopyWith<$Res>? get accessControl; +} + +/// @nodoc +class __$$PropsImplCopyWithImpl<$Res> + extends _$PropsCopyWithImpl<$Res, _$PropsImpl> + implements _$$PropsImplCopyWith<$Res> { + __$$PropsImplCopyWithImpl( + _$PropsImpl _value, $Res Function(_$PropsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessControl = freezed, + Object? allowBypass = freezed, + }) { + return _then(_$PropsImpl( + accessControl: freezed == accessControl + ? _value.accessControl + : accessControl // ignore: cast_nullable_to_non_nullable + as AccessControl?, + allowBypass: freezed == allowBypass + ? _value.allowBypass + : allowBypass // ignore: cast_nullable_to_non_nullable + as bool?, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$PropsImpl implements _Props { + const _$PropsImpl({this.accessControl, this.allowBypass}); + + factory _$PropsImpl.fromJson(Map json) => + _$$PropsImplFromJson(json); + + @override + final AccessControl? accessControl; + @override + final bool? allowBypass; + + @override + String toString() { + return 'Props(accessControl: $accessControl, allowBypass: $allowBypass)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PropsImpl && + (identical(other.accessControl, accessControl) || + other.accessControl == accessControl) && + (identical(other.allowBypass, allowBypass) || + other.allowBypass == allowBypass)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, accessControl, allowBypass); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$PropsImplCopyWith<_$PropsImpl> get copyWith => + __$$PropsImplCopyWithImpl<_$PropsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$PropsImplToJson( + this, + ); + } +} + +abstract class _Props implements Props { + const factory _Props( + {final AccessControl? accessControl, + final bool? allowBypass}) = _$PropsImpl; + + factory _Props.fromJson(Map json) = _$PropsImpl.fromJson; + + @override + AccessControl? get accessControl; + @override + bool? get allowBypass; + @override + @JsonKey(ignore: true) + _$$PropsImplCopyWith<_$PropsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 1d509ee..c6a4120 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -32,7 +32,8 @@ Config _$ConfigFromJson(Map json) => Config() : DAV.fromJson(json['dav'] as Map) ..isAnimateToPage = json['isAnimateToPage'] as bool? ?? true ..isCompatible = json['isCompatible'] as bool? ?? true - ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true; + ..autoCheckUpdate = json['autoCheckUpdate'] as bool? ?? true + ..allowBypass = json['allowBypass'] as bool? ?? true; Map _$ConfigToJson(Config instance) => { 'profiles': instance.profiles, @@ -52,6 +53,7 @@ Map _$ConfigToJson(Config instance) => { 'isAnimateToPage': instance.isAnimateToPage, 'isCompatible': instance.isCompatible, 'autoCheckUpdate': instance.autoCheckUpdate, + 'allowBypass': instance.allowBypass, }; const _$ThemeModeEnumMap = { @@ -93,3 +95,17 @@ const _$AccessControlModeEnumMap = { AccessControlMode.acceptSelected: 'acceptSelected', AccessControlMode.rejectSelected: 'rejectSelected', }; + +_$PropsImpl _$$PropsImplFromJson(Map json) => _$PropsImpl( + accessControl: json['accessControl'] == null + ? null + : AccessControl.fromJson( + json['accessControl'] as Map), + allowBypass: json['allowBypass'] as bool?, + ); + +Map _$$PropsImplToJson(_$PropsImpl instance) => + { + 'accessControl': instance.accessControl, + 'allowBypass': instance.allowBypass, + }; diff --git a/lib/pages/scan.dart b/lib/pages/scan.dart index b76a639..79e645a 100644 --- a/lib/pages/scan.dart +++ b/lib/pages/scan.dart @@ -107,6 +107,9 @@ class _ScanPageState extends State with WidgetsBindingObserver { case TorchState.unavailable: icon = const Icon(Icons.flash_off); backgroundColor = Colors.transparent; + case TorchState.auto: + icon = const Icon(Icons.flash_auto); + backgroundColor = Colors.orange; } return Container( margin: const EdgeInsets.symmetric(horizontal: 8), diff --git a/lib/state.dart b/lib/state.dart index e44fc11..747f67c 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -61,8 +61,14 @@ class GlobalState { required Config config, required ClashConfig clashConfig, }) async { - final args = - config.isAccessControl ? json.encode(config.accessControl) : null; + final args = config.isAccessControl + ? json.encode( + Props( + accessControl: config.accessControl, + allowBypass: config.allowBypass, + ), + ) + : null; await proxyManager.startProxy( port: clashConfig.mixedPort, args: args, diff --git a/plugins/flutter_distributor b/plugins/flutter_distributor index 57cdfc1..64122ab 160000 --- a/plugins/flutter_distributor +++ b/plugins/flutter_distributor @@ -1 +1 @@ -Subproject commit 57cdfc1534b7e2628967c16fdb41ee3c36e31867 +Subproject commit 64122ab7e1c6af198c91d1ca0d39a6d2de710805 diff --git a/pubspec.lock b/pubspec.lock index f62d8f4..b6ee6f8 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -277,10 +277,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "29c90806ac5f5fb896547720b73b17ee9aed9bba540dc5d91fe29f8c5745b10a" + sha256: a7eed9716c82453da5c09d3a82d4644e7070dd2da81bfe5b6c6873ae4f07cf4f url: "https://pub.dev" source: hosted - version: "8.0.3" + version: "8.0.4" file_selector_linux: dependency: transitive description: @@ -649,10 +649,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: f34c83198d9381f6c100dfaec647c275630840cbcda5d6c5eb6ba264beb96be4 + sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "5.1.1" nested: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index bd6f898..93b4808 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,7 @@ dependencies: windows_single_instance: ^1.0.1 json_annotation: ^4.9.0 file_picker: ^8.0.3 - mobile_scanner: 5.0.1 + mobile_scanner: ^5.1.1 app_links: ^3.5.0 win32_registry: ^1.1.2 tray_manager: ^0.2.1