2024-09-26 14:29:04 +08:00
|
|
|
|
import 'package:fl_clash/common/common.dart';
|
|
|
|
|
|
import 'package:fl_clash/enum/enum.dart';
|
2026-10-16 22:42:17 +08:00
|
|
|
|
import 'package:fl_clash/ui/color.dart';
|
2026-09-25 03:56:59 +08:00
|
|
|
|
import 'package:flutter/services.dart';
|
2026-09-05 20:09:39 +08:00
|
|
|
|
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
|
|
|
|
];
|
|
|
|
|
|
|
2026-09-25 03:56:59 +08:00
|
|
|
|
const defaultUserAgents = ['clash-verge/v2.4.2', 'ClashforWindows/0.19.23'];
|
|
|
|
|
|
|
2026-10-08 17:58:43 +08:00
|
|
|
|
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();
|
2026-09-05 20:09:39 +08:00
|
|
|
|
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
|
|
|
|
|
2026-09-25 03:56:59 +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 = [
|
2026-10-16 10:31:15 +08:00
|
|
|
|
DashboardWidget.currentOutbound,
|
2024-12-09 01:40:39 +08:00
|
|
|
|
DashboardWidget.networkSpeed,
|
|
|
|
|
|
DashboardWidget.outboundMode,
|
2026-10-16 10:31:15 +08:00
|
|
|
|
DashboardWidget.vpnButton,
|
|
|
|
|
|
DashboardWidget.networkTakeover,
|
2024-12-09 01:40:39 +08:00
|
|
|
|
DashboardWidget.intranetIp,
|
2026-10-16 10:31:15 +08:00
|
|
|
|
DashboardWidget.serviceStatus,
|
|
|
|
|
|
DashboardWidget.profiles,
|
|
|
|
|
|
DashboardWidget.proxyGroups,
|
|
|
|
|
|
DashboardWidget.connectionStats,
|
2024-12-09 01:40:39 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2026-10-16 10:31:15 +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,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-16 22:42:17 +08:00
|
|
|
|
const _retiredDashboardWidgets = {
|
|
|
|
|
|
'runTime',
|
|
|
|
|
|
'overrideDnsButton',
|
|
|
|
|
|
'overrideNtpButton',
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-10-16 10:31:15 +08:00
|
|
|
|
/// The current outbound card cannot be removed: a list without it predates it.
|
2026-10-08 17:58:43 +08:00
|
|
|
|
List<DashboardWidget> dashboardWidgetsFromJson(
|
2025-02-09 18:39:38 +08:00
|
|
|
|
List<dynamic>? dashboardWidgets,
|
|
|
|
|
|
) {
|
2026-10-16 10:31:15 +08:00
|
|
|
|
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;
|
2026-10-16 22:42:17 +08:00
|
|
|
|
if (kept.isEmpty &&
|
|
|
|
|
|
!dashboardWidgets.every(_retiredDashboardWidgets.contains)) {
|
2026-10-16 10:31:15 +08:00
|
|
|
|
return defaultDashboardWidgets;
|
|
|
|
|
|
}
|
|
|
|
|
|
return {DashboardWidget.currentOutbound, ...kept}.toList();
|
2024-12-09 01:40:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 03:56:59 +08:00
|
|
|
|
Object? _readSidebarExpanded(Map<dynamic, dynamic> json, String key) {
|
|
|
|
|
|
return json.containsKey(key) ? json[key] : json['showLabel'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 03:57:04 +08:00
|
|
|
|
Object? _readTabAnimation(Map<dynamic, dynamic> json, String key) {
|
|
|
|
|
|
if (json.containsKey(key)) {
|
|
|
|
|
|
return json[key];
|
|
|
|
|
|
}
|
|
|
|
|
|
return json['isAnimateToPage'] == false ? TabAnimation.fade.name : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-25 03:56:59 +08:00
|
|
|
|
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];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-08 17:58:43 +08:00
|
|
|
|
@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)
|
2026-10-08 17:58:43 +08:00
|
|
|
|
@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,
|
2026-09-05 20:09:39 +08:00
|
|
|
|
@Default(true) bool showNotificationStopAction,
|
2024-09-26 14:29:04 +08:00
|
|
|
|
@Default(false) bool autoLaunch,
|
|
|
|
|
|
@Default(false) bool silentLaunch,
|
|
|
|
|
|
@Default(false) bool autoRun,
|
2025-04-18 17:50:46 +08:00
|
|
|
|
@Default(false) bool openLogs,
|
2024-09-26 14:29:04 +08:00
|
|
|
|
@Default(true) bool closeConnections,
|
|
|
|
|
|
@Default(defaultTestUrl) String testUrl,
|
2026-09-25 03:57:04 +08:00
|
|
|
|
@Default(TabAnimation.slide)
|
|
|
|
|
|
@JsonKey(readValue: _readTabAnimation)
|
|
|
|
|
|
TabAnimation tabAnimation,
|
|
|
|
|
|
@Default(true) bool floatingNavigationBar,
|
2024-09-26 14:29:04 +08:00
|
|
|
|
@Default(true) bool autoCheckUpdate,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@Default(true)
|
|
|
|
|
|
@JsonKey(readValue: _readSidebarExpanded)
|
|
|
|
|
|
bool sidebarExpanded,
|
2026-10-08 17:58:43 +08:00
|
|
|
|
@Default(0) int acceptedDisclaimerVersion,
|
2024-09-26 14:29:04 +08:00
|
|
|
|
@Default(true) bool minimizeOnExit,
|
|
|
|
|
|
@Default(false) bool hidden,
|
2025-04-18 17:50:46 +08:00
|
|
|
|
@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,
|
2026-09-05 20:09:39 +08:00
|
|
|
|
@Default(true) bool checkCertificate,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@Default(defaultUserAgents)
|
|
|
|
|
|
@JsonKey(readValue: _readUserAgents)
|
|
|
|
|
|
List<String> userAgents,
|
2026-10-08 17:58:43 +08:00
|
|
|
|
@Default(defaultFilters) List<Filter> filters,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@Default(false) bool hideIp,
|
|
|
|
|
|
@Default(false) bool editorLineWrap,
|
2026-09-25 03:57:05 +08:00
|
|
|
|
@Default(EditorFontSize.standard) EditorFontSize editorFontSize,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@Default([]) List<String> serviceOrder,
|
|
|
|
|
|
@Default([]) List<String> disabledServices,
|
2026-10-16 10:31:15 +08:00
|
|
|
|
@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) {
|
2026-09-05 20:09:39 +08:00
|
|
|
|
if (json == null) {
|
2026-02-02 10:14:13 +08:00
|
|
|
|
return defaultAppSettingProps;
|
|
|
|
|
|
}
|
2026-10-08 17:58:43 +08:00
|
|
|
|
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,
|
2025-04-18 17:50:46 +08:00
|
|
|
|
@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 {
|
2026-10-08 17:58:43 +08:00
|
|
|
|
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,
|
2026-10-08 17:58:43 +08:00
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-16 10:31:15 +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 {
|
2026-10-16 10:31:15 +08:00
|
|
|
|
Size? get savedSize {
|
|
|
|
|
|
final size = Size(width, height);
|
|
|
|
|
|
return size.isEmpty ? null : size;
|
|
|
|
|
|
}
|
2025-10-14 15:13:52 +08:00
|
|
|
|
|
2026-10-16 10:31:15 +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
|
|
|
|
}
|
|
|
|
|
|
|
2026-09-05 20:09:39 +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,
|
2026-10-08 17:58:43 +08:00
|
|
|
|
@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,
|
2026-09-05 20:09:39 +08:00
|
|
|
|
@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
|
|
|
|
|
2026-09-25 03:56:59 +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,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-16 10:31:15 +08:00
|
|
|
|
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({
|
2026-10-16 10:31:15 +08:00
|
|
|
|
@Default(ProxiesType.list) ProxiesType type,
|
2024-09-26 14:29:04 +08:00
|
|
|
|
@Default(ProxiesSortType.none) ProxiesSortType sortType,
|
|
|
|
|
|
@Default(ProxiesLayout.standard) ProxiesLayout layout,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@Default(ProxiesIconStyle.filled)
|
|
|
|
|
|
@JsonKey(fromJson: proxiesIconStyleSafeFromJson)
|
|
|
|
|
|
ProxiesIconStyle iconStyle,
|
2026-10-16 10:31:15 +08:00
|
|
|
|
@Default(ProxyCardType.standard)
|
|
|
|
|
|
@JsonKey(fromJson: proxyCardTypeSafeFromJson)
|
|
|
|
|
|
ProxyCardType cardType,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@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
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-18 17:50:46 +08:00
|
|
|
|
@freezed
|
2025-07-31 17:09:18 +08:00
|
|
|
|
abstract class TextScale with _$TextScale {
|
2025-04-18 17:50:46 +08:00
|
|
|
|
const factory TextScale({
|
2025-06-07 01:48:34 +08:00
|
|
|
|
@Default(false) bool enable,
|
|
|
|
|
|
@Default(1.0) double scale,
|
2025-04-18 17:50:46 +08:00
|
|
|
|
}) = _TextScale;
|
|
|
|
|
|
|
|
|
|
|
|
factory TextScale.fromJson(Map<String, Object?> json) =>
|
|
|
|
|
|
_$TextScaleFromJson(json);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-10-16 22:42:17 +08:00
|
|
|
|
// 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({
|
2025-04-18 17:50:46 +08:00
|
|
|
|
int? primaryColor,
|
2025-04-09 16:46:14 +08:00
|
|
|
|
@Default(defaultPrimaryColors) List<int> primaryColors,
|
2026-10-16 10:31:15 +08:00
|
|
|
|
@Default(ThemeMode.system) ThemeMode themeMode,
|
2026-10-16 22:42:17 +08:00
|
|
|
|
@Default(ColorStyle.vivid)
|
|
|
|
|
|
@JsonKey(readValue: _readColorStyle)
|
|
|
|
|
|
ColorStyle colorStyle,
|
2025-03-05 16:08:50 +08:00
|
|
|
|
@Default(false) bool pureBlack,
|
2026-09-25 03:56:59 +08:00
|
|
|
|
@Default(true) bool sidebarBlur,
|
2026-10-16 22:42:17 +08:00
|
|
|
|
@Default(LiquidGlassMode.none)
|
|
|
|
|
|
@JsonKey(readValue: _readLiquidGlass)
|
|
|
|
|
|
LiquidGlassMode liquidGlass,
|
2025-04-18 17:50:46 +08:00
|
|
|
|
@Default(TextScale()) TextScale textScale,
|
2026-10-08 17:58:43 +08:00
|
|
|
|
// `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;
|
|
|
|
|
|
|
2025-04-18 17:50:46 +08:00
|
|
|
|
factory ThemeProps.fromJson(Map<String, Object?> json) =>
|
|
|
|
|
|
_$ThemePropsFromJson(json);
|
|
|
|
|
|
|
|
|
|
|
|
factory ThemeProps.safeFromJson(Map<String, Object?>? json) {
|
|
|
|
|
|
if (json == null) {
|
|
|
|
|
|
return defaultThemeProps;
|
|
|
|
|
|
}
|
2026-10-08 17:58:43 +08:00
|
|
|
|
return decodeSalvaging('theme settings', json, ThemeProps.fromJson);
|
2025-04-18 17:50:46 +08:00
|
|
|
|
}
|
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,
|
2025-04-18 17:50:46 +08:00
|
|
|
|
@JsonKey(fromJson: ThemeProps.safeFromJson) required ThemeProps themeProps,
|
2025-12-16 11:23:09 +08:00
|
|
|
|
@Default(defaultProxiesStyleProps) ProxiesStyleProps proxiesStyleProps,
|
2026-10-16 10:31:15 +08:00
|
|
|
|
// 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;
|
|
|
|
|
|
|
2026-10-08 17:58:43 +08:00
|
|
|
|
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) {
|
2026-09-25 03:56:59 +08:00
|
|
|
|
return Config(
|
|
|
|
|
|
themeProps: defaultThemeProps,
|
|
|
|
|
|
hotKeyActions: defaultHotKeyActions,
|
|
|
|
|
|
);
|
2025-12-16 11:23:09 +08:00
|
|
|
|
}
|
2026-10-08 17:58:43 +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
|
|
|
|
}
|
2026-10-08 17:58:43 +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 ||
|
2026-10-08 17:58:43 +08:00
|
|
|
|
!network.containsKey('routeMode') ||
|
2026-10-08 17:58:43 +08:00
|
|
|
|
network.containsKey('bypassPrivateRoute')) {
|
|
|
|
|
|
return json;
|
|
|
|
|
|
}
|
2026-10-08 17:58:43 +08:00
|
|
|
|
final bypassPrivate = network['routeMode'] == 'bypassPrivate';
|
2026-10-08 17:58:43 +08:00
|
|
|
|
final clash = json['patchClashConfig'];
|
|
|
|
|
|
final tun = clash is Map ? clash['tun'] : null;
|
|
|
|
|
|
return {
|
|
|
|
|
|
...json,
|
|
|
|
|
|
'networkProps': {
|
|
|
|
|
|
...Map<String, Object?>.from(network),
|
2026-10-08 17:58:43 +08:00
|
|
|
|
'bypassPrivateRoute': bypassPrivate,
|
2026-10-08 17:58:43 +08:00
|
|
|
|
},
|
2026-10-08 17:58:43 +08:00
|
|
|
|
if (bypassPrivate && clash is Map && tun is Map)
|
2026-10-08 17:58:43 +08:00
|
|
|
|
'patchClashConfig': {
|
|
|
|
|
|
...Map<String, Object?>.from(clash),
|
|
|
|
|
|
'tun': {...Map<String, Object?>.from(tun), 'route-address': <String>[]},
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2024-04-30 23:38:49 +08:00
|
|
|
|
}
|