From aca4a3e979b7fe991458cfdb198fc9c394f51283 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Mon, 26 Aug 2024 20:44:30 +0800 Subject: [PATCH] Add DNS override Fixed some bugs Optimize more detail --- assets/fonts/Icons.ttf | Bin 0 -> 1224 bytes core/Clash.Meta | 2 +- core/common.go | 35 +- core/hub.go | 9 +- lib/application.dart | 16 +- lib/common/common.dart | 4 +- lib/common/http.dart | 19 + lib/common/icons.dart | 6 + lib/common/other.dart | 7 + lib/common/request.dart | 44 +- lib/common/window.dart | 2 +- lib/controller.dart | 9 +- lib/enum/enum.dart | 15 +- lib/fragments/config/app.dart | 36 +- lib/fragments/config/config.dart | 30 +- lib/fragments/config/dns.dart | 824 ++++++++++++++++++ lib/fragments/config/general.dart | 690 ++++----------- lib/fragments/config/vpn.dart | 81 +- lib/fragments/dashboard/dashboard.dart | 10 +- lib/fragments/dashboard/status_switch.dart | 56 +- lib/fragments/proxies/list.dart | 116 ++- lib/fragments/proxies/providers.dart | 35 +- lib/fragments/proxies/proxies.dart | 2 +- lib/fragments/tools.dart | 49 +- lib/l10n/arb/intl_en.arb | 37 +- lib/l10n/arb/intl_zh_CN.arb | 37 +- lib/l10n/intl/messages_en.dart | 54 ++ lib/l10n/intl/messages_zh_CN.dart | 41 + lib/l10n/l10n.dart | 350 ++++++++ lib/main.dart | 2 + lib/models/clash_config.dart | 151 ++-- lib/models/config.dart | 26 +- lib/models/ffi.dart | 1 + .../generated/clash_config.freezed.dart | 815 +++++++++++++++++ lib/models/generated/clash_config.g.dart | 133 ++- lib/models/generated/config.freezed.dart | 183 ++++ lib/models/generated/config.g.dart | 19 +- lib/models/generated/ffi.freezed.dart | 28 +- lib/models/generated/ffi.g.dart | 2 + lib/models/generated/proxy.freezed.dart | 25 +- lib/models/generated/proxy.g.dart | 2 + lib/models/generated/selector.freezed.dart | 179 +++- lib/models/proxy.dart | 3 +- lib/models/selector.dart | 14 +- lib/state.dart | 1 + lib/widgets/clash_container.dart | 3 +- lib/widgets/input.dart | 293 +++++++ lib/widgets/list.dart | 98 ++- lib/widgets/open_container.dart | 134 ++- lib/widgets/sheet.dart | 14 +- lib/widgets/tray_container.dart | 32 - lib/widgets/vpn_container.dart | 58 ++ lib/widgets/widgets.dart | 4 +- macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 196 +++-- pubspec.yaml | 7 +- test/command_test.dart | 9 +- windows/CMakeLists.txt | 3 + windows/EnableLoopback.exe | Bin 0 -> 97536 bytes 59 files changed, 4053 insertions(+), 1000 deletions(-) create mode 100644 assets/fonts/Icons.ttf create mode 100644 lib/common/http.dart create mode 100644 lib/common/icons.dart create mode 100644 lib/fragments/config/dns.dart create mode 100644 lib/widgets/input.dart create mode 100644 lib/widgets/vpn_container.dart create mode 100644 windows/EnableLoopback.exe diff --git a/assets/fonts/Icons.ttf b/assets/fonts/Icons.ttf new file mode 100644 index 0000000000000000000000000000000000000000..5e960b44b708224b54e04376b191e8b5a36b7572 GIT binary patch literal 1224 zcma)5OK1~O6g@AK$*-jqZ4;3;h9>>sBBn``Xb{ta1X76hqY=8%X*wpwWF{mjw7cTQ zji3lF3a-U&+!tJQ}Qk?`JE-VlEzj@25S%>W(ZUwjPJ&5DL8ya1Gb;{JxV*@Pbp#Ak^khSt#EKWOX_ zU*h>j(`PoVO=9oJhjkL_LjKuY+f?wAR--D|VJ0hImjK4ujz2IVXc zEn+#cnp`7{**i;G*bVlC0DN%qClEP6_ji~7op$Jp?zfPedB@Kc5L69Hj+I%T*~nhD zB>&5?lkUE<%iQzGqXe8u1w&yG3Z|0Tt0J4prc)m8fbe>vW0P?)IWd{eSbQpJsY1~S zi;j+2BJXfi9h*qcWKz?~;P8+qdb{i&4X@;Kb91@eN_f;?_Kn9{#rfss`C=GNLc&ft}w z=yOXJUw^%u`@1`n;C+;nw2V2{dxfo6c6;7{l&E&Zmdmsw9KaC5cxq*+xNhIKqm%6f z`#FGD&bb1-gO2ZlmszrNd2>`G9N$g9;`l1RIO_&b;~}4S_yEqKCGU&dPbR>K?Yl65 zTaK@gPdmPwe$DY!1etB?J>;)BegHFgv|cfdHrCmI2?Oo2-fFL#Mr1mkKp8q~%AgEI zFpW48C-f)al3v}YYb`7>wTcbYp`mp`Q!tHAq^cWwOY7)sk=3n8v0|1?(}<&BRqC)- gl?NDBeGMJ%Yvfk3#b?oKwM3=tDsIhEPV}Gl2cNyVH~;_u literal 0 HcmV?d00001 diff --git a/core/Clash.Meta b/core/Clash.Meta index 0125a90..97de6c3 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 0125a90a77353ebfe3554bf027ff7a285e1db8cc +Subproject commit 97de6c34d043d0704d74908e5693716eb7e736e0 diff --git a/core/common.go b/core/common.go index 5065a94..f63e09e 100644 --- a/core/common.go +++ b/core/common.go @@ -39,6 +39,7 @@ type ConfigExtendedParams struct { IsCompatible bool `json:"is-compatible"` SelectedMap map[string]string `json:"selected-map"` TestURL *string `json:"test-url"` + OverrideDns bool `json:"override-dns"` } type GenerateConfigParams struct { @@ -380,6 +381,12 @@ func generateProxyGroupAndRule(proxyGroup *[]map[string]any, rule *[]string) { *rule = computedRule } +func genHosts(hosts, patchHosts map[string]any) { + for k, v := range patchHosts { + hosts[k] = v + } +} + func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfig) { targetConfig.ExternalController = patchConfig.ExternalController targetConfig.ExternalUI = "" @@ -387,7 +394,6 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi targetConfig.ExternalUIURL = "" targetConfig.TCPConcurrent = patchConfig.TCPConcurrent targetConfig.UnifiedDelay = patchConfig.UnifiedDelay - //targetConfig.GeodataMode = false targetConfig.IPv6 = patchConfig.IPv6 targetConfig.LogLevel = patchConfig.LogLevel targetConfig.Port = 0 @@ -405,7 +411,11 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi targetConfig.Profile.StoreSelected = false targetConfig.GeoXUrl = patchConfig.GeoXUrl targetConfig.GlobalUA = patchConfig.GlobalUA - if targetConfig.DNS.Enable == false { + //if targetConfig.DNS.Enable == false { + // targetConfig.DNS = patchConfig.DNS + //} + genHosts(targetConfig.Hosts, patchConfig.Hosts) + if configParams.OverrideDns { targetConfig.DNS = patchConfig.DNS } //if runtime.GOOS == "android" { @@ -413,11 +423,11 @@ func overwriteConfig(targetConfig *config.RawConfig, patchConfig config.RawConfi //} else if runtime.GOOS == "windows" { // targetConfig.DNS.NameServer = append(targetConfig.DNS.NameServer, dns.SystemDNSPlaceholder) //} - if configParams.IsCompatible == false { - targetConfig.ProxyProvider = make(map[string]map[string]any) - targetConfig.RuleProvider = make(map[string]map[string]any) - generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule) - } + //if configParams.IsCompatible == false { + // targetConfig.ProxyProvider = make(map[string]map[string]any) + // targetConfig.RuleProvider = make(map[string]map[string]any) + // generateProxyGroupAndRule(&targetConfig.ProxyGroup, &targetConfig.Rule) + //} } func patchConfig(general *config.General) { @@ -440,6 +450,11 @@ var isRunning = false var runLock sync.Mutex func updateListeners(general *config.General, listeners map[string]constant.InboundListener) { + if !isRunning { + return + } + runLock.Lock() + defer runLock.Unlock() listener.PatchInboundListeners(listeners, tunnel.Tunnel, true) listener.SetAllowLan(general.AllowLan) inbound.SetSkipAuthPrefixes(general.SkipAuthPrefixes) @@ -525,10 +540,8 @@ func applyConfig() error { hub.UltraApplyConfig(cfg) patchSelectGroup() } - if isRunning { - updateListeners(cfg.General, cfg.Listeners) - hcCompatibleProvider(cfg.Providers) - } + updateListeners(cfg.General, cfg.Listeners) + hcCompatibleProvider(cfg.Providers) externalProviders = getExternalProvidersRaw() return err } diff --git a/core/hub.go b/core/hub.go index b3ae314..45c0e61 100644 --- a/core/hub.go +++ b/core/hub.go @@ -48,9 +48,11 @@ func start() { //export stop func stop() { runLock.Lock() - defer runLock.Unlock() - isRunning = false - stopListeners() + go func() { + defer runLock.Unlock() + isRunning = false + stopListeners() + }() } //export initClash @@ -236,6 +238,7 @@ func asyncTestDelay(s *C.char, port C.longlong) { proxies := tunnel.ProxiesWithProviders() proxy := proxies[params.ProxyName] + proxy.Name() delayData := &Delay{ Name: params.ProxyName, diff --git a/lib/application.dart b/lib/application.dart index fa3e618..5c7553c 100644 --- a/lib/application.dart +++ b/lib/application.dart @@ -109,6 +109,17 @@ class ApplicationState extends State { ); } + _buildPage(Widget page) { + if (system.isDesktop) { + return WindowHeaderContainer( + child: page, + ); + } + return VpnContainer( + child: page, + ); + } + _updateSystemColorSchemes( ColorScheme? lightDynamic, ColorScheme? darkDynamic, @@ -147,10 +158,7 @@ class ApplicationState extends State { GlobalWidgetsLocalizations.delegate ], builder: (_, child) { - if (system.isDesktop) { - return WindowHeaderContainer(child: child!); - } - return child!; + return _buildPage(child!); }, scrollBehavior: BaseScrollBehavior(), title: appName, diff --git a/lib/common/common.dart b/lib/common/common.dart index 91c1468..7a88639 100644 --- a/lib/common/common.dart +++ b/lib/common/common.dart @@ -25,4 +25,6 @@ export 'package.dart'; export 'measure.dart'; export 'windows.dart'; export 'iterable.dart'; -export 'scroll.dart'; \ No newline at end of file +export 'scroll.dart'; +export 'icons.dart'; +export 'http.dart'; \ No newline at end of file diff --git a/lib/common/http.dart b/lib/common/http.dart new file mode 100644 index 0000000..2f55a06 --- /dev/null +++ b/lib/common/http.dart @@ -0,0 +1,19 @@ +import 'dart:io'; + +import '../state.dart'; + +class FlClashHttpOverrides extends HttpOverrides { + + @override + HttpClient createHttpClient(SecurityContext? context) { + final client = super.createHttpClient(context); + client.badCertificateCallback = (_, __, ___) => true; + client.findProxy = (url) { + final port = globalState.appController.clashConfig.mixedPort; + final isStart = globalState.appController.appState.isStart; + if(!isStart) return "DIRECT"; + return "PROXY localhost:$port;DIRECT"; + }; + return client; + } +} diff --git a/lib/common/icons.dart b/lib/common/icons.dart new file mode 100644 index 0000000..6faca51 --- /dev/null +++ b/lib/common/icons.dart @@ -0,0 +1,6 @@ +import 'package:flutter/material.dart'; + +class IconsExt{ + static const IconData target = + IconData(0xe900, fontFamily: "Icons"); +} \ No newline at end of file diff --git a/lib/common/other.dart b/lib/common/other.dart index 98d2820..393a861 100644 --- a/lib/common/other.dart +++ b/lib/common/other.dart @@ -6,6 +6,7 @@ import 'dart:typed_data'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:flutter/material.dart'; +import 'package:lpinyin/lpinyin.dart'; import 'package:zxing2/qrcode.dart'; import 'package:image/image.dart' as img; @@ -130,6 +131,12 @@ class Other { return build1.compareTo(build2); } + String getPinyin(String value) { + return value.isNotEmpty + ? PinyinHelper.getFirstWordPinyin(value.substring(0, 1)) + : ""; + } + Future parseQRCode(Uint8List? bytes) { return Isolate.run(() { if (bytes == null) return null; diff --git a/lib/common/request.dart b/lib/common/request.dart index 83c43ef..1c4a62d 100644 --- a/lib/common/request.dart +++ b/lib/common/request.dart @@ -1,50 +1,27 @@ -import 'dart:io'; import 'dart:math'; +import 'dart:typed_data'; import 'package:dio/dio.dart'; -import 'package:dio/io.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/ip.dart'; import 'package:fl_clash/state.dart'; +import 'package:flutter/cupertino.dart'; class Request { late final Dio _dio; - int? _port; - bool _isStart = false; + String? userAgent; Request() { _dio = Dio(); _dio.interceptors.add( InterceptorsWrapper( onRequest: (options, handler) { - _updateAdapter(); return handler.next(options); // 继续请求 }, ), ); } - _updateAdapter() { - final port = globalState.appController.clashConfig.mixedPort; - final isStart = globalState.appController.appState.isStart; - if (_port != port || isStart != _isStart) { - _port = port; - _isStart = isStart; - _dio.httpClientAdapter = IOHttpClientAdapter( - createHttpClient: () { - final client = HttpClient(); - if (!_isStart) return client; - client.userAgent = globalState.appController.clashConfig.globalUa; - client.findProxy = (url) { - return "PROXY localhost:$_port;DIRECT"; - }; - return client; - }, - validateCertificate: (_, __, ___) => true, - ); - } - } - Future getFileResponseForUrl(String url) async { final response = await _dio .get( @@ -62,6 +39,19 @@ class Request { return response; } + Future getImage(String url) async { + if (url.isEmpty) return null; + final response = await _dio.get( + url, + options: Options( + responseType: ResponseType.bytes, + ), + ); + final data = response.data; + if (data == null) return null; + return MemoryImage(data); + } + Future?> checkForUpdate() async { final response = await _dio.get( "https://api.github.com/repos/$repository/releases/latest", @@ -101,7 +91,7 @@ class Request { return source.value(response.data!); } } catch (e) { - if(cancelToken?.isCancelled == true){ + if (cancelToken?.isCancelled == true) { throw "cancelled"; } continue; diff --git a/lib/common/window.dart b/lib/common/window.dart index dba27d1..c4aa637 100644 --- a/lib/common/window.dart +++ b/lib/common/window.dart @@ -34,7 +34,7 @@ class Window { // await windowManager.setTitleBarStyle(TitleBarStyle.hidden); // } await windowManager.waitUntilReadyToShow(windowOptions, () async { - // await windowManager.setPreventClose(true); + await windowManager.setPreventClose(true); }); } diff --git a/lib/controller.dart b/lib/controller.dart index 04acc51..9c96919 100644 --- a/lib/controller.dart +++ b/lib/controller.dart @@ -125,6 +125,10 @@ class AppController { ); } + updateTray(){ + + } + Future applyProfile({bool isPrue = false}) async { if (isPrue) { await globalState.applyProfile( @@ -232,6 +236,7 @@ class AppController { handleExit() async { await updateStatus(false); + await proxy?.stopProxy(); await savePreferences(); clashCore.shutdown(); system.exit(); @@ -433,8 +438,8 @@ class AppController { return List.of(proxies) ..sort( (a, b) => other.sortByChar( - PinyinHelper.getPinyin(a.name), - PinyinHelper.getPinyin(b.name), + other.getPinyin(a.name), + other.getPinyin(b.name), ), ); } diff --git a/lib/enum/enum.dart b/lib/enum/enum.dart index 1794e43..95d0700 100644 --- a/lib/enum/enum.dart +++ b/lib/enum/enum.dart @@ -1,5 +1,7 @@ // ignore_for_file: constant_identifier_names +import 'package:freezed_annotation/freezed_annotation.dart'; + enum GroupType { Selector, URLTest, Fallback, LoadBalance, Relay } enum GroupName { GLOBAL, Proxy, Auto, Fallback } @@ -86,6 +88,17 @@ enum CommonCardType { plain, filled } enum ProxiesType { tab, list } -enum ProxiesLayout{ loose, standard, tight } +enum ProxiesLayout { loose, standard, tight } enum ProxyCardType { expand, shrink, min } + + +enum DnsMode { + normal, + @JsonValue("fake-ip") + fakeIp, + @JsonValue("redir-host") + redirHost, + hosts +} + diff --git a/lib/fragments/config/app.dart b/lib/fragments/config/app.dart index 9942c07..7bb2310 100644 --- a/lib/fragments/config/app.dart +++ b/lib/fragments/config/app.dart @@ -1,8 +1,11 @@ -import 'package:fl_clash/common/app_localizations.dart'; +import 'dart:io'; + +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/models/config.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; +import 'package:path/path.dart' show dirname, join; import 'package:provider/provider.dart'; class CloseConnectionsSwitch extends StatelessWidget { @@ -55,7 +58,32 @@ class UsageSwitch extends StatelessWidget { } } -const appItems = [ - CloseConnectionsSwitch(), - UsageSwitch(), +class UWPLoopbackUtil extends StatelessWidget { + const UWPLoopbackUtil({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.onlyProxy, + builder: (_, onlyProxy, __) { + return ListItem( + leading: const Icon(Icons.lock_open), + title: Text(appLocalizations.loopback), + subtitle: Text(appLocalizations.loopbackDesc), + onTap: () { + windows?.runas( + '"${join(dirname(Platform.resolvedExecutable), "EnableLoopback.exe")}"', + "", + ); + }, + ); + }, + ); + } +} + +final appItems = [ + if (Platform.isWindows) const UWPLoopbackUtil(), + const CloseConnectionsSwitch(), + const UsageSwitch(), ]; diff --git a/lib/fragments/config/config.dart b/lib/fragments/config/config.dart index 8f61b72..69d7d21 100644 --- a/lib/fragments/config/config.dart +++ b/lib/fragments/config/config.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/fragments/config/app.dart'; +import 'package:fl_clash/fragments/config/dns.dart'; import 'package:fl_clash/fragments/config/general.dart'; import 'package:fl_clash/fragments/config/vpn.dart'; import 'package:fl_clash/widgets/widgets.dart'; @@ -24,6 +25,7 @@ class _ConfigFragmentState extends State { leading: const Icon(Icons.settings_applications), delegate: OpenDelegate( title: appLocalizations.app, + isBlur: false, widget: generateListView( appItems .separated( @@ -42,14 +44,9 @@ class _ConfigFragmentState extends State { leading: const Icon(Icons.vpn_key), delegate: OpenDelegate( title: "VPN", + isBlur: false, widget: generateListView( - vpnItems - .separated( - const Divider( - height: 0, - ), - ) - .toList(), + vpnItems, ), ), ), @@ -60,21 +57,24 @@ class _ConfigFragmentState extends State { delegate: OpenDelegate( title: appLocalizations.general, widget: generateListView( - generalItems - .separated( - const Divider( - height: 0, - ), - ) - .toList(), + generalItems, ), + isBlur: false, extendPageWidth: 360, ), ), - ListItem( + ListItem.open( title: const Text("DNS"), subtitle: Text(appLocalizations.dnsDesc), leading: const Icon(Icons.dns), + delegate: OpenDelegate( + title: "DNS", + widget: generateListView( + dnsItems, + ), + isBlur: false, + extendPageWidth: 360, + ), ) ]; return generateListView( diff --git a/lib/fragments/config/dns.dart b/lib/fragments/config/dns.dart new file mode 100644 index 0000000..54ea538 --- /dev/null +++ b/lib/fragments/config/dns.dart @@ -0,0 +1,824 @@ +import 'package:collection/collection.dart'; +import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.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:provider/provider.dart'; + +class OverrideItem extends StatelessWidget { + const OverrideItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.overrideDns, + builder: (_, override, __) { + return ListItem.switchItem( + title: Text(appLocalizations.overrideDns), + subtitle: Text(appLocalizations.overrideDnsDesc), + delegate: SwitchDelegate( + value: override, + onChanged: (bool value) async { + final config = globalState.appController.config; + config.overrideDns = value; + }, + ), + ); + }, + ); + } +} + +class DnsDisabledContainer extends StatelessWidget { + final Widget child; + + const DnsDisabledContainer( + this.child, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.overrideDns, + builder: (_, enable, child) { + return AbsorbPointer( + absorbing: !enable, + child: DisabledMask( + status: !enable, + child: Container( + color: context.colorScheme.surface, + child: child!, + ), + ), + ); + }, + child: child, + ); + } +} + +class StatusItem extends StatelessWidget { + const StatusItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.enable, + builder: (_, enable, __) { + return ListItem.switchItem( + title: Text(appLocalizations.status), + subtitle: Text(appLocalizations.statusDesc), + delegate: SwitchDelegate( + value: enable, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + enable: value, + ); + }, + ), + ); + }, + ); + } +} + +class PreferH3Item extends StatelessWidget { + const PreferH3Item({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.preferH3, + builder: (_, preferH3, __) { + return ListItem.switchItem( + title: const Text("PreferH3"), + subtitle: Text(appLocalizations.preferH3Desc), + delegate: SwitchDelegate( + value: preferH3, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + preferH3: value, + ); + }, + ), + ); + }, + ); + } +} + +class IPv6Item extends StatelessWidget { + const IPv6Item({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.ipv6, + builder: (_, ipv6, __) { + return ListItem.switchItem( + title: const Text("IPv6"), + delegate: SwitchDelegate( + value: ipv6, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + ipv6: value, + ); + }, + ), + ); + }, + ); + } +} + +class RespectRulesItem extends StatelessWidget { + const RespectRulesItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.respectRules, + builder: (_, respectRules, __) { + return ListItem.switchItem( + title: Text(appLocalizations.respectRules), + subtitle: Text(appLocalizations.respectRulesDesc), + delegate: SwitchDelegate( + value: respectRules, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + respectRules: value, + ); + }, + ), + ); + }, + ); + } +} + +class DnsModeItem extends StatelessWidget { + const DnsModeItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.enhancedMode, + builder: (_, enhancedMode, __) { + return ListItem.options( + title: Text(appLocalizations.dnsMode), + subtitle: Text(enhancedMode.name), + delegate: OptionsDelegate( + title: appLocalizations.dnsMode, + options: DnsMode.values, + onChanged: (value) { + if (value == null) { + return; + } + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith(enhancedMode: value); + }, + textBuilder: (dnsMode) => dnsMode.name, + value: enhancedMode, + ), + ); + }, + ); + } +} + +class FakeIpRangeItem extends StatelessWidget { + const FakeIpRangeItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.fakeIpRange, + builder: (_, fakeIpRange, __) { + return ListItem.input( + title: Text(appLocalizations.fakeipRange), + subtitle: Text(fakeIpRange), + delegate: InputDelegate( + title: appLocalizations.fakeipRange, + value: fakeIpRange, + onChanged: (String? value) { + if (value != null) { + try { + final clashConfig = globalState.appController.clashConfig; + clashConfig.dns = clashConfig.dns.copyWith( + fakeIpRange: value, + ); + } catch (e) { + globalState.showMessage( + title: appLocalizations.fakeipRange, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + }, + ), + ); + }, + ); + } +} + +class FakeIpFilterItem extends StatelessWidget { + const FakeIpFilterItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.fakeipFilter), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.fakeipFilter, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.fakeIpFilter, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, fakeIpFilter, __) { + return UpdatePage( + title: appLocalizations.fakeipFilter, + items: fakeIpFilter, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fakeIpFilter: List.from(dns.fakeIpFilter)..remove(value), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + if (fakeIpFilter.contains(value)) return; + clashConfig.dns = dns.copyWith( + fakeIpFilter: List.from(dns.fakeIpFilter)..add(value), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class DefaultNameserverItem extends StatelessWidget { + const DefaultNameserverItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.defaultNameserver), + subtitle: Text(appLocalizations.defaultNameserverDesc), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.defaultNameserver, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.defaultNameserver, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, defaultNameserver, __) { + return UpdatePage( + title: appLocalizations.defaultNameserver, + items: defaultNameserver, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + defaultNameserver: List.from(dns.defaultNameserver) + ..remove(value), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + if (defaultNameserver.contains(value)) return; + clashConfig.dns = dns.copyWith( + defaultNameserver: List.from(dns.defaultNameserver) + ..add(value), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class NameserverItem extends StatelessWidget { + const NameserverItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.nameserver), + subtitle: Text(appLocalizations.nameserverDesc), + delegate: OpenDelegate( + title: appLocalizations.nameserver, + isBlur: false, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.nameserver, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, nameserver, __) { + return UpdatePage( + title: "域名服务器", + items: nameserver, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + nameserver: List.from(dns.nameserver)..remove(value), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + if (nameserver.contains(value)) return; + clashConfig.dns = dns.copyWith( + nameserver: List.from(dns.nameserver)..add(value), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class UseHostsItem extends StatelessWidget { + const UseHostsItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.useHosts, + builder: (_, useHosts, __) { + return ListItem.switchItem( + title: Text(appLocalizations.useHosts), + delegate: SwitchDelegate( + value: useHosts, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + useHosts: value, + ); + }, + ), + ); + }, + ); + } +} + +class UseSystemHostsItem extends StatelessWidget { + const UseSystemHostsItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.useSystemHosts, + builder: (_, useSystemHosts, __) { + return ListItem.switchItem( + title: Text(appLocalizations.useSystemHosts), + delegate: SwitchDelegate( + value: useSystemHosts, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + useSystemHosts: value, + ); + }, + ), + ); + }, + ); + } +} + +class NameserverPolicyItem extends StatelessWidget { + const NameserverPolicyItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.nameserverPolicy), + subtitle: Text(appLocalizations.nameserverPolicyDesc), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.nameserverPolicy, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.nameserverPolicy, + shouldRebuild: (prev, next) => + !const MapEquality().equals(prev, next), + builder: (_, nameserverPolicy, __) { + return UpdatePage( + title: appLocalizations.nameserverPolicy, + items: nameserverPolicy.entries, + titleBuilder: (item) => Text(item.key), + subtitleBuilder: (item) => Text(item.value), + isMap: true, + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + nameserverPolicy: Map.from(dns.nameserverPolicy) + ..remove(value.key), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + nameserverPolicy: Map.from(dns.nameserverPolicy) + ..addEntries([value]), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class ProxyServerNameserverItem extends StatelessWidget { + const ProxyServerNameserverItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.proxyNameserver), + subtitle: Text(appLocalizations.proxyNameserverDesc), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.proxyNameserver, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.proxyServerNameserver, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, proxyServerNameserver, __) { + return UpdatePage( + title: appLocalizations.proxyNameserver, + items: proxyServerNameserver, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + proxyServerNameserver: List.from(dns.proxyServerNameserver) + ..remove(value), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + if (proxyServerNameserver.contains(value)) return; + clashConfig.dns = dns.copyWith( + proxyServerNameserver: List.from(dns.proxyServerNameserver) + ..add(value), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class FallbackItem extends StatelessWidget { + const FallbackItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.fallback), + subtitle: Text(appLocalizations.fallbackDesc), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.fallback, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.fallback, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, fallback, __) { + return UpdatePage( + title: appLocalizations.fallback, + items: fallback, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallback: List.from(dns.fallback)..remove(value), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + if (fallback.contains(value)) return; + clashConfig.dns = dns.copyWith( + fallback: List.from(dns.fallback)..add(value), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class GeoipItem extends StatelessWidget { + const GeoipItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoip, + builder: (_, geoip, __) { + return ListItem.switchItem( + title: const Text("Geoip"), + delegate: SwitchDelegate( + value: geoip, + onChanged: (bool value) async { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith(geoip: value), + ); + }, + ), + ); + }, + ); + } +} + +class GeoipCodeItem extends StatelessWidget { + const GeoipCodeItem({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geoipCode, + builder: (_, geoipCode, __) { + return ListItem.input( + title: Text(appLocalizations.geoipCode), + subtitle: Text(geoipCode), + delegate: InputDelegate( + title: appLocalizations.geoipCode, + value: geoipCode, + onChanged: (String? value) { + if (value != null) { + try { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + geoipCode: value, + ), + ); + } catch (e) { + globalState.showMessage( + title: appLocalizations.geoipCode, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + }, + ), + ); + }, + ); + } +} + +class GeositeItem extends StatelessWidget { + const GeositeItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: const Text("Geosite"), + delegate: OpenDelegate( + isBlur: false, + title: "Geosite", + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.geosite, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, geosite, __) { + return UpdatePage( + title: "Geosite", + items: geosite, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + geosite: List.from(geosite)..remove(value), + ), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + geosite: List.from(geosite)..add(value), + ), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class IpcidrItem extends StatelessWidget { + const IpcidrItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.ipcidr), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.ipcidr, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.ipcidr, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, ipcidr, __) { + return UpdatePage( + title: appLocalizations.ipcidr, + items: ipcidr, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + ipcidr: List.from(ipcidr)..remove(value), + ), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + ipcidr: List.from(ipcidr)..add(value), + ), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class DomainItem extends StatelessWidget { + const DomainItem({super.key}); + + @override + Widget build(BuildContext context) { + return ListItem.open( + title: Text(appLocalizations.domain), + delegate: OpenDelegate( + isBlur: false, + title: appLocalizations.domain, + widget: Selector>( + selector: (_, clashConfig) => clashConfig.dns.fallbackFilter.domain, + shouldRebuild: (prev, next) => + !const ListEquality().equals(prev, next), + builder: (_, domain, __) { + return UpdatePage( + title: appLocalizations.domain, + items: domain, + titleBuilder: (item) => Text(item), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + domain: List.from(domain)..remove(value), + ), + ); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + final dns = clashConfig.dns; + clashConfig.dns = dns.copyWith( + fallbackFilter: dns.fallbackFilter.copyWith( + domain: List.from(domain)..add(value), + ), + ); + }, + ); + }, + ), + extendPageWidth: 360, + ), + ); + } +} + +class DnsOptions extends StatelessWidget { + const DnsOptions({super.key}); + + @override + Widget build(BuildContext context) { + return DnsDisabledContainer( + Column( + children: generateSection( + title: appLocalizations.options, + items: [ + const StatusItem(), + const UseHostsItem(), + const UseSystemHostsItem(), + const IPv6Item(), + const RespectRulesItem(), + const PreferH3Item(), + const DnsModeItem(), + const FakeIpRangeItem(), + const FakeIpFilterItem(), + const DefaultNameserverItem(), + const NameserverPolicyItem(), + const NameserverItem(), + const FallbackItem(), + const ProxyServerNameserverItem(), + ], + ), + ), + ); + } +} + +class FallbackFilterOptions extends StatelessWidget { + const FallbackFilterOptions({super.key}); + + @override + Widget build(BuildContext context) { + return DnsDisabledContainer( + Column( + children: generateSection( + title: appLocalizations.fallbackFilter, + items: [ + const GeoipItem(), + const GeoipCodeItem(), + const GeositeItem(), + const IpcidrItem(), + const DomainItem(), + ], + ), + ), + ); + } +} + +const dnsItems = [ + OverrideItem(), + DnsOptions(), + FallbackFilterOptions(), +]; diff --git a/lib/fragments/config/general.dart b/lib/fragments/config/general.dart index 507ed5a..4082389 100644 --- a/lib/fragments/config/general.dart +++ b/lib/fragments/config/general.dart @@ -1,3 +1,4 @@ +import 'package:collection/collection.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; @@ -6,250 +7,184 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class LogLevelMenu extends StatelessWidget { - const LogLevelMenu({super.key}); - - _showLogLevelDialog(BuildContext context, LogLevel value) { - globalState.showCommonDialog( - child: AlertDialog( - title: Text(appLocalizations.logLevel), - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - for (final logLevel in LogLevel.values) - ListItem.radio( - delegate: RadioDelegate( - value: logLevel, - groupValue: value, - onChanged: (LogLevel? value) { - if (value == null) { - return; - } - final appController = globalState.appController; - appController.clashConfig.logLevel = value; - Navigator.of(context).pop(); - }, - ), - title: Text(logLevel.name), - ) - ], - ), - ), - ), - ); - } +class LogLevelItem extends StatelessWidget { + const LogLevelItem({super.key}); @override Widget build(BuildContext context) { return Selector( selector: (_, clashConfig) => clashConfig.logLevel, builder: (_, value, __) { - return ListItem( + return ListItem.options( leading: const Icon(Icons.info_outline), title: Text(appLocalizations.logLevel), subtitle: Text(value.name), - onTap: () { - _showLogLevelDialog(context, value); - }, + delegate: OptionsDelegate( + title: appLocalizations.logLevel, + options: LogLevel.values, + onChanged: (LogLevel? value) { + if (value == null) { + return; + } + final appController = globalState.appController; + appController.clashConfig.logLevel = value; + }, + textBuilder: (logLevel) => logLevel.name, + value: value, + ), ); }, ); } } -class UaMenu extends StatelessWidget { - const UaMenu({super.key}); - - _showUaDialog(BuildContext context, String? value) { - const uas = [ - null, - "clash-verge/v1.6.6", - "ClashforWindows/0.19.23", - ]; - globalState.showCommonDialog( - child: AlertDialog( - title: const Text("UA"), - contentPadding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 16, - ), - content: SizedBox( - width: 250, - child: Wrap( - children: [ - for (final ua in uas) - ListItem.radio( - delegate: RadioDelegate( - value: ua, - groupValue: value, - onChanged: (String? value) { - final appController = globalState.appController; - appController.clashConfig.globalRealUa = value; - Navigator.of(context).pop(); - }, - ), - title: Text(ua ?? appLocalizations.defaultText), - ) - ], - ), - ), - ), - ); - } +class UaItem extends StatelessWidget { + const UaItem({super.key}); @override Widget build(BuildContext context) { return Selector( selector: (_, clashConfig) => clashConfig.globalRealUa, builder: (_, value, __) { - return ListItem( + return ListItem.options( leading: const Icon(Icons.computer_outlined), title: const Text("UA"), subtitle: Text(value ?? appLocalizations.defaultText), - onTap: () { - _showUaDialog(context, value); - }, + delegate: OptionsDelegate( + title: "UA", + options: [ + null, + "clash-verge/v1.6.6", + "ClashforWindows/0.19.23", + ], + value: value, + onChanged: (ua) { + final appController = globalState.appController; + appController.clashConfig.globalRealUa = ua; + }, + textBuilder: (ua) => ua ?? appLocalizations.defaultText, + ), ); }, ); } } -class KeepAliveIntervalInput extends StatelessWidget { - const KeepAliveIntervalInput({super.key}); - - _updateKeepAliveInterval(int keepAliveInterval) async { - final newKeepAliveIntervalString = - await globalState.showCommonDialog( - child: KeepAliveIntervalFormDialog( - keepAliveInterval: keepAliveInterval, - ), - ); - if (newKeepAliveIntervalString != null && - newKeepAliveIntervalString != "$keepAliveInterval") { - try { - final newKeepAliveInterval = int.parse(newKeepAliveIntervalString); - if (newKeepAliveInterval <= 0) { - throw "Invalid keepAliveInterval"; - } - globalState.appController.clashConfig.keepAliveInterval = - newKeepAliveInterval; - globalState.appController.updateClashConfigDebounce(); - } catch (e) { - globalState.showMessage( - title: appLocalizations.testUrl, - message: TextSpan( - text: e.toString(), - ), - ); - } - } - } +class KeepAliveIntervalItem extends StatelessWidget { + const KeepAliveIntervalItem({super.key}); @override Widget build(BuildContext context) { return Selector( selector: (_, config) => config.keepAliveInterval, builder: (_, value, __) { - return ListItem( + return ListItem.input( leading: const Icon(Icons.timer_outlined), title: Text(appLocalizations.keepAliveIntervalDesc), subtitle: Text("$value ${appLocalizations.seconds}"), - onTap: () { - _updateKeepAliveInterval(value); - }, + delegate: InputDelegate( + title: appLocalizations.keepAliveIntervalDesc, + suffixText: appLocalizations.seconds, + value: value.toString(), + onChanged: (String? value) { + if (value != null) { + try { + final intValue = int.parse(value); + if (intValue <= 0) { + throw "Invalid keepAliveInterval"; + } + globalState.appController.clashConfig.keepAliveInterval = + intValue; + } catch (e) { + globalState.showMessage( + title: appLocalizations.keepAliveIntervalDesc, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + }, + ), ); }, ); } } -class TestUrlInput extends StatelessWidget { - const TestUrlInput({super.key}); - - _modifyTestUrl(String testUrl) async { - final newTestUrl = await globalState.showCommonDialog( - child: TestUrlFormDialog( - testUrl: testUrl, - ), - ); - if (newTestUrl != null && newTestUrl != testUrl) { - try { - if (!newTestUrl.isUrl) { - throw "Invalid url"; - } - globalState.appController.config.testUrl = newTestUrl; - } catch (e) { - globalState.showMessage( - title: appLocalizations.testUrl, - message: TextSpan( - text: e.toString(), - ), - ); - } - } - } +class TestUrlItem extends StatelessWidget { + const TestUrlItem({super.key}); @override Widget build(BuildContext context) { return Selector( selector: (_, config) => config.testUrl, builder: (_, value, __) { - return ListItem( + return ListItem.input( leading: const Icon(Icons.timeline), title: Text(appLocalizations.testUrl), subtitle: Text(value), - onTap: () { - _modifyTestUrl(value); - }, + delegate: InputDelegate( + title: appLocalizations.testUrl, + value: value, + onChanged: (String? value) { + if (value != null) { + try { + if (!value.isUrl) { + throw "Invalid url"; + } + globalState.appController.config.testUrl = value; + } catch (e) { + globalState.showMessage( + title: appLocalizations.testUrl, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + }, + ), ); }, ); } } -class MixedPortInput extends StatelessWidget { - const MixedPortInput({super.key}); - - _modifyMixedPort(num mixedPort) async { - final port = await globalState.showCommonDialog( - child: MixedPortFormDialog( - mixedPort: mixedPort, - ), - ); - if (port != null && port != mixedPort) { - try { - final mixedPort = int.parse(port); - if (mixedPort < 1024 || mixedPort > 49151) throw "Invalid port"; - globalState.appController.clashConfig.mixedPort = mixedPort; - } catch (e) { - globalState.showMessage( - title: appLocalizations.proxyPort, - message: TextSpan( - text: e.toString(), - ), - ); - } - } - } +class MixedPortItem extends StatelessWidget { + const MixedPortItem({super.key}); @override Widget build(BuildContext context) { return Selector( selector: (_, clashConfig) => clashConfig.mixedPort, - builder: (_, mixedPort, __) { - return ListItem( - onTap: () { - _modifyMixedPort(mixedPort); - }, + builder: (_, value, __) { + return ListItem.input( leading: const Icon(Icons.adjust_outlined), title: Text(appLocalizations.proxyPort), - subtitle: Text("$mixedPort"), + subtitle: Text("$value"), + delegate: InputDelegate( + title: appLocalizations.proxyPort, + value: value.toString(), + onChanged: (String? value) { + if (value != null) { + try { + final mixedPort = int.parse(value); + if (mixedPort < 1024 || mixedPort > 49151) { + throw "Invalid port"; + } + globalState.appController.clashConfig.mixedPort = mixedPort; + } catch (e) { + globalState.showMessage( + title: appLocalizations.proxyPort, + message: TextSpan( + text: e.toString(), + ), + ); + } + } + }, + ), ); }, ); @@ -264,18 +199,42 @@ class HostsItem extends StatelessWidget { return ListItem.open( leading: const Icon(Icons.view_list_outlined), title: const Text("Hosts"), - subtitle: Text("编辑追加hosts"), + subtitle: Text(appLocalizations.hostsDesc), delegate: OpenDelegate( + isBlur: false, title: "Hosts", - widget: HostsForm(), + widget: Selector( + selector: (_, clashConfig) => clashConfig.hosts, + shouldRebuild: (prev, next) => + !const MapEquality().equals(prev, next), + builder: (_, hosts, ___) { + final entries = hosts.entries; + return UpdatePage( + title: "Hosts", + items: entries, + titleBuilder: (item) => Text(item.key), + subtitleBuilder: (item) => Text(item.value), + onRemove: (value) { + final clashConfig = globalState.appController.clashConfig; + clashConfig.hosts = Map.from(hosts)..remove(value.key); + }, + onAdd: (value) { + final clashConfig = globalState.appController.clashConfig; + clashConfig.hosts = Map.from(clashConfig.hosts) + ..addEntries([value]); + }, + isMap: true, + ); + }, + ), extendPageWidth: 360, ), ); } } -class Ipv6Switch extends StatelessWidget { - const Ipv6Switch({super.key}); +class Ipv6Item extends StatelessWidget { + const Ipv6Item({super.key}); @override Widget build(BuildContext context) { @@ -299,8 +258,8 @@ class Ipv6Switch extends StatelessWidget { } } -class AllowLanSwitch extends StatelessWidget { - const AllowLanSwitch({super.key}); +class AllowLanItem extends StatelessWidget { + const AllowLanItem({super.key}); @override Widget build(BuildContext context) { @@ -324,8 +283,8 @@ class AllowLanSwitch extends StatelessWidget { } } -class UnifiedDelaySwitch extends StatelessWidget { - const UnifiedDelaySwitch({super.key}); +class UnifiedDelayItem extends StatelessWidget { + const UnifiedDelayItem({super.key}); @override Widget build(BuildContext context) { @@ -349,8 +308,8 @@ class UnifiedDelaySwitch extends StatelessWidget { } } -class FindProcessSwitch extends StatelessWidget { - const FindProcessSwitch({super.key}); +class FindProcessItem extends StatelessWidget { + const FindProcessItem({super.key}); @override Widget build(BuildContext context) { @@ -376,8 +335,8 @@ class FindProcessSwitch extends StatelessWidget { } } -class TcpConcurrentSwitch extends StatelessWidget { - const TcpConcurrentSwitch({super.key}); +class TcpConcurrentItem extends StatelessWidget { + const TcpConcurrentItem({super.key}); @override Widget build(BuildContext context) { @@ -401,8 +360,8 @@ class TcpConcurrentSwitch extends StatelessWidget { } } -class GeodataLoaderSwitch extends StatelessWidget { - const GeodataLoaderSwitch({super.key}); +class GeodataLoaderItem extends StatelessWidget { + const GeodataLoaderItem({super.key}); @override Widget build(BuildContext context) { @@ -428,8 +387,8 @@ class GeodataLoaderSwitch extends StatelessWidget { } } -class ExternalControllerSwitch extends StatelessWidget { - const ExternalControllerSwitch({super.key}); +class ExternalControllerItem extends StatelessWidget { + const ExternalControllerItem({super.key}); @override Widget build(BuildContext context) { @@ -454,331 +413,24 @@ class ExternalControllerSwitch extends StatelessWidget { } } -const generalItems = [ - LogLevelMenu(), - UaMenu(), - KeepAliveIntervalInput(), - TestUrlInput(), - MixedPortInput(), +final generalItems = const [ + LogLevelItem(), + UaItem(), + KeepAliveIntervalItem(), + TestUrlItem(), + MixedPortItem(), HostsItem(), - Ipv6Switch(), - AllowLanSwitch(), - UnifiedDelaySwitch(), - FindProcessSwitch(), - TcpConcurrentSwitch(), - GeodataLoaderSwitch(), - ExternalControllerSwitch(), -]; - -class MixedPortFormDialog extends StatefulWidget { - final num mixedPort; - - const MixedPortFormDialog({super.key, required this.mixedPort}); - - @override - State createState() => _MixedPortFormDialogState(); -} - -class _MixedPortFormDialogState extends State { - late TextEditingController portController; - - @override - void initState() { - super.initState(); - portController = TextEditingController(text: "${widget.mixedPort}"); - } - - _handleUpdate() async { - final port = portController.value.text; - if (port.isEmpty) return; - Navigator.of(context).pop(port); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.proxyPort), - content: SizedBox( - width: 300, - child: Wrap( - runSpacing: 16, - children: [ - TextField( - controller: portController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - ), - ], - ), + Ipv6Item(), + AllowLanItem(), + UnifiedDelayItem(), + FindProcessItem(), + TcpConcurrentItem(), + GeodataLoaderItem(), + ExternalControllerItem(), +] + .separated( + const Divider( + height: 0, ), - actions: [ - TextButton( - onPressed: _handleUpdate, - child: Text(appLocalizations.submit), - ) - ], - ); - } -} - -class TestUrlFormDialog extends StatefulWidget { - final String testUrl; - - const TestUrlFormDialog({ - super.key, - required this.testUrl, - }); - - @override - State createState() => _TestUrlFormDialogState(); -} - -class _TestUrlFormDialogState extends State { - late TextEditingController testUrlController; - - @override - void initState() { - super.initState(); - testUrlController = TextEditingController(text: widget.testUrl); - } - - _handleUpdate() async { - final testUrl = testUrlController.value.text; - if (testUrl.isEmpty) return; - Navigator.of(context).pop(testUrl); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.testUrl), - content: SizedBox( - width: 300, - child: Wrap( - runSpacing: 16, - children: [ - TextField( - maxLines: 5, - minLines: 1, - controller: testUrlController, - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: _handleUpdate, - child: Text(appLocalizations.submit), - ) - ], - ); - } -} - -class KeepAliveIntervalFormDialog extends StatefulWidget { - final int keepAliveInterval; - - const KeepAliveIntervalFormDialog({ - super.key, - required this.keepAliveInterval, - }); - - @override - State createState() => - _KeepAliveIntervalFormDialogState(); -} - -class _KeepAliveIntervalFormDialogState - extends State { - late TextEditingController keepAliveIntervalController; - - @override - void initState() { - super.initState(); - keepAliveIntervalController = TextEditingController( - text: "${widget.keepAliveInterval}", - ); - } - - _handleUpdate() async { - final keepAliveInterval = keepAliveIntervalController.value.text; - if (keepAliveInterval.isEmpty) return; - Navigator.of(context).pop(keepAliveInterval); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: Text(appLocalizations.keepAliveIntervalDesc), - content: SizedBox( - width: 300, - child: Wrap( - runSpacing: 16, - children: [ - TextField( - maxLines: 1, - minLines: 1, - controller: keepAliveIntervalController, - decoration: InputDecoration( - border: const OutlineInputBorder(), - suffixText: appLocalizations.seconds, - ), - ), - ], - ), - ), - actions: [ - TextButton( - onPressed: _handleUpdate, - child: Text(appLocalizations.submit), - ) - ], - ); - } -} - -class HostsForm extends StatelessWidget { - const HostsForm({super.key}); - - @override - Widget build(BuildContext context) { - return FloatLayout( - floatingWidget: FloatWrapper( - child: FloatingActionButton( - onPressed: () async { - final entry = - await globalState.showCommonDialog>( - child: const AddHostDialog(), - ); - if (entry == null) return; - final clashConfig = globalState.appController.clashConfig; - clashConfig.hosts = Map.from(clashConfig.hosts) - ..addEntries([entry]); - }, - child: const Icon(Icons.add), - ), - ), - child: Selector( - selector: (_, clashConfig) => clashConfig.hosts, - builder: (_, hosts, ___) { - final entries = hosts.entries; - return ListView.builder( - padding: const EdgeInsets.all(16), - itemCount: entries.length, - itemBuilder: (_, index) { - final e = entries.toList()[index]; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 8), - child: CommonCard( - child: ListItem( - title: Text(e.key), - subtitle: Text(e.value), - trailing: IconButton( - icon: const Icon(Icons.delete_outline), - onPressed: () { - final clashConfig = - globalState.appController.clashConfig; - clashConfig.hosts = Map.from(hosts)..remove(e.key); - }, - ), - ), - onPressed: () {}, - ), - ); - }, - ); - }, - ), - ); - } -} - -class AddHostDialog extends StatefulWidget { - const AddHostDialog({super.key}); - - @override - State createState() => _AddHostDialogState(); -} - -class _AddHostDialogState extends State { - late TextEditingController keyController; - late TextEditingController valueController; - final GlobalKey _formKey = GlobalKey(); - - @override - void initState() { - super.initState(); - keyController = TextEditingController(); - valueController = TextEditingController(); - } - - _submit() { - if (!_formKey.currentState!.validate()) return; - Navigator.of(context).pop>( - MapEntry( - keyController.text, - valueController.text, - ), - ); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - title: const Text("Hosts"), - content: Form( - key: _formKey, - child: SizedBox( - width: dialogCommonWidth, - child: Wrap( - runSpacing: 16, - children: [ - TextFormField( - maxLines: 2, - minLines: 1, - controller: keyController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.key), - border: const OutlineInputBorder(), - labelText: appLocalizations.key, - ), - validator: (String? value) { - if (value == null || value.isEmpty) { - return appLocalizations.keyNotEmpty; - } - return null; - }, - ), - TextFormField( - maxLines: 3, - minLines: 1, - controller: valueController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.label), - border: const OutlineInputBorder(), - labelText: appLocalizations.value, - ), - validator: (String? value) { - if (value == null || value.isEmpty) { - return appLocalizations.valueNotEmpty; - } - return null; - }, - ), - ], - ), - ), - ), - actions: [ - TextButton( - onPressed: _submit, - child: Text(appLocalizations.confirm), - ) - ], - ); - } -} + ) + .toList(); diff --git a/lib/fragments/config/vpn.dart b/lib/fragments/config/vpn.dart index eae4a8b..1ce3fbb 100644 --- a/lib/fragments/config/vpn.dart +++ b/lib/fragments/config/vpn.dart @@ -1,10 +1,64 @@ -import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/common/common.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:provider/provider.dart'; +class VPNSwitch extends StatelessWidget { + const VPNSwitch({super.key}); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.vpnProps.enable, + builder: (_, enable, __) { + return ListItem.switchItem( + leading: const Icon(Icons.stacked_line_chart), + title: const Text("VPN"), + subtitle: Text(appLocalizations.vpnEnableDesc), + delegate: SwitchDelegate( + value: enable, + onChanged: (bool value) async { + final config = globalState.appController.config; + final vpnProps = config.vpnProps; + config.vpnProps = vpnProps.copyWith( + enable: value, + ); + }, + ), + ); + }, + ); + } +} + +class VPNDisabledContainer extends StatelessWidget { + final Widget child; + + const VPNDisabledContainer( + this.child, { + super.key, + }); + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => config.vpnProps.enable, + builder: (_, enable, child) { + return AbsorbPointer( + absorbing: !enable, + child: DisabledMask( + status: !enable, + child: child!, + ), + ); + }, + child: child, + ); + } +} + class AllowBypassSwitch extends StatelessWidget { const AllowBypassSwitch({super.key}); @@ -61,7 +115,26 @@ class SystemProxySwitch extends StatelessWidget { } } -const vpnItems = [ - AllowBypassSwitch(), - SystemProxySwitch(), +class VpnOptions extends StatelessWidget { + const VpnOptions({super.key}); + + @override + Widget build(BuildContext context) { + return VPNDisabledContainer( + Column( + children: generateSection( + title: appLocalizations.options, + items: [ + const SystemProxySwitch(), + const AllowBypassSwitch(), + ], + ), + ), + ); + } +} + +final vpnItems = [ + const VPNSwitch(), + const VpnOptions(), ]; diff --git a/lib/fragments/dashboard/dashboard.dart b/lib/fragments/dashboard/dashboard.dart index 08c1567..c936813 100644 --- a/lib/fragments/dashboard/dashboard.dart +++ b/lib/fragments/dashboard/dashboard.dart @@ -48,11 +48,11 @@ class _DashboardFragmentState extends State { crossAxisCellCount: 8, child: NetworkSpeed(), ), - if (Platform.isAndroid) - GridItem( - crossAxisCellCount: switchCount, - child: const VPNSwitch(), - ), + // if (Platform.isAndroid) + // GridItem( + // crossAxisCellCount: switchCount, + // child: const VPNSwitch(), + // ), if (system.isDesktop) ...[ GridItem( crossAxisCellCount: switchCount, diff --git a/lib/fragments/dashboard/status_switch.dart b/lib/fragments/dashboard/status_switch.dart index 2e10766..b9f39af 100644 --- a/lib/fragments/dashboard/status_switch.dart +++ b/lib/fragments/dashboard/status_switch.dart @@ -5,34 +5,34 @@ import 'package:fl_clash/widgets/widgets.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; -class VPNSwitch extends StatelessWidget { - const VPNSwitch({super.key}); - - @override - Widget build(BuildContext context) { - return SwitchContainer( - info: const Info( - label: "VPN", - iconData: Icons.stacked_line_chart, - ), - child: Selector( - selector: (_, config) => config.vpnProps.enable, - builder: (_, enable, __) { - return Switch( - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - value: enable, - onChanged: (value) { - final config = globalState.appController.config; - config.vpnProps = config.vpnProps.copyWith( - enable: value, - ); - }, - ); - }, - ), - ); - } -} +// class VPNSwitch extends StatelessWidget { +// const VPNSwitch({super.key}); +// +// @override +// Widget build(BuildContext context) { +// return SwitchContainer( +// info: const Info( +// label: "VPN", +// iconData: Icons.stacked_line_chart, +// ), +// child: Selector( +// selector: (_, config) => config.vpnProps.enable, +// builder: (_, enable, __) { +// return Switch( +// materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, +// value: enable, +// onChanged: (value) { +// final config = globalState.appController.config; +// config.vpnProps = config.vpnProps.copyWith( +// enable: value, +// ); +// }, +// ); +// }, +// ), +// ); +// } +// } class TUNSwitch extends StatelessWidget { const TUNSwitch({super.key}); diff --git a/lib/fragments/proxies/list.dart b/lib/fragments/proxies/list.dart index 9596fc1..d4ee902 100644 --- a/lib/fragments/proxies/list.dart +++ b/lib/fragments/proxies/list.dart @@ -1,9 +1,11 @@ +import 'package:cached_network_image/cached_network_image.dart'; import 'package:collection/collection.dart'; import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/state.dart'; import 'package:fl_clash/widgets/card.dart'; +import 'package:fl_clash/widgets/fade_box.dart'; import 'package:fl_clash/widgets/text.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -353,6 +355,8 @@ class _ListHeaderState extends State late Animation _iconTurns; var isLock = false; + String get icon => widget.group.icon; + String get groupName => widget.group.name; String get groupType => widget.group.type.name; @@ -412,6 +416,7 @@ class _ListHeaderState extends State Widget build(BuildContext context) { return CommonCard( key: widget.key, + radius: 24, type: CommonCardType.filled, child: Container( padding: const EdgeInsets.all(12), @@ -419,57 +424,96 @@ class _ListHeaderState extends State mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible( - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + child: Row( children: [ - Text( - groupName, - style: context.textTheme.titleMedium, + const SizedBox( + width: 4, + ), + Container( + height: 48, + width: 48, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: context.colorScheme.secondaryContainer, + borderRadius: BorderRadius.circular(16), + ), + clipBehavior: Clip.antiAlias, + child: icon.isNotEmpty + ? CachedNetworkImage( + imageUrl: icon, + errorWidget: (_, __, ___) => const Icon( + IconsExt.target, + size: 32, + ), + ) + : const Icon( + IconsExt.target, + size: 32, + ), ), const SizedBox( - height: 4, + width: 16, ), Flexible( - flex: 1, - child: Row( + child: Column( mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - groupType, - style: context.textTheme.labelMedium?.toLight, + groupName, + style: context.textTheme.titleMedium, + ), + const SizedBox( + height: 4, ), Flexible( flex: 1, - child: currentGroupProxyNameBuilder( - groupName: groupName, - builder: (currentGroupName) { - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (currentGroupName.isNotEmpty) ...[ - Flexible( - flex: 1, - child: EmojiText( - overflow: TextOverflow.ellipsis, - " · $currentGroupName", - style: context - .textTheme.labelMedium?.toLight, - ), - ), - ] - ], - ); - }, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + groupType, + style: context.textTheme.labelMedium?.toLight, + ), + Flexible( + flex: 1, + child: currentGroupProxyNameBuilder( + groupName: groupName, + builder: (currentGroupName) { + return Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.center, + children: [ + if (currentGroupName.isNotEmpty) ...[ + Flexible( + flex: 1, + child: EmojiText( + overflow: TextOverflow.ellipsis, + " · $currentGroupName", + style: context.textTheme + .labelMedium?.toLight, + ), + ), + ] + ], + ); + }, + ), + ), + ], ), ), + const SizedBox( + width: 4, + ), ], ), - ), + ) ], ), ), diff --git a/lib/fragments/proxies/providers.dart b/lib/fragments/proxies/providers.dart index d78c75b..99e50ca 100644 --- a/lib/fragments/proxies/providers.dart +++ b/lib/fragments/proxies/providers.dart @@ -68,19 +68,30 @@ class _ProvidersState extends State { return Selector>( selector: (_, appState) => appState.providers, builder: (_, providers, ___) { - return ListView.separated( - itemBuilder: (_, index) { - return ProviderItem( - provider: providers[index], - ); - }, - separatorBuilder: (_, index) { - return const Divider( - height: 0, - ); - }, - itemCount: providers.length, + final proxyProviders = + providers.where((item) => item.type == "Proxy").map( + (item) => ProviderItem( + provider: item, + ), + ); + final ruleProviders = + providers.where((item) => item.type == "Rule").map( + (item) => ProviderItem( + provider: item, + ), + ); + final proxySection = generateSection( + title: appLocalizations.proxyProviders, + items: proxyProviders, ); + final ruleSection = generateSection( + title: appLocalizations.ruleProviders, + items: ruleProviders, + ); + return generateListView([ + ...proxySection, + ...ruleSection, + ]); }, ); } diff --git a/lib/fragments/proxies/proxies.dart b/lib/fragments/proxies/proxies.dart index 31ee953..c2e9281 100644 --- a/lib/fragments/proxies/proxies.dart +++ b/lib/fragments/proxies/proxies.dart @@ -33,7 +33,7 @@ class _ProxiesFragmentState extends State { extendPageWidth: 360, context, body: const Providers(), - title: appLocalizations.externalResources, + title: appLocalizations.providers, ); }, icon: const Icon( diff --git a/lib/fragments/tools.dart b/lib/fragments/tools.dart index 1218c4f..f99c39a 100644 --- a/lib/fragments/tools.dart +++ b/lib/fragments/tools.dart @@ -82,44 +82,23 @@ class _ToolboxFragmentState extends State { builder: (_, localeString, __) { final subTitle = localeString ?? appLocalizations.defaultText; final currentLocale = other.getLocaleForString(localeString); - return ListTile( + return ListItem.options( 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)), - ) - ], - ), - ), - ), - ); - }, + delegate: OptionsDelegate( + title: appLocalizations.language, + options: [ + null, + ...AppLocalizations.delegate.supportedLocales + ], + onChanged: (Locale? value) { + final config = context.read(); + config.locale = value?.toString(); + }, + textBuilder: (locale) => _getLocaleString(locale), + value: currentLocale, + ), ); }, ), diff --git a/lib/l10n/arb/intl_en.arb b/lib/l10n/arb/intl_en.arb index 76e6c6f..4a4e065 100644 --- a/lib/l10n/arb/intl_en.arb +++ b/lib/l10n/arb/intl_en.arb @@ -251,5 +251,40 @@ "key": "Key", "value": "Value", "keyNotEmpty": "The key cannot be empty", - "valueNotEmpty": "The value cannot be empty" + "valueNotEmpty": "The value cannot be empty", + "hostsDesc": "Add Hosts", + "vpnTip": "Changes take effect after restarting the VPN", + "vpnEnableDesc": "Auto routes all system traffic through VpnService", + "options": "Options", + "loopback": "Loopback unlock tool", + "loopbackDesc": "Used for UWP loopback unlocking", + "providers": "Providers", + "proxyProviders": "Proxy providers", + "ruleProviders": "Rule providers", + "overrideDns": "Override Dns", + "overrideDnsDesc": "Turning it on will override the DNS options in the profile", + "status": "Status", + "statusDesc": "System DNS will be used when turned off", + "preferH3Desc": "Prioritize the use of DOH's http/3", + "respectRules": "Respect rules", + "respectRulesDesc": "DNS connection following rules, need to configure proxy-server-nameserver", + "dnsMode": "DNS mode", + "fakeipRange": "Fakeip range", + "fakeipFilter": "Fakeip filter", + "defaultNameserver": "Default nameserver", + "defaultNameserverDesc": "For resolving DNS server", + "nameserver": "Nameserver", + "nameserverDesc": "For resolving domain", + "useHosts": "Use hosts", + "useSystemHosts": "Use system hosts", + "nameserverPolicy": "Nameserver policy", + "nameserverPolicyDesc": "Specify the corresponding nameserver policy", + "proxyNameserver": "Proxy nameserver", + "proxyNameserverDesc": "Domain for resolving proxy nodes", + "fallback": "Fallback", + "fallbackDesc": "Generally use offshore DNS", + "fallbackFilter": "Fallback filter", + "geoipCode": "Geoip code", + "ipcidr": "Ipcidr", + "domain": "Domain" } \ 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 2b8eabc..49da91d 100644 --- a/lib/l10n/arb/intl_zh_CN.arb +++ b/lib/l10n/arb/intl_zh_CN.arb @@ -251,5 +251,40 @@ "key": "键", "value": "值", "keyNotEmpty": "键不能为空", - "valueNotEmpty": "值不能为空" + "valueNotEmpty": "值不能为空", + "hostsDesc": "追加Hosts", + "vpnTip": "重启VPN后改变生效", + "vpnEnableDesc": "通过VpnService自动路由系统所有流量", + "options": "选项", + "loopback": "回环解锁工具", + "loopbackDesc": "用于UWP回环解锁", + "providers": "提供者", + "proxyProviders": "代理提供者", + "ruleProviders": "规则提供者", + "overrideDns": "覆写DNS", + "overrideDnsDesc": "开启后将覆盖配置中的DNS选项", + "status": "状态", + "statusDesc": "关闭后将使用系统DNS", + "preferH3Desc": "优先使用DOH的http/3", + "respectRules": "遵守规则", + "respectRulesDesc": "DNS连接跟随rules,需配置proxy-server-nameserver", + "dnsMode": "DNS模式", + "fakeipRange": "Fakeip范围", + "fakeipFilter": "Fakeip过滤", + "defaultNameserver": "默认域名服务器", + "defaultNameserverDesc": "用于解析DNS服务器", + "nameserver": "域名服务器", + "nameserverDesc": "用于解析域名", + "useHosts": "使用Hosts", + "useSystemHosts": "使用系统Hosts", + "nameserverPolicy": "域名服务器策略", + "nameserverPolicyDesc": "指定对应域名服务器策略", + "proxyNameserver": "代理域名服务器", + "proxyNameserverDesc": "用于解析代理节点的域名", + "fallback": "Fallback", + "fallbackDesc": "一般情况下使用境外DNS", + "fallbackFilter": "Fallback过滤", + "geoipCode": "Geoip代码", + "ipcidr": "IP/掩码", + "domain": "域名" } \ No newline at end of file diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index 2e09a68..52c21ee 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -116,6 +116,10 @@ class MessageLookup extends MessageLookupByLibrary { "dark": MessageLookupByLibrary.simpleMessage("Dark"), "dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"), "days": MessageLookupByLibrary.simpleMessage("Days"), + "defaultNameserver": + MessageLookupByLibrary.simpleMessage("Default nameserver"), + "defaultNameserverDesc": + MessageLookupByLibrary.simpleMessage("For resolving DNS server"), "defaultSort": MessageLookupByLibrary.simpleMessage("Sort by default"), "defaultText": MessageLookupByLibrary.simpleMessage("Default"), "delay": MessageLookupByLibrary.simpleMessage("Delay"), @@ -132,8 +136,10 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Discovery a new version"), "dnsDesc": MessageLookupByLibrary.simpleMessage("Update DNS related settings"), + "dnsMode": MessageLookupByLibrary.simpleMessage("DNS mode"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("Do you want to pass"), + "domain": MessageLookupByLibrary.simpleMessage("Domain"), "download": MessageLookupByLibrary.simpleMessage("Download"), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "en": MessageLookupByLibrary.simpleMessage("English"), @@ -153,6 +159,13 @@ class MessageLookup extends MessageLookupByLibrary { "externalLink": MessageLookupByLibrary.simpleMessage("External link"), "externalResources": MessageLookupByLibrary.simpleMessage("External resources"), + "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip filter"), + "fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip range"), + "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), + "fallbackDesc": + MessageLookupByLibrary.simpleMessage("Generally use offshore DNS"), + "fallbackFilter": + MessageLookupByLibrary.simpleMessage("Fallback filter"), "file": MessageLookupByLibrary.simpleMessage("File"), "fileDesc": MessageLookupByLibrary.simpleMessage("Directly upload profile"), @@ -170,9 +183,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Geo Low Memory Mode"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage( "Enabling will use the Geo low memory loader"), + "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip code"), "global": MessageLookupByLibrary.simpleMessage("Global"), "go": MessageLookupByLibrary.simpleMessage("Go"), "goDownload": MessageLookupByLibrary.simpleMessage("Go to download"), + "hostsDesc": MessageLookupByLibrary.simpleMessage("Add Hosts"), "hours": MessageLookupByLibrary.simpleMessage("Hours"), "importFromURL": MessageLookupByLibrary.simpleMessage("Import from URL"), @@ -182,6 +197,7 @@ class MessageLookup extends MessageLookupByLibrary { "intelligentSelected": MessageLookupByLibrary.simpleMessage("Intelligent selection"), "intranetIP": MessageLookupByLibrary.simpleMessage("Intranet IP"), + "ipcidr": MessageLookupByLibrary.simpleMessage("Ipcidr"), "ipv6Desc": MessageLookupByLibrary.simpleMessage( "When turned on it will be able to receive IPv6 traffic"), "just": MessageLookupByLibrary.simpleMessage("Just"), @@ -205,6 +221,10 @@ class MessageLookup extends MessageLookupByLibrary { "Disabling will hide the log entry"), "logs": MessageLookupByLibrary.simpleMessage("Logs"), "logsDesc": MessageLookupByLibrary.simpleMessage("Log capture records"), + "loopback": + MessageLookupByLibrary.simpleMessage("Loopback unlock tool"), + "loopbackDesc": MessageLookupByLibrary.simpleMessage( + "Used for UWP loopback unlocking"), "loose": MessageLookupByLibrary.simpleMessage("Loose"), "min": MessageLookupByLibrary.simpleMessage("Min"), "minimizeOnExit": @@ -217,6 +237,13 @@ class MessageLookup extends MessageLookupByLibrary { "more": MessageLookupByLibrary.simpleMessage("More"), "name": MessageLookupByLibrary.simpleMessage("Name"), "nameSort": MessageLookupByLibrary.simpleMessage("Sort by name"), + "nameserver": MessageLookupByLibrary.simpleMessage("Nameserver"), + "nameserverDesc": + MessageLookupByLibrary.simpleMessage("For resolving domain"), + "nameserverPolicy": + MessageLookupByLibrary.simpleMessage("Nameserver policy"), + "nameserverPolicyDesc": MessageLookupByLibrary.simpleMessage( + "Specify the corresponding nameserver policy"), "networkDetection": MessageLookupByLibrary.simpleMessage("Network detection"), "networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"), @@ -242,6 +269,7 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("Only statistics proxy"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage( "When turned on, only statistics proxy traffic"), + "options": MessageLookupByLibrary.simpleMessage("Options"), "other": MessageLookupByLibrary.simpleMessage("Other"), "otherContributors": MessageLookupByLibrary.simpleMessage("Other contributors"), @@ -249,6 +277,9 @@ class MessageLookup extends MessageLookupByLibrary { "override": MessageLookupByLibrary.simpleMessage("Override"), "overrideDesc": MessageLookupByLibrary.simpleMessage( "Override Proxy related config"), + "overrideDns": MessageLookupByLibrary.simpleMessage("Override Dns"), + "overrideDnsDesc": MessageLookupByLibrary.simpleMessage( + "Turning it on will override the DNS options in the profile"), "password": MessageLookupByLibrary.simpleMessage("Password"), "passwordTip": MessageLookupByLibrary.simpleMessage("Password cannot be empty"), @@ -260,6 +291,8 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "Please upload a valid QR code"), "port": MessageLookupByLibrary.simpleMessage("Port"), + "preferH3Desc": MessageLookupByLibrary.simpleMessage( + "Prioritize the use of DOH\'s http/3"), "preview": MessageLookupByLibrary.simpleMessage("Preview"), "profile": MessageLookupByLibrary.simpleMessage("Profile"), "profileAutoUpdateIntervalInvalidValidationDesc": @@ -279,13 +312,20 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "profilesSort": MessageLookupByLibrary.simpleMessage("Profiles sort"), "project": MessageLookupByLibrary.simpleMessage("Project"), + "providers": MessageLookupByLibrary.simpleMessage("Providers"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("Proxies setting"), "proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"), + "proxyNameserver": + MessageLookupByLibrary.simpleMessage("Proxy nameserver"), + "proxyNameserverDesc": MessageLookupByLibrary.simpleMessage( + "Domain for resolving proxy nodes"), "proxyPort": MessageLookupByLibrary.simpleMessage("ProxyPort"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage( "Set the Clash listening port"), + "proxyProviders": + MessageLookupByLibrary.simpleMessage("Proxy providers"), "prueBlackMode": MessageLookupByLibrary.simpleMessage("Prue black mode"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"), @@ -309,7 +349,11 @@ class MessageLookup extends MessageLookupByLibrary { "resources": MessageLookupByLibrary.simpleMessage("Resources"), "resourcesDesc": MessageLookupByLibrary.simpleMessage( "External resource related info"), + "respectRules": MessageLookupByLibrary.simpleMessage("Respect rules"), + "respectRulesDesc": MessageLookupByLibrary.simpleMessage( + "DNS connection following rules, need to configure proxy-server-nameserver"), "rule": MessageLookupByLibrary.simpleMessage("Rule"), + "ruleProviders": MessageLookupByLibrary.simpleMessage("Rule providers"), "save": MessageLookupByLibrary.simpleMessage("Save"), "search": MessageLookupByLibrary.simpleMessage("Search"), "seconds": MessageLookupByLibrary.simpleMessage("Seconds"), @@ -327,6 +371,9 @@ class MessageLookup extends MessageLookupByLibrary { "standard": MessageLookupByLibrary.simpleMessage("Standard"), "start": MessageLookupByLibrary.simpleMessage("Start"), "startVpn": MessageLookupByLibrary.simpleMessage("Staring VPN..."), + "status": MessageLookupByLibrary.simpleMessage("Status"), + "statusDesc": MessageLookupByLibrary.simpleMessage( + "System DNS will be used when turned off"), "stop": MessageLookupByLibrary.simpleMessage("Stop"), "stopVpn": MessageLookupByLibrary.simpleMessage("Stopping VPN..."), "style": MessageLookupByLibrary.simpleMessage("Style"), @@ -370,12 +417,19 @@ class MessageLookup extends MessageLookupByLibrary { "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("Obtain profile through URL"), + "useHosts": MessageLookupByLibrary.simpleMessage("Use hosts"), + "useSystemHosts": + MessageLookupByLibrary.simpleMessage("Use system hosts"), "value": MessageLookupByLibrary.simpleMessage("Value"), "valueNotEmpty": MessageLookupByLibrary.simpleMessage("The value cannot be empty"), "view": MessageLookupByLibrary.simpleMessage("View"), "vpnDesc": MessageLookupByLibrary.simpleMessage("Modify VPN related settings"), + "vpnEnableDesc": MessageLookupByLibrary.simpleMessage( + "Auto routes all system traffic through VpnService"), + "vpnTip": MessageLookupByLibrary.simpleMessage( + "Changes take effect after restarting the VPN"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV configuration"), "whitelistMode": MessageLookupByLibrary.simpleMessage("Whitelist mode"), diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index aa9930d..dd16973 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -95,6 +95,9 @@ class MessageLookup extends MessageLookupByLibrary { "dark": MessageLookupByLibrary.simpleMessage("深色"), "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), "days": MessageLookupByLibrary.simpleMessage("天"), + "defaultNameserver": MessageLookupByLibrary.simpleMessage("默认域名服务器"), + "defaultNameserverDesc": + MessageLookupByLibrary.simpleMessage("用于解析DNS服务器"), "defaultSort": MessageLookupByLibrary.simpleMessage("按默认排序"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"), "delay": MessageLookupByLibrary.simpleMessage("延迟"), @@ -107,7 +110,9 @@ class MessageLookup extends MessageLookupByLibrary { "discoverNewVersion": MessageLookupByLibrary.simpleMessage("发现新版本"), "discovery": MessageLookupByLibrary.simpleMessage("发现新版本"), "dnsDesc": MessageLookupByLibrary.simpleMessage("更新DNS相关设置"), + "dnsMode": MessageLookupByLibrary.simpleMessage("DNS模式"), "doYouWantToPass": MessageLookupByLibrary.simpleMessage("是否要通过"), + "domain": MessageLookupByLibrary.simpleMessage("域名"), "download": MessageLookupByLibrary.simpleMessage("下载"), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "en": MessageLookupByLibrary.simpleMessage("英语"), @@ -123,6 +128,11 @@ class MessageLookup extends MessageLookupByLibrary { MessageLookupByLibrary.simpleMessage("开启后将可以通过9090端口控制Clash内核"), "externalLink": MessageLookupByLibrary.simpleMessage("外部链接"), "externalResources": MessageLookupByLibrary.simpleMessage("外部资源"), + "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fakeip过滤"), + "fakeipRange": MessageLookupByLibrary.simpleMessage("Fakeip范围"), + "fallback": MessageLookupByLibrary.simpleMessage("Fallback"), + "fallbackDesc": MessageLookupByLibrary.simpleMessage("一般情况下使用境外DNS"), + "fallbackFilter": MessageLookupByLibrary.simpleMessage("Fallback过滤"), "file": MessageLookupByLibrary.simpleMessage("文件"), "fileDesc": MessageLookupByLibrary.simpleMessage("直接上传配置文件"), "filterSystemApp": MessageLookupByLibrary.simpleMessage("过滤系统应用"), @@ -136,15 +146,18 @@ class MessageLookup extends MessageLookupByLibrary { "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "geodataLoaderDesc": MessageLookupByLibrary.simpleMessage("开启将使用Geo低内存加载器"), + "geoipCode": MessageLookupByLibrary.simpleMessage("Geoip代码"), "global": MessageLookupByLibrary.simpleMessage("全局"), "go": MessageLookupByLibrary.simpleMessage("前往"), "goDownload": MessageLookupByLibrary.simpleMessage("前往下载"), + "hostsDesc": MessageLookupByLibrary.simpleMessage("追加Hosts"), "hours": MessageLookupByLibrary.simpleMessage("小时"), "importFromURL": MessageLookupByLibrary.simpleMessage("从URL导入"), "infiniteTime": MessageLookupByLibrary.simpleMessage("长期有效"), "init": MessageLookupByLibrary.simpleMessage("初始化"), "intelligentSelected": MessageLookupByLibrary.simpleMessage("智能选择"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), + "ipcidr": MessageLookupByLibrary.simpleMessage("IP/掩码"), "ipv6Desc": MessageLookupByLibrary.simpleMessage("开启后将可以接收IPv6流量"), "just": MessageLookupByLibrary.simpleMessage("刚刚"), "keepAliveIntervalDesc": @@ -163,6 +176,8 @@ class MessageLookup extends MessageLookupByLibrary { "logcatDesc": MessageLookupByLibrary.simpleMessage("禁用将会隐藏日志入口"), "logs": MessageLookupByLibrary.simpleMessage("日志"), "logsDesc": MessageLookupByLibrary.simpleMessage("日志捕获记录"), + "loopback": MessageLookupByLibrary.simpleMessage("回环解锁工具"), + "loopbackDesc": MessageLookupByLibrary.simpleMessage("用于UWP回环解锁"), "loose": MessageLookupByLibrary.simpleMessage("紧凑"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), @@ -174,6 +189,11 @@ class MessageLookup extends MessageLookupByLibrary { "more": MessageLookupByLibrary.simpleMessage("更多"), "name": MessageLookupByLibrary.simpleMessage("名称"), "nameSort": MessageLookupByLibrary.simpleMessage("按名称排序"), + "nameserver": MessageLookupByLibrary.simpleMessage("域名服务器"), + "nameserverDesc": MessageLookupByLibrary.simpleMessage("用于解析域名"), + "nameserverPolicy": MessageLookupByLibrary.simpleMessage("域名服务器策略"), + "nameserverPolicyDesc": + MessageLookupByLibrary.simpleMessage("指定对应域名服务器策略"), "networkDetection": MessageLookupByLibrary.simpleMessage("网络检测"), "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), "noInfo": MessageLookupByLibrary.simpleMessage("暂无信息"), @@ -193,11 +213,15 @@ class MessageLookup extends MessageLookupByLibrary { "onlyStatisticsProxy": MessageLookupByLibrary.simpleMessage("仅统计代理"), "onlyStatisticsProxyDesc": MessageLookupByLibrary.simpleMessage("开启后,将只统计代理流量"), + "options": MessageLookupByLibrary.simpleMessage("选项"), "other": MessageLookupByLibrary.simpleMessage("其他"), "otherContributors": MessageLookupByLibrary.simpleMessage("其他贡献者"), "outboundMode": MessageLookupByLibrary.simpleMessage("出站模式"), "override": MessageLookupByLibrary.simpleMessage("覆写"), "overrideDesc": MessageLookupByLibrary.simpleMessage("覆写代理相关配置"), + "overrideDns": MessageLookupByLibrary.simpleMessage("覆写DNS"), + "overrideDnsDesc": + MessageLookupByLibrary.simpleMessage("开启后将覆盖配置中的DNS选项"), "password": MessageLookupByLibrary.simpleMessage("密码"), "passwordTip": MessageLookupByLibrary.simpleMessage("密码不能为空"), "paste": MessageLookupByLibrary.simpleMessage("粘贴"), @@ -206,6 +230,7 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage("请上传有效的二维码"), "port": MessageLookupByLibrary.simpleMessage("端口"), + "preferH3Desc": MessageLookupByLibrary.simpleMessage("优先使用DOH的http/3"), "preview": MessageLookupByLibrary.simpleMessage("预览"), "profile": MessageLookupByLibrary.simpleMessage("配置"), "profileAutoUpdateIntervalInvalidValidationDesc": @@ -223,11 +248,16 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("配置"), "profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"), "project": MessageLookupByLibrary.simpleMessage("项目"), + "providers": MessageLookupByLibrary.simpleMessage("提供者"), "proxies": MessageLookupByLibrary.simpleMessage("代理"), "proxiesSetting": MessageLookupByLibrary.simpleMessage("代理设置"), "proxyGroup": MessageLookupByLibrary.simpleMessage("代理组"), + "proxyNameserver": MessageLookupByLibrary.simpleMessage("代理域名服务器"), + "proxyNameserverDesc": + MessageLookupByLibrary.simpleMessage("用于解析代理节点的域名"), "proxyPort": MessageLookupByLibrary.simpleMessage("代理端口"), "proxyPortDesc": MessageLookupByLibrary.simpleMessage("设置Clash监听端口"), + "proxyProviders": MessageLookupByLibrary.simpleMessage("代理提供者"), "prueBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), "qrcodeDesc": MessageLookupByLibrary.simpleMessage("扫描二维码获取配置文件"), @@ -243,7 +273,11 @@ class MessageLookup extends MessageLookupByLibrary { "requestsDesc": MessageLookupByLibrary.simpleMessage("查看最近请求记录"), "resources": MessageLookupByLibrary.simpleMessage("资源"), "resourcesDesc": MessageLookupByLibrary.simpleMessage("外部资源相关信息"), + "respectRules": MessageLookupByLibrary.simpleMessage("遵守规则"), + "respectRulesDesc": MessageLookupByLibrary.simpleMessage( + "DNS连接跟随rules,需配置proxy-server-nameserver"), "rule": MessageLookupByLibrary.simpleMessage("规则"), + "ruleProviders": MessageLookupByLibrary.simpleMessage("规则提供者"), "save": MessageLookupByLibrary.simpleMessage("保存"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), @@ -260,6 +294,8 @@ class MessageLookup extends MessageLookupByLibrary { "standard": MessageLookupByLibrary.simpleMessage("标准"), "start": MessageLookupByLibrary.simpleMessage("启动"), "startVpn": MessageLookupByLibrary.simpleMessage("正在启动VPN..."), + "status": MessageLookupByLibrary.simpleMessage("状态"), + "statusDesc": MessageLookupByLibrary.simpleMessage("关闭后将使用系统DNS"), "stop": MessageLookupByLibrary.simpleMessage("暂停"), "stopVpn": MessageLookupByLibrary.simpleMessage("正在停止VPN..."), "style": MessageLookupByLibrary.simpleMessage("风格"), @@ -297,10 +333,15 @@ class MessageLookup extends MessageLookupByLibrary { "upload": MessageLookupByLibrary.simpleMessage("上传"), "url": MessageLookupByLibrary.simpleMessage("URL"), "urlDesc": MessageLookupByLibrary.simpleMessage("通过URL获取配置文件"), + "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), + "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "value": MessageLookupByLibrary.simpleMessage("值"), "valueNotEmpty": MessageLookupByLibrary.simpleMessage("值不能为空"), "view": MessageLookupByLibrary.simpleMessage("查看"), "vpnDesc": MessageLookupByLibrary.simpleMessage("修改VPN相关设置"), + "vpnEnableDesc": + MessageLookupByLibrary.simpleMessage("通过VpnService自动路由系统所有流量"), + "vpnTip": MessageLookupByLibrary.simpleMessage("重启VPN后改变生效"), "webDAVConfiguration": MessageLookupByLibrary.simpleMessage("WebDAV配置"), "whitelistMode": MessageLookupByLibrary.simpleMessage("白名单模式"), "years": MessageLookupByLibrary.simpleMessage("年"), diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index f60c712..2404c90 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -2579,6 +2579,356 @@ class AppLocalizations { args: [], ); } + + /// `Add Hosts` + String get hostsDesc { + return Intl.message( + 'Add Hosts', + name: 'hostsDesc', + desc: '', + args: [], + ); + } + + /// `Changes take effect after restarting the VPN` + String get vpnTip { + return Intl.message( + 'Changes take effect after restarting the VPN', + name: 'vpnTip', + desc: '', + args: [], + ); + } + + /// `Auto routes all system traffic through VpnService` + String get vpnEnableDesc { + return Intl.message( + 'Auto routes all system traffic through VpnService', + name: 'vpnEnableDesc', + desc: '', + args: [], + ); + } + + /// `Options` + String get options { + return Intl.message( + 'Options', + name: 'options', + desc: '', + args: [], + ); + } + + /// `Loopback unlock tool` + String get loopback { + return Intl.message( + 'Loopback unlock tool', + name: 'loopback', + desc: '', + args: [], + ); + } + + /// `Used for UWP loopback unlocking` + String get loopbackDesc { + return Intl.message( + 'Used for UWP loopback unlocking', + name: 'loopbackDesc', + desc: '', + args: [], + ); + } + + /// `Providers` + String get providers { + return Intl.message( + 'Providers', + name: 'providers', + desc: '', + args: [], + ); + } + + /// `Proxy providers` + String get proxyProviders { + return Intl.message( + 'Proxy providers', + name: 'proxyProviders', + desc: '', + args: [], + ); + } + + /// `Rule providers` + String get ruleProviders { + return Intl.message( + 'Rule providers', + name: 'ruleProviders', + desc: '', + args: [], + ); + } + + /// `Override Dns` + String get overrideDns { + return Intl.message( + 'Override Dns', + name: 'overrideDns', + desc: '', + args: [], + ); + } + + /// `Turning it on will override the DNS options in the profile` + String get overrideDnsDesc { + return Intl.message( + 'Turning it on will override the DNS options in the profile', + name: 'overrideDnsDesc', + desc: '', + args: [], + ); + } + + /// `Status` + String get status { + return Intl.message( + 'Status', + name: 'status', + desc: '', + args: [], + ); + } + + /// `System DNS will be used when turned off` + String get statusDesc { + return Intl.message( + 'System DNS will be used when turned off', + name: 'statusDesc', + desc: '', + args: [], + ); + } + + /// `Prioritize the use of DOH's http/3` + String get preferH3Desc { + return Intl.message( + 'Prioritize the use of DOH\'s http/3', + name: 'preferH3Desc', + desc: '', + args: [], + ); + } + + /// `Respect rules` + String get respectRules { + return Intl.message( + 'Respect rules', + name: 'respectRules', + desc: '', + args: [], + ); + } + + /// `DNS connection following rules, need to configure proxy-server-nameserver` + String get respectRulesDesc { + return Intl.message( + 'DNS connection following rules, need to configure proxy-server-nameserver', + name: 'respectRulesDesc', + desc: '', + args: [], + ); + } + + /// `DNS mode` + String get dnsMode { + return Intl.message( + 'DNS mode', + name: 'dnsMode', + desc: '', + args: [], + ); + } + + /// `Fakeip range` + String get fakeipRange { + return Intl.message( + 'Fakeip range', + name: 'fakeipRange', + desc: '', + args: [], + ); + } + + /// `Fakeip filter` + String get fakeipFilter { + return Intl.message( + 'Fakeip filter', + name: 'fakeipFilter', + desc: '', + args: [], + ); + } + + /// `Default nameserver` + String get defaultNameserver { + return Intl.message( + 'Default nameserver', + name: 'defaultNameserver', + desc: '', + args: [], + ); + } + + /// `For resolving DNS server` + String get defaultNameserverDesc { + return Intl.message( + 'For resolving DNS server', + name: 'defaultNameserverDesc', + desc: '', + args: [], + ); + } + + /// `Nameserver` + String get nameserver { + return Intl.message( + 'Nameserver', + name: 'nameserver', + desc: '', + args: [], + ); + } + + /// `For resolving domain` + String get nameserverDesc { + return Intl.message( + 'For resolving domain', + name: 'nameserverDesc', + desc: '', + args: [], + ); + } + + /// `Use hosts` + String get useHosts { + return Intl.message( + 'Use hosts', + name: 'useHosts', + desc: '', + args: [], + ); + } + + /// `Use system hosts` + String get useSystemHosts { + return Intl.message( + 'Use system hosts', + name: 'useSystemHosts', + desc: '', + args: [], + ); + } + + /// `Nameserver policy` + String get nameserverPolicy { + return Intl.message( + 'Nameserver policy', + name: 'nameserverPolicy', + desc: '', + args: [], + ); + } + + /// `Specify the corresponding nameserver policy` + String get nameserverPolicyDesc { + return Intl.message( + 'Specify the corresponding nameserver policy', + name: 'nameserverPolicyDesc', + desc: '', + args: [], + ); + } + + /// `Proxy nameserver` + String get proxyNameserver { + return Intl.message( + 'Proxy nameserver', + name: 'proxyNameserver', + desc: '', + args: [], + ); + } + + /// `Domain for resolving proxy nodes` + String get proxyNameserverDesc { + return Intl.message( + 'Domain for resolving proxy nodes', + name: 'proxyNameserverDesc', + desc: '', + args: [], + ); + } + + /// `Fallback` + String get fallback { + return Intl.message( + 'Fallback', + name: 'fallback', + desc: '', + args: [], + ); + } + + /// `Generally use offshore DNS` + String get fallbackDesc { + return Intl.message( + 'Generally use offshore DNS', + name: 'fallbackDesc', + desc: '', + args: [], + ); + } + + /// `Fallback filter` + String get fallbackFilter { + return Intl.message( + 'Fallback filter', + name: 'fallbackFilter', + desc: '', + args: [], + ); + } + + /// `Geoip code` + String get geoipCode { + return Intl.message( + 'Geoip code', + name: 'geoipCode', + desc: '', + args: [], + ); + } + + /// `Ipcidr` + String get ipcidr { + return Intl.message( + 'Ipcidr', + name: 'ipcidr', + desc: '', + args: [], + ); + } + + /// `Domain` + String get domain { + return Intl.message( + 'Domain', + name: 'domain', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/main.dart b/lib/main.dart index 69b07b7..790774c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:io'; import 'package:fl_clash/clash/clash.dart'; +import 'package:fl_clash/common/http.dart'; import 'package:fl_clash/plugins/app.dart'; import 'package:fl_clash/plugins/tile.dart'; import 'package:fl_clash/plugins/vpn.dart'; @@ -36,6 +37,7 @@ Future main() async { config: config, clashConfig: clashConfig, ); + HttpOverrides.global = FlClashHttpOverrides(); runAppWithPreferences( const Application(), appState: appState, diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index 2e0856d..c93eab7 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -26,84 +26,84 @@ class Tun with _$Tun { factory Tun.fromJson(Map json) => _$TunFromJson(json); } -@JsonSerializable() -class Dns { - bool enable; - bool ipv6; - @JsonKey(name: "default-nameserver") - List defaultNameserver; - @JsonKey(name: "enhanced-mode") - String enhancedMode; - @JsonKey(name: "fake-ip-range") - String fakeIpRange; - @JsonKey(name: "use-hosts") - bool useHosts; - List nameserver; - List fallback; - @JsonKey(name: "fake-ip-filter") - List fakeIpFilter; +@freezed +class FallbackFilter with _$FallbackFilter { + const factory FallbackFilter({ + @Default(true) bool geoip, + @Default("CN") @JsonKey(name: "geoip-code") String geoipCode, + @Default(["gfw"]) List geosite, + @Default(["240.0.0.0/4"]) List ipcidr, + @Default([ + "+.google.com", + "+.facebook.com", + "+.youtube.com", + ]) + List domain, + }) = _FallbackFilter; - Dns() - : enable = true, - ipv6 = false, - defaultNameserver = [ - "223.5.5.5", - "119.29.29.29", - "8.8.4.4", - "1.0.0.1", - ], - enhancedMode = "fake-ip", - fakeIpRange = "198.18.0.1/16", - useHosts = true, - nameserver = [ - "8.8.8.8", - "114.114.114.114", - "https://doh.pub/dns-query", - "https://dns.alidns.com/dns-query", - ], - fallback = [ - 'https://doh.dns.sb/dns-query', - 'https://dns.cloudflare.com/dns-query', - 'https://dns.twnic.tw/dns-query', - 'tls://8.8.4.4:853', - ], - fakeIpFilter = [ - // Stun Services - "+.stun.*.*", - "+.stun.*.*.*", - "+.stun.*.*.*.*", - "+.stun.*.*.*.*.*", + factory FallbackFilter.fromJson(Map json) => + _$FallbackFilterFromJson(json); +} - // Google Voices - "lens.l.google.com", +@freezed +class Dns with _$Dns { + const factory Dns({ + @Default(true) bool enable, + @Default(false) @JsonKey(name: "prefer-h3") bool preferH3, + @Default(true) @JsonKey(name: "use-hosts") bool useHosts, + @Default(true) @JsonKey(name: "use-system-hosts") bool useSystemHosts, + @Default(true) @JsonKey(name: "respect-rules") bool respectRules, + @Default(false) bool ipv6, + @Default(["223.5.5.5"]) + @JsonKey(name: "default-nameserver") + List defaultNameserver, + @Default(DnsMode.fakeIp) + @JsonKey(name: "enhanced-mode") + DnsMode enhancedMode, + @Default("198.18.0.1/16") + @JsonKey(name: "fake-ip-range") + String fakeIpRange, + @Default([ + "*.lan", + "localhost.ptlogin2.qq.com", + ]) + @JsonKey(name: "fake-ip-filter") + List fakeIpFilter, + @Default({ + "www.baidu.com": "114.114.114.114", + "+.internal.crop.com": "10.0.0.1", + "geosite:cn": "https://doh.pub/dns-query" + }) + @JsonKey(name: "nameserver-policy") + Map nameserverPolicy, + @Default([ + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query", + ]) + List nameserver, + @Default([ + "tls://8.8.4.4", + "tls://1.1.1.1", + ]) + List fallback, + @Default([ + "https://doh.pub/dns-query", + ]) + @JsonKey(name: "proxy-server-nameserver") + List proxyServerNameserver, + @Default(FallbackFilter()) + @JsonKey(name: "fallback-filter") + FallbackFilter fallbackFilter, + }) = _Dns; - // Nintendo Switch STUN - "*.n.n.srv.nintendo.net", + factory Dns.fromJson(Map json) => _$DnsFromJson(json); - // PlayStation STUN - "+.stun.playstation.net", - - // XBox - "xbox.*.*.microsoft.com", - "*.*.xboxlive.com", - - // Microsoft Captive Portal - "*.msftncsi.com", - "*.msftconnecttest.com", - - // Bilibili CDN - "*.mcdn.bilivideo.cn", - - // Windows Default LAN WorkGroup - "WORKGROUP", - ]; - - factory Dns.fromJson(Map json) { - return _$DnsFromJson(json); - } - - Map toJson() { - return _$DnsToJson(this); + factory Dns.safeDnsFromJson(Map json) { + try { + return Dns.fromJson(json); + } catch (_) { + return const Dns(); + } } } @@ -144,7 +144,7 @@ class ClashConfig extends ChangeNotifier { _geodataLoader = geodataLoaderMemconservative, _externalController = '', _keepAliveInterval = 30, - _dns = Dns(), + _dns = const Dns(), _geoXUrl = defaultGeoXMap, _rules = [], _hosts = {}; @@ -273,6 +273,7 @@ class ClashConfig extends ChangeNotifier { } } + @JsonKey(fromJson: Dns.safeDnsFromJson) Dns get dns => _dns; set dns(Dns value) { diff --git a/lib/models/config.dart b/lib/models/config.dart index c90f1d6..60f34df 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -49,6 +49,17 @@ class CoreState with _$CoreState { _$CoreStateFromJson(json); } +@freezed +class VPNState with _$VPNState { + const factory VPNState({ + required AccessControl? accessControl, + required VpnProps vpnProps, + }) = _VPNState; + + factory VPNState.fromJson(Map json) => + _$VPNStateFromJson(json); +} + @freezed class WindowProps with _$WindowProps { const factory WindowProps({ @@ -115,6 +126,7 @@ class Config extends ChangeNotifier { VpnProps _vpnProps; DesktopProps _desktopProps; bool _showLabel; + bool _overrideDns; Config() : _profiles = [], @@ -142,7 +154,8 @@ class Config extends ChangeNotifier { _proxiesLayout = ProxiesLayout.standard, _vpnProps = const VpnProps(), _desktopProps = const DesktopProps(), - _showLabel = false; + _showLabel = false, + _overrideDns = false; deleteProfileById(String id) { _profiles = profiles.where((element) => element.id != id).toList(); @@ -548,6 +561,16 @@ class Config extends ChangeNotifier { } } + @JsonKey(defaultValue: false) + bool get overrideDns => _overrideDns; + + set overrideDns(bool value) { + if (_overrideDns != value) { + _overrideDns = value; + notifyListeners(); + } + } + update([ Config? config, RecoveryOption recoveryOptions = RecoveryOption.all, @@ -584,6 +607,7 @@ class Config extends ChangeNotifier { _isExclude = config._isExclude; _windowProps = config._windowProps; _vpnProps = config._vpnProps; + _overrideDns = config._overrideDns; _desktopProps = config._desktopProps; } notifyListeners(); diff --git a/lib/models/ffi.dart b/lib/models/ffi.dart index 32510f0..e304986 100644 --- a/lib/models/ffi.dart +++ b/lib/models/ffi.dart @@ -14,6 +14,7 @@ class ConfigExtendedParams with _$ConfigExtendedParams { @JsonKey(name: "is-patch") required bool isPatch, @JsonKey(name: "is-compatible") required bool isCompatible, @JsonKey(name: "selected-map") required SelectedMap selectedMap, + @JsonKey(name: "override-dns") required bool overrideDns, @JsonKey(name: "test-url") required String testUrl, }) = _ConfigExtendedParams; diff --git a/lib/models/generated/clash_config.freezed.dart b/lib/models/generated/clash_config.freezed.dart index 7a4ec1a..364e450 100644 --- a/lib/models/generated/clash_config.freezed.dart +++ b/lib/models/generated/clash_config.freezed.dart @@ -220,3 +220,818 @@ abstract class _Tun implements Tun { _$$TunImplCopyWith<_$TunImpl> get copyWith => throw _privateConstructorUsedError; } + +FallbackFilter _$FallbackFilterFromJson(Map json) { + return _FallbackFilter.fromJson(json); +} + +/// @nodoc +mixin _$FallbackFilter { + bool get geoip => throw _privateConstructorUsedError; + @JsonKey(name: "geoip-code") + String get geoipCode => throw _privateConstructorUsedError; + List get geosite => throw _privateConstructorUsedError; + List get ipcidr => throw _privateConstructorUsedError; + List get domain => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $FallbackFilterCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $FallbackFilterCopyWith<$Res> { + factory $FallbackFilterCopyWith( + FallbackFilter value, $Res Function(FallbackFilter) then) = + _$FallbackFilterCopyWithImpl<$Res, FallbackFilter>; + @useResult + $Res call( + {bool geoip, + @JsonKey(name: "geoip-code") String geoipCode, + List geosite, + List ipcidr, + List domain}); +} + +/// @nodoc +class _$FallbackFilterCopyWithImpl<$Res, $Val extends FallbackFilter> + implements $FallbackFilterCopyWith<$Res> { + _$FallbackFilterCopyWithImpl(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? geoip = null, + Object? geoipCode = null, + Object? geosite = null, + Object? ipcidr = null, + Object? domain = null, + }) { + return _then(_value.copyWith( + geoip: null == geoip + ? _value.geoip + : geoip // ignore: cast_nullable_to_non_nullable + as bool, + geoipCode: null == geoipCode + ? _value.geoipCode + : geoipCode // ignore: cast_nullable_to_non_nullable + as String, + geosite: null == geosite + ? _value.geosite + : geosite // ignore: cast_nullable_to_non_nullable + as List, + ipcidr: null == ipcidr + ? _value.ipcidr + : ipcidr // ignore: cast_nullable_to_non_nullable + as List, + domain: null == domain + ? _value.domain + : domain // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$FallbackFilterImplCopyWith<$Res> + implements $FallbackFilterCopyWith<$Res> { + factory _$$FallbackFilterImplCopyWith(_$FallbackFilterImpl value, + $Res Function(_$FallbackFilterImpl) then) = + __$$FallbackFilterImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool geoip, + @JsonKey(name: "geoip-code") String geoipCode, + List geosite, + List ipcidr, + List domain}); +} + +/// @nodoc +class __$$FallbackFilterImplCopyWithImpl<$Res> + extends _$FallbackFilterCopyWithImpl<$Res, _$FallbackFilterImpl> + implements _$$FallbackFilterImplCopyWith<$Res> { + __$$FallbackFilterImplCopyWithImpl( + _$FallbackFilterImpl _value, $Res Function(_$FallbackFilterImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? geoip = null, + Object? geoipCode = null, + Object? geosite = null, + Object? ipcidr = null, + Object? domain = null, + }) { + return _then(_$FallbackFilterImpl( + geoip: null == geoip + ? _value.geoip + : geoip // ignore: cast_nullable_to_non_nullable + as bool, + geoipCode: null == geoipCode + ? _value.geoipCode + : geoipCode // ignore: cast_nullable_to_non_nullable + as String, + geosite: null == geosite + ? _value._geosite + : geosite // ignore: cast_nullable_to_non_nullable + as List, + ipcidr: null == ipcidr + ? _value._ipcidr + : ipcidr // ignore: cast_nullable_to_non_nullable + as List, + domain: null == domain + ? _value._domain + : domain // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$FallbackFilterImpl implements _FallbackFilter { + const _$FallbackFilterImpl( + {this.geoip = true, + @JsonKey(name: "geoip-code") this.geoipCode = "CN", + final List geosite = const ["gfw"], + final List ipcidr = const ["240.0.0.0/4"], + final List domain = const [ + "+.google.com", + "+.facebook.com", + "+.youtube.com" + ]}) + : _geosite = geosite, + _ipcidr = ipcidr, + _domain = domain; + + factory _$FallbackFilterImpl.fromJson(Map json) => + _$$FallbackFilterImplFromJson(json); + + @override + @JsonKey() + final bool geoip; + @override + @JsonKey(name: "geoip-code") + final String geoipCode; + final List _geosite; + @override + @JsonKey() + List get geosite { + if (_geosite is EqualUnmodifiableListView) return _geosite; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_geosite); + } + + final List _ipcidr; + @override + @JsonKey() + List get ipcidr { + if (_ipcidr is EqualUnmodifiableListView) return _ipcidr; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_ipcidr); + } + + final List _domain; + @override + @JsonKey() + List get domain { + if (_domain is EqualUnmodifiableListView) return _domain; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_domain); + } + + @override + String toString() { + return 'FallbackFilter(geoip: $geoip, geoipCode: $geoipCode, geosite: $geosite, ipcidr: $ipcidr, domain: $domain)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$FallbackFilterImpl && + (identical(other.geoip, geoip) || other.geoip == geoip) && + (identical(other.geoipCode, geoipCode) || + other.geoipCode == geoipCode) && + const DeepCollectionEquality().equals(other._geosite, _geosite) && + const DeepCollectionEquality().equals(other._ipcidr, _ipcidr) && + const DeepCollectionEquality().equals(other._domain, _domain)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + geoip, + geoipCode, + const DeepCollectionEquality().hash(_geosite), + const DeepCollectionEquality().hash(_ipcidr), + const DeepCollectionEquality().hash(_domain)); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$FallbackFilterImplCopyWith<_$FallbackFilterImpl> get copyWith => + __$$FallbackFilterImplCopyWithImpl<_$FallbackFilterImpl>( + this, _$identity); + + @override + Map toJson() { + return _$$FallbackFilterImplToJson( + this, + ); + } +} + +abstract class _FallbackFilter implements FallbackFilter { + const factory _FallbackFilter( + {final bool geoip, + @JsonKey(name: "geoip-code") final String geoipCode, + final List geosite, + final List ipcidr, + final List domain}) = _$FallbackFilterImpl; + + factory _FallbackFilter.fromJson(Map json) = + _$FallbackFilterImpl.fromJson; + + @override + bool get geoip; + @override + @JsonKey(name: "geoip-code") + String get geoipCode; + @override + List get geosite; + @override + List get ipcidr; + @override + List get domain; + @override + @JsonKey(ignore: true) + _$$FallbackFilterImplCopyWith<_$FallbackFilterImpl> get copyWith => + throw _privateConstructorUsedError; +} + +Dns _$DnsFromJson(Map json) { + return _Dns.fromJson(json); +} + +/// @nodoc +mixin _$Dns { + bool get enable => throw _privateConstructorUsedError; + @JsonKey(name: "prefer-h3") + bool get preferH3 => throw _privateConstructorUsedError; + @JsonKey(name: "use-hosts") + bool get useHosts => throw _privateConstructorUsedError; + @JsonKey(name: "use-system-hosts") + bool get useSystemHosts => throw _privateConstructorUsedError; + @JsonKey(name: "respect-rules") + bool get respectRules => throw _privateConstructorUsedError; + bool get ipv6 => throw _privateConstructorUsedError; + @JsonKey(name: "default-nameserver") + List get defaultNameserver => throw _privateConstructorUsedError; + @JsonKey(name: "enhanced-mode") + DnsMode get enhancedMode => throw _privateConstructorUsedError; + @JsonKey(name: "fake-ip-range") + String get fakeIpRange => throw _privateConstructorUsedError; + @JsonKey(name: "fake-ip-filter") + List get fakeIpFilter => throw _privateConstructorUsedError; + @JsonKey(name: "nameserver-policy") + Map get nameserverPolicy => + throw _privateConstructorUsedError; + List get nameserver => throw _privateConstructorUsedError; + List get fallback => throw _privateConstructorUsedError; + @JsonKey(name: "proxy-server-nameserver") + List get proxyServerNameserver => throw _privateConstructorUsedError; + @JsonKey(name: "fallback-filter") + FallbackFilter get fallbackFilter => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $DnsCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DnsCopyWith<$Res> { + factory $DnsCopyWith(Dns value, $Res Function(Dns) then) = + _$DnsCopyWithImpl<$Res, Dns>; + @useResult + $Res call( + {bool enable, + @JsonKey(name: "prefer-h3") bool preferH3, + @JsonKey(name: "use-hosts") bool useHosts, + @JsonKey(name: "use-system-hosts") bool useSystemHosts, + @JsonKey(name: "respect-rules") bool respectRules, + bool ipv6, + @JsonKey(name: "default-nameserver") List defaultNameserver, + @JsonKey(name: "enhanced-mode") DnsMode enhancedMode, + @JsonKey(name: "fake-ip-range") String fakeIpRange, + @JsonKey(name: "fake-ip-filter") List fakeIpFilter, + @JsonKey(name: "nameserver-policy") Map nameserverPolicy, + List nameserver, + List fallback, + @JsonKey(name: "proxy-server-nameserver") + List proxyServerNameserver, + @JsonKey(name: "fallback-filter") FallbackFilter fallbackFilter}); + + $FallbackFilterCopyWith<$Res> get fallbackFilter; +} + +/// @nodoc +class _$DnsCopyWithImpl<$Res, $Val extends Dns> implements $DnsCopyWith<$Res> { + _$DnsCopyWithImpl(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? enable = null, + Object? preferH3 = null, + Object? useHosts = null, + Object? useSystemHosts = null, + Object? respectRules = null, + Object? ipv6 = null, + Object? defaultNameserver = null, + Object? enhancedMode = null, + Object? fakeIpRange = null, + Object? fakeIpFilter = null, + Object? nameserverPolicy = null, + Object? nameserver = null, + Object? fallback = null, + Object? proxyServerNameserver = null, + Object? fallbackFilter = null, + }) { + return _then(_value.copyWith( + enable: null == enable + ? _value.enable + : enable // ignore: cast_nullable_to_non_nullable + as bool, + preferH3: null == preferH3 + ? _value.preferH3 + : preferH3 // ignore: cast_nullable_to_non_nullable + as bool, + useHosts: null == useHosts + ? _value.useHosts + : useHosts // ignore: cast_nullable_to_non_nullable + as bool, + useSystemHosts: null == useSystemHosts + ? _value.useSystemHosts + : useSystemHosts // ignore: cast_nullable_to_non_nullable + as bool, + respectRules: null == respectRules + ? _value.respectRules + : respectRules // ignore: cast_nullable_to_non_nullable + as bool, + ipv6: null == ipv6 + ? _value.ipv6 + : ipv6 // ignore: cast_nullable_to_non_nullable + as bool, + defaultNameserver: null == defaultNameserver + ? _value.defaultNameserver + : defaultNameserver // ignore: cast_nullable_to_non_nullable + as List, + enhancedMode: null == enhancedMode + ? _value.enhancedMode + : enhancedMode // ignore: cast_nullable_to_non_nullable + as DnsMode, + fakeIpRange: null == fakeIpRange + ? _value.fakeIpRange + : fakeIpRange // ignore: cast_nullable_to_non_nullable + as String, + fakeIpFilter: null == fakeIpFilter + ? _value.fakeIpFilter + : fakeIpFilter // ignore: cast_nullable_to_non_nullable + as List, + nameserverPolicy: null == nameserverPolicy + ? _value.nameserverPolicy + : nameserverPolicy // ignore: cast_nullable_to_non_nullable + as Map, + nameserver: null == nameserver + ? _value.nameserver + : nameserver // ignore: cast_nullable_to_non_nullable + as List, + fallback: null == fallback + ? _value.fallback + : fallback // ignore: cast_nullable_to_non_nullable + as List, + proxyServerNameserver: null == proxyServerNameserver + ? _value.proxyServerNameserver + : proxyServerNameserver // ignore: cast_nullable_to_non_nullable + as List, + fallbackFilter: null == fallbackFilter + ? _value.fallbackFilter + : fallbackFilter // ignore: cast_nullable_to_non_nullable + as FallbackFilter, + ) as $Val); + } + + @override + @pragma('vm:prefer-inline') + $FallbackFilterCopyWith<$Res> get fallbackFilter { + return $FallbackFilterCopyWith<$Res>(_value.fallbackFilter, (value) { + return _then(_value.copyWith(fallbackFilter: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$DnsImplCopyWith<$Res> implements $DnsCopyWith<$Res> { + factory _$$DnsImplCopyWith(_$DnsImpl value, $Res Function(_$DnsImpl) then) = + __$$DnsImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {bool enable, + @JsonKey(name: "prefer-h3") bool preferH3, + @JsonKey(name: "use-hosts") bool useHosts, + @JsonKey(name: "use-system-hosts") bool useSystemHosts, + @JsonKey(name: "respect-rules") bool respectRules, + bool ipv6, + @JsonKey(name: "default-nameserver") List defaultNameserver, + @JsonKey(name: "enhanced-mode") DnsMode enhancedMode, + @JsonKey(name: "fake-ip-range") String fakeIpRange, + @JsonKey(name: "fake-ip-filter") List fakeIpFilter, + @JsonKey(name: "nameserver-policy") Map nameserverPolicy, + List nameserver, + List fallback, + @JsonKey(name: "proxy-server-nameserver") + List proxyServerNameserver, + @JsonKey(name: "fallback-filter") FallbackFilter fallbackFilter}); + + @override + $FallbackFilterCopyWith<$Res> get fallbackFilter; +} + +/// @nodoc +class __$$DnsImplCopyWithImpl<$Res> extends _$DnsCopyWithImpl<$Res, _$DnsImpl> + implements _$$DnsImplCopyWith<$Res> { + __$$DnsImplCopyWithImpl(_$DnsImpl _value, $Res Function(_$DnsImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? enable = null, + Object? preferH3 = null, + Object? useHosts = null, + Object? useSystemHosts = null, + Object? respectRules = null, + Object? ipv6 = null, + Object? defaultNameserver = null, + Object? enhancedMode = null, + Object? fakeIpRange = null, + Object? fakeIpFilter = null, + Object? nameserverPolicy = null, + Object? nameserver = null, + Object? fallback = null, + Object? proxyServerNameserver = null, + Object? fallbackFilter = null, + }) { + return _then(_$DnsImpl( + enable: null == enable + ? _value.enable + : enable // ignore: cast_nullable_to_non_nullable + as bool, + preferH3: null == preferH3 + ? _value.preferH3 + : preferH3 // ignore: cast_nullable_to_non_nullable + as bool, + useHosts: null == useHosts + ? _value.useHosts + : useHosts // ignore: cast_nullable_to_non_nullable + as bool, + useSystemHosts: null == useSystemHosts + ? _value.useSystemHosts + : useSystemHosts // ignore: cast_nullable_to_non_nullable + as bool, + respectRules: null == respectRules + ? _value.respectRules + : respectRules // ignore: cast_nullable_to_non_nullable + as bool, + ipv6: null == ipv6 + ? _value.ipv6 + : ipv6 // ignore: cast_nullable_to_non_nullable + as bool, + defaultNameserver: null == defaultNameserver + ? _value._defaultNameserver + : defaultNameserver // ignore: cast_nullable_to_non_nullable + as List, + enhancedMode: null == enhancedMode + ? _value.enhancedMode + : enhancedMode // ignore: cast_nullable_to_non_nullable + as DnsMode, + fakeIpRange: null == fakeIpRange + ? _value.fakeIpRange + : fakeIpRange // ignore: cast_nullable_to_non_nullable + as String, + fakeIpFilter: null == fakeIpFilter + ? _value._fakeIpFilter + : fakeIpFilter // ignore: cast_nullable_to_non_nullable + as List, + nameserverPolicy: null == nameserverPolicy + ? _value._nameserverPolicy + : nameserverPolicy // ignore: cast_nullable_to_non_nullable + as Map, + nameserver: null == nameserver + ? _value._nameserver + : nameserver // ignore: cast_nullable_to_non_nullable + as List, + fallback: null == fallback + ? _value._fallback + : fallback // ignore: cast_nullable_to_non_nullable + as List, + proxyServerNameserver: null == proxyServerNameserver + ? _value._proxyServerNameserver + : proxyServerNameserver // ignore: cast_nullable_to_non_nullable + as List, + fallbackFilter: null == fallbackFilter + ? _value.fallbackFilter + : fallbackFilter // ignore: cast_nullable_to_non_nullable + as FallbackFilter, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$DnsImpl implements _Dns { + const _$DnsImpl( + {this.enable = true, + @JsonKey(name: "prefer-h3") this.preferH3 = false, + @JsonKey(name: "use-hosts") this.useHosts = true, + @JsonKey(name: "use-system-hosts") this.useSystemHosts = true, + @JsonKey(name: "respect-rules") this.respectRules = true, + this.ipv6 = false, + @JsonKey(name: "default-nameserver") + final List defaultNameserver = const ["223.5.5.5"], + @JsonKey(name: "enhanced-mode") this.enhancedMode = DnsMode.fakeIp, + @JsonKey(name: "fake-ip-range") this.fakeIpRange = "198.18.0.1/16", + @JsonKey(name: "fake-ip-filter") final List fakeIpFilter = const [ + "*.lan", + "localhost.ptlogin2.qq.com" + ], + @JsonKey(name: "nameserver-policy") + final Map nameserverPolicy = const { + "www.baidu.com": "114.114.114.114", + "+.internal.crop.com": "10.0.0.1", + "geosite:cn": "https://doh.pub/dns-query" + }, + final List nameserver = const [ + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query" + ], + final List fallback = const ["tls://8.8.4.4", "tls://1.1.1.1"], + @JsonKey(name: "proxy-server-nameserver") + final List proxyServerNameserver = const [ + "https://doh.pub/dns-query" + ], + @JsonKey(name: "fallback-filter") + this.fallbackFilter = const FallbackFilter()}) + : _defaultNameserver = defaultNameserver, + _fakeIpFilter = fakeIpFilter, + _nameserverPolicy = nameserverPolicy, + _nameserver = nameserver, + _fallback = fallback, + _proxyServerNameserver = proxyServerNameserver; + + factory _$DnsImpl.fromJson(Map json) => + _$$DnsImplFromJson(json); + + @override + @JsonKey() + final bool enable; + @override + @JsonKey(name: "prefer-h3") + final bool preferH3; + @override + @JsonKey(name: "use-hosts") + final bool useHosts; + @override + @JsonKey(name: "use-system-hosts") + final bool useSystemHosts; + @override + @JsonKey(name: "respect-rules") + final bool respectRules; + @override + @JsonKey() + final bool ipv6; + final List _defaultNameserver; + @override + @JsonKey(name: "default-nameserver") + List get defaultNameserver { + if (_defaultNameserver is EqualUnmodifiableListView) + return _defaultNameserver; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_defaultNameserver); + } + + @override + @JsonKey(name: "enhanced-mode") + final DnsMode enhancedMode; + @override + @JsonKey(name: "fake-ip-range") + final String fakeIpRange; + final List _fakeIpFilter; + @override + @JsonKey(name: "fake-ip-filter") + List get fakeIpFilter { + if (_fakeIpFilter is EqualUnmodifiableListView) return _fakeIpFilter; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_fakeIpFilter); + } + + final Map _nameserverPolicy; + @override + @JsonKey(name: "nameserver-policy") + Map get nameserverPolicy { + if (_nameserverPolicy is EqualUnmodifiableMapView) return _nameserverPolicy; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_nameserverPolicy); + } + + final List _nameserver; + @override + @JsonKey() + List get nameserver { + if (_nameserver is EqualUnmodifiableListView) return _nameserver; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_nameserver); + } + + final List _fallback; + @override + @JsonKey() + List get fallback { + if (_fallback is EqualUnmodifiableListView) return _fallback; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_fallback); + } + + final List _proxyServerNameserver; + @override + @JsonKey(name: "proxy-server-nameserver") + List get proxyServerNameserver { + if (_proxyServerNameserver is EqualUnmodifiableListView) + return _proxyServerNameserver; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_proxyServerNameserver); + } + + @override + @JsonKey(name: "fallback-filter") + final FallbackFilter fallbackFilter; + + @override + String toString() { + return 'Dns(enable: $enable, preferH3: $preferH3, useHosts: $useHosts, useSystemHosts: $useSystemHosts, respectRules: $respectRules, ipv6: $ipv6, defaultNameserver: $defaultNameserver, enhancedMode: $enhancedMode, fakeIpRange: $fakeIpRange, fakeIpFilter: $fakeIpFilter, nameserverPolicy: $nameserverPolicy, nameserver: $nameserver, fallback: $fallback, proxyServerNameserver: $proxyServerNameserver, fallbackFilter: $fallbackFilter)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DnsImpl && + (identical(other.enable, enable) || other.enable == enable) && + (identical(other.preferH3, preferH3) || + other.preferH3 == preferH3) && + (identical(other.useHosts, useHosts) || + other.useHosts == useHosts) && + (identical(other.useSystemHosts, useSystemHosts) || + other.useSystemHosts == useSystemHosts) && + (identical(other.respectRules, respectRules) || + other.respectRules == respectRules) && + (identical(other.ipv6, ipv6) || other.ipv6 == ipv6) && + const DeepCollectionEquality() + .equals(other._defaultNameserver, _defaultNameserver) && + (identical(other.enhancedMode, enhancedMode) || + other.enhancedMode == enhancedMode) && + (identical(other.fakeIpRange, fakeIpRange) || + other.fakeIpRange == fakeIpRange) && + const DeepCollectionEquality() + .equals(other._fakeIpFilter, _fakeIpFilter) && + const DeepCollectionEquality() + .equals(other._nameserverPolicy, _nameserverPolicy) && + const DeepCollectionEquality() + .equals(other._nameserver, _nameserver) && + const DeepCollectionEquality().equals(other._fallback, _fallback) && + const DeepCollectionEquality() + .equals(other._proxyServerNameserver, _proxyServerNameserver) && + (identical(other.fallbackFilter, fallbackFilter) || + other.fallbackFilter == fallbackFilter)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash( + runtimeType, + enable, + preferH3, + useHosts, + useSystemHosts, + respectRules, + ipv6, + const DeepCollectionEquality().hash(_defaultNameserver), + enhancedMode, + fakeIpRange, + const DeepCollectionEquality().hash(_fakeIpFilter), + const DeepCollectionEquality().hash(_nameserverPolicy), + const DeepCollectionEquality().hash(_nameserver), + const DeepCollectionEquality().hash(_fallback), + const DeepCollectionEquality().hash(_proxyServerNameserver), + fallbackFilter); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$DnsImplCopyWith<_$DnsImpl> get copyWith => + __$$DnsImplCopyWithImpl<_$DnsImpl>(this, _$identity); + + @override + Map toJson() { + return _$$DnsImplToJson( + this, + ); + } +} + +abstract class _Dns implements Dns { + const factory _Dns( + {final bool enable, + @JsonKey(name: "prefer-h3") final bool preferH3, + @JsonKey(name: "use-hosts") final bool useHosts, + @JsonKey(name: "use-system-hosts") final bool useSystemHosts, + @JsonKey(name: "respect-rules") final bool respectRules, + final bool ipv6, + @JsonKey(name: "default-nameserver") final List defaultNameserver, + @JsonKey(name: "enhanced-mode") final DnsMode enhancedMode, + @JsonKey(name: "fake-ip-range") final String fakeIpRange, + @JsonKey(name: "fake-ip-filter") final List fakeIpFilter, + @JsonKey(name: "nameserver-policy") + final Map nameserverPolicy, + final List nameserver, + final List fallback, + @JsonKey(name: "proxy-server-nameserver") + final List proxyServerNameserver, + @JsonKey(name: "fallback-filter") + final FallbackFilter fallbackFilter}) = _$DnsImpl; + + factory _Dns.fromJson(Map json) = _$DnsImpl.fromJson; + + @override + bool get enable; + @override + @JsonKey(name: "prefer-h3") + bool get preferH3; + @override + @JsonKey(name: "use-hosts") + bool get useHosts; + @override + @JsonKey(name: "use-system-hosts") + bool get useSystemHosts; + @override + @JsonKey(name: "respect-rules") + bool get respectRules; + @override + bool get ipv6; + @override + @JsonKey(name: "default-nameserver") + List get defaultNameserver; + @override + @JsonKey(name: "enhanced-mode") + DnsMode get enhancedMode; + @override + @JsonKey(name: "fake-ip-range") + String get fakeIpRange; + @override + @JsonKey(name: "fake-ip-filter") + List get fakeIpFilter; + @override + @JsonKey(name: "nameserver-policy") + Map get nameserverPolicy; + @override + List get nameserver; + @override + List get fallback; + @override + @JsonKey(name: "proxy-server-nameserver") + List get proxyServerNameserver; + @override + @JsonKey(name: "fallback-filter") + FallbackFilter get fallbackFilter; + @override + @JsonKey(ignore: true) + _$$DnsImplCopyWith<_$DnsImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 8235686..7f88196 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -6,35 +6,6 @@ part of '../clash_config.dart'; // JsonSerializableGenerator // ************************************************************************** -Dns _$DnsFromJson(Map json) => Dns() - ..enable = json['enable'] as bool - ..ipv6 = json['ipv6'] as bool - ..defaultNameserver = (json['default-nameserver'] as List) - .map((e) => e as String) - .toList() - ..enhancedMode = json['enhanced-mode'] as String - ..fakeIpRange = json['fake-ip-range'] as String - ..useHosts = json['use-hosts'] as bool - ..nameserver = - (json['nameserver'] as List).map((e) => e as String).toList() - ..fallback = - (json['fallback'] as List).map((e) => e as String).toList() - ..fakeIpFilter = (json['fake-ip-filter'] as List) - .map((e) => e as String) - .toList(); - -Map _$DnsToJson(Dns instance) => { - 'enable': instance.enable, - 'ipv6': instance.ipv6, - 'default-nameserver': instance.defaultNameserver, - 'enhanced-mode': instance.enhancedMode, - 'fake-ip-range': instance.fakeIpRange, - 'use-hosts': instance.useHosts, - 'nameserver': instance.nameserver, - 'fallback': instance.fallback, - 'fake-ip-filter': instance.fakeIpFilter, - }; - ClashConfig _$ClashConfigFromJson(Map json) => ClashConfig() ..mixedPort = (json['mixed-port'] as num?)?.toInt() ?? 7890 ..mode = $enumDecodeNullable(_$ModeEnumMap, json['mode']) ?? Mode.rule @@ -51,7 +22,7 @@ ClashConfig _$ClashConfigFromJson(Map json) => ClashConfig() ..unifiedDelay = json['unified-delay'] as bool? ?? false ..tcpConcurrent = json['tcp-concurrent'] as bool? ?? false ..tun = Tun.fromJson(json['tun'] as Map) - ..dns = Dns.fromJson(json['dns'] as Map) + ..dns = Dns.safeDnsFromJson(json['dns'] as Map) ..rules = (json['rules'] as List).map((e) => e as String).toList() ..globalRealUa = json['global-real-ua'] as String? ..geoXUrl = (json['geox-url'] as Map?)?.map( @@ -136,3 +107,105 @@ const _$TunStackEnumMap = { TunStack.system: 'system', TunStack.mixed: 'mixed', }; + +_$FallbackFilterImpl _$$FallbackFilterImplFromJson(Map json) => + _$FallbackFilterImpl( + geoip: json['geoip'] as bool? ?? true, + geoipCode: json['geoip-code'] as String? ?? "CN", + geosite: (json['geosite'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["gfw"], + ipcidr: (json['ipcidr'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["240.0.0.0/4"], + domain: (json['domain'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["+.google.com", "+.facebook.com", "+.youtube.com"], + ); + +Map _$$FallbackFilterImplToJson( + _$FallbackFilterImpl instance) => + { + 'geoip': instance.geoip, + 'geoip-code': instance.geoipCode, + 'geosite': instance.geosite, + 'ipcidr': instance.ipcidr, + 'domain': instance.domain, + }; + +_$DnsImpl _$$DnsImplFromJson(Map json) => _$DnsImpl( + enable: json['enable'] as bool? ?? true, + preferH3: json['prefer-h3'] as bool? ?? false, + useHosts: json['use-hosts'] as bool? ?? true, + useSystemHosts: json['use-system-hosts'] as bool? ?? true, + respectRules: json['respect-rules'] as bool? ?? true, + ipv6: json['ipv6'] as bool? ?? false, + defaultNameserver: (json['default-nameserver'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["223.5.5.5"], + enhancedMode: + $enumDecodeNullable(_$DnsModeEnumMap, json['enhanced-mode']) ?? + DnsMode.fakeIp, + fakeIpRange: json['fake-ip-range'] as String? ?? "198.18.0.1/16", + fakeIpFilter: (json['fake-ip-filter'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["*.lan", "localhost.ptlogin2.qq.com"], + nameserverPolicy: + (json['nameserver-policy'] as Map?)?.map( + (k, e) => MapEntry(k, e as String), + ) ?? + const { + "www.baidu.com": "114.114.114.114", + "+.internal.crop.com": "10.0.0.1", + "geosite:cn": "https://doh.pub/dns-query" + }, + nameserver: (json['nameserver'] as List?) + ?.map((e) => e as String) + .toList() ?? + const [ + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query" + ], + fallback: (json['fallback'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["tls://8.8.4.4", "tls://1.1.1.1"], + proxyServerNameserver: (json['proxy-server-nameserver'] as List?) + ?.map((e) => e as String) + .toList() ?? + const ["https://doh.pub/dns-query"], + fallbackFilter: json['fallback-filter'] == null + ? const FallbackFilter() + : FallbackFilter.fromJson( + json['fallback-filter'] as Map), + ); + +Map _$$DnsImplToJson(_$DnsImpl instance) => { + 'enable': instance.enable, + 'prefer-h3': instance.preferH3, + 'use-hosts': instance.useHosts, + 'use-system-hosts': instance.useSystemHosts, + 'respect-rules': instance.respectRules, + 'ipv6': instance.ipv6, + 'default-nameserver': instance.defaultNameserver, + 'enhanced-mode': _$DnsModeEnumMap[instance.enhancedMode]!, + 'fake-ip-range': instance.fakeIpRange, + 'fake-ip-filter': instance.fakeIpFilter, + 'nameserver-policy': instance.nameserverPolicy, + 'nameserver': instance.nameserver, + 'fallback': instance.fallback, + 'proxy-server-nameserver': instance.proxyServerNameserver, + 'fallback-filter': instance.fallbackFilter, + }; + +const _$DnsModeEnumMap = { + DnsMode.normal: 'normal', + DnsMode.fakeIp: 'fake-ip', + DnsMode.redirHost: 'redir-host', + DnsMode.hosts: 'hosts', +}; diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index abe8fdc..ec469d1 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -552,6 +552,189 @@ abstract class _CoreState implements CoreState { throw _privateConstructorUsedError; } +VPNState _$VPNStateFromJson(Map json) { + return _VPNState.fromJson(json); +} + +/// @nodoc +mixin _$VPNState { + AccessControl? get accessControl => throw _privateConstructorUsedError; + VpnProps get vpnProps => throw _privateConstructorUsedError; + + Map toJson() => throw _privateConstructorUsedError; + @JsonKey(ignore: true) + $VPNStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $VPNStateCopyWith<$Res> { + factory $VPNStateCopyWith(VPNState value, $Res Function(VPNState) then) = + _$VPNStateCopyWithImpl<$Res, VPNState>; + @useResult + $Res call({AccessControl? accessControl, VpnProps vpnProps}); + + $AccessControlCopyWith<$Res>? get accessControl; + $VpnPropsCopyWith<$Res> get vpnProps; +} + +/// @nodoc +class _$VPNStateCopyWithImpl<$Res, $Val extends VPNState> + implements $VPNStateCopyWith<$Res> { + _$VPNStateCopyWithImpl(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? vpnProps = null, + }) { + return _then(_value.copyWith( + accessControl: freezed == accessControl + ? _value.accessControl + : accessControl // ignore: cast_nullable_to_non_nullable + as AccessControl?, + vpnProps: null == vpnProps + ? _value.vpnProps + : vpnProps // ignore: cast_nullable_to_non_nullable + as VpnProps, + ) 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); + }); + } + + @override + @pragma('vm:prefer-inline') + $VpnPropsCopyWith<$Res> get vpnProps { + return $VpnPropsCopyWith<$Res>(_value.vpnProps, (value) { + return _then(_value.copyWith(vpnProps: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$VPNStateImplCopyWith<$Res> + implements $VPNStateCopyWith<$Res> { + factory _$$VPNStateImplCopyWith( + _$VPNStateImpl value, $Res Function(_$VPNStateImpl) then) = + __$$VPNStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({AccessControl? accessControl, VpnProps vpnProps}); + + @override + $AccessControlCopyWith<$Res>? get accessControl; + @override + $VpnPropsCopyWith<$Res> get vpnProps; +} + +/// @nodoc +class __$$VPNStateImplCopyWithImpl<$Res> + extends _$VPNStateCopyWithImpl<$Res, _$VPNStateImpl> + implements _$$VPNStateImplCopyWith<$Res> { + __$$VPNStateImplCopyWithImpl( + _$VPNStateImpl _value, $Res Function(_$VPNStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? accessControl = freezed, + Object? vpnProps = null, + }) { + return _then(_$VPNStateImpl( + accessControl: freezed == accessControl + ? _value.accessControl + : accessControl // ignore: cast_nullable_to_non_nullable + as AccessControl?, + vpnProps: null == vpnProps + ? _value.vpnProps + : vpnProps // ignore: cast_nullable_to_non_nullable + as VpnProps, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$VPNStateImpl implements _VPNState { + const _$VPNStateImpl({required this.accessControl, required this.vpnProps}); + + factory _$VPNStateImpl.fromJson(Map json) => + _$$VPNStateImplFromJson(json); + + @override + final AccessControl? accessControl; + @override + final VpnProps vpnProps; + + @override + String toString() { + return 'VPNState(accessControl: $accessControl, vpnProps: $vpnProps)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$VPNStateImpl && + (identical(other.accessControl, accessControl) || + other.accessControl == accessControl) && + (identical(other.vpnProps, vpnProps) || + other.vpnProps == vpnProps)); + } + + @JsonKey(ignore: true) + @override + int get hashCode => Object.hash(runtimeType, accessControl, vpnProps); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$VPNStateImplCopyWith<_$VPNStateImpl> get copyWith => + __$$VPNStateImplCopyWithImpl<_$VPNStateImpl>(this, _$identity); + + @override + Map toJson() { + return _$$VPNStateImplToJson( + this, + ); + } +} + +abstract class _VPNState implements VPNState { + const factory _VPNState( + {required final AccessControl? accessControl, + required final VpnProps vpnProps}) = _$VPNStateImpl; + + factory _VPNState.fromJson(Map json) = + _$VPNStateImpl.fromJson; + + @override + AccessControl? get accessControl; + @override + VpnProps get vpnProps; + @override + @JsonKey(ignore: true) + _$$VPNStateImplCopyWith<_$VPNStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + WindowProps _$WindowPropsFromJson(Map json) { return _WindowProps.fromJson(json); } diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 1f532d3..7a5819f 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -53,7 +53,8 @@ Config _$ConfigFromJson(Map json) => Config() ..vpnProps = VpnProps.fromJson(json['vpnProps'] as Map?) ..desktopProps = DesktopProps.fromJson(json['desktopProps'] as Map?) - ..showLabel = json['showLabel'] as bool? ?? false; + ..showLabel = json['showLabel'] as bool? ?? false + ..overrideDns = json['overrideDns'] as bool? ?? false; Map _$ConfigToJson(Config instance) => { 'profiles': instance.profiles, @@ -85,6 +86,7 @@ Map _$ConfigToJson(Config instance) => { 'vpnProps': instance.vpnProps, 'desktopProps': instance.desktopProps, 'showLabel': instance.showLabel, + 'overrideDns': instance.overrideDns, }; const _$ThemeModeEnumMap = { @@ -178,6 +180,21 @@ Map _$$CoreStateImplToJson(_$CoreStateImpl instance) => 'onlyProxy': instance.onlyProxy, }; +_$VPNStateImpl _$$VPNStateImplFromJson(Map json) => + _$VPNStateImpl( + accessControl: json['accessControl'] == null + ? null + : AccessControl.fromJson( + json['accessControl'] as Map), + vpnProps: VpnProps.fromJson(json['vpnProps'] as Map?), + ); + +Map _$$VPNStateImplToJson(_$VPNStateImpl instance) => + { + 'accessControl': instance.accessControl, + 'vpnProps': instance.vpnProps, + }; + _$WindowPropsImpl _$$WindowPropsImplFromJson(Map json) => _$WindowPropsImpl( width: (json['width'] as num?)?.toDouble() ?? 1000, diff --git a/lib/models/generated/ffi.freezed.dart b/lib/models/generated/ffi.freezed.dart index 8174de3..6da4112 100644 --- a/lib/models/generated/ffi.freezed.dart +++ b/lib/models/generated/ffi.freezed.dart @@ -26,6 +26,8 @@ mixin _$ConfigExtendedParams { bool get isCompatible => throw _privateConstructorUsedError; @JsonKey(name: "selected-map") Map get selectedMap => throw _privateConstructorUsedError; + @JsonKey(name: "override-dns") + bool get overrideDns => throw _privateConstructorUsedError; @JsonKey(name: "test-url") String get testUrl => throw _privateConstructorUsedError; @@ -45,6 +47,7 @@ abstract class $ConfigExtendedParamsCopyWith<$Res> { {@JsonKey(name: "is-patch") bool isPatch, @JsonKey(name: "is-compatible") bool isCompatible, @JsonKey(name: "selected-map") Map selectedMap, + @JsonKey(name: "override-dns") bool overrideDns, @JsonKey(name: "test-url") String testUrl}); } @@ -65,6 +68,7 @@ class _$ConfigExtendedParamsCopyWithImpl<$Res, Object? isPatch = null, Object? isCompatible = null, Object? selectedMap = null, + Object? overrideDns = null, Object? testUrl = null, }) { return _then(_value.copyWith( @@ -80,6 +84,10 @@ class _$ConfigExtendedParamsCopyWithImpl<$Res, ? _value.selectedMap : selectedMap // ignore: cast_nullable_to_non_nullable as Map, + overrideDns: null == overrideDns + ? _value.overrideDns + : overrideDns // ignore: cast_nullable_to_non_nullable + as bool, testUrl: null == testUrl ? _value.testUrl : testUrl // ignore: cast_nullable_to_non_nullable @@ -100,6 +108,7 @@ abstract class _$$ConfigExtendedParamsImplCopyWith<$Res> {@JsonKey(name: "is-patch") bool isPatch, @JsonKey(name: "is-compatible") bool isCompatible, @JsonKey(name: "selected-map") Map selectedMap, + @JsonKey(name: "override-dns") bool overrideDns, @JsonKey(name: "test-url") String testUrl}); } @@ -117,6 +126,7 @@ class __$$ConfigExtendedParamsImplCopyWithImpl<$Res> Object? isPatch = null, Object? isCompatible = null, Object? selectedMap = null, + Object? overrideDns = null, Object? testUrl = null, }) { return _then(_$ConfigExtendedParamsImpl( @@ -132,6 +142,10 @@ class __$$ConfigExtendedParamsImplCopyWithImpl<$Res> ? _value._selectedMap : selectedMap // ignore: cast_nullable_to_non_nullable as Map, + overrideDns: null == overrideDns + ? _value.overrideDns + : overrideDns // ignore: cast_nullable_to_non_nullable + as bool, testUrl: null == testUrl ? _value.testUrl : testUrl // ignore: cast_nullable_to_non_nullable @@ -148,6 +162,7 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { @JsonKey(name: "is-compatible") required this.isCompatible, @JsonKey(name: "selected-map") required final Map selectedMap, + @JsonKey(name: "override-dns") required this.overrideDns, @JsonKey(name: "test-url") required this.testUrl}) : _selectedMap = selectedMap; @@ -169,13 +184,16 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { return EqualUnmodifiableMapView(_selectedMap); } + @override + @JsonKey(name: "override-dns") + final bool overrideDns; @override @JsonKey(name: "test-url") final String testUrl; @override String toString() { - return 'ConfigExtendedParams(isPatch: $isPatch, isCompatible: $isCompatible, selectedMap: $selectedMap, testUrl: $testUrl)'; + return 'ConfigExtendedParams(isPatch: $isPatch, isCompatible: $isCompatible, selectedMap: $selectedMap, overrideDns: $overrideDns, testUrl: $testUrl)'; } @override @@ -188,13 +206,15 @@ class _$ConfigExtendedParamsImpl implements _ConfigExtendedParams { other.isCompatible == isCompatible) && const DeepCollectionEquality() .equals(other._selectedMap, _selectedMap) && + (identical(other.overrideDns, overrideDns) || + other.overrideDns == overrideDns) && (identical(other.testUrl, testUrl) || other.testUrl == testUrl)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, isPatch, isCompatible, - const DeepCollectionEquality().hash(_selectedMap), testUrl); + const DeepCollectionEquality().hash(_selectedMap), overrideDns, testUrl); @JsonKey(ignore: true) @override @@ -218,6 +238,7 @@ abstract class _ConfigExtendedParams implements ConfigExtendedParams { @JsonKey(name: "is-compatible") required final bool isCompatible, @JsonKey(name: "selected-map") required final Map selectedMap, + @JsonKey(name: "override-dns") required final bool overrideDns, @JsonKey(name: "test-url") required final String testUrl}) = _$ConfigExtendedParamsImpl; @@ -234,6 +255,9 @@ abstract class _ConfigExtendedParams implements ConfigExtendedParams { @JsonKey(name: "selected-map") Map get selectedMap; @override + @JsonKey(name: "override-dns") + bool get overrideDns; + @override @JsonKey(name: "test-url") String get testUrl; @override diff --git a/lib/models/generated/ffi.g.dart b/lib/models/generated/ffi.g.dart index 42670ec..3c9e139 100644 --- a/lib/models/generated/ffi.g.dart +++ b/lib/models/generated/ffi.g.dart @@ -12,6 +12,7 @@ _$ConfigExtendedParamsImpl _$$ConfigExtendedParamsImplFromJson( isPatch: json['is-patch'] as bool, isCompatible: json['is-compatible'] as bool, selectedMap: Map.from(json['selected-map'] as Map), + overrideDns: json['override-dns'] as bool, testUrl: json['test-url'] as String, ); @@ -21,6 +22,7 @@ Map _$$ConfigExtendedParamsImplToJson( 'is-patch': instance.isPatch, 'is-compatible': instance.isCompatible, 'selected-map': instance.selectedMap, + 'override-dns': instance.overrideDns, 'test-url': instance.testUrl, }; diff --git a/lib/models/generated/proxy.freezed.dart b/lib/models/generated/proxy.freezed.dart index d9c81dc..2e20232 100644 --- a/lib/models/generated/proxy.freezed.dart +++ b/lib/models/generated/proxy.freezed.dart @@ -24,6 +24,7 @@ mixin _$Group { List get all => throw _privateConstructorUsedError; String? get now => throw _privateConstructorUsedError; bool? get hidden => throw _privateConstructorUsedError; + String get icon => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; Map toJson() => throw _privateConstructorUsedError; @@ -41,6 +42,7 @@ abstract class $GroupCopyWith<$Res> { List all, String? now, bool? hidden, + String icon, String name}); } @@ -61,6 +63,7 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group> Object? all = null, Object? now = freezed, Object? hidden = freezed, + Object? icon = null, Object? name = null, }) { return _then(_value.copyWith( @@ -80,6 +83,10 @@ class _$GroupCopyWithImpl<$Res, $Val extends Group> ? _value.hidden : hidden // ignore: cast_nullable_to_non_nullable as bool?, + icon: null == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -100,6 +107,7 @@ abstract class _$$GroupImplCopyWith<$Res> implements $GroupCopyWith<$Res> { List all, String? now, bool? hidden, + String icon, String name}); } @@ -118,6 +126,7 @@ class __$$GroupImplCopyWithImpl<$Res> Object? all = null, Object? now = freezed, Object? hidden = freezed, + Object? icon = null, Object? name = null, }) { return _then(_$GroupImpl( @@ -137,6 +146,10 @@ class __$$GroupImplCopyWithImpl<$Res> ? _value.hidden : hidden // ignore: cast_nullable_to_non_nullable as bool?, + icon: null == icon + ? _value.icon + : icon // ignore: cast_nullable_to_non_nullable + as String, name: null == name ? _value.name : name // ignore: cast_nullable_to_non_nullable @@ -153,6 +166,7 @@ class _$GroupImpl implements _Group { final List all = const [], this.now, this.hidden, + this.icon = "", required this.name}) : _all = all; @@ -175,11 +189,14 @@ class _$GroupImpl implements _Group { @override final bool? hidden; @override + @JsonKey() + final String icon; + @override final String name; @override String toString() { - return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, name: $name)'; + return 'Group(type: $type, all: $all, now: $now, hidden: $hidden, icon: $icon, name: $name)'; } @override @@ -191,13 +208,14 @@ class _$GroupImpl implements _Group { const DeepCollectionEquality().equals(other._all, _all) && (identical(other.now, now) || other.now == now) && (identical(other.hidden, hidden) || other.hidden == hidden) && + (identical(other.icon, icon) || other.icon == icon) && (identical(other.name, name) || other.name == name)); } @JsonKey(ignore: true) @override int get hashCode => Object.hash(runtimeType, type, - const DeepCollectionEquality().hash(_all), now, hidden, name); + const DeepCollectionEquality().hash(_all), now, hidden, icon, name); @JsonKey(ignore: true) @override @@ -219,6 +237,7 @@ abstract class _Group implements Group { final List all, final String? now, final bool? hidden, + final String icon, required final String name}) = _$GroupImpl; factory _Group.fromJson(Map json) = _$GroupImpl.fromJson; @@ -232,6 +251,8 @@ abstract class _Group implements Group { @override bool? get hidden; @override + String get icon; + @override String get name; @override @JsonKey(ignore: true) diff --git a/lib/models/generated/proxy.g.dart b/lib/models/generated/proxy.g.dart index 4383835..c9a1213 100644 --- a/lib/models/generated/proxy.g.dart +++ b/lib/models/generated/proxy.g.dart @@ -14,6 +14,7 @@ _$GroupImpl _$$GroupImplFromJson(Map json) => _$GroupImpl( const [], now: json['now'] as String?, hidden: json['hidden'] as bool?, + icon: json['icon'] as String? ?? "", name: json['name'] as String, ); @@ -23,6 +24,7 @@ Map _$$GroupImplToJson(_$GroupImpl instance) => 'all': instance.all, 'now': instance.now, 'hidden': instance.hidden, + 'icon': instance.icon, 'name': instance.name, }; diff --git a/lib/models/generated/selector.freezed.dart b/lib/models/generated/selector.freezed.dart index d98ec52..e87814c 100644 --- a/lib/models/generated/selector.freezed.dart +++ b/lib/models/generated/selector.freezed.dart @@ -3331,6 +3331,142 @@ abstract class _ProxyState implements ProxyState { throw _privateConstructorUsedError; } +/// @nodoc +mixin _$HttpOverridesState { + bool get isStart => throw _privateConstructorUsedError; + int get port => throw _privateConstructorUsedError; + + @JsonKey(ignore: true) + $HttpOverridesStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $HttpOverridesStateCopyWith<$Res> { + factory $HttpOverridesStateCopyWith( + HttpOverridesState value, $Res Function(HttpOverridesState) then) = + _$HttpOverridesStateCopyWithImpl<$Res, HttpOverridesState>; + @useResult + $Res call({bool isStart, int port}); +} + +/// @nodoc +class _$HttpOverridesStateCopyWithImpl<$Res, $Val extends HttpOverridesState> + implements $HttpOverridesStateCopyWith<$Res> { + _$HttpOverridesStateCopyWithImpl(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? isStart = null, + Object? port = null, + }) { + return _then(_value.copyWith( + isStart: null == isStart + ? _value.isStart + : isStart // ignore: cast_nullable_to_non_nullable + as bool, + port: null == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as int, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$HttpOverridesStateImplCopyWith<$Res> + implements $HttpOverridesStateCopyWith<$Res> { + factory _$$HttpOverridesStateImplCopyWith(_$HttpOverridesStateImpl value, + $Res Function(_$HttpOverridesStateImpl) then) = + __$$HttpOverridesStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({bool isStart, int port}); +} + +/// @nodoc +class __$$HttpOverridesStateImplCopyWithImpl<$Res> + extends _$HttpOverridesStateCopyWithImpl<$Res, _$HttpOverridesStateImpl> + implements _$$HttpOverridesStateImplCopyWith<$Res> { + __$$HttpOverridesStateImplCopyWithImpl(_$HttpOverridesStateImpl _value, + $Res Function(_$HttpOverridesStateImpl) _then) + : super(_value, _then); + + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? isStart = null, + Object? port = null, + }) { + return _then(_$HttpOverridesStateImpl( + isStart: null == isStart + ? _value.isStart + : isStart // ignore: cast_nullable_to_non_nullable + as bool, + port: null == port + ? _value.port + : port // ignore: cast_nullable_to_non_nullable + as int, + )); + } +} + +/// @nodoc + +class _$HttpOverridesStateImpl implements _HttpOverridesState { + const _$HttpOverridesStateImpl({required this.isStart, required this.port}); + + @override + final bool isStart; + @override + final int port; + + @override + String toString() { + return 'HttpOverridesState(isStart: $isStart, port: $port)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$HttpOverridesStateImpl && + (identical(other.isStart, isStart) || other.isStart == isStart) && + (identical(other.port, port) || other.port == port)); + } + + @override + int get hashCode => Object.hash(runtimeType, isStart, port); + + @JsonKey(ignore: true) + @override + @pragma('vm:prefer-inline') + _$$HttpOverridesStateImplCopyWith<_$HttpOverridesStateImpl> get copyWith => + __$$HttpOverridesStateImplCopyWithImpl<_$HttpOverridesStateImpl>( + this, _$identity); +} + +abstract class _HttpOverridesState implements HttpOverridesState { + const factory _HttpOverridesState( + {required final bool isStart, + required final int port}) = _$HttpOverridesStateImpl; + + @override + bool get isStart; + @override + int get port; + @override + @JsonKey(ignore: true) + _$$HttpOverridesStateImplCopyWith<_$HttpOverridesStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + /// @nodoc mixin _$ClashConfigState { int get mixedPort => throw _privateConstructorUsedError; @@ -3344,6 +3480,7 @@ mixin _$ClashConfigState { int get keepAliveInterval => throw _privateConstructorUsedError; bool get unifiedDelay => throw _privateConstructorUsedError; bool get tcpConcurrent => throw _privateConstructorUsedError; + Map get hosts => throw _privateConstructorUsedError; Tun get tun => throw _privateConstructorUsedError; Dns get dns => throw _privateConstructorUsedError; Map get geoXUrl => throw _privateConstructorUsedError; @@ -3373,6 +3510,7 @@ abstract class $ClashConfigStateCopyWith<$Res> { int keepAliveInterval, bool unifiedDelay, bool tcpConcurrent, + Map hosts, Tun tun, Dns dns, Map geoXUrl, @@ -3380,6 +3518,7 @@ abstract class $ClashConfigStateCopyWith<$Res> { String? globalRealUa}); $TunCopyWith<$Res> get tun; + $DnsCopyWith<$Res> get dns; } /// @nodoc @@ -3406,6 +3545,7 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> Object? keepAliveInterval = null, Object? unifiedDelay = null, Object? tcpConcurrent = null, + Object? hosts = null, Object? tun = null, Object? dns = null, Object? geoXUrl = null, @@ -3457,6 +3597,10 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> ? _value.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable as bool, + hosts: null == hosts + ? _value.hosts + : hosts // ignore: cast_nullable_to_non_nullable + as Map, tun: null == tun ? _value.tun : tun // ignore: cast_nullable_to_non_nullable @@ -3487,6 +3631,14 @@ class _$ClashConfigStateCopyWithImpl<$Res, $Val extends ClashConfigState> return _then(_value.copyWith(tun: value) as $Val); }); } + + @override + @pragma('vm:prefer-inline') + $DnsCopyWith<$Res> get dns { + return $DnsCopyWith<$Res>(_value.dns, (value) { + return _then(_value.copyWith(dns: value) as $Val); + }); + } } /// @nodoc @@ -3509,6 +3661,7 @@ abstract class _$$ClashConfigStateImplCopyWith<$Res> int keepAliveInterval, bool unifiedDelay, bool tcpConcurrent, + Map hosts, Tun tun, Dns dns, Map geoXUrl, @@ -3517,6 +3670,8 @@ abstract class _$$ClashConfigStateImplCopyWith<$Res> @override $TunCopyWith<$Res> get tun; + @override + $DnsCopyWith<$Res> get dns; } /// @nodoc @@ -3541,6 +3696,7 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> Object? keepAliveInterval = null, Object? unifiedDelay = null, Object? tcpConcurrent = null, + Object? hosts = null, Object? tun = null, Object? dns = null, Object? geoXUrl = null, @@ -3592,6 +3748,10 @@ class __$$ClashConfigStateImplCopyWithImpl<$Res> ? _value.tcpConcurrent : tcpConcurrent // ignore: cast_nullable_to_non_nullable as bool, + hosts: null == hosts + ? _value._hosts + : hosts // ignore: cast_nullable_to_non_nullable + as Map, tun: null == tun ? _value.tun : tun // ignore: cast_nullable_to_non_nullable @@ -3631,12 +3791,14 @@ class _$ClashConfigStateImpl implements _ClashConfigState { required this.keepAliveInterval, required this.unifiedDelay, required this.tcpConcurrent, + required final Map hosts, required this.tun, required this.dns, required final Map geoXUrl, required final List rules, required this.globalRealUa}) - : _geoXUrl = geoXUrl, + : _hosts = hosts, + _geoXUrl = geoXUrl, _rules = rules; @override @@ -3661,6 +3823,14 @@ class _$ClashConfigStateImpl implements _ClashConfigState { final bool unifiedDelay; @override final bool tcpConcurrent; + final Map _hosts; + @override + Map get hosts { + if (_hosts is EqualUnmodifiableMapView) return _hosts; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(_hosts); + } + @override final Tun tun; @override @@ -3686,7 +3856,7 @@ class _$ClashConfigStateImpl implements _ClashConfigState { @override String toString() { - return 'ClashConfigState(mixedPort: $mixedPort, allowLan: $allowLan, ipv6: $ipv6, geodataLoader: $geodataLoader, logLevel: $logLevel, externalController: $externalController, mode: $mode, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, rules: $rules, globalRealUa: $globalRealUa)'; + return 'ClashConfigState(mixedPort: $mixedPort, allowLan: $allowLan, ipv6: $ipv6, geodataLoader: $geodataLoader, logLevel: $logLevel, externalController: $externalController, mode: $mode, findProcessMode: $findProcessMode, keepAliveInterval: $keepAliveInterval, unifiedDelay: $unifiedDelay, tcpConcurrent: $tcpConcurrent, hosts: $hosts, tun: $tun, dns: $dns, geoXUrl: $geoXUrl, rules: $rules, globalRealUa: $globalRealUa)'; } @override @@ -3714,6 +3884,7 @@ class _$ClashConfigStateImpl implements _ClashConfigState { other.unifiedDelay == unifiedDelay) && (identical(other.tcpConcurrent, tcpConcurrent) || other.tcpConcurrent == tcpConcurrent) && + const DeepCollectionEquality().equals(other._hosts, _hosts) && (identical(other.tun, tun) || other.tun == tun) && (identical(other.dns, dns) || other.dns == dns) && const DeepCollectionEquality().equals(other._geoXUrl, _geoXUrl) && @@ -3736,6 +3907,7 @@ class _$ClashConfigStateImpl implements _ClashConfigState { keepAliveInterval, unifiedDelay, tcpConcurrent, + const DeepCollectionEquality().hash(_hosts), tun, dns, const DeepCollectionEquality().hash(_geoXUrl), @@ -3763,6 +3935,7 @@ abstract class _ClashConfigState implements ClashConfigState { required final int keepAliveInterval, required final bool unifiedDelay, required final bool tcpConcurrent, + required final Map hosts, required final Tun tun, required final Dns dns, required final Map geoXUrl, @@ -3792,6 +3965,8 @@ abstract class _ClashConfigState implements ClashConfigState { @override bool get tcpConcurrent; @override + Map get hosts; + @override Tun get tun; @override Dns get dns; diff --git a/lib/models/proxy.dart b/lib/models/proxy.dart index c3cb7cb..631c57a 100644 --- a/lib/models/proxy.dart +++ b/lib/models/proxy.dart @@ -15,6 +15,7 @@ class Group with _$Group { @Default([]) List all, String? now, bool? hidden, + @Default("") String icon, required String name, }) = _Group; @@ -41,4 +42,4 @@ class Proxy with _$Proxy { }) = _Proxy; factory Proxy.fromJson(Map json) => _$ProxyFromJson(json); -} \ No newline at end of file +} diff --git a/lib/models/selector.dart b/lib/models/selector.dart index d2ff749..8bb363c 100644 --- a/lib/models/selector.dart +++ b/lib/models/selector.dart @@ -4,7 +4,6 @@ import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/models/models.dart'; import 'package:flutter/material.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:lpinyin/lpinyin.dart'; part 'generated/selector.freezed.dart'; @@ -162,8 +161,8 @@ extension PackageListSelectorStateExt on PackageListSelectorState { return switch (sort) { AccessSortType.none => 0, AccessSortType.name => other.sortByChar( - PinyinHelper.getPinyin(a.label), - PinyinHelper.getPinyin(b.label), + other.getPinyin(a.label), + other.getPinyin(b.label), ), AccessSortType.time => a.firstInstallTime.compareTo(b.firstInstallTime), @@ -215,6 +214,14 @@ class ProxyState with _$ProxyState { }) = _ProxyState; } +@freezed +class HttpOverridesState with _$HttpOverridesState { + const factory HttpOverridesState({ + required bool isStart, + required int port, + }) = _HttpOverridesState; +} + @freezed class ClashConfigState with _$ClashConfigState { const factory ClashConfigState({ @@ -229,6 +236,7 @@ class ClashConfigState with _$ClashConfigState { required int keepAliveInterval, required bool unifiedDelay, required bool tcpConcurrent, + required HostsMap hosts, required Tun tun, required Dns dns, required GeoXMap geoXUrl, diff --git a/lib/state.dart b/lib/state.dart index c42c5b5..ee98b03 100644 --- a/lib/state.dart +++ b/lib/state.dart @@ -59,6 +59,7 @@ class GlobalState { isPatch: isPatch, isCompatible: true, selectedMap: config.currentSelectedMap, + overrideDns: config.overrideDns, testUrl: config.testUrl, ), ), diff --git a/lib/widgets/clash_container.dart b/lib/widgets/clash_container.dart index 51ed096..6ccae7c 100644 --- a/lib/widgets/clash_container.dart +++ b/lib/widgets/clash_container.dart @@ -39,6 +39,7 @@ class _ClashContainerState extends State tcpConcurrent: clashConfig.tcpConcurrent, tun: clashConfig.tun, dns: clashConfig.dns, + hosts: clashConfig.hosts, geoXUrl: clashConfig.geoXUrl, rules: clashConfig.rules, globalRealUa: clashConfig.globalRealUa, @@ -137,7 +138,7 @@ class _ClashContainerState extends State if (log.logLevel == LogLevel.error) { globalState.appController.showSnackBar(log.payload ?? ''); } - debugPrint("$log"); + // debugPrint("$log"); super.onLog(log); } diff --git a/lib/widgets/input.dart b/lib/widgets/input.dart new file mode 100644 index 0000000..7fa3ef4 --- /dev/null +++ b/lib/widgets/input.dart @@ -0,0 +1,293 @@ +import 'package:fl_clash/common/constant.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; + +import '../common/app_localizations.dart'; +import 'card.dart'; +import 'float_layout.dart'; +import 'list.dart'; + +class OptionsDialog extends StatelessWidget { + final String title; + final List options; + final T value; + final String Function(T value) textBuilder; + + const OptionsDialog({ + super.key, + required this.title, + required this.options, + required this.textBuilder, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(title), + contentPadding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 16, + ), + content: SizedBox( + width: 250, + child: Wrap( + children: [ + for (final option in options) + ListItem.radio( + delegate: RadioDelegate( + value: option, + groupValue: value, + onChanged: (T? value) { + Navigator.of(context).pop(value); + }, + ), + title: Text( + this.textBuilder(option), + ), + ), + ], + ), + ), + ); + } +} + +class InputDialog extends StatefulWidget { + final String title; + final String value; + final String? suffixText; + + const InputDialog({ + super.key, + required this.title, + required this.value, + this.suffixText, + }); + + @override + State createState() => _InputDialogState(); +} + +class _InputDialogState extends State { + late TextEditingController textController; + + String get value => widget.value; + + String get title => widget.title; + + String? get suffixText => widget.suffixText; + + @override + void initState() { + super.initState(); + textController = TextEditingController( + text: value, + ); + } + + _handleUpdate() async { + final text = textController.value.text; + if (text.isEmpty) return; + Navigator.of(context).pop(text); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(title), + content: SizedBox( + width: 300, + child: Wrap( + runSpacing: 16, + children: [ + TextField( + maxLines: 1, + minLines: 1, + controller: textController, + decoration: InputDecoration( + border: const OutlineInputBorder(), + suffixText: suffixText, + ), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: _handleUpdate, + child: Text(appLocalizations.submit), + ) + ], + ); + } +} + +class UpdatePage extends StatelessWidget { + final String title; + final Iterable items; + final Widget Function(T item) titleBuilder; + final Widget Function(T item)? subtitleBuilder; + final Function(T item) onAdd; + final Function(T item) onRemove; + final bool isMap; + + const UpdatePage({ + super.key, + required this.title, + required this.items, + required this.titleBuilder, + required this.onRemove, + required this.onAdd, + this.isMap = false, + this.subtitleBuilder, + }); + + @override + Widget build(BuildContext context) { + return FloatLayout( + floatingWidget: FloatWrapper( + child: FloatingActionButton( + onPressed: () async { + final value = await globalState.showCommonDialog( + child: AddDialog( + isMap: isMap, + title: title, + ), + ); + if (value == null) return; + onAdd(value); + }, + child: const Icon(Icons.add), + ), + ), + child: ListView.builder( + padding: const EdgeInsets.only( + bottom: 16 + 64, + left: 16, + right: 16, + ), + itemCount: items.length, + itemBuilder: (_, index) { + final e = items.toList()[index]; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: CommonCard( + child: ListItem( + title: titleBuilder(e), + subtitle: subtitleBuilder != null ? subtitleBuilder!(e) : null, + trailing: IconButton( + icon: const Icon(Icons.delete_outline), + onPressed: () { + onRemove(e); + }, + ), + ), + onPressed: () {}, + ), + ); + }, + ), + ); + } +} + +class AddDialog extends StatefulWidget { + final String title; + final bool isMap; + + const AddDialog({ + super.key, + required this.title, + required this.isMap, + }); + + @override + State createState() => _AddDialogState(); +} + +class _AddDialogState extends State { + late TextEditingController keyController; + late TextEditingController valueController; + final GlobalKey _formKey = GlobalKey(); + + @override + void initState() { + super.initState(); + keyController = TextEditingController(); + valueController = TextEditingController(); + } + + _submit() { + if (!_formKey.currentState!.validate()) return; + if (widget.isMap) { + Navigator.of(context).pop>( + MapEntry( + keyController.text, + valueController.text, + ), + ); + } else { + Navigator.of(context).pop( + valueController.text, + ); + } + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + title: Text(widget.title), + content: Form( + key: _formKey, + child: SizedBox( + width: dialogCommonWidth, + child: Wrap( + runSpacing: 16, + children: [ + if (widget.isMap) + TextFormField( + maxLines: 2, + minLines: 1, + controller: keyController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.key), + border: const OutlineInputBorder(), + labelText: appLocalizations.key, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.keyNotEmpty; + } + return null; + }, + ), + TextFormField( + maxLines: 3, + minLines: 1, + controller: valueController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.label), + border: const OutlineInputBorder(), + labelText: appLocalizations.value, + ), + validator: (String? value) { + if (value == null || value.isEmpty) { + return appLocalizations.valueNotEmpty; + } + return null; + }, + ), + ], + ), + ), + ), + actions: [ + TextButton( + onPressed: _submit, + child: Text(appLocalizations.confirm), + ) + ], + ); + } +} diff --git a/lib/widgets/list.dart b/lib/widgets/list.dart index 46e40a0..e419829 100644 --- a/lib/widgets/list.dart +++ b/lib/widgets/list.dart @@ -5,6 +5,7 @@ import 'package:fl_clash/widgets/open_container.dart'; import 'package:flutter/material.dart'; import 'card.dart'; +import 'input.dart'; import 'sheet.dart'; import 'scaffold.dart'; @@ -48,11 +49,13 @@ class OpenDelegate extends Delegate { final Widget widget; final String title; final double? extendPageWidth; + final bool isBlur; const OpenDelegate({ required this.title, required this.widget, this.extendPageWidth, + this.isBlur = true, }); } @@ -68,6 +71,36 @@ class NextDelegate extends Delegate { }); } +class OptionsDelegate extends Delegate { + final List options; + final String title; + final T value; + final String Function(T value) textBuilder; + final Function(T? value) onChanged; + + const OptionsDelegate({ + required this.title, + required this.options, + required this.textBuilder, + required this.value, + required this.onChanged, + }); +} + +class InputDelegate extends Delegate { + final String title; + final String value; + final String? suffixText; + final Function(String? value) onChanged; + + const InputDelegate({ + required this.title, + required this.value, + this.suffixText, + required this.onChanged, + }); +} + class ListItem extends StatelessWidget { final Widget? leading; final Widget title; @@ -106,6 +139,32 @@ class ListItem extends StatelessWidget { this.tileTitleAlignment = ListTileTitleAlignment.center, }) : onTap = null; + const ListItem.options({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.trailing, + required OptionsDelegate this.delegate, + this.horizontalTitleGap, + this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, + }) : onTap = null; + + const ListItem.input({ + super.key, + required this.title, + this.subtitle, + this.leading, + this.padding = const EdgeInsets.symmetric(horizontal: 16), + this.trailing, + required InputDelegate this.delegate, + this.horizontalTitleGap, + this.prue, + this.tileTitleAlignment = ListTileTitleAlignment.center, + }) : onTap = null; + const ListItem.next({ super.key, required this.title, @@ -125,7 +184,7 @@ class ListItem extends StatelessWidget { this.subtitle, this.leading, this.padding = const EdgeInsets.only(left: 16, right: 8), - required CheckboxDelegate this.delegate, + required CheckboxDelegate this.delegate, this.horizontalTitleGap, this.prue, this.tileTitleAlignment = ListTileTitleAlignment.center, @@ -138,7 +197,7 @@ class ListItem extends StatelessWidget { this.subtitle, this.leading, this.padding = const EdgeInsets.only(left: 16, right: 8), - required SwitchDelegate this.delegate, + required SwitchDelegate this.delegate, this.horizontalTitleGap, this.prue, this.tileTitleAlignment = ListTileTitleAlignment.center, @@ -227,6 +286,7 @@ class ListItem extends StatelessWidget { body: child, title: openDelegate.title, extendPageWidth: openDelegate.extendPageWidth, + isBlur: openDelegate.isBlur, ); return; } @@ -247,6 +307,37 @@ class ListItem extends StatelessWidget { }, ); } + if (delegate is OptionsDelegate) { + final optionsDelegate = delegate as OptionsDelegate; + return _buildListTile( + onTap: () async { + final value = await globalState.showCommonDialog( + child: OptionsDialog( + title: optionsDelegate.title, + options: optionsDelegate.options, + textBuilder: optionsDelegate.textBuilder, + value: optionsDelegate.value, + ), + ); + optionsDelegate.onChanged(value); + }, + ); + } + if (delegate is InputDelegate) { + final inputDelegate = delegate as InputDelegate; + return _buildListTile( + onTap: () async { + final value = await globalState.showCommonDialog( + child: InputDialog( + title: inputDelegate.title, + value: inputDelegate.value, + suffixText: inputDelegate.suffixText, + ), + ); + inputDelegate.onChanged(value); + }, + ); + } if (delegate is NextDelegate) { final nextDelegate = delegate as NextDelegate; return _buildListTile( @@ -423,5 +514,8 @@ Widget generateListView(List items) { return ListView.builder( itemCount: items.length, itemBuilder: (_, index) => items[index], + padding: const EdgeInsets.only( + bottom: 16, + ), ); } diff --git a/lib/widgets/open_container.dart b/lib/widgets/open_container.dart index 5197d37..fb4ad3d 100644 --- a/lib/widgets/open_container.dart +++ b/lib/widgets/open_container.dart @@ -286,23 +286,6 @@ class _OpenContainerRoute extends ModalRoute { final _FlippableTweenSequence _closedOpacityTween; final _FlippableTweenSequence _openOpacityTween; late _FlippableTweenSequence _colorTween; - - static final TweenSequence _scrimFadeInTween = TweenSequence( - >[ - TweenSequenceItem( - tween: ColorTween(begin: Colors.transparent, end: Colors.black54), - weight: 1 / 5, - ), - TweenSequenceItem( - tween: ConstantTween(Colors.black54), - weight: 4 / 5, - ), - ], - ); - static final Tween _scrimFadeOutTween = ColorTween( - begin: Colors.transparent, - end: Colors.black54, - ); final GlobalKey _openBuilderKey = GlobalKey(); final RectTween _rectTween = RectTween(); @@ -486,20 +469,17 @@ class _OpenContainerRoute extends ModalRoute { closedOpacityTween = _closedOpacityTween; openOpacityTween = _openOpacityTween; colorTween = _colorTween; - scrimTween = _scrimFadeInTween; break; case AnimationStatus.reverse: if (_transitionWasInterrupted) { closedOpacityTween = _closedOpacityTween; openOpacityTween = _openOpacityTween; colorTween = _colorTween; - scrimTween = _scrimFadeInTween; break; } closedOpacityTween = _closedOpacityTween.flipped; openOpacityTween = _openOpacityTween.flipped; colorTween = _colorTween.flipped; - scrimTween = _scrimFadeOutTween; break; case AnimationStatus.completed: assert(false); // Unreachable. @@ -508,74 +488,70 @@ class _OpenContainerRoute extends ModalRoute { assert(colorTween != null); assert(closedOpacityTween != null); assert(openOpacityTween != null); - assert(scrimTween != null); final Rect rect = _rectTween.evaluate(curvedAnimation)!; return SizedBox.expand( - child: Container( - color: scrimTween!.evaluate(curvedAnimation), - child: Align( - alignment: Alignment.topLeft, - child: Transform.translate( - offset: Offset(rect.left, rect.top), - child: SizedBox( - width: rect.width, - height: rect.height, - child: Material( - clipBehavior: Clip.antiAlias, - animationDuration: Duration.zero, - color: colorTween!.evaluate(animation), - child: Stack( - fit: StackFit.passthrough, - children: [ - // Closed child fading out. - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.topLeft, - child: SizedBox( - width: _rectTween.begin!.width, - height: _rectTween.begin!.height, - child: (hideableKey.currentState?.isInTree ?? - false) - ? null - : FadeTransition( - opacity: closedOpacityTween! - .animate(animation), - child: Builder( - key: closedBuilderKey, - builder: (BuildContext context) { - // Use dummy "open container" callback - // since we are in the process of opening. - return closedBuilder( - context, () {}); - }, - ), + child: Align( + alignment: Alignment.topLeft, + child: Transform.translate( + offset: Offset(rect.left, rect.top), + child: SizedBox( + width: rect.width, + height: rect.height, + child: Material( + clipBehavior: Clip.antiAlias, + animationDuration: Duration.zero, + color: colorTween!.evaluate(animation), + child: Stack( + fit: StackFit.passthrough, + children: [ + // Closed child fading out. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.begin!.width, + height: _rectTween.begin!.height, + child: (hideableKey.currentState?.isInTree ?? + false) + ? null + : FadeTransition( + opacity: closedOpacityTween! + .animate(animation), + child: Builder( + key: closedBuilderKey, + builder: (BuildContext context) { + // Use dummy "open container" callback + // since we are in the process of opening. + return closedBuilder( + context, () {}); + }, ), - ), + ), ), + ), - // Open child fading in. - FittedBox( - fit: BoxFit.fitWidth, - alignment: Alignment.topLeft, - child: SizedBox( - width: _rectTween.end!.width, - height: _rectTween.end!.height, - child: FadeTransition( - opacity: - openOpacityTween!.animate(animation), - child: Builder( - key: _openBuilderKey, - builder: (BuildContext context) { - return openBuilder( - context, closeContainer); - }, - ), + // Open child fading in. + FittedBox( + fit: BoxFit.fitWidth, + alignment: Alignment.topLeft, + child: SizedBox( + width: _rectTween.end!.width, + height: _rectTween.end!.height, + child: FadeTransition( + opacity: + openOpacityTween!.animate(animation), + child: Builder( + key: _openBuilderKey, + builder: (BuildContext context) { + return openBuilder( + context, closeContainer); + }, ), ), ), - ], - ), + ), + ], ), ), ), diff --git a/lib/widgets/sheet.dart b/lib/widgets/sheet.dart index 2eccab3..724a574 100644 --- a/lib/widgets/sheet.dart +++ b/lib/widgets/sheet.dart @@ -11,6 +11,7 @@ showExtendPage( required String title, double? extendPageWidth, bool forceNotSide = false, + bool isBlur = true, Widget? action, }) { final NavigatorState navigator = Navigator.of(context); @@ -21,6 +22,17 @@ showExtendPage( ); final isMobile = globalState.appController.appState.viewMode == ViewMode.mobile; + if (isMobile) { + Navigator.of(context).push( + MaterialPageRoute( + builder: (_) => CommonScaffold( + title: title, + body: uniqueBody, + ), + ), + ); + return; + } final isNotSide = isMobile || forceNotSide; navigator.push( ModalSideSheetRoute( @@ -46,7 +58,7 @@ showExtendPage( ); }, constraints: const BoxConstraints(), - filter: filter, + filter: isBlur ? filter : null, ), ); } diff --git a/lib/widgets/tray_container.dart b/lib/widgets/tray_container.dart index d8d34bc..0a06aec 100644 --- a/lib/widgets/tray_container.dart +++ b/lib/widgets/tray_container.dart @@ -74,36 +74,6 @@ class _TrayContainerState extends State with TrayListener { ); menuItems.add(startMenuItem); menuItems.add(MenuItem.separator()); - // for (final group in state.groups) { - // List subMenuItems = []; - // final isCurrentGroup = group.name == state.currentGroupName; - // for (final proxy in group.all) { - // final isCurrentProxy = proxy.name == state.currentProxyName; - // subMenuItems.add( - // MenuItem.checkbox( - // label: proxy.name, - // checked: isCurrentGroup && isCurrentProxy, - // onClick: (_) { - // final config = globalState.appController.config; - // config.currentProfile?.groupName = group.name; - // config.currentProfile?.proxyName = proxy.name; - // config.update(); - // globalState.appController.changeProxy(); - // }), - // ); - // } - // menuItems.add( - // MenuItem.submenu( - // label: group.name, - // submenu: Menu( - // items: subMenuItems, - // ), - // ), - // ); - // } - // if (state.groups.isNotEmpty) { - // menuItems.add(MenuItem.separator()); - // } for (final mode in Mode.values) { menuItems.add( MenuItem.checkbox( @@ -116,7 +86,6 @@ class _TrayContainerState extends State with TrayListener { ); } menuItems.add(MenuItem.separator()); - if (state.isStart) { menuItems.add( MenuItem.checkbox( @@ -142,7 +111,6 @@ class _TrayContainerState extends State with TrayListener { ); menuItems.add(MenuItem.separator()); } - final autoStartMenuItem = MenuItem.checkbox( label: appLocalizations.autoLaunch, onClick: (_) async { diff --git a/lib/widgets/vpn_container.dart b/lib/widgets/vpn_container.dart new file mode 100644 index 0000000..d6c03ee --- /dev/null +++ b/lib/widgets/vpn_container.dart @@ -0,0 +1,58 @@ +import 'package:fl_clash/common/app_localizations.dart'; +import 'package:fl_clash/models/config.dart'; +import 'package:fl_clash/state.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../common/function.dart'; + +class VpnContainer extends StatefulWidget { + final Widget child; + + const VpnContainer({ + super.key, + required this.child, + }); + + @override + State createState() => _VpnContainerState(); +} + +class _VpnContainerState extends State { + Function? vpnTipDebounce; + + showTip() { + vpnTipDebounce ??= debounce(() async { + WidgetsBinding.instance.addPostFrameCallback((_) { + final appState = globalState.appController.appState; + if (appState.isStart) { + globalState.showSnackBar( + context, + message: appLocalizations.vpnTip, + ); + } + }); + }); + vpnTipDebounce!(); + } + + @override + Widget build(BuildContext context) { + return Selector( + selector: (_, config) => VPNState( + accessControl: config.accessControl, + vpnProps: config.vpnProps, + ), + shouldRebuild: (prev,next){ + if(prev != next){ + showTip(); + } + return prev != next; + }, + builder: (_, __, child) { + return child!; + }, + child: widget.child, + ); + } +} diff --git a/lib/widgets/widgets.dart b/lib/widgets/widgets.dart index 2aabbcf..b7a96a0 100644 --- a/lib/widgets/widgets.dart +++ b/lib/widgets/widgets.dart @@ -25,4 +25,6 @@ export 'app_state_container.dart'; export 'text.dart'; export 'connection_item.dart'; export 'builder.dart'; -export 'setting.dart'; \ No newline at end of file +export 'setting.dart'; +export 'vpn_container.dart'; +export 'input.dart'; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 8745e66..48ddff5 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import package_info_plus import path_provider_foundation import screen_retriever import shared_preferences_foundation +import sqflite import tray_manager import url_launcher_macos import window_manager @@ -26,6 +27,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index c187850..f457e60 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -129,6 +129,30 @@ packages: url: "https://pub.dev" source: hosted version: "8.9.2" + cached_network_image: + dependency: "direct main" + description: + name: cached_network_image + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" + url: "https://pub.dev" + source: hosted + version: "3.4.0" + cached_network_image_platform_interface: + dependency: transitive + description: + name: cached_network_image_platform_interface + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" + url: "https://pub.dev" + source: hosted + version: "4.1.1" + cached_network_image_web: + dependency: transitive + description: + name: cached_network_image_web + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" + url: "https://pub.dev" + source: hosted + version: "1.3.0" characters: dependency: transitive description: @@ -197,18 +221,18 @@ packages: dependency: transitive description: name: cross_file - sha256: "55d7b444feb71301ef6b8838dbc1ae02e63dd48c8773f3810ff53bb1e2945b32" + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.4+1" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: ec30d999af904f33454ba22ed9a86162b35e52b44ac4807d1d93c288041d7d27 url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" dart_style: dependency: transitive description: @@ -221,18 +245,18 @@ packages: dependency: "direct main" description: name: dio - sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714 + sha256: "0dfb6b6a1979dac1c1245e17cef824d7b452ea29bd33d3467269f9bef3715fb0" url: "https://pub.dev" source: hosted - version: "5.5.0+1" + version: "5.6.0" dio_web_adapter: dependency: transitive description: name: dio_web_adapter - sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "2.0.0" dynamic_color: dependency: "direct main" description: @@ -261,10 +285,10 @@ packages: dependency: "direct main" description: name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" ffigen: dependency: "direct dev" description: @@ -285,10 +309,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "824f5b9f389bfc4dddac3dea76cd70c51092d9dff0b2ece7ef4f53db8547d258" + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.6" + version: "8.0.7" file_selector_linux: dependency: transitive description: @@ -334,6 +358,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" + url: "https://pub.dev" + source: hosted + version: "3.4.1" flutter_lints: dependency: "direct dev" description: @@ -351,10 +383,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9ee02950848f61c4129af3d6ec84a1cfc0e47931abc746b03e7a3bc3e8ff6eda" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.22" flutter_test: dependency: "direct dev" description: flutter @@ -457,18 +489,18 @@ packages: dependency: transitive description: name: image_picker_android - sha256: a26dc9a03fe042440c1e4be554fb0fceae2bf6d887d7467fc48c688fa4a81889 + sha256: "8c5abf0dcc24fe6e8e0b4a5c0b51a5cf30cefdf6407a3213dae61edc75a70f56" url: "https://pub.dev" source: hosted - version: "0.8.12+7" + version: "0.8.12+12" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "5d6eb13048cd47b60dbf1a5495424dea226c5faf3950e20bf8120a58efb5b5f3" + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.5" image_picker_ios: dependency: transitive description: @@ -665,10 +697,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: b8c0e9afcfd52534f85ec666f3d52156f560b5e6c25b1e3d4fe2087763607926 + sha256: "6ac2913ad98c83f558d2c8a55bc8f511bdcf28b86639701c04b04c16da1e9ee1" url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "5.2.1" nested: dependency: transitive description: @@ -677,6 +709,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + octo_image: + dependency: transitive + description: + name: octo_image + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" package_config: dependency: transitive description: @@ -697,10 +737,10 @@ packages: dependency: transitive description: name: package_info_plus_platform_interface - sha256: f49918f3433a3146047372f9d4f1f847511f2acd5cd030e1f44fe5a50036b70e + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.0.1" path: dependency: "direct main" description: @@ -713,18 +753,18 @@ packages: dependency: "direct main" description: name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "30c5aa827a6ae95ce2853cdc5fe3971daaac00f6f081c419c013f7f57bff2f5e" + sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7" url: "https://pub.dev" source: hosted - version: "2.2.7" + version: "2.2.10" path_provider_foundation: dependency: transitive description: @@ -789,6 +829,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + process_run: + dependency: "direct main" + description: + name: process_run + sha256: c917dfb5f7afad4c7485bc00a4df038621248fce046105020cea276d1a87c820 + url: "https://pub.dev" + source: hosted + version: "1.1.0" provider: dependency: "direct main" description: @@ -844,6 +892,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.3" + rxdart: + dependency: transitive + description: + name: rxdart + sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" + url: "https://pub.dev" + source: hosted + version: "0.28.0" screen_retriever: dependency: transitive description: @@ -856,58 +912,58 @@ packages: dependency: "direct main" description: name: shared_preferences - sha256: d3bbe5553a986e83980916ded2f0b435ef2e1893dfaa29d5a7a790d0eca12180 + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "93d0ec9dd902d85f326068e6a899487d1f65ffcd5798721a95330b26c8131577" + sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "0a8a893bf4fd1152f93fec03a415d11c27c74454d96e2318a7ac38dd18683ab7" + sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.5.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "034650b71e73629ca08a0bd789fd1d83cc63c2d1e405946f7cef7bc37432f93a" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "9aee1089b36bd2aafe06582b7d7817fd317ef05fc30e6ba14bff247d0933042a" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -961,6 +1017,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" + sqflite: + dependency: transitive + description: + name: sqflite + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d + url: "https://pub.dev" + source: hosted + version: "2.3.3+1" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" + url: "https://pub.dev" + source: hosted + version: "2.5.4" stack_trace: dependency: transitive description: @@ -993,6 +1073,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + synchronized: + dependency: transitive + description: + name: synchronized + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + url: "https://pub.dev" + source: hosted + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1045,10 +1133,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac" + sha256: f0c73347dfcfa5b3db8bc06e1502668265d39c08f310c29bff4e28eea9699f79 url: "https://pub.dev" source: hosted - version: "6.3.5" + version: "6.3.9" url_launcher_ios: dependency: transitive description: @@ -1061,10 +1149,10 @@ packages: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: @@ -1085,10 +1173,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.3" url_launcher_windows: dependency: transitive description: @@ -1097,6 +1185,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + uuid: + dependency: transitive + description: + name: uuid + sha256: f33d6bb662f0e4f79dcd7ada2e6170f3b3a2530c28fc41f49a411ddedd576a77 + url: "https://pub.dev" + source: hosted + version: "4.5.0" vector_math: dependency: transitive description: @@ -1157,18 +1253,18 @@ packages: dependency: "direct main" description: name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + sha256: "68d1e89a91ed61ad9c370f9f8b6effed9ae5e0ede22a270bdfa6daf79fc2290a" url: "https://pub.dev" source: hosted - version: "5.5.1" + version: "5.5.4" win32_registry: dependency: "direct main" description: name: win32_registry - sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" url: "https://pub.dev" source: hosted - version: "1.1.3" + version: "1.1.4" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index b57e0fd..ff96e1c 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.56+202408261 +version: 0.8.57+202409021 environment: sdk: '>=3.1.0 <4.0.0' flutter: 3.22.3 @@ -45,6 +45,8 @@ dependencies: archive: ^3.6.1 lpinyin: ^2.0.3 emoji_regex: ^0.0.5 + process_run: ^1.1.0 + cached_network_image: ^3.4.0 dev_dependencies: flutter_test: sdk: flutter @@ -66,6 +68,9 @@ flutter: - family: Twemoji fonts: - asset: assets/fonts/Twemoji.Mozilla.ttf + - family: Icons + fonts: + - asset: assets/fonts/Icons.ttf ffigen: name: "ClashFFI" output: 'lib/clash/generated/clash_ffi.dart' diff --git a/test/command_test.dart b/test/command_test.dart index 0267bc8..be6097c 100644 --- a/test/command_test.dart +++ b/test/command_test.dart @@ -6,11 +6,12 @@ import 'package:fl_clash/common/other.dart'; import 'package:lpinyin/lpinyin.dart'; void main() { - print(PinyinHelper.getPinyin("ABC")); - print(PinyinHelper.getPinyin("阿里巴巴")); + final res = const [ + "https://doh.pub/dns-query", + "https://dns.alidns.com/dns-query" + ]; - print('a'.compareTo('B')); - print('A'.compareTo('B')); + List.from(res)..remove("https://doh.pub/dns-query"); } startService() async { diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt index a447a59..cb3721c 100644 --- a/windows/CMakeLists.txt +++ b/windows/CMakeLists.txt @@ -93,6 +93,9 @@ set(CLASH_DIR "../libclash/windows") install(FILES "${CLASH_DIR}/libclash.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime) +install(FILES "EnableLoopback.exe" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + if(PLUGIN_BUNDLED_LIBRARIES) install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" diff --git a/windows/EnableLoopback.exe b/windows/EnableLoopback.exe new file mode 100644 index 0000000000000000000000000000000000000000..5389d7e527ded80d9f8a96173b6b19701593a732 GIT binary patch literal 97536 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 literal 0 HcmV?d00001