Files
FlClash/lib/models/config.dart
T

563 lines
18 KiB
Dart
Raw Normal View History

2024-09-26 14:29:04 +08:00
import 'package:fl_clash/common/common.dart';
import 'package:fl_clash/enum/enum.dart';
import 'package:fl_clash/ui/color.dart';
import 'package:flutter/services.dart';
import 'package:material_ui/material_ui.dart';
2024-06-03 11:24:41 +08:00
import 'package:freezed_annotation/freezed_annotation.dart';
2024-04-30 23:38:49 +08:00
import 'models.dart';
2024-06-03 11:24:41 +08:00
part 'generated/config.freezed.dart';
2024-11-09 20:17:57 +08:00
part 'generated/config.g.dart';
2024-06-03 11:24:41 +08:00
2025-02-09 18:39:38 +08:00
const defaultBypassDomain = [
2025-06-07 01:48:34 +08:00
'*zhihu.com',
'*zhimg.com',
'*jd.com',
'100ime-iat-api.xfyun.cn',
'*360buyimg.com',
'localhost',
'*.local',
'127.*',
'10.*',
'172.16.*',
'172.17.*',
'172.18.*',
'172.19.*',
'172.2*',
'172.30.*',
'172.31.*',
2025-07-31 17:09:18 +08:00
'192.168.*',
2025-02-09 18:39:38 +08:00
];
const defaultUserAgents = ['clash-verge/v2.4.2', 'ClashforWindows/0.19.23'];
const defaultFilters = [
Filter(
label: 'Hong Kong',
regex: r'(?i)🇭🇰|港|(?<![a-z])hk(?![a-z])|hong\s*kong',
),
Filter(
label: 'Taiwan',
regex: r'(?i)🇹🇼|台湾|台灣|台北|新北|彰化|(?<![a-z])tw(?![a-z])|taiwan',
),
Filter(
label: 'Japan',
regex: r'(?i)🇯🇵|日本|东京|東京|大阪|埼玉|(?<![a-z])jp(?![a-z])|japan|tokyo|osaka',
),
Filter(
label: 'Singapore',
regex: r'(?i)🇸🇬|新加坡|狮城|獅城|(?<![a-z])sg(?![a-z])|singapore',
),
Filter(
label: 'United States',
regex:
r'(?i)🇺🇸|美国|美國|美西|美东|美東|洛杉矶|圣何塞|西雅图|硅谷|纽约'
r'|(?<![a-z])usa?(?![a-z])|united\s*states',
),
Filter(
label: 'South Korea',
regex: r'(?i)🇰🇷|韩国|韓國|首尔|首爾|(?<![a-z])kr(?![a-z])|korea|seoul',
),
Filter(
label: 'United Kingdom',
regex:
r'(?i)🇬🇧|英国|英國|伦敦|倫敦|(?<![a-z])(uk|gb)(?![a-z])'
r'|united\s*kingdom|britain|london',
),
Filter(
label: 'Remaining Traffic',
regex: r'(?i)剩余|剩餘|已用|流量\s*[::]|traffic|remaining',
),
Filter(label: 'Expiry Date', regex: r'(?i)到期|过期|過期|有效期|expir'),
Filter(label: 'Traffic Reset', regex: r'(?i)重置|reset'),
Filter(
label: 'Website & Notices',
regex:
r'(?i)官网|官網|网址|網址|域名|公告|套餐|客服|群组|群組|频道|頻道'
r'|website|telegram|(?<![a-z])tg(?![a-z])',
),
];
2025-02-09 18:39:38 +08:00
const defaultAppSettingProps = AppSettingProps();
const defaultVpnProps = VpnProps();
const defaultAuthenticationProps = AuthenticationProps();
2025-02-09 18:39:38 +08:00
const defaultNetworkProps = NetworkProps();
2025-12-16 11:23:09 +08:00
const defaultProxiesStyleProps = ProxiesStyleProps();
2025-02-09 18:39:38 +08:00
const defaultWindowProps = WindowProps();
2025-12-16 11:23:09 +08:00
const defaultAccessControlProps = AccessControlProps();
2026-02-02 10:14:13 +08:00
const defaultThemeProps = ThemeProps(primaryColor: defaultPrimaryColor);
2024-09-26 14:29:04 +08:00
HotKeyAction _defaultHotKeyAction(HotAction action, PhysicalKeyboardKey key) {
return HotKeyAction(
action: action,
key: key.usbHidUsage,
modifiers: const {KeyboardModifier.control, KeyboardModifier.alt},
);
}
// Windows reports AltGr as Ctrl+Alt, and every letter is an AltGr character
// on some layout (@ on German Q, ś on Polish S), so a default there would fire
// while the user types.
final List<HotKeyAction> defaultHotKeyActions = system.isWindows
? const []
: [
_defaultHotKeyAction(HotAction.view, PhysicalKeyboardKey.keyV),
_defaultHotKeyAction(HotAction.start, PhysicalKeyboardKey.keyS),
_defaultHotKeyAction(HotAction.mode, PhysicalKeyboardKey.keyM),
_defaultHotKeyAction(HotAction.proxy, PhysicalKeyboardKey.keyP),
_defaultHotKeyAction(HotAction.delayTest, PhysicalKeyboardKey.keyD),
];
2024-12-09 01:40:39 +08:00
const List<DashboardWidget> defaultDashboardWidgets = [
DashboardWidget.currentOutbound,
2024-12-09 01:40:39 +08:00
DashboardWidget.networkSpeed,
DashboardWidget.outboundMode,
DashboardWidget.vpnButton,
DashboardWidget.networkTakeover,
2024-12-09 01:40:39 +08:00
DashboardWidget.intranetIp,
DashboardWidget.serviceStatus,
DashboardWidget.profiles,
DashboardWidget.proxyGroups,
DashboardWidget.connectionStats,
2024-12-09 01:40:39 +08:00
];
const _legacyDefaultDashboardWidgets = {
'networkSpeed',
'systemProxyButton',
'tunButton',
'outboundMode',
'networkDetection',
'trafficUsage',
'intranetIp',
};
/// The run time has no replacement: it moved onto the current outbound card,
/// as did the outbound IP, whose card became the intranet one.
const _legacyDashboardWidgets = {
'networkDetection': DashboardWidget.intranetIp,
'outboundModeV2': DashboardWidget.outboundMode,
'systemProxyButton': DashboardWidget.networkTakeover,
'tunButton': DashboardWidget.networkTakeover,
'connections': DashboardWidget.connectionStats,
'requests': DashboardWidget.connectionStats,
'dnsQueries': DashboardWidget.connectionStats,
'trafficUsage': DashboardWidget.networkSpeed,
};
const _retiredDashboardWidgets = {
'runTime',
'overrideDnsButton',
'overrideNtpButton',
};
/// The current outbound card cannot be removed: a list without it predates it.
List<DashboardWidget> dashboardWidgetsFromJson(
2025-02-09 18:39:38 +08:00
List<dynamic>? dashboardWidgets,
) {
if (dashboardWidgets == null) {
return defaultDashboardWidgets;
}
final byName = DashboardWidget.values.asNameMap();
if (dashboardWidgets.contains(DashboardWidget.currentOutbound.name)) {
return dashboardWidgets
.map((e) => _legacyDashboardWidgets[e] ?? byName[e])
.nonNulls
.toSet()
.toList();
}
final names = {
for (final name in dashboardWidgets)
name == 'outboundModeV2' ? DashboardWidget.outboundMode.name : name,
};
if (names.length == _legacyDefaultDashboardWidgets.length &&
names.containsAll(_legacyDefaultDashboardWidgets)) {
return defaultDashboardWidgets;
}
final kept = dashboardWidgets
.map((e) => _legacyDashboardWidgets[e] ?? byName[e])
.nonNulls;
if (kept.isEmpty &&
!dashboardWidgets.every(_retiredDashboardWidgets.contains)) {
return defaultDashboardWidgets;
}
return {DashboardWidget.currentOutbound, ...kept}.toList();
2024-12-09 01:40:39 +08:00
}
Object? _readSidebarExpanded(Map<dynamic, dynamic> json, String key) {
return json.containsKey(key) ? json[key] : json['showLabel'];
}
Object? _readTabAnimation(Map<dynamic, dynamic> json, String key) {
if (json.containsKey(key)) {
return json[key];
}
return json['isAnimateToPage'] == false ? TabAnimation.fade.name : null;
}
Object? _readUserAgents(Map<dynamic, dynamic> json, String key) {
if (json.containsKey(key)) {
return json[key];
}
final legacy = json['customUserAgent'];
if (legacy is! String) {
return null;
}
final custom = legacy.trim();
if (custom.isEmpty || defaultUserAgents.contains(custom)) {
return null;
}
return [...defaultUserAgents, custom];
}
@freezed
abstract class Filter with _$Filter {
const factory Filter({required String label, required String regex}) =
_Filter;
factory Filter.fromJson(Map<String, Object?> json) => _$FilterFromJson(json);
}
2024-09-26 14:29:04 +08:00
@freezed
2025-07-31 17:09:18 +08:00
abstract class AppSettingProps with _$AppSettingProps {
2025-02-09 18:39:38 +08:00
const factory AppSettingProps({
2024-09-26 14:29:04 +08:00
String? locale,
2024-12-09 01:40:39 +08:00
@Default(defaultDashboardWidgets)
@JsonKey(fromJson: dashboardWidgetsFromJson)
2024-12-09 01:40:39 +08:00
List<DashboardWidget> dashboardWidgets,
2025-01-13 19:08:17 +08:00
@Default(false) bool onlyStatisticsProxy,
@Default(true) bool showNotificationStopAction,
2024-09-26 14:29:04 +08:00
@Default(false) bool autoLaunch,
@Default(false) bool silentLaunch,
@Default(false) bool autoRun,
@Default(false) bool openLogs,
2024-09-26 14:29:04 +08:00
@Default(true) bool closeConnections,
@Default(defaultTestUrl) String testUrl,
@Default(TabAnimation.slide)
@JsonKey(readValue: _readTabAnimation)
TabAnimation tabAnimation,
@Default(true) bool floatingNavigationBar,
2024-09-26 14:29:04 +08:00
@Default(true) bool autoCheckUpdate,
@Default(true)
@JsonKey(readValue: _readSidebarExpanded)
bool sidebarExpanded,
@Default(0) int acceptedDisclaimerVersion,
2024-09-26 14:29:04 +08:00
@Default(true) bool minimizeOnExit,
@Default(false) bool hidden,
@Default(false) bool developerMode,
2025-12-16 11:23:09 +08:00
@Default(RestoreStrategy.compatible) RestoreStrategy restoreStrategy,
2025-10-14 15:13:52 +08:00
@Default(true) bool showTrayTitle,
@Default(true) bool checkCertificate,
@Default(defaultUserAgents)
@JsonKey(readValue: _readUserAgents)
List<String> userAgents,
@Default(defaultFilters) List<Filter> filters,
@Default(false) bool hideIp,
@Default(false) bool editorLineWrap,
@Default(EditorFontSize.standard) EditorFontSize editorFontSize,
@Default([]) List<String> serviceOrder,
@Default([]) List<String> disabledServices,
@Default({}) Map<String, Map<String, double>> tableColumnWidths,
2025-02-09 18:39:38 +08:00
}) = _AppSettingProps;
2024-09-26 14:29:04 +08:00
2025-02-09 18:39:38 +08:00
factory AppSettingProps.fromJson(Map<String, Object?> json) =>
_$AppSettingPropsFromJson(json);
2024-09-26 14:29:04 +08:00
2025-02-09 18:39:38 +08:00
factory AppSettingProps.safeFromJson(Map<String, Object?>? json) {
if (json == null) {
2026-02-02 10:14:13 +08:00
return defaultAppSettingProps;
}
return decodeSalvaging('app settings', json, AppSettingProps.fromJson);
2024-09-26 14:29:04 +08:00
}
}
2024-06-03 11:24:41 +08:00
@freezed
2025-12-16 11:23:09 +08:00
abstract class AccessControlProps with _$AccessControlProps {
const factory AccessControlProps({
2025-02-09 18:39:38 +08:00
@Default(false) bool enable,
2024-06-03 11:24:41 +08:00
@Default(AccessControlMode.rejectSelected) AccessControlMode mode,
@Default([]) List<String> acceptList,
@Default([]) List<String> rejectList,
2024-08-05 19:25:35 +08:00
@Default(AccessSortType.none) AccessSortType sort,
2024-06-03 11:24:41 +08:00
@Default(true) bool isFilterSystemApp,
@Default(true) bool isFilterNonInternetApp,
2025-12-16 11:23:09 +08:00
}) = _AccessControlProps;
2024-06-03 11:24:41 +08:00
2025-12-16 11:23:09 +08:00
factory AccessControlProps.fromJson(Map<String, Object?> json) =>
_$AccessControlPropsFromJson(json);
2024-04-30 23:38:49 +08:00
}
2025-12-16 11:23:09 +08:00
extension AccessControlPropsExt on AccessControlProps {
bool get hasPackages => acceptList.isNotEmpty || rejectList.isNotEmpty;
2024-08-05 19:25:35 +08:00
List<String> get currentList => switch (mode) {
2025-07-31 17:09:18 +08:00
AccessControlMode.acceptSelected => acceptList,
AccessControlMode.rejectSelected => rejectList,
};
2025-10-14 15:13:52 +08:00
2025-12-16 11:23:09 +08:00
AccessControlProps copyWithNewList(List<String> value) => switch (mode) {
2025-10-14 15:13:52 +08:00
AccessControlMode.acceptSelected => copyWith(acceptList: value),
AccessControlMode.rejectSelected => copyWith(rejectList: value),
};
2024-08-05 19:25:35 +08:00
}
2024-07-15 22:06:09 +08:00
@freezed
2025-07-31 17:09:18 +08:00
abstract class WindowProps with _$WindowProps {
2024-07-15 22:06:09 +08:00
const factory WindowProps({
2025-10-14 15:13:52 +08:00
@Default(0) double width,
@Default(0) double height,
2024-07-15 22:06:09 +08:00
double? top,
double? left,
double? scale,
2024-07-15 22:06:09 +08:00
}) = _WindowProps;
factory WindowProps.fromJson(Map<String, Object?>? json) =>
2024-08-15 16:18:00 +08:00
json == null ? const WindowProps() : _$WindowPropsFromJson(json);
2024-07-15 22:06:09 +08:00
}
/// Four dashboard columns at full density beside the expanded sidebar, with
/// room for the Windows and Linux title bar.
const defaultWindowSize = Size(1280, 880);
2025-10-14 15:13:52 +08:00
extension WindowPropsExt on WindowProps {
Size? get savedSize {
final size = Size(width, height);
return size.isEmpty ? null : size;
}
2025-10-14 15:13:52 +08:00
Size get size => savedSize ?? defaultWindowSize;
2025-10-14 15:13:52 +08:00
}
2024-08-15 16:18:00 +08:00
@freezed
2025-07-31 17:09:18 +08:00
abstract class VpnProps with _$VpnProps {
2024-08-15 16:18:00 +08:00
const factory VpnProps({
@Default(true) bool enable,
2024-09-20 14:32:57 +08:00
@Default(true) bool systemProxy,
@Default(false) bool ipv6,
2024-08-15 16:18:00 +08:00
@Default(true) bool allowBypass,
2025-07-31 17:09:18 +08:00
@Default(false) bool dnsHijacking,
2025-12-16 11:23:09 +08:00
@Default(defaultAccessControlProps) AccessControlProps accessControlProps,
2024-08-15 16:18:00 +08:00
}) = _VpnProps;
factory VpnProps.fromJson(Map<String, Object?>? json) =>
2025-04-09 16:46:14 +08:00
json == null ? defaultVpnProps : _$VpnPropsFromJson(json);
2024-08-15 16:18:00 +08:00
}
@freezed
abstract class AuthenticationProps with _$AuthenticationProps {
const factory AuthenticationProps({
@Default(false) bool enable,
@Default('') String username,
@Default('') String password,
}) = _AuthenticationProps;
factory AuthenticationProps.fromJson(Map<String, Object?>? json) =>
json == null
? defaultAuthenticationProps
: _$AuthenticationPropsFromJson(json);
}
extension AuthenticationPropsExt on AuthenticationProps {
List<String> get credentials =>
enable && username.isNotEmpty ? ['$username:$password'] : [];
}
2024-08-15 16:18:00 +08:00
@freezed
2025-07-31 17:09:18 +08:00
abstract class NetworkProps with _$NetworkProps {
2024-11-09 20:17:57 +08:00
const factory NetworkProps({
2024-08-15 16:18:00 +08:00
@Default(true) bool systemProxy,
2024-11-09 20:17:57 +08:00
@Default(defaultBypassDomain) List<String> bypassDomain,
@Default(true) bool bypassPrivateRoute,
2025-05-02 02:24:12 +08:00
@Default(true) bool autoSetSystemDns,
2025-09-27 23:39:20 +08:00
@Default(false) bool appendSystemDns,
@Default(defaultAuthenticationProps) AuthenticationProps authentication,
2024-11-09 20:17:57 +08:00
}) = _NetworkProps;
2024-08-15 16:18:00 +08:00
2024-11-09 20:17:57 +08:00
factory NetworkProps.fromJson(Map<String, Object?>? json) =>
json == null ? const NetworkProps() : _$NetworkPropsFromJson(json);
2024-08-15 16:18:00 +08:00
}
2024-07-15 22:06:09 +08:00
/// Reads the styles named `standard`, `icon` and `none` before they became
/// [ProxiesIconStyle.filled], [ProxiesIconStyle.plain] and
/// [ProxiesIconStyle.hidden].
ProxiesIconStyle proxiesIconStyleSafeFromJson(Object? iconStyle) {
return switch (iconStyle) {
'filled' || 'standard' => ProxiesIconStyle.filled,
'plain' || 'icon' => ProxiesIconStyle.plain,
'hidden' || 'none' => ProxiesIconStyle.hidden,
_ => ProxiesIconStyle.filled,
};
}
ProxyCardType proxyCardTypeSafeFromJson(Object? cardType) {
return switch (cardType) {
'loose' || 'expand' => ProxyCardType.loose,
'standard' || 'shrink' => ProxyCardType.standard,
'compact' || 'min' => ProxyCardType.compact,
_ => ProxyCardType.standard,
};
}
2024-09-05 08:59:45 +08:00
@freezed
2025-12-16 11:23:09 +08:00
abstract class ProxiesStyleProps with _$ProxiesStyleProps {
const factory ProxiesStyleProps({
@Default(ProxiesType.list) ProxiesType type,
2024-09-26 14:29:04 +08:00
@Default(ProxiesSortType.none) ProxiesSortType sortType,
@Default(ProxiesLayout.standard) ProxiesLayout layout,
@Default(ProxiesIconStyle.filled)
@JsonKey(fromJson: proxiesIconStyleSafeFromJson)
ProxiesIconStyle iconStyle,
@Default(ProxyCardType.standard)
@JsonKey(fromJson: proxyCardTypeSafeFromJson)
ProxyCardType cardType,
@Default(false) bool hideTimeoutProxies,
2025-12-16 11:23:09 +08:00
}) = _ProxiesStyleProps;
2024-09-26 14:29:04 +08:00
2025-12-16 11:23:09 +08:00
factory ProxiesStyleProps.fromJson(Map<String, Object?>? json) => json == null
? defaultProxiesStyleProps
: _$ProxiesStylePropsFromJson(json);
2024-09-05 08:59:45 +08:00
}
@freezed
2025-07-31 17:09:18 +08:00
abstract class TextScale with _$TextScale {
const factory TextScale({
2025-06-07 01:48:34 +08:00
@Default(false) bool enable,
@Default(1.0) double scale,
}) = _TextScale;
factory TextScale.fromJson(Map<String, Object?> json) =>
_$TextScaleFromJson(json);
}
// Older configs name one of Material's nine scheme variants instead.
Object? _readColorStyle(Map<dynamic, dynamic> json, String key) {
if (json.containsKey(key)) {
return json[key];
}
return switch (json['schemeVariant']) {
'monochrome' => ColorStyle.monochrome.name,
'tonalSpot' || 'neutral' => ColorStyle.soft.name,
_ => null,
};
}
// Older configs say whether glass is on.
Object? _readLiquidGlass(Map<dynamic, dynamic> json, String key) {
return switch (json[key]) {
true => LiquidGlassMode.standard.name,
false => LiquidGlassMode.none.name,
final value => value,
};
}
2024-10-26 16:52:10 +08:00
@freezed
2025-07-31 17:09:18 +08:00
abstract class ThemeProps with _$ThemeProps {
2024-10-26 16:52:10 +08:00
const factory ThemeProps({
int? primaryColor,
2025-04-09 16:46:14 +08:00
@Default(defaultPrimaryColors) List<int> primaryColors,
@Default(ThemeMode.system) ThemeMode themeMode,
@Default(ColorStyle.vivid)
@JsonKey(readValue: _readColorStyle)
ColorStyle colorStyle,
2025-03-05 16:08:50 +08:00
@Default(false) bool pureBlack,
@Default(true) bool sidebarBlur,
@Default(LiquidGlassMode.none)
@JsonKey(readValue: _readLiquidGlass)
LiquidGlassMode liquidGlass,
@Default(TextScale()) TextScale textScale,
// `fontFamily` held the names of a removed enum, which old configs may
// still carry.
@JsonKey(name: 'systemFontFamily') String? fontFamily,
2024-10-26 16:52:10 +08:00
}) = _ThemeProps;
factory ThemeProps.fromJson(Map<String, Object?> json) =>
_$ThemePropsFromJson(json);
factory ThemeProps.safeFromJson(Map<String, Object?>? json) {
if (json == null) {
return defaultThemeProps;
}
return decodeSalvaging('theme settings', json, ThemeProps.fromJson);
}
2024-10-26 16:52:10 +08:00
}
2025-02-09 18:39:38 +08:00
@freezed
2025-07-31 17:09:18 +08:00
abstract class Config with _$Config {
2025-02-09 18:39:38 +08:00
const factory Config({
2025-12-16 11:23:09 +08:00
int? currentProfileId,
@Default([]) List<HotKeyAction> hotKeyActions,
2025-02-09 18:39:38 +08:00
@JsonKey(fromJson: AppSettingProps.safeFromJson)
@Default(defaultAppSettingProps)
2025-12-16 11:23:09 +08:00
AppSettingProps appSettingProps,
DAVProps? davProps,
2025-02-09 18:39:38 +08:00
@Default(defaultNetworkProps) NetworkProps networkProps,
@Default(defaultVpnProps) VpnProps vpnProps,
@JsonKey(fromJson: ThemeProps.safeFromJson) required ThemeProps themeProps,
2025-12-16 11:23:09 +08:00
@Default(defaultProxiesStyleProps) ProxiesStyleProps proxiesStyleProps,
// A new key opens every window once at defaultWindowSize.
@Default(defaultWindowProps)
@JsonKey(name: 'windowGeometry')
WindowProps windowProps,
2026-02-02 10:14:13 +08:00
@Default(defaultClashConfig) PatchClashConfig patchClashConfig,
@Default([]) List<String> excludeSSIDs,
2025-02-09 18:39:38 +08:00
}) = _Config;
factory Config.fromJson(Map<String, Object?> json) =>
decodeSalvaging('config', json, Config.strictFromJson);
factory Config.strictFromJson(Map<String, Object?> json) =>
_withLegacyOverrideDns(
_$ConfigFromJson(_withLegacyRouteMode(json)),
json,
);
2025-02-09 18:39:38 +08:00
2025-12-16 11:23:09 +08:00
factory Config.realFromJson(Map<String, Object?>? json) {
if (json == null) {
return Config(
themeProps: defaultThemeProps,
hotKeyActions: defaultHotKeyActions,
);
2025-12-16 11:23:09 +08:00
}
return Config.fromJson(json);
}
}
const _legacyOverrideDnsKey = 'overrideDns';
/// An off `overrideDns` left a profile's own DNS section alone.
Config _withLegacyOverrideDns(Config config, Map<String, Object?> json) {
if (!json.containsKey(_legacyOverrideDnsKey)) {
return config;
2024-04-30 23:38:49 +08:00
}
final keys = json[_legacyOverrideDnsKey] == true
? config.patchClashConfig.dnsOverrideKeys.intersection(
DnsOverrideKey.normalProfileKeys,
)
: <DnsOverrideKey>{};
return config.copyWith.patchClashConfig(dnsOverrideKeys: keys);
}
Map<String, Object?> _withLegacyRouteMode(Map<String, Object?> json) {
final network = json['networkProps'];
if (network is! Map ||
!network.containsKey('routeMode') ||
network.containsKey('bypassPrivateRoute')) {
return json;
}
final bypassPrivate = network['routeMode'] == 'bypassPrivate';
final clash = json['patchClashConfig'];
final tun = clash is Map ? clash['tun'] : null;
return {
...json,
'networkProps': {
...Map<String, Object?>.from(network),
'bypassPrivateRoute': bypassPrivate,
},
if (bypassPrivate && clash is Map && tun is Map)
'patchClashConfig': {
...Map<String, Object?>.from(clash),
'tun': {...Map<String, Object?>.from(tun), 'route-address': <String>[]},
},
};
2024-04-30 23:38:49 +08:00
}