From 491fae10f51ef4c5d1e39dc48f375e8e0400b248 Mon Sep 17 00:00:00 2001 From: chen08209 Date: Thu, 8 Oct 2026 17:58:43 +0800 Subject: [PATCH] feat(views)!: set DNS, NTP, sniffer and TUN key by key BREAKING CHANGE: DNS and NTP override toggles are removed; picked keys apply to every profile Changelog: Per-key DNS, NTP, sniffer and TUN settings --- arb/intl_en.arb | 53 +- arb/intl_ja.arb | 53 +- arb/intl_ru.arb | 53 +- arb/intl_zh_CN.arb | 53 +- assets/images/services/bilibili.svg | 2 +- assets/images/services/claude.svg | 2 +- assets/images/services/disneyplus.svg | 2 +- assets/images/services/gemini.svg | 2 +- assets/images/services/github.svg | 2 +- assets/images/services/google.svg | 2 +- assets/images/services/netflix.svg | 2 +- assets/images/services/openai.svg | 2 +- assets/images/services/primevideo.svg | 2 +- assets/images/services/spotify.svg | 2 +- assets/images/services/tiktok.svg | 2 +- assets/images/services/youtube.svg | 2 +- core/Clash.Meta | 2 +- lib/common/constant.dart | 8 +- lib/common/datetime.dart | 27 +- lib/common/font.dart | 78 ++ lib/common/input_entries.dart | 19 +- lib/common/system_fonts.dart | 363 +++++++++ lib/common/task.dart | 5 +- lib/database/custom_proxies.dart | 14 +- lib/database/database.dart | 19 +- lib/database/generated/database.g.dart | 84 +- lib/database/groups.dart | 12 + .../connection/tracker_info_item.dart | 88 +- .../connection/tracker_info_list.dart | 1 + lib/features/editor/clash_schema.dart | 1 + lib/features/ip_quality/ip_quality_sheet.dart | 9 +- lib/l10n/intl/messages_en.dart | 317 +++++--- lib/l10n/intl/messages_ja.dart | 285 ++++--- lib/l10n/intl/messages_ru.dart | 315 ++++--- lib/l10n/intl/messages_zh_CN.dart | 279 ++++--- lib/l10n/l10n.dart | 405 ++++++++- lib/manager/theme_manager.dart | 2 +- lib/models/clash_config.dart | 163 +++- lib/models/config.dart | 9 +- .../generated/clash_config.freezed.dart | 188 +++-- lib/models/generated/clash_config.g.dart | 92 ++- lib/models/generated/config.freezed.dart | 2 +- lib/models/generated/config.g.dart | 2 +- lib/models/generated/state.freezed.dart | 45 +- lib/models/state.dart | 3 + lib/pages/error.dart | 444 +++++++--- lib/providers/actions/setup.dart | 15 +- lib/providers/generated/action.g.dart | 4 +- lib/providers/state/custom.dart | 71 +- lib/providers/state/custom_import.dart | 46 +- lib/views/about.dart | 9 +- lib/views/access.dart | 33 +- lib/views/config/advanced.dart | 22 +- lib/views/config/custom_resources.dart | 49 ++ lib/views/config/dns.dart | 766 +++++++----------- lib/views/config/filters.dart | 280 +++++++ lib/views/config/general.dart | 84 +- lib/views/config/icon_sets.dart | 314 +++++++ lib/views/config/network.dart | 262 +++++- lib/views/config/ntp.dart | 375 ++------- lib/views/config/override.dart | 667 +++++++++++++++ lib/views/config/providers.dart | 531 ++++++------ lib/views/config/rules.dart | 72 +- lib/views/config/scripts.dart | 222 +++-- lib/views/config/sniffer.dart | 285 +++++++ lib/views/config/tun.dart | 77 ++ lib/views/config/user_agents.dart | 122 ++- lib/views/connection/connections.dart | 4 +- lib/views/connection/requests.dart | 8 +- lib/views/dashboard/dashboard.dart | 71 +- lib/views/dashboard/widget_registry.dart | 10 - lib/views/dashboard/widgets/connections.dart | 4 +- lib/views/dashboard/widgets/dns_queries.dart | 4 +- lib/views/dashboard/widgets/feed_card.dart | 6 +- .../dashboard/widgets/network_detection.dart | 5 +- .../dashboard/widgets/profile_detail.dart | 54 +- lib/views/dashboard/widgets/profiles.dart | 63 +- lib/views/dashboard/widgets/proxy_groups.dart | 228 ++---- .../dashboard/widgets/quick_options.dart | 36 - lib/views/dashboard/widgets/requests.dart | 4 +- lib/views/dashboard/widgets/row_card.dart | 17 +- .../dashboard/widgets/service_status.dart | 202 +++-- lib/views/dashboard/widgets/start_button.dart | 140 +--- lib/views/disclaimer.dart | 482 ++++++++--- lib/views/dns_queries.dart | 11 +- lib/views/font_family.dart | 196 +++++ lib/views/logs.dart | 12 +- lib/views/profiles/custom/custom.dart | 5 +- lib/views/profiles/custom/custom_proxies.dart | 85 +- lib/views/profiles/custom/members.dart | 168 +++- lib/views/profiles/extend/standard.dart | 59 +- lib/views/profiles/profiles.dart | 1 + lib/views/proxies/card.dart | 4 +- lib/views/proxies/list.dart | 30 +- lib/views/proxies/providers.dart | 154 ++-- lib/views/proxies/proxies.dart | 1 - lib/views/proxies/setting.dart | 65 +- lib/views/proxies/tab.dart | 8 +- lib/views/resources.dart | 679 +++++++++++----- lib/views/theme.dart | 97 ++- lib/views/theme_preview.dart | 725 ++++++++--------- lib/views/tools.dart | 53 +- lib/widgets/config_item.dart | 99 ++- lib/widgets/edit_view.dart | 25 +- lib/widgets/paged_sheet.dart | 12 - lib/widgets/text.dart | 4 +- test/common/datetime_test.dart | 12 + test/common/delay_test.dart | 13 +- test/common/font_test.dart | 98 +++ test/common/input_entries_test.dart | 16 + test/common/system_fonts_test.dart | 55 ++ test/common/task_dns_override_test.dart | 24 + test/database/dao_coverage_test.dart | 21 + test/database/migration_v1_to_v2_test.dart | 65 +- test/features/custom_profile_views_test.dart | 18 +- test/features/custom_quick_edit_test.dart | 115 +++ test/lint/reorder_proxy_decorator_test.dart | 64 ++ test/manager/android_manager_test.dart | 5 + test/manager/theme_manager_test.dart | 33 + test/manager/vpn_manager_test.dart | 9 +- test/models/config_test.dart | 74 +- test/models/dns_override_test.dart | 41 +- test/models/ntp_override_test.dart | 5 +- test/pages/error_test.dart | 113 ++- test/providers/action_test.dart | 48 +- test/providers/custom_import_test.dart | 19 + test/providers/custom_state_test.dart | 79 ++ test/providers/setup_action_test.dart | 106 ++- test/providers/state_derived_test.dart | 6 + test/views/access_test.dart | 29 + test/views/added_rules_test.dart | 2 +- test/views/clash_providers_view_test.dart | 569 ++++++------- test/views/connections_test.dart | 14 +- test/views/custom_profile_edit_test.dart | 112 ++- test/views/dashboard/add_widget_test.dart | 85 +- .../dashboard/connections_card_test.dart | 16 + .../dashboard/dns_queries_card_test.dart | 67 +- test/views/dashboard/profiles_card_test.dart | 75 +- .../dashboard/proxy_groups_card_test.dart | 83 +- test/views/dashboard/quick_options_test.dart | 10 - test/views/dashboard/requests_card_test.dart | 61 +- test/views/dashboard/service_status_test.dart | 35 +- test/views/dns_view_test.dart | 471 ++++++++--- test/views/filters_test.dart | 197 +++++ test/views/icon_picker_view_test.dart | 2 +- test/views/icon_sets_view_test.dart | 273 +++++++ test/views/logs_view_test.dart | 2 +- test/views/network_config_test.dart | 85 +- test/views/ntp_view_test.dart | 293 ++++++- test/views/proxy_providers_view_test.dart | 182 +++-- test/views/requests_test.dart | 6 +- test/views/resources_view_test.dart | 401 ++++++++- test/views/script_content_test.dart | 42 +- test/views/sniffer_view_test.dart | 205 +++++ test/views/theme_test.dart | 166 ++++ test/views/tun_view_test.dart | 90 ++ test/views/user_agents_test.dart | 58 +- test/widgets/config_item_test.dart | 66 ++ test/widgets/connections_view_test.dart | 16 +- test/widgets/dashboard_layout_test.dart | 4 +- test/widgets/profile_focus_test.dart | 4 +- test/widgets/proxies_list_test.dart | 5 +- test/widgets/start_button_test.dart | 67 +- test/widgets/uncovered_views_test.dart | 119 ++- test/widgets/views_smoke_test.dart | 124 ++- 165 files changed, 12012 insertions(+), 4717 deletions(-) create mode 100644 lib/common/font.dart create mode 100644 lib/common/system_fonts.dart create mode 100644 lib/views/config/custom_resources.dart create mode 100644 lib/views/config/filters.dart create mode 100644 lib/views/config/icon_sets.dart create mode 100644 lib/views/config/override.dart create mode 100644 lib/views/config/sniffer.dart create mode 100644 lib/views/config/tun.dart create mode 100644 lib/views/font_family.dart create mode 100644 test/common/font_test.dart create mode 100644 test/common/system_fonts_test.dart create mode 100644 test/lint/reorder_proxy_decorator_test.dart create mode 100644 test/manager/theme_manager_test.dart create mode 100644 test/views/filters_test.dart create mode 100644 test/views/icon_sets_view_test.dart create mode 100644 test/views/sniffer_view_test.dart create mode 100644 test/views/tun_view_test.dart create mode 100644 test/widgets/config_item_test.dart diff --git a/arb/intl_en.arb b/arb/intl_en.arb index 9a4f7139..7ed01b08 100644 --- a/arb/intl_en.arb +++ b/arb/intl_en.arb @@ -417,6 +417,7 @@ "sourceIp": "Source IP", "noResolve": "Don't resolve IP", "saveChanges": "Save the changes?", + "cannotSave": "Can't save", "navigationBarStyle": "Bottom bar", "floating": "Floating", "docked": "Docked", @@ -545,6 +546,11 @@ "hoursAgo": "{count, plural, =1{1 hour ago} other{{count} hours ago}}", "minutesAgo": "{count, plural, =1{1 minute ago} other{{count} minutes ago}}", "justNow": "Just now", + "yearsAgoShort": "{count, plural, other{{count}y ago}}", + "monthsAgoShort": "{count, plural, other{{count}mo ago}}", + "daysAgoShort": "{count, plural, other{{count}d ago}}", + "hoursAgoShort": "{count, plural, other{{count}h ago}}", + "minutesAgoShort": "{count, plural, other{{count}m ago}}", "noLongerRemind": "Don't remind me again", "accessControlSettings": "Access control settings", "turnOn": "Turn on", @@ -555,6 +561,8 @@ "resetPageChangesTip": "This page has changes. Are you sure you want to reset?", "customProfile": "Custom", "customProfileDesc": "Assemble proxy groups and rules from app proxies and other profiles", + "customProfileResources": "Custom profile resources", + "customProfileResourcesDesc": "Proxies, rule providers, filters, and icon sets", "unknownNetworkError": "Unknown network error", "backupFromNewerVersion": "This backup comes from a newer version of the app. Update the app before restoring it", "networkTimeoutError": "The request timed out. Check your network or proxy, then try again", @@ -592,6 +600,7 @@ "selectProxies": "Select proxies", "inputProxyGroupName": "Enter the proxy group name", "helperCorruptTip": "Helper service unavailable; TUN mode cannot be enabled. Reinstall FlClash to restore it.", + "tunAuthorizationFailed": "TUN could not be authorized, so it was turned off.", "sudoPasswordTitle": "Enter your password for sudo", "coreBlockedByPolicyTip": "Windows refused to run FlClashCore.exe (error {code}). An app control policy such as Smart App Control or AppLocker blocks unsigned programs; allow FlClash in that policy or turn it off, then try again.", "coreBlockedBySmartAppControlTip": "Windows Smart App Control blocked FlClashCore.exe because it is not signed. Open Windows Security → App & browser control → Smart App Control settings, choose Off, then start FlClash again. Smart App Control cannot be turned back on without reinstalling Windows.", @@ -674,6 +683,10 @@ "invalidNetworkContent": "Only tcp or udp is supported", "invalidRangeContent": "Enter numbers or ranges such as 80 or 8000-9000, separated by /", "invalidDscpContent": "A DSCP mark cannot exceed 63", + "invalidCidrContent": "Enter an IP range in CIDR form such as 192.168.0.0/16", + "invalidPortRangeContent": "Enter a port or a range such as 443 or 8000-9000", + "invalidHostContent": "Enter a domain or an IP address", + "invalidListenContent": "Enter an address and port such as 0.0.0.0:1053", "invalidProxy": "{proxyName} is an invalid proxy", "locationPermissionRequired": "Location permission required", "locationPermissionGuide": "1. Open System Settings > Privacy & Security\n2. Choose Location Services\n3. Find and check {appName} in the list\n\nWhen you are done, return to the app to continue. Thank you for your cooperation.", @@ -701,8 +714,8 @@ "geoSkipped": "{name} is already up to date", "geoUpdated": "{name} updated", "secondsCount": "{count, plural, =1{1 second} other{{count} seconds}}", - "proxiesCount": "{count, plural, =1{1 proxy} other{{count} proxies}}", - "rulesCount": "{count, plural, =1{1 rule} other{{count} rules}}", + "proxiesCount": "{count, plural, =1{1 proxy} other{{label} proxies}}", + "rulesCount": "{count, plural, =1{1 rule} other{{label} rules}}", "changelogBreaking": "Breaking changes", "changelogFeatures": "New features", "changelogFixes": "Bug fixes", @@ -816,7 +829,6 @@ "extraLarge": "Extra large", "switchProfile": "Switch profile", "lastUpdated": "Last updated", - "universal": "General", "filters": "Filters", "label": "Label", "regex": "Regex", @@ -824,7 +836,7 @@ "invalidIconSet": "Not a valid icon set", "recommendedIcons": "Suggested", "localImage": "Local image", - "iconSetsEmptyTip": "No icon sets yet. Add them in Advanced configuration → Icon sets", + "iconSetsEmptyTip": "No icon sets yet. Add them in Custom profile resources → Icon sets", "outbound": "Outbound", "strictRoute": "Strict route", "congestionController": "TCP congestion control", @@ -862,5 +874,36 @@ "peers": "Peers", "virtualIpv4": "Virtual IPv4", "hostname": "Hostname", - "exitNodes": "Exit nodes" + "exitNodes": "Exit nodes", + "cipher": "Cipher", + "protocol": "Protocol", + "protocolParam": "Protocol parameter", + "obfs": "Obfuscation", + "obfsParam": "Obfuscation parameter", + "obfsPassword": "Obfuscation password", + "obfsMode": "Obfuscation mode", + "obfsHost": "Obfuscation host", + "plugin": "Plugin", + "pluginMode": "Plugin mode", + "pluginHost": "Plugin host", + "privateKey": "Private key", + "publicKey": "Public key", + "privateKeyPassphrase": "Private key passphrase", + "preSharedKey": "Pre-shared key", + "secretKey": "Key", + "transport": "Transport", + "uploadBandwidth": "Upload bandwidth", + "downloadBandwidth": "Download bandwidth", + "protocolVersion": "Protocol version", + "portHopping": "Port hopping", + "hopInterval": "Hop interval", + "congestionControl": "Congestion control", + "udpRelayMode": "UDP relay mode", + "allowedIps": "Allowed IPs", + "remoteDnsResolve": "Remote DNS resolution", + "clientFingerprint": "Client fingerprint", + "required": "Required", + "ageSecretKey": "Age secret key", + "ageEncryptedProfile": "Encrypted profile", + "ageSecretKeyRequiredTip": "This profile is encrypted with age and needs an age secret key to decrypt" } diff --git a/arb/intl_ja.arb b/arb/intl_ja.arb index e85a2a3c..78b839ba 100644 --- a/arb/intl_ja.arb +++ b/arb/intl_ja.arb @@ -417,6 +417,7 @@ "sourceIp": "送信元IP", "noResolve": "IPを解決しない", "saveChanges": "変更を保存しますか?", + "cannotSave": "保存できません", "navigationBarStyle": "ボトムバー", "floating": "フローティング", "docked": "固定", @@ -545,6 +546,11 @@ "hoursAgo": "{count} 時間前", "minutesAgo": "{count} 分前", "justNow": "たった今", + "yearsAgoShort": "{count} 年前", + "monthsAgoShort": "{count} か月前", + "daysAgoShort": "{count} 日前", + "hoursAgoShort": "{count} 時間前", + "minutesAgoShort": "{count} 分前", "noLongerRemind": "今後表示しない", "accessControlSettings": "アクセス制御の設定", "turnOn": "オンにする", @@ -555,6 +561,8 @@ "resetPageChangesTip": "このページには変更があります。リセットしてもよろしいですか?", "customProfile": "カスタム", "customProfileDesc": "アプリのプロキシと他のプロファイルからプロキシグループとルールを組み立てます", + "customProfileResources": "カスタムプロファイルのリソース", + "customProfileResourcesDesc": "プロキシ、ルールプロバイダー、フィルター、アイコンセット", "unknownNetworkError": "不明なネットワークエラー", "backupFromNewerVersion": "このバックアップは新しいバージョンのアプリで作成されています。アプリを更新してから復元してください", "networkTimeoutError": "リクエストがタイムアウトしました。ネットワークまたはプロキシを確認してから再試行してください", @@ -592,6 +600,7 @@ "selectProxies": "プロキシを選択", "inputProxyGroupName": "プロキシグループ名を入力してください", "helperCorruptTip": "Helper サービスが利用できないため、TUN モードを有効にできません。FlClash を再インストールしてください。", + "tunAuthorizationFailed": "TUN の権限を取得できなかったため、オフにしました。", "sudoPasswordTitle": "sudo のパスワードを入力", "coreBlockedByPolicyTip": "Windows が FlClashCore.exe の実行を拒否しました(エラー {code})。スマート アプリ コントロールや AppLocker などのアプリ制御ポリシーは未署名のプログラムをブロックします。ポリシーで FlClash を許可するか、ポリシーを無効にしてから再試行してください。", "coreBlockedBySmartAppControlTip": "Windows のスマート アプリ コントロールが、署名されていない FlClashCore.exe をブロックしました。Windows セキュリティ → アプリとブラウザーの制御 → スマート アプリ コントロールの設定で「オフ」を選び、FlClash を再起動してください。一度オフにすると、Windows を再インストールしない限り再度オンにはできません。", @@ -674,6 +683,10 @@ "invalidNetworkContent": "tcp または udp のみ対応しています", "invalidRangeContent": "80 や 8000-9000 のような数値または範囲を / 区切りで入力してください", "invalidDscpContent": "DSCP マークは 63 を超えられません", + "invalidCidrContent": "192.168.0.0/16 のような CIDR 形式の IP 範囲を入力してください", + "invalidPortRangeContent": "443 や 8000-9000 のようなポートまたは範囲を入力してください", + "invalidHostContent": "ドメインまたは IP アドレスを入力してください", + "invalidListenContent": "0.0.0.0:1053 のようなアドレスとポートを入力してください", "invalidProxy": "{proxyName} は無効なプロキシです", "locationPermissionRequired": "位置情報の権限が必要です", "locationPermissionGuide": "1. システム設定 > プライバシーとセキュリティ を開く\n2. 位置情報サービス を選択\n3. リストで {appName} を見つけてチェックを入れる\n\n設定が完了したらアプリに戻ると、通常どおり使用できます。ご協力ありがとうございます。", @@ -701,8 +714,8 @@ "geoSkipped": "{name} はすでに最新です", "geoUpdated": "{name} を更新しました", "secondsCount": "{count} 秒", - "proxiesCount": "プロキシ {count} 件", - "rulesCount": "ルール {count} 件", + "proxiesCount": "{count, plural, other{プロキシ {label} 件}}", + "rulesCount": "{count, plural, other{ルール {label} 件}}", "changelogBreaking": "破壊的変更", "changelogFeatures": "新機能", "changelogFixes": "不具合修正", @@ -816,7 +829,6 @@ "extraLarge": "特大", "switchProfile": "プロファイルを切り替え", "lastUpdated": "最終更新", - "universal": "共通", "filters": "フィルター", "label": "ラベル", "regex": "正規表現", @@ -824,7 +836,7 @@ "invalidIconSet": "有効なアイコンセットではありません", "recommendedIcons": "おすすめ", "localImage": "ローカル画像", - "iconSetsEmptyTip": "アイコンセットはまだありません。「詳細設定 → アイコンセット」で追加できます", + "iconSetsEmptyTip": "アイコンセットはまだありません。「カスタムプロファイルのリソース → アイコンセット」で追加できます", "outbound": "アウトバウンド", "strictRoute": "厳格なルーティング", "congestionController": "TCP 輻輳制御", @@ -862,5 +874,36 @@ "peers": "ピア", "virtualIpv4": "仮想 IPv4", "hostname": "ホスト名", - "exitNodes": "出口ノード" + "exitNodes": "出口ノード", + "cipher": "暗号方式", + "protocol": "プロトコル", + "protocolParam": "プロトコルパラメータ", + "obfs": "難読化", + "obfsParam": "難読化パラメータ", + "obfsPassword": "難読化パスワード", + "obfsMode": "難読化モード", + "obfsHost": "難読化ホスト", + "plugin": "プラグイン", + "pluginMode": "プラグインモード", + "pluginHost": "プラグインホスト", + "privateKey": "秘密鍵", + "publicKey": "公開鍵", + "privateKeyPassphrase": "秘密鍵のパスフレーズ", + "preSharedKey": "事前共有鍵", + "secretKey": "キー", + "transport": "トランスポート", + "uploadBandwidth": "アップロード帯域幅", + "downloadBandwidth": "ダウンロード帯域幅", + "protocolVersion": "プロトコルバージョン", + "portHopping": "ポートホッピング", + "hopInterval": "ホップ間隔", + "congestionControl": "輻輳制御", + "udpRelayMode": "UDP リレーモード", + "allowedIps": "許可する IP", + "remoteDnsResolve": "リモート DNS 解決", + "clientFingerprint": "クライアントフィンガープリント", + "required": "必須", + "ageSecretKey": "age 秘密鍵", + "ageEncryptedProfile": "暗号化されたプロファイル", + "ageSecretKeyRequiredTip": "このプロファイルは age で暗号化されており、復号には age 秘密鍵が必要です" } diff --git a/arb/intl_ru.arb b/arb/intl_ru.arb index fe13ff67..7e39fee4 100644 --- a/arb/intl_ru.arb +++ b/arb/intl_ru.arb @@ -417,6 +417,7 @@ "sourceIp": "IP источника", "noResolve": "Не разрешать IP", "saveChanges": "Сохранить изменения?", + "cannotSave": "Не удалось сохранить", "navigationBarStyle": "Нижняя панель", "floating": "Плавающая", "docked": "Закреплённая", @@ -545,6 +546,11 @@ "hoursAgo": "{count, plural, one{{count} час назад} few{{count} часа назад} many{{count} часов назад} other{{count} часа назад}}", "minutesAgo": "{count, plural, one{{count} минуту назад} few{{count} минуты назад} many{{count} минут назад} other{{count} минуты назад}}", "justNow": "Только что", + "yearsAgoShort": "{count, plural, one{{count} г. назад} few{{count} г. назад} many{{count} л. назад} other{{count} г. назад}}", + "monthsAgoShort": "{count} мес. назад", + "daysAgoShort": "{count} дн. назад", + "hoursAgoShort": "{count} ч назад", + "minutesAgoShort": "{count} мин назад", "noLongerRemind": "Больше не напоминать", "accessControlSettings": "Настройки контроля доступа", "turnOn": "Включить", @@ -555,6 +561,8 @@ "resetPageChangesTip": "На этой странице есть изменения. Вы уверены, что хотите выполнить сброс?", "customProfile": "Пользовательский", "customProfileDesc": "Группы прокси и правила из прокси приложения и других профилей", + "customProfileResources": "Ресурсы пользовательских профилей", + "customProfileResourcesDesc": "Прокси, провайдеры правил, фильтры и наборы значков", "unknownNetworkError": "Неизвестная сетевая ошибка", "backupFromNewerVersion": "Резервная копия создана более новой версией приложения. Обновите приложение перед восстановлением", "networkTimeoutError": "Время ожидания запроса истекло. Проверьте сеть или прокси и повторите попытку", @@ -592,6 +600,7 @@ "selectProxies": "Выбрать прокси", "inputProxyGroupName": "Введите название группы прокси", "helperCorruptTip": "Служба Helper недоступна, поэтому TUN-режим включить нельзя. Переустановите FlClash.", + "tunAuthorizationFailed": "Не удалось получить права для TUN, поэтому он выключен.", "sudoPasswordTitle": "Введите пароль для sudo", "coreBlockedByPolicyTip": "Windows отказалась запускать FlClashCore.exe (ошибка {code}). Политики контроля приложений, такие как Smart App Control или AppLocker, блокируют неподписанные программы; разрешите FlClash в этой политике или отключите её и повторите попытку.", "coreBlockedBySmartAppControlTip": "Smart App Control в Windows заблокировал неподписанный FlClashCore.exe. Откройте Безопасность Windows → Управление приложениями и браузером → Параметры Smart App Control, выберите «Выкл.» и снова запустите FlClash. Повторно включить Smart App Control без переустановки Windows нельзя.", @@ -674,6 +683,10 @@ "invalidNetworkContent": "Поддерживаются только tcp и udp", "invalidRangeContent": "Введите числа или диапазоны, например 80 или 8000-9000, через /", "invalidDscpContent": "Метка DSCP не может превышать 63", + "invalidCidrContent": "Введите диапазон IP в формате CIDR, например 192.168.0.0/16", + "invalidPortRangeContent": "Введите порт или диапазон, например 443 или 8000-9000", + "invalidHostContent": "Введите домен или IP-адрес", + "invalidListenContent": "Введите адрес и порт, например 0.0.0.0:1053", "invalidProxy": "{proxyName} — недопустимый прокси", "locationPermissionRequired": "Требуется разрешение на геолокацию", "locationPermissionGuide": "1. Откройте Системные настройки > Конфиденциальность и безопасность\n2. Выберите Службы геолокации\n3. Найдите и отметьте {appName} в списке\n\nПосле настройки вернитесь в приложение и продолжайте работу. Спасибо за сотрудничество.", @@ -701,8 +714,8 @@ "geoSkipped": "{name}: уже последняя версия", "geoUpdated": "{name}: обновлено", "secondsCount": "{count, plural, one{{count} секунда} few{{count} секунды} many{{count} секунд} other{{count} секунды}}", - "proxiesCount": "{count} прокси", - "rulesCount": "{count, plural, one{{count} правило} few{{count} правила} many{{count} правил} other{{count} правила}}", + "proxiesCount": "{count, plural, other{{label} прокси}}", + "rulesCount": "{count, plural, one{{label} правило} few{{label} правила} many{{label} правил} other{{label} правила}}", "changelogBreaking": "Важные изменения", "changelogFeatures": "Новые функции", "changelogFixes": "Исправления", @@ -816,7 +829,6 @@ "extraLarge": "Очень крупный", "switchProfile": "Сменить профиль", "lastUpdated": "Последнее обновление", - "universal": "Общие", "filters": "Фильтры", "label": "Метка", "regex": "Регулярное выражение", @@ -824,7 +836,7 @@ "invalidIconSet": "Это не набор значков", "recommendedIcons": "Рекомендуемые", "localImage": "Локальное изображение", - "iconSetsEmptyTip": "Наборов значков пока нет. Добавьте их в разделе «Расширенная конфигурация → Наборы значков»", + "iconSetsEmptyTip": "Наборов значков пока нет. Добавьте их в разделе «Ресурсы пользовательских профилей → Наборы значков»", "outbound": "Исходящие", "strictRoute": "Строгая маршрутизация", "congestionController": "Управление перегрузкой TCP", @@ -862,5 +874,36 @@ "peers": "Пиры", "virtualIpv4": "Виртуальный IPv4", "hostname": "Имя хоста", - "exitNodes": "Выходные узлы" + "exitNodes": "Выходные узлы", + "cipher": "Шифр", + "protocol": "Протокол", + "protocolParam": "Параметр протокола", + "obfs": "Обфускация", + "obfsParam": "Параметр обфускации", + "obfsPassword": "Пароль обфускации", + "obfsMode": "Режим обфускации", + "obfsHost": "Хост обфускации", + "plugin": "Плагин", + "pluginMode": "Режим плагина", + "pluginHost": "Хост плагина", + "privateKey": "Закрытый ключ", + "publicKey": "Открытый ключ", + "privateKeyPassphrase": "Пароль закрытого ключа", + "preSharedKey": "Общий ключ", + "secretKey": "Ключ", + "transport": "Транспорт", + "uploadBandwidth": "Скорость отдачи", + "downloadBandwidth": "Скорость загрузки", + "protocolVersion": "Версия протокола", + "portHopping": "Перескок портов", + "hopInterval": "Интервал перескока", + "congestionControl": "Управление перегрузкой", + "udpRelayMode": "Режим ретрансляции UDP", + "allowedIps": "Разрешённые IP", + "remoteDnsResolve": "Удалённое разрешение DNS", + "clientFingerprint": "Отпечаток клиента", + "required": "Обязательно", + "ageSecretKey": "Секретный ключ age", + "ageEncryptedProfile": "Зашифрованный профиль", + "ageSecretKeyRequiredTip": "Профиль зашифрован с помощью age, для расшифровки нужен секретный ключ age" } diff --git a/arb/intl_zh_CN.arb b/arb/intl_zh_CN.arb index 5b6ff694..298227be 100644 --- a/arb/intl_zh_CN.arb +++ b/arb/intl_zh_CN.arb @@ -417,6 +417,7 @@ "sourceIp": "源IP", "noResolve": "不解析IP", "saveChanges": "是否保存更改?", + "cannotSave": "无法保存", "navigationBarStyle": "底栏", "floating": "悬浮", "docked": "固定", @@ -545,6 +546,11 @@ "hoursAgo": "{count} 小时前", "minutesAgo": "{count} 分钟前", "justNow": "刚刚", + "yearsAgoShort": "{count} 年前", + "monthsAgoShort": "{count} 个月前", + "daysAgoShort": "{count} 天前", + "hoursAgoShort": "{count} 小时前", + "minutesAgoShort": "{count} 分钟前", "noLongerRemind": "不再提示", "accessControlSettings": "访问控制设置", "turnOn": "开启", @@ -555,6 +561,8 @@ "resetPageChangesTip": "当前页面存在更改,确定重置吗?", "customProfile": "自定义", "customProfileDesc": "基于应用代理和其他配置组建策略组与规则", + "customProfileResources": "自定义配置资源", + "customProfileResourcesDesc": "代理、规则集、筛选与图标集", "unknownNetworkError": "未知网络错误", "backupFromNewerVersion": "该备份来自更高版本的应用,请先更新应用再恢复", "networkTimeoutError": "请求超时,请检查网络或代理后重试", @@ -592,6 +600,7 @@ "selectProxies": "选择代理", "inputProxyGroupName": "输入策略组名称", "helperCorruptTip": "Helper 服务不可用,无法启用 TUN 模式,请重新安装 FlClash。", + "tunAuthorizationFailed": "虚拟网卡授权失败,已关闭。", "sudoPasswordTitle": "输入 sudo 密码", "coreBlockedByPolicyTip": "Windows 拒绝运行 FlClashCore.exe(错误 {code})。智能应用控制、AppLocker 等应用控制策略会拦截未签名程序,请在该策略中放行 FlClash 或关闭策略后重试。", "coreBlockedBySmartAppControlTip": "Windows 智能应用控制拦截了未签名的 FlClashCore.exe。请打开 Windows 安全中心 → 应用和浏览器控制 → 智能应用控制设置,选择「关闭」后重新启动 FlClash。智能应用控制关闭后无法再开启,除非重装 Windows。", @@ -674,6 +683,10 @@ "invalidNetworkContent": "仅支持 tcp 或 udp", "invalidRangeContent": "请输入数字或范围,如 80 或 8000-9000,多个用 / 分隔", "invalidDscpContent": "DSCP 标记不能超过 63", + "invalidCidrContent": "请输入 CIDR 格式的 IP 段,如 192.168.0.0/16", + "invalidPortRangeContent": "请输入端口或范围,如 443 或 8000-9000", + "invalidHostContent": "请输入域名或 IP 地址", + "invalidListenContent": "请输入地址和端口,如 0.0.0.0:1053", "invalidProxy": "{proxyName} 是一个无效的代理", "locationPermissionRequired": "需要位置权限", "locationPermissionGuide": "1. 打开 系统设置 > 隐私与安全性\n2. 选择 定位服务\n3. 在右侧列表中找到并勾选 {appName}\n\n完成设置后,返回应用即可正常使用。感谢您的配合。", @@ -701,8 +714,8 @@ "geoSkipped": "{name} 已是最新版本", "geoUpdated": "{name} 已更新", "secondsCount": "{count} 秒", - "proxiesCount": "{count} 个代理", - "rulesCount": "{count} 条规则", + "proxiesCount": "{count, plural, other{{label} 个代理}}", + "rulesCount": "{count, plural, other{{label} 条规则}}", "changelogBreaking": "重大变更", "changelogFeatures": "新功能", "changelogFixes": "问题修复", @@ -816,7 +829,6 @@ "extraLarge": "超大", "switchProfile": "切换配置", "lastUpdated": "上次更新", - "universal": "通用", "filters": "筛选", "label": "标签", "regex": "正则", @@ -824,7 +836,7 @@ "invalidIconSet": "不是有效的图标集", "recommendedIcons": "推荐", "localImage": "本地图片", - "iconSetsEmptyTip": "暂无图标集,可在「进阶配置 → 图标集」中添加", + "iconSetsEmptyTip": "暂无图标集,可在「自定义配置资源 → 图标集」中添加", "outbound": "出站", "strictRoute": "严格路由", "congestionController": "TCP 拥塞控制", @@ -862,5 +874,36 @@ "peers": "初始节点", "virtualIpv4": "虚拟 IPv4", "hostname": "主机名", - "exitNodes": "出口节点" + "exitNodes": "出口节点", + "cipher": "加密方式", + "protocol": "协议", + "protocolParam": "协议参数", + "obfs": "混淆", + "obfsParam": "混淆参数", + "obfsPassword": "混淆密码", + "obfsMode": "混淆模式", + "obfsHost": "混淆主机", + "plugin": "插件", + "pluginMode": "插件模式", + "pluginHost": "插件主机", + "privateKey": "私钥", + "publicKey": "公钥", + "privateKeyPassphrase": "私钥密码", + "preSharedKey": "预共享密钥", + "secretKey": "密钥", + "transport": "传输协议", + "uploadBandwidth": "上行带宽", + "downloadBandwidth": "下行带宽", + "protocolVersion": "协议版本", + "portHopping": "端口跳跃", + "hopInterval": "跳跃间隔", + "congestionControl": "拥塞控制", + "udpRelayMode": "UDP 中继模式", + "allowedIps": "允许的 IP", + "remoteDnsResolve": "远程 DNS 解析", + "clientFingerprint": "客户端指纹", + "required": "必填", + "ageSecretKey": "age 私钥", + "ageEncryptedProfile": "配置已加密", + "ageSecretKeyRequiredTip": "该配置已用 age 加密,需要 age 私钥才能解密" } diff --git a/assets/images/services/bilibili.svg b/assets/images/services/bilibili.svg index 6c114c71..8f929282 100644 --- a/assets/images/services/bilibili.svg +++ b/assets/images/services/bilibili.svg @@ -1 +1 @@ -Bilibili \ No newline at end of file +Bilibili \ No newline at end of file diff --git a/assets/images/services/claude.svg b/assets/images/services/claude.svg index 0867feb2..12a2fbe9 100644 --- a/assets/images/services/claude.svg +++ b/assets/images/services/claude.svg @@ -1 +1 @@ - + diff --git a/assets/images/services/disneyplus.svg b/assets/images/services/disneyplus.svg index 9f75d2d9..1a75d4bf 100644 --- a/assets/images/services/disneyplus.svg +++ b/assets/images/services/disneyplus.svg @@ -1 +1 @@ -Disney+ +Disney+ diff --git a/assets/images/services/gemini.svg b/assets/images/services/gemini.svg index 765adb08..4fe4d361 100644 --- a/assets/images/services/gemini.svg +++ b/assets/images/services/gemini.svg @@ -1 +1 @@ -Google Gemini \ No newline at end of file +Google Gemini \ No newline at end of file diff --git a/assets/images/services/github.svg b/assets/images/services/github.svg index 538ec5bf..2786dfa3 100644 --- a/assets/images/services/github.svg +++ b/assets/images/services/github.svg @@ -1 +1 @@ -GitHub \ No newline at end of file +GitHub \ No newline at end of file diff --git a/assets/images/services/google.svg b/assets/images/services/google.svg index 2eaf9155..5a6a708a 100644 --- a/assets/images/services/google.svg +++ b/assets/images/services/google.svg @@ -1 +1 @@ -Google \ No newline at end of file +Google \ No newline at end of file diff --git a/assets/images/services/netflix.svg b/assets/images/services/netflix.svg index 920215bd..32a7c973 100644 --- a/assets/images/services/netflix.svg +++ b/assets/images/services/netflix.svg @@ -1 +1 @@ -Netflix \ No newline at end of file +Netflix \ No newline at end of file diff --git a/assets/images/services/openai.svg b/assets/images/services/openai.svg index ebbdab0e..71e60fc3 100644 --- a/assets/images/services/openai.svg +++ b/assets/images/services/openai.svg @@ -1 +1 @@ -OpenAI \ No newline at end of file +OpenAI \ No newline at end of file diff --git a/assets/images/services/primevideo.svg b/assets/images/services/primevideo.svg index 3dffd474..97738e34 100644 --- a/assets/images/services/primevideo.svg +++ b/assets/images/services/primevideo.svg @@ -1 +1 @@ -Prime Video \ No newline at end of file +Prime Video \ No newline at end of file diff --git a/assets/images/services/spotify.svg b/assets/images/services/spotify.svg index 8d4d095f..be4bfefc 100644 --- a/assets/images/services/spotify.svg +++ b/assets/images/services/spotify.svg @@ -1 +1 @@ -Spotify \ No newline at end of file +Spotify \ No newline at end of file diff --git a/assets/images/services/tiktok.svg b/assets/images/services/tiktok.svg index 57ce3ae5..d517ec83 100644 --- a/assets/images/services/tiktok.svg +++ b/assets/images/services/tiktok.svg @@ -1 +1 @@ -TikTok \ No newline at end of file +TikTok \ No newline at end of file diff --git a/assets/images/services/youtube.svg b/assets/images/services/youtube.svg index 0492366a..82dbb9c4 100644 --- a/assets/images/services/youtube.svg +++ b/assets/images/services/youtube.svg @@ -1 +1 @@ -YouTube \ No newline at end of file +YouTube \ No newline at end of file diff --git a/core/Clash.Meta b/core/Clash.Meta index 86a42fd8..8380d0e7 160000 --- a/core/Clash.Meta +++ b/core/Clash.Meta @@ -1 +1 @@ -Subproject commit 86a42fd8986243306705a525067dbcaa67bf0bb3 +Subproject commit 8380d0e7462f061a964323e9bf3129a236fcc5d5 diff --git a/lib/common/constant.dart b/lib/common/constant.dart index a12ac8ca..401541e2 100644 --- a/lib/common/constant.dart +++ b/lib/common/constant.dart @@ -58,8 +58,12 @@ String _randomPipeId() { ).join(); } -final defaultTextScaleFactor = - WidgetsBinding.instance.platformDispatcher.textScaleFactor; +/// Depends on [context]'s text scaler, so the caller rebuilds when the system +/// font scale changes. +double systemTextScaleOf(BuildContext context) { + MediaQuery.textScalerOf(context); + return WidgetsBinding.instance.platformDispatcher.textScaleFactor; +} /// How long the Core may spend on one delay test. It spends this twice in the /// worst case - once queueing for a slot, once on the probe itself - so the diff --git a/lib/common/datetime.dart b/lib/common/datetime.dart index fe4ca716..003fcaf8 100644 --- a/lib/common/datetime.dart +++ b/lib/common/datetime.dart @@ -14,32 +14,47 @@ extension DateTimeExtension on DateTime { return true; } - String getLastUpdateTimeDesc(BuildContext context) { + String getLastUpdateTimeDesc(BuildContext context, {bool compact = false}) { final appLocalizations = context.appLocalizations; if (year <= 1970) { return appLocalizations.unknown; } + final (yearsAgo, monthsAgo, daysAgo, hoursAgo, minutesAgo) = compact + ? ( + appLocalizations.yearsAgoShort, + appLocalizations.monthsAgoShort, + appLocalizations.daysAgoShort, + appLocalizations.hoursAgoShort, + appLocalizations.minutesAgoShort, + ) + : ( + appLocalizations.yearsAgo, + appLocalizations.monthsAgo, + appLocalizations.daysAgo, + appLocalizations.hoursAgo, + appLocalizations.minutesAgo, + ); final currentDateTime = DateTime.now(); final difference = currentDateTime.difference(this); final days = difference.inDays; if (days >= 365) { final years = (days / 365).floor(); - return appLocalizations.yearsAgo(years); + return yearsAgo(years); } if (days >= 30) { final months = (days / 30).floor(); - return appLocalizations.monthsAgo(months); + return monthsAgo(months); } if (days >= 1) { - return appLocalizations.daysAgo(days); + return daysAgo(days); } final hours = difference.inHours; if (hours >= 1) { - return appLocalizations.hoursAgo(hours); + return hoursAgo(hours); } final minutes = difference.inMinutes; if (minutes >= 1) { - return appLocalizations.minutesAgo(minutes); + return minutesAgo(minutes); } return appLocalizations.justNow; } diff --git a/lib/common/font.dart b/lib/common/font.dart new file mode 100644 index 00000000..f10572fe --- /dev/null +++ b/lib/common/font.dart @@ -0,0 +1,78 @@ +import 'package:material_ui/material_ui.dart'; + +enum _HanVariant { simplified, traditional, japanese, korean } + +// The system fallback picks Han glyph forms by the UI locale, which draws +// Chinese proxy names with Japanese forms under an English UI on Windows. +const _hanFamilies = >>{ + TargetPlatform.windows: { + _HanVariant.simplified: ['Microsoft YaHei UI'], + _HanVariant.traditional: ['Microsoft JhengHei UI'], + _HanVariant.japanese: ['Yu Gothic UI', 'Meiryo UI'], + _HanVariant.korean: ['Malgun Gothic'], + }, + TargetPlatform.macOS: { + _HanVariant.simplified: ['PingFang SC'], + _HanVariant.traditional: ['PingFang TC'], + _HanVariant.japanese: ['Hiragino Sans'], + _HanVariant.korean: ['Apple SD Gothic Neo'], + }, + TargetPlatform.linux: { + _HanVariant.simplified: [ + 'Noto Sans CJK SC', + 'Noto Sans SC', + 'Source Han Sans SC', + 'WenQuanYi Micro Hei', + ], + _HanVariant.traditional: [ + 'Noto Sans CJK TC', + 'Noto Sans TC', + 'Source Han Sans TC', + ], + _HanVariant.japanese: [ + 'Noto Sans CJK JP', + 'Noto Sans JP', + 'Source Han Sans', + ], + _HanVariant.korean: [ + 'Noto Sans CJK KR', + 'Noto Sans KR', + 'Source Han Sans K', + ], + }, +}; + +_HanVariant _hanVariantOf(Iterable locales) { + for (final locale in locales) { + switch (locale.languageCode) { + case 'zh': + return _isTraditional(locale) + ? _HanVariant.traditional + : _HanVariant.simplified; + case 'ja': + return _HanVariant.japanese; + case 'ko': + return _HanVariant.korean; + } + } + return _HanVariant.simplified; +} + +bool _isTraditional(Locale locale) => switch (locale.scriptCode) { + 'Hant' => true, + 'Hans' => false, + _ => const {'TW', 'HK', 'MO'}.contains(locale.countryCode), +}; + +List appFontFamilyFallback({ + required TargetPlatform platform, + required Iterable locales, + String? fontFamily, +}) { + final base = Typography.material2021(platform: platform).black.bodyMedium!; + return [ + if (fontFamily != null) ?base.fontFamily, + ...?base.fontFamilyFallback, + ...?_hanFamilies[platform]?[_hanVariantOf(locales)], + ]; +} diff --git a/lib/common/input_entries.dart b/lib/common/input_entries.dart index 7b964bf3..c2c0fc4a 100644 --- a/lib/common/input_entries.dart +++ b/lib/common/input_entries.dart @@ -2,7 +2,13 @@ import 'package:fl_clash/enum/enum.dart'; import 'string.dart'; -enum InputIssueKind { keyTooLong, valueTooLong, missingValue, invalidUrl } +enum InputIssueKind { + keyTooLong, + valueTooLong, + missingValue, + invalidUrl, + invalidValue, +} class InputIssue { final int line; @@ -80,6 +86,7 @@ ParsedInput parseListInput( String text, { Set existing = const {}, int? maxLength, + bool Function(String value)? isValid, }) { final entries = []; final issues = []; @@ -105,6 +112,16 @@ ParsedInput parseListInput( ); continue; } + if (isValid != null && !isValid(value)) { + issues.add( + InputIssue( + line: index + 1, + raw: value, + kind: InputIssueKind.invalidValue, + ), + ); + continue; + } if (!seen.add(value)) { continue; } diff --git a/lib/common/system_fonts.dart b/lib/common/system_fonts.dart new file mode 100644 index 00000000..f19d148f --- /dev/null +++ b/lib/common/system_fonts.dart @@ -0,0 +1,363 @@ +import 'dart:ffi'; +import 'dart:io'; +import 'dart:isolate'; + +import 'package:ffi/ffi.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:flutter/foundation.dart' show visibleForTesting; +import 'package:win32/win32.dart' show GUID; + +import 'print.dart'; + +@visibleForTesting +Future> Function() systemFontFamiliesReader = () => + Isolate.run(_readSystemFontFamilies); + +/// The font families the engine resolves by name on this host, each listed +/// through the API the engine itself matches names against. +Future> loadSystemFontFamilies() async { + try { + return await systemFontFamiliesReader(); + } catch (e) { + commonPrint.log('loadSystemFontFamilies: $e', logLevel: LogLevel.warning); + return const []; + } +} + +List _readSystemFontFamilies() { + return sortFontFamilies(switch (Platform.operatingSystem) { + 'macos' => _coreTextFamilies(), + 'windows' => _directWriteFamilies(), + 'linux' => _fontconfigFamilies(), + 'android' => androidFontFamilies( + File('/system/etc/fonts.xml').readAsStringSync(), + ), + _ => const [], + }); +} + +@visibleForTesting +List sortFontFamilies(Iterable families) { + final unique = {}; + for (final family in families) { + final name = family.trim(); + if (name.isEmpty || name.startsWith('.')) { + continue; + } + unique.putIfAbsent(name.toLowerCase(), () => name); + } + return unique.values.toList() + ..sort((a, b) => a.toLowerCase().compareTo(b.toLowerCase())); +} + +@visibleForTesting +List androidFontFamilies(String fontsXml) => [ + for (final match in RegExp( + r']*\bname="([^"]+)"', + ).allMatches(fontsXml)) + match.group(1)!, +]; + +final class _CFRange extends Struct { + @IntPtr() + external int location; + + @IntPtr() + external int length; +} + +List _coreTextFamilies() { + final coreText = DynamicLibrary.open( + '/System/Library/Frameworks/CoreText.framework/CoreText', + ); + final coreFoundation = DynamicLibrary.open( + '/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation', + ); + final copyFamilyNames = coreText + .lookupFunction Function(), Pointer Function()>( + 'CTFontManagerCopyAvailableFontFamilyNames', + ); + final arrayCount = coreFoundation + .lookupFunction< + IntPtr Function(Pointer), + int Function(Pointer) + >('CFArrayGetCount'); + final arrayValue = coreFoundation + .lookupFunction< + Pointer Function(Pointer, IntPtr), + Pointer Function(Pointer, int) + >('CFArrayGetValueAtIndex'); + final stringLength = coreFoundation + .lookupFunction< + IntPtr Function(Pointer), + int Function(Pointer) + >('CFStringGetLength'); + final stringCharacters = coreFoundation + .lookupFunction< + Void Function(Pointer, _CFRange, Pointer), + void Function(Pointer, _CFRange, Pointer) + >('CFStringGetCharacters'); + final release = coreFoundation + .lookupFunction< + Void Function(Pointer), + void Function(Pointer) + >('CFRelease'); + + String read(Pointer string, Arena arena) { + final length = stringLength(string); + final range = Struct.create<_CFRange>() + ..location = 0 + ..length = length; + final buffer = arena(length); + stringCharacters(string, range, buffer); + return buffer.cast().toDartString(length: length); + } + + final names = copyFamilyNames(); + try { + return using( + (arena) => [ + for (var i = 0; i < arrayCount(names); i++) + read(arrayValue(names, i), arena), + ], + ); + } finally { + release(names); + } +} + +extension type const _ComObject(Pointer pointer) { + Pointer> method(int index) => + pointer.cast>>>().value[index]; + + void release() => method)>( + 2, + ).asFunction)>()(pointer); +} + +typedef _GetObjectNative = + Int32 Function(Pointer, Pointer>); +typedef _GetObject = int Function(Pointer, Pointer>); +typedef _GetIndexedNative = + Int32 Function(Pointer, Uint32, Pointer>); +typedef _GetIndexed = int Function(Pointer, int, Pointer>); + +const _dwriteFactoryTypeShared = 0; + +const _iidDWriteFactory = '{B859EE5A-D838-4B5B-A2E8-1ADC7D93DB48}'; + +void _check(int hresult, String call) { + if (hresult < 0) { + throw OSError(call, hresult); + } +} + +List _directWriteFamilies() => using((arena) { + final createFactory = DynamicLibrary.open('dwrite.dll') + .lookupFunction< + Int32 Function(Int32, Pointer, Pointer>), + int Function(int, Pointer, Pointer>) + >('DWriteCreateFactory'); + final out = arena>(); + _check( + createFactory( + _dwriteFactoryTypeShared, + GUID(_iidDWriteFactory).toNative(allocator: arena), + out, + ), + 'DWriteCreateFactory', + ); + final factory = _ComObject(out.value); + try { + _check( + factory + .method, Pointer>, Int32)>( + 3, + ) + .asFunction< + int Function(Pointer, Pointer>, int) + >()(factory.pointer, out, 0), + 'GetSystemFontCollection', + ); + final collection = _ComObject(out.value); + try { + final count = collection + .method)>(3) + .asFunction)>()(collection.pointer); + final family = collection + .method<_GetIndexedNative>(4) + .asFunction<_GetIndexed>(); + return [ + for (var i = 0; i < count; i++) + if (family(collection.pointer, i, out) >= 0) + ?_directWriteFamilyName(_ComObject(out.value), arena), + ]; + } finally { + collection.release(); + } + } finally { + factory.release(); + } +}); + +String? _directWriteFamilyName(_ComObject family, Arena arena) { + try { + final out = arena>(); + final familyNames = family + .method<_GetObjectNative>(6) + .asFunction<_GetObject>(); + if (familyNames(family.pointer, out) < 0) { + return null; + } + final names = _ComObject(out.value); + try { + final index = arena(); + final exists = arena(); + final findLocale = names + .method< + Int32 Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(4) + .asFunction< + int Function( + Pointer, + Pointer, + Pointer, + Pointer, + ) + >(); + final found = + findLocale( + names.pointer, + 'en-us'.toNativeUtf16(allocator: arena), + index, + exists, + ) >= + 0 && + exists.value != 0; + final at = found ? index.value : 0; + final length = arena(); + final stringLength = names + .method, Uint32, Pointer)>(7) + .asFunction, int, Pointer)>(); + if (stringLength(names.pointer, at, length) < 0) { + return null; + } + final size = length.value + 1; + final buffer = arena(size); + final string = names + .method< + Int32 Function(Pointer, Uint32, Pointer, Uint32) + >(8) + .asFunction, int, Pointer, int)>(); + if (string(names.pointer, at, buffer, size) < 0) { + return null; + } + return buffer.cast().toDartString(length: length.value); + } finally { + names.release(); + } + } finally { + family.release(); + } +} + +final class _FcFontSet extends Struct { + @Int32() + external int nfont; + + @Int32() + external int sfont; + + external Pointer> fonts; +} + +List _fontconfigFamilies() => using((arena) { + final fontconfig = DynamicLibrary.open('libfontconfig.so.1'); + final createPattern = fontconfig + .lookupFunction Function(), Pointer Function()>( + 'FcPatternCreate', + ); + final addBool = fontconfig + .lookupFunction< + Int32 Function(Pointer, Pointer, Int32), + int Function(Pointer, Pointer, int) + >('FcPatternAddBool'); + final createObjectSet = fontconfig + .lookupFunction Function(), Pointer Function()>( + 'FcObjectSetCreate', + ); + final addObject = fontconfig + .lookupFunction< + Int32 Function(Pointer, Pointer), + int Function(Pointer, Pointer) + >('FcObjectSetAdd'); + final listFonts = fontconfig + .lookupFunction< + Pointer<_FcFontSet> Function( + Pointer, + Pointer, + Pointer, + ), + Pointer<_FcFontSet> Function( + Pointer, + Pointer, + Pointer, + ) + >('FcFontList'); + final getString = fontconfig + .lookupFunction< + Int32 Function( + Pointer, + Pointer, + Int32, + Pointer>, + ), + int Function(Pointer, Pointer, int, Pointer>) + >('FcPatternGetString'); + final destroyFontSet = fontconfig + .lookupFunction< + Void Function(Pointer<_FcFontSet>), + void Function(Pointer<_FcFontSet>) + >('FcFontSetDestroy'); + final destroyObjectSet = fontconfig + .lookupFunction< + Void Function(Pointer), + void Function(Pointer) + >('FcObjectSetDestroy'); + final destroyPattern = fontconfig + .lookupFunction< + Void Function(Pointer), + void Function(Pointer) + >('FcPatternDestroy'); + + final family = 'family'.toNativeUtf8(allocator: arena); + final pattern = createPattern(); + final objectSet = createObjectSet(); + try { + addBool(pattern, 'scalable'.toNativeUtf8(allocator: arena), 1); + addObject(objectSet, family); + final fontSet = listFonts(nullptr, pattern, objectSet); + if (fontSet == nullptr) { + return const []; + } + try { + final value = arena>(); + final fonts = fontSet.ref.fonts; + return [ + for (var i = 0; i < fontSet.ref.nfont; i++) + if (getString(fonts[i], family, 0, value) == 0) + value.value.toDartString(), + ]; + } finally { + destroyFontSet(fontSet); + } + } finally { + destroyObjectSet(objectSet); + destroyPattern(pattern); + } +}); diff --git a/lib/common/task.dart b/lib/common/task.dart index aa388bf5..0667df0e 100644 --- a/lib/common/task.dart +++ b/lib/common/task.dart @@ -282,7 +282,10 @@ Future<({String yaml, String md5})> _makeRealProfileTask( } rawDns = mergeDnsOverride( rawDns, - overrides.dns.overrideJson(overrides.dnsOverrideKeys), + overrides.dns.overrideJson( + overrides.dnsOverrideKeys, + keepEmpty: data.keepEmptyDnsOverrides, + ), ); rawConfig['dns'] = rawDns; final ntpOverride = overrides.ntp.overrideJson(overrides.ntpOverrideKeys); diff --git a/lib/database/custom_proxies.dart b/lib/database/custom_proxies.dart index 4c7c3c71..fbe097ec 100644 --- a/lib/database/custom_proxies.dart +++ b/lib/database/custom_proxies.dart @@ -11,6 +11,9 @@ class CustomProxies extends Table { TextColumn get order => text().nullable()(); + TextColumn get emptyOptions => + text().map(const StringSetConverter()).nullable()(); + @override Set get primaryKey => {id}; } @@ -59,7 +62,15 @@ class CustomProxiesDao extends DatabaseAccessor extension RawCustomProxyExt on RawCustomProxy { CustomProxy toCustomProxy() { - return CustomProxy(id: id, definition: definition, order: order); + final fields = ProxyField.values.asNameMap(); + return CustomProxy( + id: id, + definition: definition, + order: order, + emptyOptions: { + for (final name in emptyOptions ?? const {}) ?fields[name], + }, + ); } } @@ -69,6 +80,7 @@ extension CustomProxiesCompanionExt on CustomProxy { id: Value(id), definition: definition, order: Value(order ?? this.order), + emptyOptions: Value({for (final field in emptyOptions) field.name}), ); } } diff --git a/lib/database/database.dart b/lib/database/database.dart index 556c61a2..330523d6 100644 --- a/lib/database/database.dart +++ b/lib/database/database.dart @@ -50,7 +50,7 @@ class Database extends _$Database { Database([QueryExecutor? executor]) : super(executor ?? _openConnection()); @override - int get schemaVersion => 19; + int get schemaVersion => 20; static LazyDatabase _openConnection() { return LazyDatabase(() async { @@ -104,6 +104,13 @@ class Database extends _$Database { if (from < 19) { await _addColumnIfMissing(m, profiles, profiles.ageSecretKey); } + if (from < 20) { + await _addColumnIfMissing( + m, + customProxies, + customProxies.emptyOptions, + ); + } }), beforeOpen: (_) => customStatement('PRAGMA foreign_keys = ON'), ); @@ -160,7 +167,15 @@ class Database extends _$Database { await customStatement( 'DROP INDEX IF EXISTS idx_custom_proxies_profile_order', ); - await m.alterTable(TableMigration(customProxies)); + await m.alterTable( + TableMigration( + customProxies, + newColumns: [ + if (!columns.contains(customProxies.emptyOptions.name)) + customProxies.emptyOptions, + ], + ), + ); } /// Schema 17 kept each group option in a column of its own. diff --git a/lib/database/generated/database.g.dart b/lib/database/generated/database.g.dart index a8661a7d..c020674e 100644 --- a/lib/database/generated/database.g.dart +++ b/lib/database/generated/database.g.dart @@ -3793,7 +3793,16 @@ class $CustomProxiesTable extends CustomProxies requiredDuringInsert: false, ); @override - List get $columns => [id, definition, order]; + late final GeneratedColumnWithTypeConverter?, String> + emptyOptions = GeneratedColumn( + 'empty_options', + aliasedName, + true, + type: DriftSqlType.string, + requiredDuringInsert: false, + ).withConverter?>($CustomProxiesTable.$converteremptyOptionsn); + @override + List get $columns => [id, definition, order, emptyOptions]; @override String get aliasedName => _alias ?? actualTableName; @override @@ -3838,6 +3847,12 @@ class $CustomProxiesTable extends CustomProxies DriftSqlType.string, data['${effectivePrefix}order'], ), + emptyOptions: $CustomProxiesTable.$converteremptyOptionsn.fromSql( + attachedDatabase.typeMapping.read( + DriftSqlType.string, + data['${effectivePrefix}empty_options'], + ), + ), ); } @@ -3848,16 +3863,22 @@ class $CustomProxiesTable extends CustomProxies static TypeConverter, String> $converterdefinition = const JsonMapConverter(); + static TypeConverter, String> $converteremptyOptions = + const StringSetConverter(); + static TypeConverter?, String?> $converteremptyOptionsn = + NullAwareTypeConverter.wrap($converteremptyOptions); } class RawCustomProxy extends DataClass implements Insertable { final int id; final Map definition; final String? order; + final Set? emptyOptions; const RawCustomProxy({ required this.id, required this.definition, this.order, + this.emptyOptions, }); @override Map toColumns(bool nullToAbsent) { @@ -3871,6 +3892,11 @@ class RawCustomProxy extends DataClass implements Insertable { if (!nullToAbsent || order != null) { map['order'] = Variable(order); } + if (!nullToAbsent || emptyOptions != null) { + map['empty_options'] = Variable( + $CustomProxiesTable.$converteremptyOptionsn.toSql(emptyOptions), + ); + } return map; } @@ -3881,6 +3907,9 @@ class RawCustomProxy extends DataClass implements Insertable { order: order == null && nullToAbsent ? const Value.absent() : Value(order), + emptyOptions: emptyOptions == null && nullToAbsent + ? const Value.absent() + : Value(emptyOptions), ); } @@ -3893,6 +3922,7 @@ class RawCustomProxy extends DataClass implements Insertable { id: serializer.fromJson(json['id']), definition: serializer.fromJson>(json['definition']), order: serializer.fromJson(json['order']), + emptyOptions: serializer.fromJson?>(json['emptyOptions']), ); } @override @@ -3902,6 +3932,7 @@ class RawCustomProxy extends DataClass implements Insertable { 'id': serializer.toJson(id), 'definition': serializer.toJson>(definition), 'order': serializer.toJson(order), + 'emptyOptions': serializer.toJson?>(emptyOptions), }; } @@ -3909,10 +3940,12 @@ class RawCustomProxy extends DataClass implements Insertable { int? id, Map? definition, Value order = const Value.absent(), + Value?> emptyOptions = const Value.absent(), }) => RawCustomProxy( id: id ?? this.id, definition: definition ?? this.definition, order: order.present ? order.value : this.order, + emptyOptions: emptyOptions.present ? emptyOptions.value : this.emptyOptions, ); RawCustomProxy copyWithCompanion(CustomProxiesCompanion data) { return RawCustomProxy( @@ -3921,6 +3954,9 @@ class RawCustomProxy extends DataClass implements Insertable { ? data.definition.value : this.definition, order: data.order.present ? data.order.value : this.order, + emptyOptions: data.emptyOptions.present + ? data.emptyOptions.value + : this.emptyOptions, ); } @@ -3929,45 +3965,52 @@ class RawCustomProxy extends DataClass implements Insertable { return (StringBuffer('RawCustomProxy(') ..write('id: $id, ') ..write('definition: $definition, ') - ..write('order: $order') + ..write('order: $order, ') + ..write('emptyOptions: $emptyOptions') ..write(')')) .toString(); } @override - int get hashCode => Object.hash(id, definition, order); + int get hashCode => Object.hash(id, definition, order, emptyOptions); @override bool operator ==(Object other) => identical(this, other) || (other is RawCustomProxy && other.id == this.id && other.definition == this.definition && - other.order == this.order); + other.order == this.order && + other.emptyOptions == this.emptyOptions); } class CustomProxiesCompanion extends UpdateCompanion { final Value id; final Value> definition; final Value order; + final Value?> emptyOptions; const CustomProxiesCompanion({ this.id = const Value.absent(), this.definition = const Value.absent(), this.order = const Value.absent(), + this.emptyOptions = const Value.absent(), }); CustomProxiesCompanion.insert({ this.id = const Value.absent(), required Map definition, this.order = const Value.absent(), + this.emptyOptions = const Value.absent(), }) : definition = Value(definition); static Insertable custom({ Expression? id, Expression? definition, Expression? order, + Expression? emptyOptions, }) { return RawValuesInsertable({ if (id != null) 'id': id, if (definition != null) 'definition': definition, if (order != null) 'order': order, + if (emptyOptions != null) 'empty_options': emptyOptions, }); } @@ -3975,11 +4018,13 @@ class CustomProxiesCompanion extends UpdateCompanion { Value? id, Value>? definition, Value? order, + Value?>? emptyOptions, }) { return CustomProxiesCompanion( id: id ?? this.id, definition: definition ?? this.definition, order: order ?? this.order, + emptyOptions: emptyOptions ?? this.emptyOptions, ); } @@ -3997,6 +4042,11 @@ class CustomProxiesCompanion extends UpdateCompanion { if (order.present) { map['order'] = Variable(order.value); } + if (emptyOptions.present) { + map['empty_options'] = Variable( + $CustomProxiesTable.$converteremptyOptionsn.toSql(emptyOptions.value), + ); + } return map; } @@ -4005,7 +4055,8 @@ class CustomProxiesCompanion extends UpdateCompanion { return (StringBuffer('CustomProxiesCompanion(') ..write('id: $id, ') ..write('definition: $definition, ') - ..write('order: $order') + ..write('order: $order, ') + ..write('emptyOptions: $emptyOptions') ..write(')')) .toString(); } @@ -7291,12 +7342,14 @@ typedef $$CustomProxiesTableCreateCompanionBuilder = Value id, required Map definition, Value order, + Value?> emptyOptions, }); typedef $$CustomProxiesTableUpdateCompanionBuilder = CustomProxiesCompanion Function({ Value id, Value> definition, Value order, + Value?> emptyOptions, }); final class $$CustomProxiesTableReferences @@ -7355,6 +7408,12 @@ class $$CustomProxiesTableFilterComposer builder: (column) => ColumnFilters(column), ); + ColumnWithTypeConverterFilters?, Set, String> + get emptyOptions => $composableBuilder( + column: $table.emptyOptions, + builder: (column) => ColumnWithTypeConverterFilters(column), + ); + Expression proxyDialersRefs( Expression Function($$ProxyDialersTableFilterComposer f) f, ) { @@ -7404,6 +7463,11 @@ class $$CustomProxiesTableOrderingComposer column: $table.order, builder: (column) => ColumnOrderings(column), ); + + ColumnOrderings get emptyOptions => $composableBuilder( + column: $table.emptyOptions, + builder: (column) => ColumnOrderings(column), + ); } class $$CustomProxiesTableAnnotationComposer @@ -7427,6 +7491,12 @@ class $$CustomProxiesTableAnnotationComposer GeneratedColumn get order => $composableBuilder(column: $table.order, builder: (column) => column); + GeneratedColumnWithTypeConverter?, String> get emptyOptions => + $composableBuilder( + column: $table.emptyOptions, + builder: (column) => column, + ); + Expression proxyDialersRefs( Expression Function($$ProxyDialersTableAnnotationComposer a) f, ) { @@ -7484,20 +7554,24 @@ class $$CustomProxiesTableTableManager Value id = const Value.absent(), Value> definition = const Value.absent(), Value order = const Value.absent(), + Value?> emptyOptions = const Value.absent(), }) => CustomProxiesCompanion( id: id, definition: definition, order: order, + emptyOptions: emptyOptions, ), createCompanionCallback: ({ Value id = const Value.absent(), required Map definition, Value order = const Value.absent(), + Value?> emptyOptions = const Value.absent(), }) => CustomProxiesCompanion.insert( id: id, definition: definition, order: order, + emptyOptions: emptyOptions, ), withReferenceMapper: (p0) => p0 .map( diff --git a/lib/database/groups.dart b/lib/database/groups.dart index c0a2b50f..9667859b 100644 --- a/lib/database/groups.dart +++ b/lib/database/groups.dart @@ -113,11 +113,23 @@ class ProxyGroupsDao extends DatabaseAccessor if (!use.contains(oldName)) { continue; } + final usePosition = row.toProxyGroup().usePosition; await (proxyGroups.update()..where((t) => t.id.equals(row.id))).write( ProxyGroupsCompanion( use: Value([ for (final name in use) name == oldName ? newName : name, ]), + definition: usePosition?.containsKey(oldName) == true + ? Value( + json.encode({ + ...json.decode(row.definition) as Map, + 'use-position': { + for (final MapEntry(:key, :value) in usePosition!.entries) + key == oldName ? newName : key: value, + }, + }), + ) + : const Value.absent(), ), ); } diff --git a/lib/features/connection/tracker_info_item.dart b/lib/features/connection/tracker_info_item.dart index d76ae660..5cdea17b 100644 --- a/lib/features/connection/tracker_info_item.dart +++ b/lib/features/connection/tracker_info_item.dart @@ -52,7 +52,7 @@ class TrackerInfoItem extends ConsumerWidget { ); return RecordListItem( onTap: () { - showExtend( + showInSheet( context, builder: (_) { return CommonScaffold( @@ -63,29 +63,11 @@ class TrackerInfoItem extends ConsumerWidget { ); }, header: _buildHeader(context), - body: _buildBody(showIcon: showIcon), - ); - } - - Widget _buildBody({required bool showIcon}) { - final process = trackerInfo.metadata.process; - final body = _TrackerInfoBody( - trackerInfo: trackerInfo, - onClickKeyword: onClickKeyword, - ); - if (!showIcon || process.isEmpty) { - return body; - } - return Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: 12, - children: [ - GestureDetector( - onTap: () => onClickKeyword?.call(process), - child: PackageIcon(packageName: process, size: 40), - ), - Expanded(child: body), - ], + body: _TrackerInfoBody( + trackerInfo: trackerInfo, + showIcon: showIcon, + onClickKeyword: onClickKeyword, + ), ); } @@ -128,7 +110,7 @@ class TrackerInfoItem extends ConsumerWidget { ], ), children: [ - Text(trackerInfo.start.getLastUpdateTimeDesc(context)), + Text(trackerInfo.start.getLastUpdateTimeDesc(context, compact: true)), network, ], ); @@ -137,9 +119,44 @@ class TrackerInfoItem extends ConsumerWidget { class _TrackerInfoBody extends StatelessWidget { final TrackerInfo trackerInfo; + final bool showIcon; final Function(String)? onClickKeyword; - const _TrackerInfoBody({required this.trackerInfo, this.onClickKeyword}); + const _TrackerInfoBody({ + required this.trackerInfo, + required this.showIcon, + this.onClickKeyword, + }); + + Widget _buildTitle(RecordTextStyles styles) { + final metadata = trackerInfo.metadata; + final title = Text.rich( + TextSpan( + children: [ + TextSpan( + text: _endpointText(trackerInfo.title, metadata.destinationPort), + style: styles.primary?.copyWith(fontWeight: FontWeight.w500), + ), + if (metadata.host.isNotEmpty && metadata.destinationIP.isNotEmpty) + TextSpan(text: ' ${metadata.destinationIP}', style: styles.muted), + ], + ), + ); + final process = metadata.process; + if (!showIcon || process.isEmpty) { + return title; + } + return Row( + spacing: 8, + children: [ + GestureDetector( + onTap: () => onClickKeyword?.call(process), + child: PackageIcon(packageName: process, size: 24), + ), + Flexible(child: title), + ], + ); + } @override Widget build(BuildContext context) { @@ -155,24 +172,7 @@ class _TrackerInfoBody extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, spacing: 4, children: [ - Text.rich( - TextSpan( - children: [ - TextSpan( - text: _endpointText( - trackerInfo.title, - metadata.destinationPort, - ), - style: styles.primary?.copyWith(fontWeight: FontWeight.w500), - ), - if (metadata.host.isNotEmpty && metadata.destinationIP.isNotEmpty) - TextSpan( - text: ' ${metadata.destinationIP}', - style: styles.muted, - ), - ], - ), - ), + _buildTitle(styles), Wrap( spacing: 6, runSpacing: 4, diff --git a/lib/features/connection/tracker_info_list.dart b/lib/features/connection/tracker_info_list.dart index e407bade..6486cdd6 100644 --- a/lib/features/connection/tracker_info_list.dart +++ b/lib/features/connection/tracker_info_list.dart @@ -72,6 +72,7 @@ class TrackerInfoList extends StatelessWidget { controller: controller, padding: padding, itemCount: trackerInfos.length, + extentEstimation: zeroDividerExtentEstimation, separatorBuilder: (_, _) => const Divider(height: 0), itemBuilder: (_, index) => _buildTrackerInfoItem( context, diff --git a/lib/features/editor/clash_schema.dart b/lib/features/editor/clash_schema.dart index 2c1f49b3..d57656f1 100644 --- a/lib/features/editor/clash_schema.dart +++ b/lib/features/editor/clash_schema.dart @@ -1302,6 +1302,7 @@ const _proxyGroup = YamlSchema.map( ]), 'proxies': YamlSchema.list(_policy), 'use': YamlSchema.list(YamlSchema.of(YamlScalar.proxyProvider)), + 'use-position': YamlSchema.dict(_str), 'url': YamlSchema.scalar(['https://www.gstatic.com/generate_204']), 'interval': _str, 'lazy': _bool, diff --git a/lib/features/ip_quality/ip_quality_sheet.dart b/lib/features/ip_quality/ip_quality_sheet.dart index 7318da95..476a5c0f 100644 --- a/lib/features/ip_quality/ip_quality_sheet.dart +++ b/lib/features/ip_quality/ip_quality_sheet.dart @@ -34,7 +34,7 @@ class _IpQualitySheet extends ConsumerWidget { final isLoading = ref.watch( ipQualityProvider(ip).select((result) => result.isLoading), ); - final page = CommonScaffold( + return CommonScaffold( title: localizations.outboundIp, actions: [ AppBarActionButton( @@ -48,13 +48,6 @@ class _IpQualitySheet extends ConsumerWidget { ], body: _IpQualityDetail(ip: ip), ); - if (!isSheetPage(context)) return page; - return ConstrainedBox( - constraints: BoxConstraints( - maxHeight: ref.sheetHeight(context, shortSheetMaxHeight), - ), - child: page, - ); } } diff --git a/lib/l10n/intl/messages_en.dart b/lib/l10n/intl/messages_en.dart index b10be9d1..c2ef8b44 100644 --- a/lib/l10n/intl/messages_en.dart +++ b/lib/l10n/intl/messages_en.dart @@ -66,133 +66,143 @@ class MessageLookup extends MessageLookupByLibrary { static String m16(count) => "${Intl.plural(count, one: '1 day ago', other: '${count} days ago')}"; - static String m17(label) => + static String m17(count) => "${Intl.plural(count, other: '${count}d ago')}"; + + static String m18(label) => "Are you sure you want to delete the selected ${label}?"; - static String m18(label) => "Are you sure you want to delete this ${label}?"; + static String m19(label) => "Are you sure you want to delete this ${label}?"; - static String m19(label) => "${label} details"; + static String m20(label) => "${label} details"; - static String m20(name) => + static String m21(name) => "Dialing through ${name} leads back to the proxy that uses it, so the connection was stopped"; - static String m21(label) => "${label} cannot be empty"; + static String m22(label) => "${label} cannot be empty"; - static String m22(label) => "${label} already exists"; + static String m23(label) => "${label} already exists"; - static String m23(count) => "${count} items failed"; + static String m24(count) => "${count} items failed"; - static String m24(label, message) => "${label}: ${message}"; + static String m25(label, message) => "${label}: ${message}"; - static String m25(name) => "${name} is already up to date"; + static String m26(name) => "${name} is already up to date"; - static String m26(name) => "${name} updated"; + static String m27(name) => "${name} updated"; - static String m27(action) => + static String m28(action) => "Already used by “${action}”. Saving moves it here."; - static String m28(modifiers) => "Include at least one of ${modifiers}"; - - static String m29(count) => - "${Intl.plural(count, one: '1 hour ago', other: '${count} hours ago')}"; + static String m29(modifiers) => "Include at least one of ${modifiers}"; static String m30(count) => + "${Intl.plural(count, one: '1 hour ago', other: '${count} hours ago')}"; + + static String m31(count) => "${Intl.plural(count, other: '${count}h ago')}"; + + static String m32(count) => "${Intl.plural(count, one: '1 hour', other: '${count} hours')}"; - static String m31(target) => "${target} is an invalid policy"; + static String m33(target) => "${target} is an invalid policy"; - static String m32(proxyName) => "${proxyName} is an invalid proxy"; + static String m34(proxyName) => "${proxyName} is an invalid proxy"; - static String m33(providerName) => + static String m35(providerName) => "${providerName} is an invalid proxy provider"; - static String m34(ruleSet) => "${ruleSet} is an invalid rule set"; + static String m36(ruleSet) => "${ruleSet} is an invalid rule set"; - static String m35(subRule) => "${subRule} is an invalid SUB_RULE"; + static String m37(subRule) => "${subRule} is an invalid SUB_RULE"; - static String m36(count) => + static String m38(count) => "${Intl.plural(count, one: '1 item', other: '${count} items')}"; - static String m37(line, message) => "Line ${line}: ${message}"; + static String m39(line, message) => "Line ${line}: ${message}"; - static String m38(appName) => + static String m40(appName) => "1. Open System Settings > Privacy & Security\n2. Choose Location Services\n3. Find and check ${appName} in the list\n\nWhen you are done, return to the app to continue. Thank you for your cooperation."; - static String m39(label, max) => "${label} must be at most ${max} characters"; + static String m41(label, max) => "${label} must be at most ${max} characters"; - static String m40(size) => "Released ${size}"; + static String m42(size) => "Released ${size}"; - static String m41(count) => + static String m43(count) => "${Intl.plural(count, one: '1 minute ago', other: '${count} minutes ago')}"; - static String m42(count) => + static String m44(count) => "${Intl.plural(count, other: '${count}m ago')}"; + + static String m45(count) => "${Intl.plural(count, one: '1 month ago', other: '${count} months ago')}"; - static String m43(code) => - "The server denied access (HTTP ${code}). The link may have expired, or the credentials are wrong"; - - static String m44(code) => "The server rejected the request (HTTP ${code})"; - - static String m45(code) => - "Nothing was found at this address (HTTP ${code}). Check that the URL is correct"; - - static String m46(detail) => "Network request failed: ${detail}"; + static String m46(count) => "${Intl.plural(count, other: '${count}mo ago')}"; static String m47(code) => + "The server denied access (HTTP ${code}). The link may have expired, or the credentials are wrong"; + + static String m48(code) => "The server rejected the request (HTTP ${code})"; + + static String m49(code) => + "Nothing was found at this address (HTTP ${code}). Check that the URL is correct"; + + static String m50(detail) => "Network request failed: ${detail}"; + + static String m51(code) => "The server ran into a problem (HTTP ${code}). Try again later"; - static String m48(label) => "No ${label} yet"; + static String m52(label) => "No ${label} yet"; - static String m49(label, min, max) => + static String m53(label, min, max) => "${label} must be between ${min} and ${max}"; - static String m50(label) => "${label} must be a number"; + static String m54(label) => "${label} must be a number"; - static String m51(label) => "${label} must be between 1024 and 49151"; + static String m55(label) => "${label} must be between 1024 and 49151"; - static String m52(label, profiles) => + static String m56(label, profiles) => "${label} is still used by ${profiles}, which will not work properly once it is deleted. Are you sure you want to delete it?"; - static String m53(count) => - "${Intl.plural(count, one: '1 proxy', other: '${count} proxies')}"; + static String m57(count, label) => + "${Intl.plural(count, one: '1 proxy', other: '${label} proxies')}"; - static String m54(name, count) => "${name} and others (${count} proxies)"; + static String m58(name, count) => "${name} and others (${count} proxies)"; - static String m55(removed) => + static String m59(removed) => "These proxies will be deleted: ${removed}. A line whose name changed counts as a new proxy, and custom profiles naming the deleted ones will flag them as missing. Apply anyway?"; - static String m56(removed, added) => + static String m60(removed, added) => "These proxies will be deleted: ${removed}. These will be added: ${added}. A line whose name changed counts as a new proxy, and custom profiles naming the deleted ones will flag them as missing. Apply anyway?"; - static String m57(label) => + static String m61(label) => "${label} is still named by rules, proxy groups, dialer proxies, DNS or NTP in this profile, which stop working once it is deleted. Are you sure you want to delete it?"; - static String m58(count) => - "${Intl.plural(count, one: '1 rule', other: '${count} rules')}"; + static String m62(count, label) => + "${Intl.plural(count, one: '1 rule', other: '${label} rules')}"; - static String m59(appName) => "${appName} (Safe mode)"; - - static String m60(count) => - "${Intl.plural(count, one: '1 second', other: '${count} seconds')}"; - - static String m61(count) => "${count} in effect"; - - static String m62(count) => "${count} selected"; - - static String m63(time) => "Checked at ${time}"; + static String m63(appName) => "${appName} (Safe mode)"; static String m64(count) => - "${Intl.plural(count, one: '1 setting', other: '${count} settings')}"; + "${Intl.plural(count, one: '1 second', other: '${count} seconds')}"; - static String m65(link) => "No proxy could be read from ${link}"; + static String m65(count) => "${count} in effect"; - static String m66(label) => "${label} must be a single item"; + static String m66(count) => "${count} selected"; - static String m67(label) => "${label} must be a URL"; + static String m67(time) => "Checked at ${time}"; static String m68(count) => + "${Intl.plural(count, one: '1 setting', other: '${count} settings')}"; + + static String m69(link) => "No proxy could be read from ${link}"; + + static String m70(label) => "${label} must be a single item"; + + static String m71(label) => "${label} must be a URL"; + + static String m72(count) => "${Intl.plural(count, one: '1 year ago', other: '${count} years ago')}"; + static String m73(count) => "${Intl.plural(count, other: '${count}y ago')}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("About"), @@ -261,11 +271,19 @@ class MessageLookup extends MessageLookupByLibrary { "advancedConfigDesc": MessageLookupByLibrary.simpleMessage( "Network, DNS, added rules, and scripts", ), + "ageEncryptedProfile": MessageLookupByLibrary.simpleMessage( + "Encrypted profile", + ), + "ageSecretKey": MessageLookupByLibrary.simpleMessage("Age secret key"), + "ageSecretKeyRequiredTip": MessageLookupByLibrary.simpleMessage( + "This profile is encrypted with age and needs an age secret key to decrypt", + ), "agree": MessageLookupByLibrary.simpleMessage("Agree"), "allowBypass": MessageLookupByLibrary.simpleMessage( "Allow apps to bypass VPN", ), "allowLan": MessageLookupByLibrary.simpleMessage("Allow LAN"), + "allowedIps": MessageLookupByLibrary.simpleMessage("Allowed IPs"), "answers": MessageLookupByLibrary.simpleMessage("Answers"), "app": MessageLookupByLibrary.simpleMessage("App"), "appAccessControl": MessageLookupByLibrary.simpleMessage( @@ -369,6 +387,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "cancel": MessageLookupByLibrary.simpleMessage("Cancel"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("Deselect all"), + "cannotSave": MessageLookupByLibrary.simpleMessage("Can\'t save"), "changeProxyFailedTip": MessageLookupByLibrary.simpleMessage( "Failed to switch proxy; the previous selection has been restored", ), @@ -389,12 +408,16 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdateError": MessageLookupByLibrary.simpleMessage( "The app is already up to date", ), + "cipher": MessageLookupByLibrary.simpleMessage("Cipher"), "clearData": MessageLookupByLibrary.simpleMessage("Clear data"), "clearDataAndExitTip": MessageLookupByLibrary.simpleMessage( "This deletes all profiles, settings, and local data, then closes the app. Open it again to start fresh.", ), "clearDataFailed": m1, "clearSearch": MessageLookupByLibrary.simpleMessage("Clear search"), + "clientFingerprint": MessageLookupByLibrary.simpleMessage( + "Client fingerprint", + ), "clipboardExport": MessageLookupByLibrary.simpleMessage( "Export to clipboard", ), @@ -425,6 +448,9 @@ class MessageLookup extends MessageLookupByLibrary { "confirmForceCrashCore": MessageLookupByLibrary.simpleMessage( "Are you sure you want to force crash the core?", ), + "congestionControl": MessageLookupByLibrary.simpleMessage( + "Congestion control", + ), "congestionController": MessageLookupByLibrary.simpleMessage( "TCP congestion control", ), @@ -486,6 +512,12 @@ class MessageLookup extends MessageLookupByLibrary { "customProfileDesc": MessageLookupByLibrary.simpleMessage( "Assemble proxy groups and rules from app proxies and other profiles", ), + "customProfileResources": MessageLookupByLibrary.simpleMessage( + "Custom profile resources", + ), + "customProfileResourcesDesc": MessageLookupByLibrary.simpleMessage( + "Proxies, rule providers, filters, and icon sets", + ), "cut": MessageLookupByLibrary.simpleMessage("Cut"), "dark": MessageLookupByLibrary.simpleMessage("Dark"), "dashboard": MessageLookupByLibrary.simpleMessage("Dashboard"), @@ -499,6 +531,7 @@ class MessageLookup extends MessageLookupByLibrary { "Failed to save the change; it has been rolled back", ), "daysAgo": m16, + "daysAgoShort": m17, "defaultSelected": MessageLookupByLibrary.simpleMessage( "Default selection", ), @@ -510,8 +543,8 @@ class MessageLookup extends MessageLookupByLibrary { "delayFailed": MessageLookupByLibrary.simpleMessage("Failed"), "delayTest": MessageLookupByLibrary.simpleMessage("Delay test"), "delete": MessageLookupByLibrary.simpleMessage("Delete"), - "deleteMultipTip": m17, - "deleteTip": m18, + "deleteMultipTip": m18, + "deleteTip": m19, "desc": MessageLookupByLibrary.simpleMessage( "A multi-platform proxy client based on ClashMeta, simple and easy to use, open-source and ad-free.", ), @@ -522,7 +555,7 @@ class MessageLookup extends MessageLookupByLibrary { "destinationIPASN": MessageLookupByLibrary.simpleMessage( "Destination IP ASN", ), - "details": m19, + "details": m20, "detectionTip": MessageLookupByLibrary.simpleMessage( "Relies on a third-party API; for reference only", ), @@ -534,7 +567,7 @@ class MessageLookup extends MessageLookupByLibrary { "dialerProxyDesc": MessageLookupByLibrary.simpleMessage( "Dialer proxy is the outbound used to reach the NTP server.", ), - "dialerProxyLoopStopped": m20, + "dialerProxyLoopStopped": m21, "direct": MessageLookupByLibrary.simpleMessage("Direct"), "disableIcmpForwarding": MessageLookupByLibrary.simpleMessage( "Disable ICMP forwarding", @@ -667,6 +700,9 @@ class MessageLookup extends MessageLookupByLibrary { "docked": MessageLookupByLibrary.simpleMessage("Docked"), "domain": MessageLookupByLibrary.simpleMessage("Domain"), "download": MessageLookupByLibrary.simpleMessage("Download"), + "downloadBandwidth": MessageLookupByLibrary.simpleMessage( + "Download bandwidth", + ), "edit": MessageLookupByLibrary.simpleMessage("Edit"), "editGlobalRules": MessageLookupByLibrary.simpleMessage( "Edit global rules", @@ -679,7 +715,7 @@ class MessageLookup extends MessageLookupByLibrary { "Editor unavailable", ), "emptyFallback": MessageLookupByLibrary.simpleMessage("Empty fallback"), - "emptyTip": m21, + "emptyTip": m22, "en": MessageLookupByLibrary.simpleMessage("English"), "enabled": MessageLookupByLibrary.simpleMessage("Enabled"), "entries": MessageLookupByLibrary.simpleMessage(" entries"), @@ -697,7 +733,7 @@ class MessageLookup extends MessageLookupByLibrary { "excludeSsidsDesc": MessageLookupByLibrary.simpleMessage( "When connected to Wi-Fi with an excluded SSID, the app\'s running state switches automatically.", ), - "existsTip": m22, + "existsTip": m23, "exit": MessageLookupByLibrary.simpleMessage("Exit"), "exitFullScreen": MessageLookupByLibrary.simpleMessage("Exit full screen"), "exitNodes": MessageLookupByLibrary.simpleMessage("Exit nodes"), @@ -723,8 +759,8 @@ class MessageLookup extends MessageLookupByLibrary { "externalLink": MessageLookupByLibrary.simpleMessage("External link"), "extraLarge": MessageLookupByLibrary.simpleMessage("Extra large"), "fade": MessageLookupByLibrary.simpleMessage("Fade"), - "failedCount": m23, - "failedItem": m24, + "failedCount": m24, + "failedItem": m25, "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fake-IP filter"), "fakeipFilterMode": MessageLookupByLibrary.simpleMessage( "Fake-IP filter mode", @@ -771,8 +807,8 @@ class MessageLookup extends MessageLookupByLibrary { "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("Fruit salad"), "general": MessageLookupByLibrary.simpleMessage("General"), "geoResources": MessageLookupByLibrary.simpleMessage("Geo"), - "geoSkipped": m25, - "geoUpdated": m26, + "geoSkipped": m26, + "geoUpdated": m27, "geodataLoader": MessageLookupByLibrary.simpleMessage( "Geo low-memory mode", ), @@ -804,28 +840,30 @@ class MessageLookup extends MessageLookupByLibrary { "hideTimeoutProxiesDesc": MessageLookupByLibrary.simpleMessage( "Leave out proxies whose last delay test timed out", ), + "hopInterval": MessageLookupByLibrary.simpleMessage("Hop interval"), "host": MessageLookupByLibrary.simpleMessage("Host"), "hostname": MessageLookupByLibrary.simpleMessage("Hostname"), - "hotkeyConflictWith": m27, + "hotkeyConflictWith": m28, "hotkeyDesc": MessageLookupByLibrary.simpleMessage( "Global hotkeys work even while the window is hidden. Tap an action to record its key combination.", ), "hotkeyManagement": MessageLookupByLibrary.simpleMessage( "Hotkey management", ), - "hotkeyNeedsModifier": m28, + "hotkeyNeedsModifier": m29, "hotkeyNotSet": MessageLookupByLibrary.simpleMessage("Not set"), "hotkeyUnavailable": MessageLookupByLibrary.simpleMessage( "Not registered, it may be taken by another app", ), "hours": MessageLookupByLibrary.simpleMessage("hours"), - "hoursAgo": m29, - "hoursCount": m30, + "hoursAgo": m30, + "hoursAgoShort": m31, + "hoursCount": m32, "icon": MessageLookupByLibrary.simpleMessage("Icon"), "iconRecords": MessageLookupByLibrary.simpleMessage("Icon records"), "iconSets": MessageLookupByLibrary.simpleMessage("Icon sets"), "iconSetsEmptyTip": MessageLookupByLibrary.simpleMessage( - "No icon sets yet. Add them in Advanced configuration → Icon sets", + "No icon sets yet. Add them in Custom profile resources → Icon sets", ), "iconStyle": MessageLookupByLibrary.simpleMessage("Icon style"), "iconStyleFilled": MessageLookupByLibrary.simpleMessage("Filled"), @@ -890,29 +928,41 @@ class MessageLookup extends MessageLookupByLibrary { "invalidBackupFile": MessageLookupByLibrary.simpleMessage( "Invalid backup file", ), + "invalidCidrContent": MessageLookupByLibrary.simpleMessage( + "Enter an IP range in CIDR form such as 192.168.0.0/16", + ), "invalidDscpContent": MessageLookupByLibrary.simpleMessage( "A DSCP mark cannot exceed 63", ), + "invalidHostContent": MessageLookupByLibrary.simpleMessage( + "Enter a domain or an IP address", + ), "invalidIconSet": MessageLookupByLibrary.simpleMessage( "Not a valid icon set", ), "invalidLinkTip": MessageLookupByLibrary.simpleMessage( "Not a subscription link or a proxy share link", ), + "invalidListenContent": MessageLookupByLibrary.simpleMessage( + "Enter an address and port such as 0.0.0.0:1053", + ), "invalidNetworkContent": MessageLookupByLibrary.simpleMessage( "Only tcp or udp is supported", ), - "invalidPolicy": m31, + "invalidPolicy": m33, + "invalidPortRangeContent": MessageLookupByLibrary.simpleMessage( + "Enter a port or a range such as 443 or 8000-9000", + ), "invalidProfileQrcode": MessageLookupByLibrary.simpleMessage( "This QR code doesn\'t contain a profile link", ), - "invalidProxy": m32, - "invalidProxyProvider": m33, + "invalidProxy": m34, + "invalidProxyProvider": m35, "invalidRangeContent": MessageLookupByLibrary.simpleMessage( "Enter numbers or ranges such as 80 or 8000-9000, separated by /", ), - "invalidRuleSet": m34, - "invalidSubRule": m35, + "invalidRuleSet": m36, + "invalidSubRule": m37, "ipAddress": MessageLookupByLibrary.simpleMessage("IP address"), "ipAsn": MessageLookupByLibrary.simpleMessage("ASN"), "ipFlagAbuser": MessageLookupByLibrary.simpleMessage("Abuse history"), @@ -949,7 +999,7 @@ class MessageLookup extends MessageLookupByLibrary { "With IPv6 on, the VPN allows IPv6 inbound.", ), "ipv6Timeout": MessageLookupByLibrary.simpleMessage("IPv6 timeout (ms)"), - "itemsCount": m36, + "itemsCount": m38, "ja": MessageLookupByLibrary.simpleMessage("Japanese"), "justNow": MessageLookupByLibrary.simpleMessage("Just now"), "keepAliveIdle": MessageLookupByLibrary.simpleMessage( @@ -971,7 +1021,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "layout": MessageLookupByLibrary.simpleMessage("Layout"), "light": MessageLookupByLibrary.simpleMessage("Light"), - "lineIssueTip": m37, + "lineIssueTip": m39, "lineWrap": MessageLookupByLibrary.simpleMessage("Word wrap"), "link": MessageLookupByLibrary.simpleMessage("Link"), "linkDesc": MessageLookupByLibrary.simpleMessage( @@ -1002,7 +1052,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "The system requires location permission to read the Wi-Fi name. On Android choose \"Allow all the time\", otherwise the Wi-Fi name cannot be read while the app is in the background.", ), - "locationPermissionGuide": m38, + "locationPermissionGuide": m40, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "Location permission required", ), @@ -1022,7 +1072,7 @@ class MessageLookup extends MessageLookupByLibrary { "matchSourceIp": MessageLookupByLibrary.simpleMessage("Match source IP"), "matchTarget": MessageLookupByLibrary.simpleMessage("MATCH-TARGET"), "maxFailedTimes": MessageLookupByLibrary.simpleMessage("Max failures"), - "maxLengthTip": m39, + "maxLengthTip": m41, "maximize": MessageLookupByLibrary.simpleMessage("Maximize"), "memoryAppResident": MessageLookupByLibrary.simpleMessage( "Resident memory", @@ -1045,7 +1095,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "memoryInfo": MessageLookupByLibrary.simpleMessage("Memory info"), "memoryReleased": MessageLookupByLibrary.simpleMessage("Memory released"), - "memoryReleasedSize": m40, + "memoryReleasedSize": m42, "messageTest": MessageLookupByLibrary.simpleMessage("Message test"), "messageTestTip": MessageLookupByLibrary.simpleMessage( "This is a message.", @@ -1053,17 +1103,19 @@ class MessageLookup extends MessageLookupByLibrary { "min": MessageLookupByLibrary.simpleMessage("Minimal"), "minimize": MessageLookupByLibrary.simpleMessage("Minimize"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("Minimize on exit"), - "minutesAgo": m41, + "minutesAgo": m43, + "minutesAgoShort": m44, "mixedPort": MessageLookupByLibrary.simpleMessage("Mixed port"), "mode": MessageLookupByLibrary.simpleMessage("Mode"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("Monochrome"), - "monthsAgo": m42, + "monthsAgo": m45, + "monthsAgoShort": m46, "more": MessageLookupByLibrary.simpleMessage("More"), "name": MessageLookupByLibrary.simpleMessage("Name"), "navigationBarStyle": MessageLookupByLibrary.simpleMessage("Bottom bar"), "network": MessageLookupByLibrary.simpleMessage("Network"), - "networkAccessDeniedError": m43, - "networkBadResponseError": m44, + "networkAccessDeniedError": m47, + "networkBadResponseError": m48, "networkCancelledError": MessageLookupByLibrary.simpleMessage( "The request was cancelled", ), @@ -1077,13 +1129,13 @@ class MessageLookup extends MessageLookupByLibrary { "Couldn\'t resolve the server address. Check that the URL is correct and DNS is working", ), "networkName": MessageLookupByLibrary.simpleMessage("Network name"), - "networkNotFoundError": m45, + "networkNotFoundError": m49, "networkRateLimitedError": MessageLookupByLibrary.simpleMessage( "Too many requests (HTTP 429). Wait a moment and try again", ), - "networkRequestFailed": m46, + "networkRequestFailed": m50, "networkSecret": MessageLookupByLibrary.simpleMessage("Network secret"), - "networkServerError": m47, + "networkServerError": m51, "networkSpeed": MessageLookupByLibrary.simpleMessage("Network speed"), "networkTimeoutError": MessageLookupByLibrary.simpleMessage( "The request timed out. Check your network or proxy, then try again", @@ -1127,9 +1179,16 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "Add a profile to get started", ), - "nullTip": m48, - "numberRangeTip": m49, - "numberTip": m50, + "nullTip": m52, + "numberRangeTip": m53, + "numberTip": m54, + "obfs": MessageLookupByLibrary.simpleMessage("Obfuscation"), + "obfsHost": MessageLookupByLibrary.simpleMessage("Obfuscation host"), + "obfsMode": MessageLookupByLibrary.simpleMessage("Obfuscation mode"), + "obfsParam": MessageLookupByLibrary.simpleMessage("Obfuscation parameter"), + "obfsPassword": MessageLookupByLibrary.simpleMessage( + "Obfuscation password", + ), "onDemand": MessageLookupByLibrary.simpleMessage("On demand"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "On demand pauses the proxy on excluded Wi-Fi.", @@ -1172,18 +1231,27 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "Please upload a valid QR code", ), + "plugin": MessageLookupByLibrary.simpleMessage("Plugin"), + "pluginHost": MessageLookupByLibrary.simpleMessage("Plugin host"), + "pluginMode": MessageLookupByLibrary.simpleMessage("Plugin mode"), "port": MessageLookupByLibrary.simpleMessage("Port"), "portConflictTip": MessageLookupByLibrary.simpleMessage( "Please enter a different port", ), - "portTip": m51, + "portHopping": MessageLookupByLibrary.simpleMessage("Port hopping"), + "portTip": m55, "ports": MessageLookupByLibrary.simpleMessage("Ports"), + "preSharedKey": MessageLookupByLibrary.simpleMessage("Pre-shared key"), "prerequisites": MessageLookupByLibrary.simpleMessage("Prerequisites"), "pressKeyboard": MessageLookupByLibrary.simpleMessage( "Press a key combination", ), "preview": MessageLookupByLibrary.simpleMessage("Preview"), "previousMatch": MessageLookupByLibrary.simpleMessage("Previous match"), + "privateKey": MessageLookupByLibrary.simpleMessage("Private key"), + "privateKeyPassphrase": MessageLookupByLibrary.simpleMessage( + "Private key passphrase", + ), "process": MessageLookupByLibrary.simpleMessage("Process"), "profile": MessageLookupByLibrary.simpleMessage("Profile"), "profileAutoUpdateIntervalInvalidValidationDesc": @@ -1210,23 +1278,26 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("Profiles"), "profilesSort": MessageLookupByLibrary.simpleMessage("Sort profiles"), "project": MessageLookupByLibrary.simpleMessage("Project"), - "providerInUse": m52, + "protocol": MessageLookupByLibrary.simpleMessage("Protocol"), + "protocolParam": MessageLookupByLibrary.simpleMessage("Protocol parameter"), + "protocolVersion": MessageLookupByLibrary.simpleMessage("Protocol version"), + "providerInUse": m56, "providerUrlTip": MessageLookupByLibrary.simpleMessage( "Only remote providers are supported", ), "providers": MessageLookupByLibrary.simpleMessage("External resources"), "proxies": MessageLookupByLibrary.simpleMessage("Proxies"), - "proxiesCount": m53, + "proxiesCount": m57, "proxiesEmpty": MessageLookupByLibrary.simpleMessage("Proxies are empty"), - "proxiesProfileLabel": m54, - "proxiesRemovedTip": m55, - "proxiesReplacedTip": m56, + "proxiesProfileLabel": m58, + "proxiesRemovedTip": m59, + "proxiesReplacedTip": m60, "proxyChains": MessageLookupByLibrary.simpleMessage("Proxy chain"), "proxyGroup": MessageLookupByLibrary.simpleMessage("Proxy group"), "proxyGroupEmpty": MessageLookupByLibrary.simpleMessage( "Proxy group is empty", ), - "proxyGroupInUse": m57, + "proxyGroupInUse": m61, "proxyGroupNameDuplicate": MessageLookupByLibrary.simpleMessage( "Duplicate proxy group name", ), @@ -1239,6 +1310,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "proxyType": MessageLookupByLibrary.simpleMessage("Proxy type"), "pruneCache": MessageLookupByLibrary.simpleMessage("Prune cache"), + "publicKey": MessageLookupByLibrary.simpleMessage("Public key"), "pureBlack": MessageLookupByLibrary.simpleMessage("Pure black"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("Pure black mode"), "qrcode": MessageLookupByLibrary.simpleMessage("QR code"), @@ -1262,6 +1334,9 @@ class MessageLookup extends MessageLookupByLibrary { "remoteDestination": MessageLookupByLibrary.simpleMessage( "Remote destination", ), + "remoteDnsResolve": MessageLookupByLibrary.simpleMessage( + "Remote DNS resolution", + ), "remove": MessageLookupByLibrary.simpleMessage("Remove"), "rename": MessageLookupByLibrary.simpleMessage("Rename"), "replace": MessageLookupByLibrary.simpleMessage("Replace"), @@ -1271,6 +1346,7 @@ class MessageLookup extends MessageLookupByLibrary { "requestsAndUpdates": MessageLookupByLibrary.simpleMessage( "Requests and updates", ), + "required": MessageLookupByLibrary.simpleMessage("Required"), "reset": MessageLookupByLibrary.simpleMessage("Reset"), "resetPageChangesTip": MessageLookupByLibrary.simpleMessage( "This page has changes. Are you sure you want to reset?", @@ -1460,10 +1536,10 @@ class MessageLookup extends MessageLookupByLibrary { "The rule must start with a supported type, such as DOMAIN,example.com,DIRECT", ), "rules": MessageLookupByLibrary.simpleMessage("Rules"), - "rulesCount": m58, + "rulesCount": m62, "runTime": MessageLookupByLibrary.simpleMessage("Run time"), "safeMode": MessageLookupByLibrary.simpleMessage("Safe mode"), - "safeModeAppTitle": m59, + "safeModeAppTitle": m63, "save": MessageLookupByLibrary.simpleMessage("Save"), "saveChanges": MessageLookupByLibrary.simpleMessage("Save the changes?"), "script": MessageLookupByLibrary.simpleMessage("Script"), @@ -1475,8 +1551,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "search": MessageLookupByLibrary.simpleMessage("Search"), "seconds": MessageLookupByLibrary.simpleMessage("seconds"), - "secondsCount": m60, - "sectionsInEffect": m61, + "secondsCount": m64, + "secretKey": MessageLookupByLibrary.simpleMessage("Key"), + "sectionsInEffect": m65, "selectAll": MessageLookupByLibrary.simpleMessage("Select all"), "selectProxies": MessageLookupByLibrary.simpleMessage("Select proxies"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage( @@ -1492,13 +1569,13 @@ class MessageLookup extends MessageLookupByLibrary { "Please select a sub-rule", ), "selected": MessageLookupByLibrary.simpleMessage("Selected"), - "selectedCountTitle": m62, + "selectedCountTitle": m66, "server": MessageLookupByLibrary.simpleMessage("Server"), "serviceAvailable": MessageLookupByLibrary.simpleMessage("Available"), "serviceBlocked": MessageLookupByLibrary.simpleMessage("Blocked"), "serviceCheck": MessageLookupByLibrary.simpleMessage("Check"), "serviceCheckAll": MessageLookupByLibrary.simpleMessage("Check all"), - "serviceCheckedAt": m63, + "serviceCheckedAt": m67, "serviceComingSoon": MessageLookupByLibrary.simpleMessage("Coming soon"), "serviceDisallowedIsp": MessageLookupByLibrary.simpleMessage( "Disallowed ISP", @@ -1519,8 +1596,8 @@ class MessageLookup extends MessageLookupByLibrary { ), "settingEntries": MessageLookupByLibrary.simpleMessage("settings"), "settings": MessageLookupByLibrary.simpleMessage("Settings"), - "settingsCount": m64, - "shareLinkUnreadable": m65, + "settingsCount": m68, + "shareLinkUnreadable": m69, "shareLinksInvalid": MessageLookupByLibrary.simpleMessage( "No proxy could be read from the links", ), @@ -1545,7 +1622,7 @@ class MessageLookup extends MessageLookupByLibrary { "singleShareLinkOnly": MessageLookupByLibrary.simpleMessage( "Enter one link here; edit several from the quick edit of the list", ), - "singleValueTip": m66, + "singleValueTip": m70, "size": MessageLookupByLibrary.simpleMessage("Size"), "skipCertVerify": MessageLookupByLibrary.simpleMessage( "Skip certificate verification", @@ -1652,15 +1729,19 @@ class MessageLookup extends MessageLookupByLibrary { "totalTraffic": MessageLookupByLibrary.simpleMessage("Total traffic"), "tproxyPort": MessageLookupByLibrary.simpleMessage("TProxy port"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Traffic usage"), + "transport": MessageLookupByLibrary.simpleMessage("Transport"), "tun": MessageLookupByLibrary.simpleMessage("TUN"), + "tunAuthorizationFailed": MessageLookupByLibrary.simpleMessage( + "TUN could not be authorized, so it was turned off.", + ), "tunDesc": MessageLookupByLibrary.simpleMessage( "TUN works only in administrator mode.", ), "turnOff": MessageLookupByLibrary.simpleMessage("Turn off"), "turnOn": MessageLookupByLibrary.simpleMessage("Turn on"), + "udpRelayMode": MessageLookupByLibrary.simpleMessage("UDP relay mode"), "undo": MessageLookupByLibrary.simpleMessage("Undo"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("Unified delay"), - "universal": MessageLookupByLibrary.simpleMessage("General"), "unknown": MessageLookupByLibrary.simpleMessage("Unknown"), "unknownNetworkError": MessageLookupByLibrary.simpleMessage( "Unknown network error", @@ -1670,8 +1751,9 @@ class MessageLookup extends MessageLookupByLibrary { "unpinWindow": MessageLookupByLibrary.simpleMessage("Unpin window"), "update": MessageLookupByLibrary.simpleMessage("Update"), "upload": MessageLookupByLibrary.simpleMessage("Upload"), + "uploadBandwidth": MessageLookupByLibrary.simpleMessage("Upload bandwidth"), "url": MessageLookupByLibrary.simpleMessage("URL"), - "urlTip": m67, + "urlTip": m71, "useHosts": MessageLookupByLibrary.simpleMessage("Use hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("Use system hosts"), "usedTraffic": MessageLookupByLibrary.simpleMessage("Used traffic"), @@ -1698,7 +1780,8 @@ class MessageLookup extends MessageLookupByLibrary { "writeToSystemDesc": MessageLookupByLibrary.simpleMessage( "Write to system also sets the system clock; Android ignores it.", ), - "yearsAgo": m68, + "yearsAgo": m72, + "yearsAgoShort": m73, "yes": MessageLookupByLibrary.simpleMessage("Yes"), "zhCN": MessageLookupByLibrary.simpleMessage("Simplified Chinese"), }; diff --git a/lib/l10n/intl/messages_ja.dart b/lib/l10n/intl/messages_ja.dart index bb559bd8..db5eb0f6 100644 --- a/lib/l10n/intl/messages_ja.dart +++ b/lib/l10n/intl/messages_ja.dart @@ -58,116 +58,128 @@ class MessageLookup extends MessageLookupByLibrary { static String m16(count) => "${count} 日前"; - static String m17(label) => "選択した${label}を削除してもよろしいですか?"; + static String m17(count) => "${count} 日前"; - static String m18(label) => "この${label}を削除してもよろしいですか?"; + static String m18(label) => "選択した${label}を削除してもよろしいですか?"; - static String m19(label) => "${label}の詳細"; + static String m19(label) => "この${label}を削除してもよろしいですか?"; - static String m20(name) => "${name} 経由の接続が使用元のプロキシに戻るため、接続を中止しました"; + static String m20(label) => "${label}の詳細"; - static String m21(label) => "${label}は空にできません"; + static String m21(name) => "${name} 経由の接続が使用元のプロキシに戻るため、接続を中止しました"; - static String m22(label) => "${label}はすでに存在します"; + static String m22(label) => "${label}は空にできません"; - static String m23(count) => "${count} 件が失敗しました"; + static String m23(label) => "${label}はすでに存在します"; - static String m24(label, message) => "${label}:${message}"; + static String m24(count) => "${count} 件が失敗しました"; - static String m25(name) => "${name} はすでに最新です"; + static String m25(label, message) => "${label}:${message}"; - static String m26(name) => "${name} を更新しました"; + static String m26(name) => "${name} はすでに最新です"; - static String m27(action) => "「${action}」で使用中です。保存するとこちらに移動します。"; + static String m27(name) => "${name} を更新しました"; - static String m28(modifiers) => "${modifiers} のいずれかを含めてください"; + static String m28(action) => "「${action}」で使用中です。保存するとこちらに移動します。"; - static String m29(count) => "${count} 時間前"; + static String m29(modifiers) => "${modifiers} のいずれかを含めてください"; - static String m30(count) => "${count} 時間"; + static String m30(count) => "${count} 時間前"; - static String m31(target) => "${target} は無効なポリシーです"; + static String m31(count) => "${count} 時間前"; - static String m32(proxyName) => "${proxyName} は無効なプロキシです"; + static String m32(count) => "${count} 時間"; - static String m33(providerName) => "${providerName} は無効なプロキシプロバイダーです"; + static String m33(target) => "${target} は無効なポリシーです"; - static String m34(ruleSet) => "${ruleSet} は無効なルールセットです"; + static String m34(proxyName) => "${proxyName} は無効なプロキシです"; - static String m35(subRule) => "${subRule} は無効な SUB_RULE です"; + static String m35(providerName) => "${providerName} は無効なプロキシプロバイダーです"; - static String m36(count) => "${count} 件"; + static String m36(ruleSet) => "${ruleSet} は無効なルールセットです"; - static String m37(line, message) => "${line}行目:${message}"; + static String m37(subRule) => "${subRule} は無効な SUB_RULE です"; - static String m38(appName) => + static String m38(count) => "${count} 件"; + + static String m39(line, message) => "${line}行目:${message}"; + + static String m40(appName) => "1. システム設定 > プライバシーとセキュリティ を開く\n2. 位置情報サービス を選択\n3. リストで ${appName} を見つけてチェックを入れる\n\n設定が完了したらアプリに戻ると、通常どおり使用できます。ご協力ありがとうございます。"; - static String m39(label, max) => "${label}は最大${max}文字です"; + static String m41(label, max) => "${label}は最大${max}文字です"; - static String m40(size) => "${size} を解放しました"; + static String m42(size) => "${size} を解放しました"; - static String m41(count) => "${count} 分前"; + static String m43(count) => "${count} 分前"; - static String m42(count) => "${count} か月前"; + static String m44(count) => "${count} 分前"; - static String m43(code) => + static String m45(count) => "${count} か月前"; + + static String m46(count) => "${count} か月前"; + + static String m47(code) => "サーバーがアクセスを拒否しました(HTTP ${code})。リンクの期限切れか、認証情報が誤っている可能性があります"; - static String m44(code) => "サーバーがリクエストを拒否しました(HTTP ${code})"; + static String m48(code) => "サーバーがリクエストを拒否しました(HTTP ${code})"; - static String m45(code) => + static String m49(code) => "このアドレスには何も見つかりませんでした(HTTP ${code})。URL が正しいか確認してください"; - static String m46(detail) => "ネットワークリクエストに失敗しました:${detail}"; + static String m50(detail) => "ネットワークリクエストに失敗しました:${detail}"; - static String m47(code) => "サーバーで問題が発生しました(HTTP ${code})。しばらくしてから再試行してください"; + static String m51(code) => "サーバーで問題が発生しました(HTTP ${code})。しばらくしてから再試行してください"; - static String m48(label) => "${label}はまだありません"; + static String m52(label) => "${label}はまだありません"; - static String m49(label, min, max) => "${label}は${min}から${max}の範囲で指定してください"; + static String m53(label, min, max) => "${label}は${min}から${max}の範囲で指定してください"; - static String m50(label) => "${label}は数値である必要があります"; + static String m54(label) => "${label}は数値である必要があります"; - static String m51(label) => "${label} は 1024〜49151 の範囲で指定してください"; + static String m55(label) => "${label} は 1024〜49151 の範囲で指定してください"; - static String m52(label, profiles) => + static String m56(label, profiles) => "${label} は ${profiles} でまだ使用されており、削除するとそれらが正常に動作しなくなります。削除してもよろしいですか?"; - static String m53(count) => "プロキシ ${count} 件"; + static String m57(count, label) => + "${Intl.plural(count, other: 'プロキシ ${label} 件')}"; - static String m54(name, count) => "${name} など${count}件のプロキシ"; + static String m58(name, count) => "${name} など${count}件のプロキシ"; - static String m55(removed) => + static String m59(removed) => "次のプロキシが削除されます:${removed}。名前を変えた行は新しいプロキシとして扱われ、削除されたプロキシを参照するカスタムプロファイルでは見つからないと表示されます。適用しますか?"; - static String m56(removed, added) => + static String m60(removed, added) => "次のプロキシが削除されます:${removed}。次のプロキシが追加されます:${added}。名前を変えた行は新しいプロキシとして扱われ、削除されたプロキシを参照するカスタムプロファイルでは見つからないと表示されます。適用しますか?"; - static String m57(label) => + static String m61(label) => "${label} はこのプロファイルのルール、プロキシグループ、ダイヤラープロキシ、DNS または NTP からまだ参照されており、削除するとそれらの参照は無効になります。削除してもよろしいですか?"; - static String m58(count) => "ルール ${count} 件"; + static String m62(count, label) => + "${Intl.plural(count, other: 'ルール ${label} 件')}"; - static String m59(appName) => "${appName}(セーフモード)"; + static String m63(appName) => "${appName}(セーフモード)"; - static String m60(count) => "${count} 秒"; + static String m64(count) => "${count} 秒"; - static String m61(count) => "${count} 件が有効"; + static String m65(count) => "${count} 件が有効"; - static String m62(count) => "${count} 件選択中"; + static String m66(count) => "${count} 件選択中"; - static String m63(time) => "${time} に検査"; + static String m67(time) => "${time} に検査"; - static String m64(count) => "設定 ${count} 件"; + static String m68(count) => "設定 ${count} 件"; - static String m65(link) => "${link} からプロキシを読み取れませんでした"; + static String m69(link) => "${link} からプロキシを読み取れませんでした"; - static String m66(label) => "${label}は1項目のみ指定できます"; + static String m70(label) => "${label}は1項目のみ指定できます"; - static String m67(label) => "${label}はURLである必要があります"; + static String m71(label) => "${label}はURLである必要があります"; - static String m68(count) => "${count} 年前"; + static String m72(count) => "${count} 年前"; + + static String m73(count) => "${count} 年前"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -223,9 +235,15 @@ class MessageLookup extends MessageLookupByLibrary { "advancedConfigDesc": MessageLookupByLibrary.simpleMessage( "ネットワーク、DNS、追加ルール、スクリプト", ), + "ageEncryptedProfile": MessageLookupByLibrary.simpleMessage("暗号化されたプロファイル"), + "ageSecretKey": MessageLookupByLibrary.simpleMessage("age 秘密鍵"), + "ageSecretKeyRequiredTip": MessageLookupByLibrary.simpleMessage( + "このプロファイルは age で暗号化されており、復号には age 秘密鍵が必要です", + ), "agree": MessageLookupByLibrary.simpleMessage("同意する"), "allowBypass": MessageLookupByLibrary.simpleMessage("アプリによるVPNバイパスを許可"), "allowLan": MessageLookupByLibrary.simpleMessage("LANプロキシ"), + "allowedIps": MessageLookupByLibrary.simpleMessage("許可する IP"), "answers": MessageLookupByLibrary.simpleMessage("応答"), "app": MessageLookupByLibrary.simpleMessage("アプリ"), "appAccessControl": MessageLookupByLibrary.simpleMessage("アプリアクセス制御"), @@ -313,6 +331,7 @@ class MessageLookup extends MessageLookupByLibrary { "cameraUnavailable": MessageLookupByLibrary.simpleMessage("カメラを使用できません"), "cancel": MessageLookupByLibrary.simpleMessage("キャンセル"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("すべて選択解除"), + "cannotSave": MessageLookupByLibrary.simpleMessage("保存できません"), "changeProxyFailedTip": MessageLookupByLibrary.simpleMessage( "プロキシの切り替えに失敗したため、前回の選択に戻しました", ), @@ -327,12 +346,16 @@ class MessageLookup extends MessageLookupByLibrary { ), "checkUpdate": MessageLookupByLibrary.simpleMessage("更新を確認"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("すでに最新バージョンです"), + "cipher": MessageLookupByLibrary.simpleMessage("暗号方式"), "clearData": MessageLookupByLibrary.simpleMessage("データを消去"), "clearDataAndExitTip": MessageLookupByLibrary.simpleMessage( "すべてのプロファイル、設定、ローカルデータを削除してアプリを終了します。もう一度開くと最初から始められます。", ), "clearDataFailed": m1, "clearSearch": MessageLookupByLibrary.simpleMessage("検索をクリア"), + "clientFingerprint": MessageLookupByLibrary.simpleMessage( + "クライアントフィンガープリント", + ), "clipboardExport": MessageLookupByLibrary.simpleMessage("クリップボードへエクスポート"), "clipboardImport": MessageLookupByLibrary.simpleMessage("クリップボードからインポート"), "clipboardWriteFailed": MessageLookupByLibrary.simpleMessage( @@ -357,6 +380,7 @@ class MessageLookup extends MessageLookupByLibrary { "confirmForceCrashCore": MessageLookupByLibrary.simpleMessage( "コアを強制クラッシュさせてもよろしいですか?", ), + "congestionControl": MessageLookupByLibrary.simpleMessage("輻輳制御"), "congestionController": MessageLookupByLibrary.simpleMessage("TCP 輻輳制御"), "connected": MessageLookupByLibrary.simpleMessage("接続済み"), "connecting": MessageLookupByLibrary.simpleMessage("接続中…"), @@ -410,6 +434,12 @@ class MessageLookup extends MessageLookupByLibrary { "customProfileDesc": MessageLookupByLibrary.simpleMessage( "アプリのプロキシと他のプロファイルからプロキシグループとルールを組み立てます", ), + "customProfileResources": MessageLookupByLibrary.simpleMessage( + "カスタムプロファイルのリソース", + ), + "customProfileResourcesDesc": MessageLookupByLibrary.simpleMessage( + "プロキシ、ルールプロバイダー、フィルター、アイコンセット", + ), "cut": MessageLookupByLibrary.simpleMessage("切り取り"), "dark": MessageLookupByLibrary.simpleMessage("ダーク"), "dashboard": MessageLookupByLibrary.simpleMessage("ダッシュボード"), @@ -421,6 +451,7 @@ class MessageLookup extends MessageLookupByLibrary { "変更の保存に失敗したため、元に戻しました", ), "daysAgo": m16, + "daysAgoShort": m17, "defaultSelected": MessageLookupByLibrary.simpleMessage("デフォルトの選択"), "defaultText": MessageLookupByLibrary.simpleMessage("デフォルト"), "definitionNotMap": MessageLookupByLibrary.simpleMessage( @@ -430,15 +461,15 @@ class MessageLookup extends MessageLookupByLibrary { "delayFailed": MessageLookupByLibrary.simpleMessage("失敗"), "delayTest": MessageLookupByLibrary.simpleMessage("遅延テスト"), "delete": MessageLookupByLibrary.simpleMessage("削除"), - "deleteMultipTip": m17, - "deleteTip": m18, + "deleteMultipTip": m18, + "deleteTip": m19, "desc": MessageLookupByLibrary.simpleMessage( "ClashMetaベースのマルチプラットフォーム対応プロキシクライアント。シンプルで使いやすく、オープンソースで広告もありません。", ), "destination": MessageLookupByLibrary.simpleMessage("宛先"), "destinationGeoIP": MessageLookupByLibrary.simpleMessage("宛先GeoIP"), "destinationIPASN": MessageLookupByLibrary.simpleMessage("宛先IP ASN"), - "details": m19, + "details": m20, "detectionTip": MessageLookupByLibrary.simpleMessage( "サードパーティAPIに依存しているため、参考値です", ), @@ -450,7 +481,7 @@ class MessageLookup extends MessageLookupByLibrary { "dialerProxyDesc": MessageLookupByLibrary.simpleMessage( "ダイヤラープロキシは NTP サーバーへの接続に使用するアウトバウンドです。", ), - "dialerProxyLoopStopped": m20, + "dialerProxyLoopStopped": m21, "direct": MessageLookupByLibrary.simpleMessage("ダイレクト"), "disableIcmpForwarding": MessageLookupByLibrary.simpleMessage( "ICMP 転送を無効化", @@ -563,6 +594,7 @@ class MessageLookup extends MessageLookupByLibrary { "docked": MessageLookupByLibrary.simpleMessage("固定"), "domain": MessageLookupByLibrary.simpleMessage("ドメイン"), "download": MessageLookupByLibrary.simpleMessage("ダウンロード"), + "downloadBandwidth": MessageLookupByLibrary.simpleMessage("ダウンロード帯域幅"), "edit": MessageLookupByLibrary.simpleMessage("編集"), "editGlobalRules": MessageLookupByLibrary.simpleMessage("グローバルルールを編集"), "editProxy": MessageLookupByLibrary.simpleMessage("プロキシを編集"), @@ -571,7 +603,7 @@ class MessageLookup extends MessageLookupByLibrary { "editSsid": MessageLookupByLibrary.simpleMessage("SSIDを編集"), "editorUnavailable": MessageLookupByLibrary.simpleMessage("エディターを利用できません"), "emptyFallback": MessageLookupByLibrary.simpleMessage("空グループのフォールバック"), - "emptyTip": m21, + "emptyTip": m22, "en": MessageLookupByLibrary.simpleMessage("英語"), "enabled": MessageLookupByLibrary.simpleMessage("有効"), "entries": MessageLookupByLibrary.simpleMessage(" 件"), @@ -587,7 +619,7 @@ class MessageLookup extends MessageLookupByLibrary { "excludeSsidsDesc": MessageLookupByLibrary.simpleMessage( "除外した SSID の Wi-Fi に接続すると、アプリの実行状態が自動的に切り替わります。", ), - "existsTip": m22, + "existsTip": m23, "exit": MessageLookupByLibrary.simpleMessage("終了"), "exitFullScreen": MessageLookupByLibrary.simpleMessage("全画面表示を終了"), "exitNodes": MessageLookupByLibrary.simpleMessage("出口ノード"), @@ -611,8 +643,8 @@ class MessageLookup extends MessageLookupByLibrary { "externalLink": MessageLookupByLibrary.simpleMessage("外部リンク"), "extraLarge": MessageLookupByLibrary.simpleMessage("特大"), "fade": MessageLookupByLibrary.simpleMessage("フェード"), - "failedCount": m23, - "failedItem": m24, + "failedCount": m24, + "failedItem": m25, "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fake-IPフィルター"), "fakeipFilterMode": MessageLookupByLibrary.simpleMessage("Fake-IPフィルターモード"), "fakeipFilterModeDesc": MessageLookupByLibrary.simpleMessage( @@ -651,8 +683,8 @@ class MessageLookup extends MessageLookupByLibrary { "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("フルーツサラダ"), "general": MessageLookupByLibrary.simpleMessage("一般"), "geoResources": MessageLookupByLibrary.simpleMessage("Geo"), - "geoSkipped": m25, - "geoUpdated": m26, + "geoSkipped": m26, + "geoUpdated": m27, "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低メモリモード"), "global": MessageLookupByLibrary.simpleMessage("グローバル"), "go": MessageLookupByLibrary.simpleMessage("開く"), @@ -676,26 +708,28 @@ class MessageLookup extends MessageLookupByLibrary { "hideTimeoutProxiesDesc": MessageLookupByLibrary.simpleMessage( "前回の遅延テストがタイムアウトしたプロキシを表示しない", ), + "hopInterval": MessageLookupByLibrary.simpleMessage("ホップ間隔"), "host": MessageLookupByLibrary.simpleMessage("ホスト"), "hostname": MessageLookupByLibrary.simpleMessage("ホスト名"), - "hotkeyConflictWith": m27, + "hotkeyConflictWith": m28, "hotkeyDesc": MessageLookupByLibrary.simpleMessage( "グローバルホットキーはウィンドウが非表示でも有効です。アクションをタップしてキーの組み合わせを記録します。", ), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("ホットキー管理"), - "hotkeyNeedsModifier": m28, + "hotkeyNeedsModifier": m29, "hotkeyNotSet": MessageLookupByLibrary.simpleMessage("未設定"), "hotkeyUnavailable": MessageLookupByLibrary.simpleMessage( "登録できませんでした。他のアプリが使用している可能性があります", ), "hours": MessageLookupByLibrary.simpleMessage("時間"), - "hoursAgo": m29, - "hoursCount": m30, + "hoursAgo": m30, + "hoursAgoShort": m31, + "hoursCount": m32, "icon": MessageLookupByLibrary.simpleMessage("アイコン"), "iconRecords": MessageLookupByLibrary.simpleMessage("アイコン履歴"), "iconSets": MessageLookupByLibrary.simpleMessage("アイコンセット"), "iconSetsEmptyTip": MessageLookupByLibrary.simpleMessage( - "アイコンセットはまだありません。「詳細設定 → アイコンセット」で追加できます", + "アイコンセットはまだありません。「カスタムプロファイルのリソース → アイコンセット」で追加できます", ), "iconStyle": MessageLookupByLibrary.simpleMessage("アイコンスタイル"), "iconStyleFilled": MessageLookupByLibrary.simpleMessage("背景あり"), @@ -753,27 +787,39 @@ class MessageLookup extends MessageLookupByLibrary { "interval": MessageLookupByLibrary.simpleMessage("間隔"), "intranetIP": MessageLookupByLibrary.simpleMessage("イントラネットIP"), "invalidBackupFile": MessageLookupByLibrary.simpleMessage("無効なバックアップファイル"), + "invalidCidrContent": MessageLookupByLibrary.simpleMessage( + "192.168.0.0/16 のような CIDR 形式の IP 範囲を入力してください", + ), "invalidDscpContent": MessageLookupByLibrary.simpleMessage( "DSCP マークは 63 を超えられません", ), + "invalidHostContent": MessageLookupByLibrary.simpleMessage( + "ドメインまたは IP アドレスを入力してください", + ), "invalidIconSet": MessageLookupByLibrary.simpleMessage("有効なアイコンセットではありません"), "invalidLinkTip": MessageLookupByLibrary.simpleMessage( "サブスクリプションリンクまたはプロキシの共有リンクではありません", ), + "invalidListenContent": MessageLookupByLibrary.simpleMessage( + "0.0.0.0:1053 のようなアドレスとポートを入力してください", + ), "invalidNetworkContent": MessageLookupByLibrary.simpleMessage( "tcp または udp のみ対応しています", ), - "invalidPolicy": m31, + "invalidPolicy": m33, + "invalidPortRangeContent": MessageLookupByLibrary.simpleMessage( + "443 や 8000-9000 のようなポートまたは範囲を入力してください", + ), "invalidProfileQrcode": MessageLookupByLibrary.simpleMessage( "このQRコードにはプロファイルのリンクが含まれていません", ), - "invalidProxy": m32, - "invalidProxyProvider": m33, + "invalidProxy": m34, + "invalidProxyProvider": m35, "invalidRangeContent": MessageLookupByLibrary.simpleMessage( "80 や 8000-9000 のような数値または範囲を / 区切りで入力してください", ), - "invalidRuleSet": m34, - "invalidSubRule": m35, + "invalidRuleSet": m36, + "invalidSubRule": m37, "ipAddress": MessageLookupByLibrary.simpleMessage("IP アドレス"), "ipAsn": MessageLookupByLibrary.simpleMessage("ASN"), "ipFlagAbuser": MessageLookupByLibrary.simpleMessage("不正利用の履歴"), @@ -810,7 +856,7 @@ class MessageLookup extends MessageLookupByLibrary { "IPv6 を有効にすると、VPN で IPv6 インバウンドを許可します。", ), "ipv6Timeout": MessageLookupByLibrary.simpleMessage("IPv6タイムアウト(ms)"), - "itemsCount": m36, + "itemsCount": m38, "ja": MessageLookupByLibrary.simpleMessage("日本語"), "justNow": MessageLookupByLibrary.simpleMessage("たった今"), "keepAliveIdle": MessageLookupByLibrary.simpleMessage("TCP キープアライブ待機時間"), @@ -828,7 +874,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "layout": MessageLookupByLibrary.simpleMessage("レイアウト"), "light": MessageLookupByLibrary.simpleMessage("ライト"), - "lineIssueTip": m37, + "lineIssueTip": m39, "lineWrap": MessageLookupByLibrary.simpleMessage("折り返し"), "link": MessageLookupByLibrary.simpleMessage("リンク"), "linkDesc": MessageLookupByLibrary.simpleMessage( @@ -855,7 +901,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "システムの要件により、Wi-Fi 名の取得には位置情報の権限が必要です。Android では「常に許可」を選択してください。そうしないと、アプリがバックグラウンドにあるときに Wi-Fi 名を取得できません。", ), - "locationPermissionGuide": m38, + "locationPermissionGuide": m40, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "位置情報の権限が必要です", ), @@ -873,7 +919,7 @@ class MessageLookup extends MessageLookupByLibrary { "matchSourceIp": MessageLookupByLibrary.simpleMessage("送信元IPにマッチ"), "matchTarget": MessageLookupByLibrary.simpleMessage("MATCH-TARGET"), "maxFailedTimes": MessageLookupByLibrary.simpleMessage("最大失敗回数"), - "maxLengthTip": m39, + "maxLengthTip": m41, "maximize": MessageLookupByLibrary.simpleMessage("最大化"), "memoryAppResident": MessageLookupByLibrary.simpleMessage("常駐メモリ"), "memoryAppShared": MessageLookupByLibrary.simpleMessage("アプリと共有"), @@ -892,23 +938,25 @@ class MessageLookup extends MessageLookupByLibrary { ), "memoryInfo": MessageLookupByLibrary.simpleMessage("メモリ情報"), "memoryReleased": MessageLookupByLibrary.simpleMessage("メモリを解放しました"), - "memoryReleasedSize": m40, + "memoryReleasedSize": m42, "messageTest": MessageLookupByLibrary.simpleMessage("メッセージテスト"), "messageTestTip": MessageLookupByLibrary.simpleMessage("これはメッセージです。"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimize": MessageLookupByLibrary.simpleMessage("最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("終了時に最小化"), - "minutesAgo": m41, + "minutesAgo": m43, + "minutesAgoShort": m44, "mixedPort": MessageLookupByLibrary.simpleMessage("Mixedポート"), "mode": MessageLookupByLibrary.simpleMessage("モード"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("モノクローム"), - "monthsAgo": m42, + "monthsAgo": m45, + "monthsAgoShort": m46, "more": MessageLookupByLibrary.simpleMessage("その他"), "name": MessageLookupByLibrary.simpleMessage("名前"), "navigationBarStyle": MessageLookupByLibrary.simpleMessage("ボトムバー"), "network": MessageLookupByLibrary.simpleMessage("ネットワーク"), - "networkAccessDeniedError": m43, - "networkBadResponseError": m44, + "networkAccessDeniedError": m47, + "networkBadResponseError": m48, "networkCancelledError": MessageLookupByLibrary.simpleMessage( "リクエストはキャンセルされました", ), @@ -920,13 +968,13 @@ class MessageLookup extends MessageLookupByLibrary { "サーバーのアドレスを解決できませんでした。URL が正しいこと、DNS が使えることを確認してください", ), "networkName": MessageLookupByLibrary.simpleMessage("ネットワーク名"), - "networkNotFoundError": m45, + "networkNotFoundError": m49, "networkRateLimitedError": MessageLookupByLibrary.simpleMessage( "リクエストが多すぎます(HTTP 429)。しばらく待ってから再試行してください", ), - "networkRequestFailed": m46, + "networkRequestFailed": m50, "networkSecret": MessageLookupByLibrary.simpleMessage("ネットワークシークレット"), - "networkServerError": m47, + "networkServerError": m51, "networkSpeed": MessageLookupByLibrary.simpleMessage("ネットワーク速度"), "networkTimeoutError": MessageLookupByLibrary.simpleMessage( "リクエストがタイムアウトしました。ネットワークまたはプロキシを確認してから再試行してください", @@ -962,9 +1010,14 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "プロファイルを追加して始めましょう", ), - "nullTip": m48, - "numberRangeTip": m49, - "numberTip": m50, + "nullTip": m52, + "numberRangeTip": m53, + "numberTip": m54, + "obfs": MessageLookupByLibrary.simpleMessage("難読化"), + "obfsHost": MessageLookupByLibrary.simpleMessage("難読化ホスト"), + "obfsMode": MessageLookupByLibrary.simpleMessage("難読化モード"), + "obfsParam": MessageLookupByLibrary.simpleMessage("難読化パラメータ"), + "obfsPassword": MessageLookupByLibrary.simpleMessage("難読化パスワード"), "onDemand": MessageLookupByLibrary.simpleMessage("オンデマンド"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "オンデマンドは、除外した Wi-Fi に接続している間プロキシを一時停止します。", @@ -1001,14 +1054,21 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "有効なQRコードをアップロードしてください", ), + "plugin": MessageLookupByLibrary.simpleMessage("プラグイン"), + "pluginHost": MessageLookupByLibrary.simpleMessage("プラグインホスト"), + "pluginMode": MessageLookupByLibrary.simpleMessage("プラグインモード"), "port": MessageLookupByLibrary.simpleMessage("ポート"), "portConflictTip": MessageLookupByLibrary.simpleMessage("別のポートを入力してください"), - "portTip": m51, + "portHopping": MessageLookupByLibrary.simpleMessage("ポートホッピング"), + "portTip": m55, "ports": MessageLookupByLibrary.simpleMessage("ポート"), + "preSharedKey": MessageLookupByLibrary.simpleMessage("事前共有鍵"), "prerequisites": MessageLookupByLibrary.simpleMessage("前提条件"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("キーの組み合わせを押してください"), "preview": MessageLookupByLibrary.simpleMessage("プレビュー"), "previousMatch": MessageLookupByLibrary.simpleMessage("前の一致"), + "privateKey": MessageLookupByLibrary.simpleMessage("秘密鍵"), + "privateKeyPassphrase": MessageLookupByLibrary.simpleMessage("秘密鍵のパスフレーズ"), "process": MessageLookupByLibrary.simpleMessage("プロセス"), "profile": MessageLookupByLibrary.simpleMessage("プロファイル"), "profileAutoUpdateIntervalInvalidValidationDesc": @@ -1033,19 +1093,22 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("プロファイル"), "profilesSort": MessageLookupByLibrary.simpleMessage("プロファイルの並べ替え"), "project": MessageLookupByLibrary.simpleMessage("プロジェクト"), - "providerInUse": m52, + "protocol": MessageLookupByLibrary.simpleMessage("プロトコル"), + "protocolParam": MessageLookupByLibrary.simpleMessage("プロトコルパラメータ"), + "protocolVersion": MessageLookupByLibrary.simpleMessage("プロトコルバージョン"), + "providerInUse": m56, "providerUrlTip": MessageLookupByLibrary.simpleMessage("リモートリソースのみ対応しています"), "providers": MessageLookupByLibrary.simpleMessage("外部リソース"), "proxies": MessageLookupByLibrary.simpleMessage("プロキシ"), - "proxiesCount": m53, + "proxiesCount": m57, "proxiesEmpty": MessageLookupByLibrary.simpleMessage("プロキシが空です"), - "proxiesProfileLabel": m54, - "proxiesRemovedTip": m55, - "proxiesReplacedTip": m56, + "proxiesProfileLabel": m58, + "proxiesRemovedTip": m59, + "proxiesReplacedTip": m60, "proxyChains": MessageLookupByLibrary.simpleMessage("プロキシチェーン"), "proxyGroup": MessageLookupByLibrary.simpleMessage("プロキシグループ"), "proxyGroupEmpty": MessageLookupByLibrary.simpleMessage("プロキシグループが空です"), - "proxyGroupInUse": m57, + "proxyGroupInUse": m61, "proxyGroupNameDuplicate": MessageLookupByLibrary.simpleMessage( "プロキシグループ名が重複しています", ), @@ -1058,6 +1121,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "proxyType": MessageLookupByLibrary.simpleMessage("プロキシタイプ"), "pruneCache": MessageLookupByLibrary.simpleMessage("キャッシュを整理"), + "publicKey": MessageLookupByLibrary.simpleMessage("公開鍵"), "pureBlack": MessageLookupByLibrary.simpleMessage("ピュアブラック"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("ピュアブラックモード"), "qrcode": MessageLookupByLibrary.simpleMessage("QRコード"), @@ -1079,6 +1143,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "remote": MessageLookupByLibrary.simpleMessage("リモート"), "remoteDestination": MessageLookupByLibrary.simpleMessage("リモート宛先"), + "remoteDnsResolve": MessageLookupByLibrary.simpleMessage("リモート DNS 解決"), "remove": MessageLookupByLibrary.simpleMessage("削除"), "rename": MessageLookupByLibrary.simpleMessage("名前を変更"), "replace": MessageLookupByLibrary.simpleMessage("置換"), @@ -1086,6 +1151,7 @@ class MessageLookup extends MessageLookupByLibrary { "request": MessageLookupByLibrary.simpleMessage("リクエスト"), "requests": MessageLookupByLibrary.simpleMessage("リクエスト"), "requestsAndUpdates": MessageLookupByLibrary.simpleMessage("リクエストと更新"), + "required": MessageLookupByLibrary.simpleMessage("必須"), "reset": MessageLookupByLibrary.simpleMessage("リセット"), "resetPageChangesTip": MessageLookupByLibrary.simpleMessage( "このページには変更があります。リセットしてもよろしいですか?", @@ -1251,10 +1317,10 @@ class MessageLookup extends MessageLookupByLibrary { "ルールは対応する種類で始めてください。例:DOMAIN,example.com,DIRECT", ), "rules": MessageLookupByLibrary.simpleMessage("ルール"), - "rulesCount": m58, + "rulesCount": m62, "runTime": MessageLookupByLibrary.simpleMessage("起動時間"), "safeMode": MessageLookupByLibrary.simpleMessage("セーフモード"), - "safeModeAppTitle": m59, + "safeModeAppTitle": m63, "save": MessageLookupByLibrary.simpleMessage("保存"), "saveChanges": MessageLookupByLibrary.simpleMessage("変更を保存しますか?"), "script": MessageLookupByLibrary.simpleMessage("スクリプト"), @@ -1264,8 +1330,9 @@ class MessageLookup extends MessageLookupByLibrary { "scrollToSelected": MessageLookupByLibrary.simpleMessage("選択項目へスクロール"), "search": MessageLookupByLibrary.simpleMessage("検索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), - "secondsCount": m60, - "sectionsInEffect": m61, + "secondsCount": m64, + "secretKey": MessageLookupByLibrary.simpleMessage("キー"), + "sectionsInEffect": m65, "selectAll": MessageLookupByLibrary.simpleMessage("すべて選択"), "selectProxies": MessageLookupByLibrary.simpleMessage("プロキシを選択"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage( @@ -1277,13 +1344,13 @@ class MessageLookup extends MessageLookupByLibrary { ), "selectSubRule": MessageLookupByLibrary.simpleMessage("サブルールを選択してください"), "selected": MessageLookupByLibrary.simpleMessage("選択済み"), - "selectedCountTitle": m62, + "selectedCountTitle": m66, "server": MessageLookupByLibrary.simpleMessage("サーバー"), "serviceAvailable": MessageLookupByLibrary.simpleMessage("利用可能"), "serviceBlocked": MessageLookupByLibrary.simpleMessage("ブロック済み"), "serviceCheck": MessageLookupByLibrary.simpleMessage("検査"), "serviceCheckAll": MessageLookupByLibrary.simpleMessage("すべて検査"), - "serviceCheckedAt": m63, + "serviceCheckedAt": m67, "serviceComingSoon": MessageLookupByLibrary.simpleMessage("近日提供予定"), "serviceDisallowedIsp": MessageLookupByLibrary.simpleMessage( "許可されていない ISP", @@ -1298,8 +1365,8 @@ class MessageLookup extends MessageLookupByLibrary { "serviceUnsupportedRegion": MessageLookupByLibrary.simpleMessage("対象外の地域"), "settingEntries": MessageLookupByLibrary.simpleMessage("設定項目"), "settings": MessageLookupByLibrary.simpleMessage("設定"), - "settingsCount": m64, - "shareLinkUnreadable": m65, + "settingsCount": m68, + "shareLinkUnreadable": m69, "shareLinksInvalid": MessageLookupByLibrary.simpleMessage( "リンクからプロキシを読み取れませんでした", ), @@ -1324,7 +1391,7 @@ class MessageLookup extends MessageLookupByLibrary { "singleShareLinkOnly": MessageLookupByLibrary.simpleMessage( "ここにはリンクを 1 つだけ入力してください。複数の場合は一覧のクイック編集を使ってください", ), - "singleValueTip": m66, + "singleValueTip": m70, "size": MessageLookupByLibrary.simpleMessage("サイズ"), "skipCertVerify": MessageLookupByLibrary.simpleMessage("証明書の検証をスキップ"), "skipDomain": MessageLookupByLibrary.simpleMessage("スキップするドメイン"), @@ -1413,13 +1480,17 @@ class MessageLookup extends MessageLookupByLibrary { "totalTraffic": MessageLookupByLibrary.simpleMessage("合計トラフィック"), "tproxyPort": MessageLookupByLibrary.simpleMessage("TProxyポート"), "trafficUsage": MessageLookupByLibrary.simpleMessage("トラフィック統計"), + "transport": MessageLookupByLibrary.simpleMessage("トランスポート"), "tun": MessageLookupByLibrary.simpleMessage("TUN"), + "tunAuthorizationFailed": MessageLookupByLibrary.simpleMessage( + "TUN の権限を取得できなかったため、オフにしました。", + ), "tunDesc": MessageLookupByLibrary.simpleMessage("TUN は管理者モードでのみ有効です。"), "turnOff": MessageLookupByLibrary.simpleMessage("オフにする"), "turnOn": MessageLookupByLibrary.simpleMessage("オンにする"), + "udpRelayMode": MessageLookupByLibrary.simpleMessage("UDP リレーモード"), "undo": MessageLookupByLibrary.simpleMessage("元に戻す"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("統一遅延"), - "universal": MessageLookupByLibrary.simpleMessage("共通"), "unknown": MessageLookupByLibrary.simpleMessage("不明"), "unknownNetworkError": MessageLookupByLibrary.simpleMessage("不明なネットワークエラー"), "unmaximize": MessageLookupByLibrary.simpleMessage("元に戻す"), @@ -1427,8 +1498,9 @@ class MessageLookup extends MessageLookupByLibrary { "unpinWindow": MessageLookupByLibrary.simpleMessage("固定を解除"), "update": MessageLookupByLibrary.simpleMessage("更新"), "upload": MessageLookupByLibrary.simpleMessage("アップロード"), + "uploadBandwidth": MessageLookupByLibrary.simpleMessage("アップロード帯域幅"), "url": MessageLookupByLibrary.simpleMessage("URL"), - "urlTip": m67, + "urlTip": m71, "useHosts": MessageLookupByLibrary.simpleMessage("Hostsを使用"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("システムのHostsを使用"), "usedTraffic": MessageLookupByLibrary.simpleMessage("使用済みトラフィック"), @@ -1451,7 +1523,8 @@ class MessageLookup extends MessageLookupByLibrary { "writeToSystemDesc": MessageLookupByLibrary.simpleMessage( "システムに書き込むを有効にするとシステムクロックも設定します。Android では無視されます。", ), - "yearsAgo": m68, + "yearsAgo": m72, + "yearsAgoShort": m73, "yes": MessageLookupByLibrary.simpleMessage("はい"), "zhCN": MessageLookupByLibrary.simpleMessage("簡体字中国語"), }; diff --git a/lib/l10n/intl/messages_ru.dart b/lib/l10n/intl/messages_ru.dart index b3ec9532..e7dd2647 100644 --- a/lib/l10n/intl/messages_ru.dart +++ b/lib/l10n/intl/messages_ru.dart @@ -66,134 +66,146 @@ class MessageLookup extends MessageLookupByLibrary { static String m16(count) => "${Intl.plural(count, one: '${count} день назад', few: '${count} дня назад', many: '${count} дней назад', other: '${count} дня назад')}"; - static String m17(label) => + static String m17(count) => "${count} дн. назад"; + + static String m18(label) => "Вы уверены, что хотите удалить выбранные элементы (${label})?"; - static String m18(label) => "Вы уверены, что хотите удалить «${label}»?"; + static String m19(label) => "Вы уверены, что хотите удалить «${label}»?"; - static String m19(label) => "Сведения: ${label}"; + static String m20(label) => "Сведения: ${label}"; - static String m20(name) => + static String m21(name) => "Подключение через ${name} ведёт обратно к прокси, который его использует, поэтому соединение остановлено"; - static String m21(label) => "Поле «${label}» не может быть пустым"; + static String m22(label) => "Поле «${label}» не может быть пустым"; - static String m22(label) => "«${label}» уже существует"; + static String m23(label) => "«${label}» уже существует"; - static String m23(count) => "Ошибок: ${count}"; + static String m24(count) => "Ошибок: ${count}"; - static String m24(label, message) => "${label}: ${message}"; + static String m25(label, message) => "${label}: ${message}"; - static String m25(name) => "${name}: уже последняя версия"; + static String m26(name) => "${name}: уже последняя версия"; - static String m26(name) => "${name}: обновлено"; + static String m27(name) => "${name}: обновлено"; - static String m27(action) => + static String m28(action) => "Уже используется для «${action}». При сохранении будет перенесено сюда."; - static String m28(modifiers) => + static String m29(modifiers) => "Добавьте хотя бы одну из клавиш: ${modifiers}"; - static String m29(count) => + static String m30(count) => "${Intl.plural(count, one: '${count} час назад', few: '${count} часа назад', many: '${count} часов назад', other: '${count} часа назад')}"; - static String m30(count) => + static String m31(count) => "${count} ч назад"; + + static String m32(count) => "${Intl.plural(count, one: '${count} час', few: '${count} часа', many: '${count} часов', other: '${count} часа')}"; - static String m31(target) => "${target} — недопустимая политика"; + static String m33(target) => "${target} — недопустимая политика"; - static String m32(proxyName) => "${proxyName} — недопустимый прокси"; + static String m34(proxyName) => "${proxyName} — недопустимый прокси"; - static String m33(providerName) => + static String m35(providerName) => "${providerName} — недопустимый провайдер прокси"; - static String m34(ruleSet) => "${ruleSet} — недопустимый набор правил"; + static String m36(ruleSet) => "${ruleSet} — недопустимый набор правил"; - static String m35(subRule) => "${subRule} — недопустимый SUB_RULE"; + static String m37(subRule) => "${subRule} — недопустимый SUB_RULE"; - static String m36(count) => + static String m38(count) => "${Intl.plural(count, one: '${count} элемент', few: '${count} элемента', many: '${count} элементов', other: '${count} элемента')}"; - static String m37(line, message) => "Строка ${line}: ${message}"; + static String m39(line, message) => "Строка ${line}: ${message}"; - static String m38(appName) => + static String m40(appName) => "1. Откройте Системные настройки > Конфиденциальность и безопасность\n2. Выберите Службы геолокации\n3. Найдите и отметьте ${appName} в списке\n\nПосле настройки вернитесь в приложение и продолжайте работу. Спасибо за сотрудничество."; - static String m39(label, max) => "«${label}» — не более ${max} символов"; + static String m41(label, max) => "«${label}» — не более ${max} символов"; - static String m40(size) => "Освобождено ${size}"; + static String m42(size) => "Освобождено ${size}"; - static String m41(count) => + static String m43(count) => "${Intl.plural(count, one: '${count} минуту назад', few: '${count} минуты назад', many: '${count} минут назад', other: '${count} минуты назад')}"; - static String m42(count) => + static String m44(count) => "${count} мин назад"; + + static String m45(count) => "${Intl.plural(count, one: '${count} месяц назад', few: '${count} месяца назад', many: '${count} месяцев назад', other: '${count} месяца назад')}"; - static String m43(code) => - "Сервер запретил доступ (HTTP ${code}). Возможно, ссылка устарела или учётные данные неверны"; - - static String m44(code) => "Сервер отклонил запрос (HTTP ${code})"; - - static String m45(code) => - "По этому адресу ничего не найдено (HTTP ${code}). Проверьте правильность URL"; - - static String m46(detail) => "Сетевой запрос не выполнен: ${detail}"; + static String m46(count) => "${count} мес. назад"; static String m47(code) => + "Сервер запретил доступ (HTTP ${code}). Возможно, ссылка устарела или учётные данные неверны"; + + static String m48(code) => "Сервер отклонил запрос (HTTP ${code})"; + + static String m49(code) => + "По этому адресу ничего не найдено (HTTP ${code}). Проверьте правильность URL"; + + static String m50(detail) => "Сетевой запрос не выполнен: ${detail}"; + + static String m51(code) => "На сервере произошла ошибка (HTTP ${code}). Повторите попытку позже"; - static String m48(label) => "Пока нет: ${label}"; + static String m52(label) => "Пока нет: ${label}"; - static String m49(label, min, max) => + static String m53(label, min, max) => "Значение «${label}» должно быть от ${min} до ${max}"; - static String m50(label) => "Значение «${label}» должно быть числом"; + static String m54(label) => "Значение «${label}» должно быть числом"; - static String m51(label) => + static String m55(label) => "Значение «${label}» должно быть от 1024 до 49151"; - static String m52(label, profiles) => + static String m56(label, profiles) => "«${label}» всё ещё используется профилями: ${profiles}. После удаления они будут работать неправильно. Вы уверены, что хотите удалить его?"; - static String m53(count) => "${count} прокси"; + static String m57(count, label) => + "${Intl.plural(count, other: '${label} прокси')}"; - static String m54(name, count) => "${name} и другие (${count} прокси)"; + static String m58(name, count) => "${name} и другие (${count} прокси)"; - static String m55(removed) => + static String m59(removed) => "Будут удалены прокси: ${removed}. Строка с изменённым именем считается новым прокси, а пользовательские профили, ссылающиеся на удалённые, пометят их как отсутствующие. Применить?"; - static String m56(removed, added) => + static String m60(removed, added) => "Будут удалены прокси: ${removed}. Будут добавлены: ${added}. Строка с изменённым именем считается новым прокси, а пользовательские профили, ссылающиеся на удалённые, пометят их как отсутствующие. Применить?"; - static String m57(label) => + static String m61(label) => "На «${label}» всё ещё ссылаются правила, группы прокси, прокси для подключения, DNS или NTP этого профиля, и после удаления эти ссылки перестанут работать. Вы уверены, что хотите удалить её?"; - static String m58(count) => - "${Intl.plural(count, one: '${count} правило', few: '${count} правила', many: '${count} правил', other: '${count} правила')}"; + static String m62(count, label) => + "${Intl.plural(count, one: '${label} правило', few: '${label} правила', many: '${label} правил', other: '${label} правила')}"; - static String m59(appName) => "${appName} (Безопасный режим)"; - - static String m60(count) => - "${Intl.plural(count, one: '${count} секунда', few: '${count} секунды', many: '${count} секунд', other: '${count} секунды')}"; - - static String m61(count) => "Действуют: ${count}"; - - static String m62(count) => "Выбрано: ${count}"; - - static String m63(time) => "Проверено в ${time}"; + static String m63(appName) => "${appName} (Безопасный режим)"; static String m64(count) => - "${Intl.plural(count, one: '${count} настройка', few: '${count} настройки', many: '${count} настроек', other: '${count} настройки')}"; + "${Intl.plural(count, one: '${count} секунда', few: '${count} секунды', many: '${count} секунд', other: '${count} секунды')}"; - static String m65(link) => "Не удалось прочитать прокси из ${link}"; + static String m65(count) => "Действуют: ${count}"; - static String m66(label) => "«${label}» — только одно значение"; + static String m66(count) => "Выбрано: ${count}"; - static String m67(label) => "Значение «${label}» должно быть URL"; + static String m67(time) => "Проверено в ${time}"; static String m68(count) => + "${Intl.plural(count, one: '${count} настройка', few: '${count} настройки', many: '${count} настроек', other: '${count} настройки')}"; + + static String m69(link) => "Не удалось прочитать прокси из ${link}"; + + static String m70(label) => "«${label}» — только одно значение"; + + static String m71(label) => "Значение «${label}» должно быть URL"; + + static String m72(count) => "${Intl.plural(count, one: '${count} год назад', few: '${count} года назад', many: '${count} лет назад', other: '${count} года назад')}"; + static String m73(count) => + "${Intl.plural(count, one: '${count} г. назад', few: '${count} г. назад', many: '${count} л. назад', other: '${count} г. назад')}"; + final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { "about": MessageLookupByLibrary.simpleMessage("О программе"), @@ -268,11 +280,19 @@ class MessageLookup extends MessageLookupByLibrary { "advancedConfigDesc": MessageLookupByLibrary.simpleMessage( "Сеть, DNS, добавленные правила и скрипты", ), + "ageEncryptedProfile": MessageLookupByLibrary.simpleMessage( + "Зашифрованный профиль", + ), + "ageSecretKey": MessageLookupByLibrary.simpleMessage("Секретный ключ age"), + "ageSecretKeyRequiredTip": MessageLookupByLibrary.simpleMessage( + "Профиль зашифрован с помощью age, для расшифровки нужен секретный ключ age", + ), "agree": MessageLookupByLibrary.simpleMessage("Согласен"), "allowBypass": MessageLookupByLibrary.simpleMessage( "Разрешить приложениям обходить VPN", ), "allowLan": MessageLookupByLibrary.simpleMessage("Разрешить LAN"), + "allowedIps": MessageLookupByLibrary.simpleMessage("Разрешённые IP"), "answers": MessageLookupByLibrary.simpleMessage("Ответы"), "app": MessageLookupByLibrary.simpleMessage("Приложение"), "appAccessControl": MessageLookupByLibrary.simpleMessage( @@ -386,6 +406,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "cancel": MessageLookupByLibrary.simpleMessage("Отмена"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("Снять выделение"), + "cannotSave": MessageLookupByLibrary.simpleMessage("Не удалось сохранить"), "changeProxyFailedTip": MessageLookupByLibrary.simpleMessage( "Не удалось переключить прокси; восстановлен предыдущий выбор", ), @@ -408,12 +429,16 @@ class MessageLookup extends MessageLookupByLibrary { "checkUpdateError": MessageLookupByLibrary.simpleMessage( "У вас уже последняя версия", ), + "cipher": MessageLookupByLibrary.simpleMessage("Шифр"), "clearData": MessageLookupByLibrary.simpleMessage("Очистить данные"), "clearDataAndExitTip": MessageLookupByLibrary.simpleMessage( "Все профили, настройки и локальные данные будут удалены, после чего приложение закроется. Откройте его снова, чтобы начать заново.", ), "clearDataFailed": m1, "clearSearch": MessageLookupByLibrary.simpleMessage("Очистить поиск"), + "clientFingerprint": MessageLookupByLibrary.simpleMessage( + "Отпечаток клиента", + ), "clipboardExport": MessageLookupByLibrary.simpleMessage( "Экспорт в буфер обмена", ), @@ -444,6 +469,9 @@ class MessageLookup extends MessageLookupByLibrary { "confirmForceCrashCore": MessageLookupByLibrary.simpleMessage( "Вы уверены, что хотите принудительно завершить ядро со сбоем?", ), + "congestionControl": MessageLookupByLibrary.simpleMessage( + "Управление перегрузкой", + ), "congestionController": MessageLookupByLibrary.simpleMessage( "Управление перегрузкой TCP", ), @@ -505,6 +533,12 @@ class MessageLookup extends MessageLookupByLibrary { "customProfileDesc": MessageLookupByLibrary.simpleMessage( "Группы прокси и правила из прокси приложения и других профилей", ), + "customProfileResources": MessageLookupByLibrary.simpleMessage( + "Ресурсы пользовательских профилей", + ), + "customProfileResourcesDesc": MessageLookupByLibrary.simpleMessage( + "Прокси, провайдеры правил, фильтры и наборы значков", + ), "cut": MessageLookupByLibrary.simpleMessage("Вырезать"), "dark": MessageLookupByLibrary.simpleMessage("Тёмная"), "dashboard": MessageLookupByLibrary.simpleMessage("Панель"), @@ -518,6 +552,7 @@ class MessageLookup extends MessageLookupByLibrary { "Не удалось сохранить изменение; оно отменено", ), "daysAgo": m16, + "daysAgoShort": m17, "defaultSelected": MessageLookupByLibrary.simpleMessage( "Выбор по умолчанию", ), @@ -529,8 +564,8 @@ class MessageLookup extends MessageLookupByLibrary { "delayFailed": MessageLookupByLibrary.simpleMessage("Сбой"), "delayTest": MessageLookupByLibrary.simpleMessage("Тест задержки"), "delete": MessageLookupByLibrary.simpleMessage("Удалить"), - "deleteMultipTip": m17, - "deleteTip": m18, + "deleteMultipTip": m18, + "deleteTip": m19, "desc": MessageLookupByLibrary.simpleMessage( "Многоплатформенный прокси-клиент на основе ClashMeta: простой и удобный, с открытым исходным кодом и без рекламы.", ), @@ -541,7 +576,7 @@ class MessageLookup extends MessageLookupByLibrary { "destinationIPASN": MessageLookupByLibrary.simpleMessage( "ASN IP назначения", ), - "details": m19, + "details": m20, "detectionTip": MessageLookupByLibrary.simpleMessage( "Использует сторонний API; только для справки", ), @@ -555,7 +590,7 @@ class MessageLookup extends MessageLookupByLibrary { "dialerProxyDesc": MessageLookupByLibrary.simpleMessage( "Прокси для подключения — исход, через который идёт обращение к NTP-серверу.", ), - "dialerProxyLoopStopped": m20, + "dialerProxyLoopStopped": m21, "direct": MessageLookupByLibrary.simpleMessage("Прямой"), "disableIcmpForwarding": MessageLookupByLibrary.simpleMessage( "Отключить пересылку ICMP", @@ -692,6 +727,9 @@ class MessageLookup extends MessageLookupByLibrary { "docked": MessageLookupByLibrary.simpleMessage("Закреплённая"), "domain": MessageLookupByLibrary.simpleMessage("Домен"), "download": MessageLookupByLibrary.simpleMessage("Загрузка"), + "downloadBandwidth": MessageLookupByLibrary.simpleMessage( + "Скорость загрузки", + ), "edit": MessageLookupByLibrary.simpleMessage("Редактировать"), "editGlobalRules": MessageLookupByLibrary.simpleMessage( "Редактировать глобальные правила", @@ -708,7 +746,7 @@ class MessageLookup extends MessageLookupByLibrary { "emptyFallback": MessageLookupByLibrary.simpleMessage( "Резерв для пустой группы", ), - "emptyTip": m21, + "emptyTip": m22, "en": MessageLookupByLibrary.simpleMessage("Английский"), "enabled": MessageLookupByLibrary.simpleMessage("Включено"), "entries": MessageLookupByLibrary.simpleMessage(" записей"), @@ -726,7 +764,7 @@ class MessageLookup extends MessageLookupByLibrary { "excludeSsidsDesc": MessageLookupByLibrary.simpleMessage( "При подключении к Wi-Fi с исключённым SSID состояние работы приложения переключается автоматически.", ), - "existsTip": m22, + "existsTip": m23, "exit": MessageLookupByLibrary.simpleMessage("Выход"), "exitFullScreen": MessageLookupByLibrary.simpleMessage( "Выйти из полноэкранного режима", @@ -754,8 +792,8 @@ class MessageLookup extends MessageLookupByLibrary { "externalLink": MessageLookupByLibrary.simpleMessage("Внешняя ссылка"), "extraLarge": MessageLookupByLibrary.simpleMessage("Очень крупный"), "fade": MessageLookupByLibrary.simpleMessage("Растворение"), - "failedCount": m23, - "failedItem": m24, + "failedCount": m24, + "failedItem": m25, "fakeipFilter": MessageLookupByLibrary.simpleMessage("Фильтр Fake-IP"), "fakeipFilterMode": MessageLookupByLibrary.simpleMessage( "Режим фильтра Fake-IP", @@ -806,8 +844,8 @@ class MessageLookup extends MessageLookupByLibrary { "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("Фруктовый микс"), "general": MessageLookupByLibrary.simpleMessage("Общие"), "geoResources": MessageLookupByLibrary.simpleMessage("Geo"), - "geoSkipped": m25, - "geoUpdated": m26, + "geoSkipped": m26, + "geoUpdated": m27, "geodataLoader": MessageLookupByLibrary.simpleMessage( "Geo: экономия памяти", ), @@ -839,26 +877,28 @@ class MessageLookup extends MessageLookupByLibrary { "hideTimeoutProxiesDesc": MessageLookupByLibrary.simpleMessage( "Не показывать прокси, у которых последний тест задержки завершился таймаутом", ), + "hopInterval": MessageLookupByLibrary.simpleMessage("Интервал перескока"), "host": MessageLookupByLibrary.simpleMessage("Хост"), "hostname": MessageLookupByLibrary.simpleMessage("Имя хоста"), - "hotkeyConflictWith": m27, + "hotkeyConflictWith": m28, "hotkeyDesc": MessageLookupByLibrary.simpleMessage( "Глобальные горячие клавиши работают, даже когда окно скрыто. Нажмите на действие, чтобы записать сочетание клавиш.", ), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("Горячие клавиши"), - "hotkeyNeedsModifier": m28, + "hotkeyNeedsModifier": m29, "hotkeyNotSet": MessageLookupByLibrary.simpleMessage("Не задано"), "hotkeyUnavailable": MessageLookupByLibrary.simpleMessage( "Не зарегистрировано: сочетание может быть занято другим приложением", ), "hours": MessageLookupByLibrary.simpleMessage("часов"), - "hoursAgo": m29, - "hoursCount": m30, + "hoursAgo": m30, + "hoursAgoShort": m31, + "hoursCount": m32, "icon": MessageLookupByLibrary.simpleMessage("Значок"), "iconRecords": MessageLookupByLibrary.simpleMessage("История значков"), "iconSets": MessageLookupByLibrary.simpleMessage("Наборы значков"), "iconSetsEmptyTip": MessageLookupByLibrary.simpleMessage( - "Наборов значков пока нет. Добавьте их в разделе «Расширенная конфигурация → Наборы значков»", + "Наборов значков пока нет. Добавьте их в разделе «Ресурсы пользовательских профилей → Наборы значков»", ), "iconStyle": MessageLookupByLibrary.simpleMessage("Стиль значков"), "iconStyleFilled": MessageLookupByLibrary.simpleMessage("С подложкой"), @@ -923,29 +963,41 @@ class MessageLookup extends MessageLookupByLibrary { "invalidBackupFile": MessageLookupByLibrary.simpleMessage( "Недопустимый файл резервной копии", ), + "invalidCidrContent": MessageLookupByLibrary.simpleMessage( + "Введите диапазон IP в формате CIDR, например 192.168.0.0/16", + ), "invalidDscpContent": MessageLookupByLibrary.simpleMessage( "Метка DSCP не может превышать 63", ), + "invalidHostContent": MessageLookupByLibrary.simpleMessage( + "Введите домен или IP-адрес", + ), "invalidIconSet": MessageLookupByLibrary.simpleMessage( "Это не набор значков", ), "invalidLinkTip": MessageLookupByLibrary.simpleMessage( "Это не ссылка на подписку и не ссылка на прокси", ), + "invalidListenContent": MessageLookupByLibrary.simpleMessage( + "Введите адрес и порт, например 0.0.0.0:1053", + ), "invalidNetworkContent": MessageLookupByLibrary.simpleMessage( "Поддерживаются только tcp и udp", ), - "invalidPolicy": m31, + "invalidPolicy": m33, + "invalidPortRangeContent": MessageLookupByLibrary.simpleMessage( + "Введите порт или диапазон, например 443 или 8000-9000", + ), "invalidProfileQrcode": MessageLookupByLibrary.simpleMessage( "Этот QR-код не содержит ссылку на профиль", ), - "invalidProxy": m32, - "invalidProxyProvider": m33, + "invalidProxy": m34, + "invalidProxyProvider": m35, "invalidRangeContent": MessageLookupByLibrary.simpleMessage( "Введите числа или диапазоны, например 80 или 8000-9000, через /", ), - "invalidRuleSet": m34, - "invalidSubRule": m35, + "invalidRuleSet": m36, + "invalidSubRule": m37, "ipAddress": MessageLookupByLibrary.simpleMessage("IP-адрес"), "ipAsn": MessageLookupByLibrary.simpleMessage("ASN"), "ipFlagAbuser": MessageLookupByLibrary.simpleMessage("Злоупотребления"), @@ -984,7 +1036,7 @@ class MessageLookup extends MessageLookupByLibrary { "При включённом IPv6 VPN принимает входящий IPv6.", ), "ipv6Timeout": MessageLookupByLibrary.simpleMessage("Тайм-аут IPv6 (мс)"), - "itemsCount": m36, + "itemsCount": m38, "ja": MessageLookupByLibrary.simpleMessage("Японский"), "justNow": MessageLookupByLibrary.simpleMessage("Только что"), "keepAliveIdle": MessageLookupByLibrary.simpleMessage( @@ -1006,7 +1058,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "layout": MessageLookupByLibrary.simpleMessage("Макет"), "light": MessageLookupByLibrary.simpleMessage("Светлая"), - "lineIssueTip": m37, + "lineIssueTip": m39, "lineWrap": MessageLookupByLibrary.simpleMessage("Перенос строк"), "link": MessageLookupByLibrary.simpleMessage("Ссылка"), "linkDesc": MessageLookupByLibrary.simpleMessage( @@ -1039,7 +1091,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "По требованию системы для получения имени сети Wi-Fi необходимо разрешение на геолокацию. На Android выберите «Разрешить всегда», иначе имя сети Wi-Fi нельзя получить, пока приложение в фоне.", ), - "locationPermissionGuide": m38, + "locationPermissionGuide": m40, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "Требуется разрешение на геолокацию", ), @@ -1065,7 +1117,7 @@ class MessageLookup extends MessageLookupByLibrary { "maxFailedTimes": MessageLookupByLibrary.simpleMessage( "Макс. число неудач", ), - "maxLengthTip": m39, + "maxLengthTip": m41, "maximize": MessageLookupByLibrary.simpleMessage("Развернуть"), "memoryAppResident": MessageLookupByLibrary.simpleMessage( "Резидентная память", @@ -1096,7 +1148,7 @@ class MessageLookup extends MessageLookupByLibrary { "memoryReleased": MessageLookupByLibrary.simpleMessage( "Память освобождена", ), - "memoryReleasedSize": m40, + "memoryReleasedSize": m42, "messageTest": MessageLookupByLibrary.simpleMessage("Тест сообщения"), "messageTestTip": MessageLookupByLibrary.simpleMessage("Это сообщение."), "min": MessageLookupByLibrary.simpleMessage("Минимальный"), @@ -1104,17 +1156,19 @@ class MessageLookup extends MessageLookupByLibrary { "minimizeOnExit": MessageLookupByLibrary.simpleMessage( "Сворачивать при выходе", ), - "minutesAgo": m41, + "minutesAgo": m43, + "minutesAgoShort": m44, "mixedPort": MessageLookupByLibrary.simpleMessage("Смешанный порт"), "mode": MessageLookupByLibrary.simpleMessage("Режим"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("Монохром"), - "monthsAgo": m42, + "monthsAgo": m45, + "monthsAgoShort": m46, "more": MessageLookupByLibrary.simpleMessage("Ещё"), "name": MessageLookupByLibrary.simpleMessage("Название"), "navigationBarStyle": MessageLookupByLibrary.simpleMessage("Нижняя панель"), "network": MessageLookupByLibrary.simpleMessage("Сеть"), - "networkAccessDeniedError": m43, - "networkBadResponseError": m44, + "networkAccessDeniedError": m47, + "networkBadResponseError": m48, "networkCancelledError": MessageLookupByLibrary.simpleMessage( "Запрос отменён", ), @@ -1126,13 +1180,13 @@ class MessageLookup extends MessageLookupByLibrary { "Не удалось определить адрес сервера. Проверьте правильность URL и работу DNS", ), "networkName": MessageLookupByLibrary.simpleMessage("Имя сети"), - "networkNotFoundError": m45, + "networkNotFoundError": m49, "networkRateLimitedError": MessageLookupByLibrary.simpleMessage( "Слишком много запросов (HTTP 429). Подождите немного и повторите попытку", ), - "networkRequestFailed": m46, + "networkRequestFailed": m50, "networkSecret": MessageLookupByLibrary.simpleMessage("Секрет сети"), - "networkServerError": m47, + "networkServerError": m51, "networkSpeed": MessageLookupByLibrary.simpleMessage("Скорость сети"), "networkTimeoutError": MessageLookupByLibrary.simpleMessage( "Время ожидания запроса истекло. Проверьте сеть или прокси и повторите попытку", @@ -1176,9 +1230,14 @@ class MessageLookup extends MessageLookupByLibrary { "nullProfileDesc": MessageLookupByLibrary.simpleMessage( "Добавьте профиль, чтобы начать", ), - "nullTip": m48, - "numberRangeTip": m49, - "numberTip": m50, + "nullTip": m52, + "numberRangeTip": m53, + "numberTip": m54, + "obfs": MessageLookupByLibrary.simpleMessage("Обфускация"), + "obfsHost": MessageLookupByLibrary.simpleMessage("Хост обфускации"), + "obfsMode": MessageLookupByLibrary.simpleMessage("Режим обфускации"), + "obfsParam": MessageLookupByLibrary.simpleMessage("Параметр обфускации"), + "obfsPassword": MessageLookupByLibrary.simpleMessage("Пароль обфускации"), "onDemand": MessageLookupByLibrary.simpleMessage("По условию"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "«По условию» приостанавливает прокси в исключённых сетях Wi-Fi.", @@ -1227,12 +1286,17 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "Загрузите корректный QR-код", ), + "plugin": MessageLookupByLibrary.simpleMessage("Плагин"), + "pluginHost": MessageLookupByLibrary.simpleMessage("Хост плагина"), + "pluginMode": MessageLookupByLibrary.simpleMessage("Режим плагина"), "port": MessageLookupByLibrary.simpleMessage("Порт"), "portConflictTip": MessageLookupByLibrary.simpleMessage( "Введите другой порт", ), - "portTip": m51, + "portHopping": MessageLookupByLibrary.simpleMessage("Перескок портов"), + "portTip": m55, "ports": MessageLookupByLibrary.simpleMessage("Порты"), + "preSharedKey": MessageLookupByLibrary.simpleMessage("Общий ключ"), "prerequisites": MessageLookupByLibrary.simpleMessage( "Предварительные условия", ), @@ -1243,6 +1307,10 @@ class MessageLookup extends MessageLookupByLibrary { "previousMatch": MessageLookupByLibrary.simpleMessage( "Предыдущее совпадение", ), + "privateKey": MessageLookupByLibrary.simpleMessage("Закрытый ключ"), + "privateKeyPassphrase": MessageLookupByLibrary.simpleMessage( + "Пароль закрытого ключа", + ), "process": MessageLookupByLibrary.simpleMessage("Процесс"), "profile": MessageLookupByLibrary.simpleMessage("Профиль"), "profileAutoUpdateIntervalInvalidValidationDesc": @@ -1267,23 +1335,26 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("Профили"), "profilesSort": MessageLookupByLibrary.simpleMessage("Сортировка профилей"), "project": MessageLookupByLibrary.simpleMessage("Проект"), - "providerInUse": m52, + "protocol": MessageLookupByLibrary.simpleMessage("Протокол"), + "protocolParam": MessageLookupByLibrary.simpleMessage("Параметр протокола"), + "protocolVersion": MessageLookupByLibrary.simpleMessage("Версия протокола"), + "providerInUse": m56, "providerUrlTip": MessageLookupByLibrary.simpleMessage( "Поддерживаются только удалённые ресурсы", ), "providers": MessageLookupByLibrary.simpleMessage("Внешние ресурсы"), "proxies": MessageLookupByLibrary.simpleMessage("Прокси"), - "proxiesCount": m53, + "proxiesCount": m57, "proxiesEmpty": MessageLookupByLibrary.simpleMessage("Список прокси пуст"), - "proxiesProfileLabel": m54, - "proxiesRemovedTip": m55, - "proxiesReplacedTip": m56, + "proxiesProfileLabel": m58, + "proxiesRemovedTip": m59, + "proxiesReplacedTip": m60, "proxyChains": MessageLookupByLibrary.simpleMessage("Цепочка прокси"), "proxyGroup": MessageLookupByLibrary.simpleMessage("Группа прокси"), "proxyGroupEmpty": MessageLookupByLibrary.simpleMessage( "Группа прокси пуста", ), - "proxyGroupInUse": m57, + "proxyGroupInUse": m61, "proxyGroupNameDuplicate": MessageLookupByLibrary.simpleMessage( "Название группы прокси уже используется", ), @@ -1296,6 +1367,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "proxyType": MessageLookupByLibrary.simpleMessage("Тип прокси"), "pruneCache": MessageLookupByLibrary.simpleMessage("Очистить кэш"), + "publicKey": MessageLookupByLibrary.simpleMessage("Открытый ключ"), "pureBlack": MessageLookupByLibrary.simpleMessage("Чисто чёрный"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("Чисто чёрный режим"), "qrcode": MessageLookupByLibrary.simpleMessage("QR-код"), @@ -1319,6 +1391,9 @@ class MessageLookup extends MessageLookupByLibrary { "remoteDestination": MessageLookupByLibrary.simpleMessage( "Удалённое назначение", ), + "remoteDnsResolve": MessageLookupByLibrary.simpleMessage( + "Удалённое разрешение DNS", + ), "remove": MessageLookupByLibrary.simpleMessage("Убрать"), "rename": MessageLookupByLibrary.simpleMessage("Переименовать"), "replace": MessageLookupByLibrary.simpleMessage("Заменить"), @@ -1328,6 +1403,7 @@ class MessageLookup extends MessageLookupByLibrary { "requestsAndUpdates": MessageLookupByLibrary.simpleMessage( "Запросы и обновления", ), + "required": MessageLookupByLibrary.simpleMessage("Обязательно"), "reset": MessageLookupByLibrary.simpleMessage("Сброс"), "resetPageChangesTip": MessageLookupByLibrary.simpleMessage( "На этой странице есть изменения. Вы уверены, что хотите выполнить сброс?", @@ -1531,10 +1607,10 @@ class MessageLookup extends MessageLookupByLibrary { "Правило должно начинаться с поддерживаемого типа, например DOMAIN,example.com,DIRECT", ), "rules": MessageLookupByLibrary.simpleMessage("Правила"), - "rulesCount": m58, + "rulesCount": m62, "runTime": MessageLookupByLibrary.simpleMessage("Время работы"), "safeMode": MessageLookupByLibrary.simpleMessage("Безопасный режим"), - "safeModeAppTitle": m59, + "safeModeAppTitle": m63, "save": MessageLookupByLibrary.simpleMessage("Сохранить"), "saveChanges": MessageLookupByLibrary.simpleMessage("Сохранить изменения?"), "script": MessageLookupByLibrary.simpleMessage("Скрипт"), @@ -1546,8 +1622,9 @@ class MessageLookup extends MessageLookupByLibrary { ), "search": MessageLookupByLibrary.simpleMessage("Поиск"), "seconds": MessageLookupByLibrary.simpleMessage("секунд"), - "secondsCount": m60, - "sectionsInEffect": m61, + "secondsCount": m64, + "secretKey": MessageLookupByLibrary.simpleMessage("Ключ"), + "sectionsInEffect": m65, "selectAll": MessageLookupByLibrary.simpleMessage("Выбрать всё"), "selectProxies": MessageLookupByLibrary.simpleMessage("Выбрать прокси"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage( @@ -1563,13 +1640,13 @@ class MessageLookup extends MessageLookupByLibrary { "Выберите подправило", ), "selected": MessageLookupByLibrary.simpleMessage("Выбрано"), - "selectedCountTitle": m62, + "selectedCountTitle": m66, "server": MessageLookupByLibrary.simpleMessage("Сервер"), "serviceAvailable": MessageLookupByLibrary.simpleMessage("Доступен"), "serviceBlocked": MessageLookupByLibrary.simpleMessage("Заблокировано"), "serviceCheck": MessageLookupByLibrary.simpleMessage("Проверить"), "serviceCheckAll": MessageLookupByLibrary.simpleMessage("Проверить все"), - "serviceCheckedAt": m63, + "serviceCheckedAt": m67, "serviceComingSoon": MessageLookupByLibrary.simpleMessage("Скоро появится"), "serviceDisallowedIsp": MessageLookupByLibrary.simpleMessage( "Недопустимый провайдер", @@ -1592,8 +1669,8 @@ class MessageLookup extends MessageLookupByLibrary { ), "settingEntries": MessageLookupByLibrary.simpleMessage("параметры"), "settings": MessageLookupByLibrary.simpleMessage("Настройки"), - "settingsCount": m64, - "shareLinkUnreadable": m65, + "settingsCount": m68, + "shareLinkUnreadable": m69, "shareLinksInvalid": MessageLookupByLibrary.simpleMessage( "Не удалось прочитать прокси из ссылок", ), @@ -1620,7 +1697,7 @@ class MessageLookup extends MessageLookupByLibrary { "singleShareLinkOnly": MessageLookupByLibrary.simpleMessage( "Здесь укажите одну ссылку, несколько можно задать в быстром редактировании списка", ), - "singleValueTip": m66, + "singleValueTip": m70, "size": MessageLookupByLibrary.simpleMessage("Размер"), "skipCertVerify": MessageLookupByLibrary.simpleMessage( "Пропускать проверку сертификата", @@ -1737,15 +1814,21 @@ class MessageLookup extends MessageLookupByLibrary { "totalTraffic": MessageLookupByLibrary.simpleMessage("Общий трафик"), "tproxyPort": MessageLookupByLibrary.simpleMessage("Порт TProxy"), "trafficUsage": MessageLookupByLibrary.simpleMessage("Статистика трафика"), + "transport": MessageLookupByLibrary.simpleMessage("Транспорт"), "tun": MessageLookupByLibrary.simpleMessage("TUN"), + "tunAuthorizationFailed": MessageLookupByLibrary.simpleMessage( + "Не удалось получить права для TUN, поэтому он выключен.", + ), "tunDesc": MessageLookupByLibrary.simpleMessage( "TUN работает только в режиме администратора.", ), "turnOff": MessageLookupByLibrary.simpleMessage("Выключить"), "turnOn": MessageLookupByLibrary.simpleMessage("Включить"), + "udpRelayMode": MessageLookupByLibrary.simpleMessage( + "Режим ретрансляции UDP", + ), "undo": MessageLookupByLibrary.simpleMessage("Отменить"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("Единая задержка"), - "universal": MessageLookupByLibrary.simpleMessage("Общие"), "unknown": MessageLookupByLibrary.simpleMessage("Неизвестно"), "unknownNetworkError": MessageLookupByLibrary.simpleMessage( "Неизвестная сетевая ошибка", @@ -1755,8 +1838,9 @@ class MessageLookup extends MessageLookupByLibrary { "unpinWindow": MessageLookupByLibrary.simpleMessage("Открепить окно"), "update": MessageLookupByLibrary.simpleMessage("Обновить"), "upload": MessageLookupByLibrary.simpleMessage("Отдача"), + "uploadBandwidth": MessageLookupByLibrary.simpleMessage("Скорость отдачи"), "url": MessageLookupByLibrary.simpleMessage("URL"), - "urlTip": m67, + "urlTip": m71, "useHosts": MessageLookupByLibrary.simpleMessage("Использовать hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage( "Использовать системный hosts", @@ -1791,7 +1875,8 @@ class MessageLookup extends MessageLookupByLibrary { "writeToSystemDesc": MessageLookupByLibrary.simpleMessage( "«Записывать в систему» также устанавливает системные часы; Android это игнорирует.", ), - "yearsAgo": m68, + "yearsAgo": m72, + "yearsAgoShort": m73, "yes": MessageLookupByLibrary.simpleMessage("Да"), "zhCN": MessageLookupByLibrary.simpleMessage("Упрощённый китайский"), }; diff --git a/lib/l10n/intl/messages_zh_CN.dart b/lib/l10n/intl/messages_zh_CN.dart index 81675e03..d3b6074e 100644 --- a/lib/l10n/intl/messages_zh_CN.dart +++ b/lib/l10n/intl/messages_zh_CN.dart @@ -57,114 +57,126 @@ class MessageLookup extends MessageLookupByLibrary { static String m16(count) => "${count} 天前"; - static String m17(label) => "确定删除选中的${label}吗?"; + static String m17(count) => "${count} 天前"; - static String m18(label) => "确定删除当前${label}吗?"; + static String m18(label) => "确定删除选中的${label}吗?"; - static String m19(label) => "${label}详情"; + static String m19(label) => "确定删除当前${label}吗?"; - static String m20(name) => "经 ${name} 拨号会绕回使用它的代理,连接已中止"; + static String m20(label) => "${label}详情"; - static String m21(label) => "${label}不能为空"; + static String m21(name) => "经 ${name} 拨号会绕回使用它的代理,连接已中止"; - static String m22(label) => "${label}当前已存在"; + static String m22(label) => "${label}不能为空"; - static String m23(count) => "${count} 项失败"; + static String m23(label) => "${label}当前已存在"; - static String m24(label, message) => "${label}:${message}"; + static String m24(count) => "${count} 项失败"; - static String m25(name) => "${name} 已是最新版本"; + static String m25(label, message) => "${label}:${message}"; - static String m26(name) => "${name} 已更新"; + static String m26(name) => "${name} 已是最新版本"; - static String m27(action) => "已被“${action}”使用,保存后将移到这里"; + static String m27(name) => "${name} 已更新"; - static String m28(modifiers) => "至少包含 ${modifiers} 中的一个"; + static String m28(action) => "已被“${action}”使用,保存后将移到这里"; - static String m29(count) => "${count} 小时前"; + static String m29(modifiers) => "至少包含 ${modifiers} 中的一个"; - static String m30(count) => "${count} 小时"; + static String m30(count) => "${count} 小时前"; - static String m31(target) => "${target} 是一个无效的策略"; + static String m31(count) => "${count} 小时前"; - static String m32(proxyName) => "${proxyName} 是一个无效的代理"; + static String m32(count) => "${count} 小时"; - static String m33(providerName) => "${providerName} 是一个无效的代理集"; + static String m33(target) => "${target} 是一个无效的策略"; - static String m34(ruleSet) => "${ruleSet} 不是有效的规则集"; + static String m34(proxyName) => "${proxyName} 是一个无效的代理"; - static String m35(subRule) => "${subRule} 是一个无效的SUB_RULE"; + static String m35(providerName) => "${providerName} 是一个无效的代理集"; - static String m36(count) => "${count} 项"; + static String m36(ruleSet) => "${ruleSet} 不是有效的规则集"; - static String m37(line, message) => "第 ${line} 行:${message}"; + static String m37(subRule) => "${subRule} 是一个无效的SUB_RULE"; - static String m38(appName) => + static String m38(count) => "${count} 项"; + + static String m39(line, message) => "第 ${line} 行:${message}"; + + static String m40(appName) => "1. 打开 系统设置 > 隐私与安全性\n2. 选择 定位服务\n3. 在右侧列表中找到并勾选 ${appName}\n\n完成设置后,返回应用即可正常使用。感谢您的配合。"; - static String m39(label, max) => "${label}最多${max}个字符"; + static String m41(label, max) => "${label}最多${max}个字符"; - static String m40(size) => "已释放 ${size}"; + static String m42(size) => "已释放 ${size}"; - static String m41(count) => "${count} 分钟前"; + static String m43(count) => "${count} 分钟前"; - static String m42(count) => "${count} 个月前"; + static String m44(count) => "${count} 分钟前"; - static String m43(code) => "服务器拒绝访问(HTTP ${code}),链接可能已过期或凭据有误"; + static String m45(count) => "${count} 个月前"; - static String m44(code) => "服务器拒绝了请求(HTTP ${code})"; + static String m46(count) => "${count} 个月前"; - static String m45(code) => "该地址下没有内容(HTTP ${code}),请确认链接是否正确"; + static String m47(code) => "服务器拒绝访问(HTTP ${code}),链接可能已过期或凭据有误"; - static String m46(detail) => "网络请求失败:${detail}"; + static String m48(code) => "服务器拒绝了请求(HTTP ${code})"; - static String m47(code) => "服务器出现问题(HTTP ${code}),请稍后再试"; + static String m49(code) => "该地址下没有内容(HTTP ${code}),请确认链接是否正确"; - static String m48(label) => "暂无${label}"; + static String m50(detail) => "网络请求失败:${detail}"; - static String m49(label, min, max) => "${label}必须在 ${min} 到 ${max} 之间"; + static String m51(code) => "服务器出现问题(HTTP ${code}),请稍后再试"; - static String m50(label) => "${label}必须为数字"; + static String m52(label) => "暂无${label}"; - static String m51(label) => "${label} 必须在 1024 到 49151 之间"; + static String m53(label, min, max) => "${label}必须在 ${min} 到 ${max} 之间"; - static String m52(label, profiles) => + static String m54(label) => "${label}必须为数字"; + + static String m55(label) => "${label} 必须在 1024 到 49151 之间"; + + static String m56(label, profiles) => "${label} 仍被 ${profiles} 使用,删除后这些配置会异常。确定删除吗?"; - static String m53(count) => "${count} 个代理"; + static String m57(count, label) => + "${Intl.plural(count, other: '${label} 个代理')}"; - static String m54(name, count) => "${name} 等 ${count} 个代理"; + static String m58(name, count) => "${name} 等 ${count} 个代理"; - static String m55(removed) => + static String m59(removed) => "以下代理将被删除:${removed}。改了名字的行按新代理处理,自定义配置中对被删除代理的引用会标为缺失。确定应用吗?"; - static String m56(removed, added) => + static String m60(removed, added) => "以下代理将被删除:${removed}。以下代理将新增:${added}。改了名字的行按新代理处理,自定义配置中对被删除代理的引用会标为缺失。确定应用吗?"; - static String m57(label) => + static String m61(label) => "${label} 仍被当前配置的规则、策略组、拨号代理、DNS 或 NTP 引用,删除后这些引用会失效。确定删除吗?"; - static String m58(count) => "${count} 条规则"; + static String m62(count, label) => + "${Intl.plural(count, other: '${label} 条规则')}"; - static String m59(appName) => "${appName}(安全模式)"; + static String m63(appName) => "${appName}(安全模式)"; - static String m60(count) => "${count} 秒"; + static String m64(count) => "${count} 秒"; - static String m61(count) => "${count} 项生效"; + static String m65(count) => "${count} 项生效"; - static String m62(count) => "已选择 ${count} 项"; + static String m66(count) => "已选择 ${count} 项"; - static String m63(time) => "检测于 ${time}"; + static String m67(time) => "检测于 ${time}"; - static String m64(count) => "${count} 项设置"; + static String m68(count) => "${count} 项设置"; - static String m65(link) => "无法从 ${link} 中读取代理"; + static String m69(link) => "无法从 ${link} 中读取代理"; - static String m66(label) => "${label}只能是一项"; + static String m70(label) => "${label}只能是一项"; - static String m67(label) => "${label}必须为URL"; + static String m71(label) => "${label}必须为URL"; - static String m68(count) => "${count} 年前"; + static String m72(count) => "${count} 年前"; + + static String m73(count) => "${count} 年前"; final messages = _notInlinedMessages(_notInlinedMessages); static Map _notInlinedMessages(_) => { @@ -216,9 +228,15 @@ class MessageLookup extends MessageLookupByLibrary { "advancedConfigDesc": MessageLookupByLibrary.simpleMessage( "网络、DNS、附加规则与脚本", ), + "ageEncryptedProfile": MessageLookupByLibrary.simpleMessage("配置已加密"), + "ageSecretKey": MessageLookupByLibrary.simpleMessage("age 私钥"), + "ageSecretKeyRequiredTip": MessageLookupByLibrary.simpleMessage( + "该配置已用 age 加密,需要 age 私钥才能解密", + ), "agree": MessageLookupByLibrary.simpleMessage("同意"), "allowBypass": MessageLookupByLibrary.simpleMessage("允许应用绕过VPN"), "allowLan": MessageLookupByLibrary.simpleMessage("局域网代理"), + "allowedIps": MessageLookupByLibrary.simpleMessage("允许的 IP"), "answers": MessageLookupByLibrary.simpleMessage("应答"), "app": MessageLookupByLibrary.simpleMessage("应用"), "appAccessControl": MessageLookupByLibrary.simpleMessage("应用访问控制"), @@ -290,6 +308,7 @@ class MessageLookup extends MessageLookupByLibrary { "cameraUnavailable": MessageLookupByLibrary.simpleMessage("相机不可用"), "cancel": MessageLookupByLibrary.simpleMessage("取消"), "cancelSelectAll": MessageLookupByLibrary.simpleMessage("取消全选"), + "cannotSave": MessageLookupByLibrary.simpleMessage("无法保存"), "changeProxyFailedTip": MessageLookupByLibrary.simpleMessage( "切换代理失败,已恢复上一次的选择", ), @@ -304,12 +323,14 @@ class MessageLookup extends MessageLookupByLibrary { ), "checkUpdate": MessageLookupByLibrary.simpleMessage("检查更新"), "checkUpdateError": MessageLookupByLibrary.simpleMessage("当前应用已经是最新版了"), + "cipher": MessageLookupByLibrary.simpleMessage("加密方式"), "clearData": MessageLookupByLibrary.simpleMessage("清除数据"), "clearDataAndExitTip": MessageLookupByLibrary.simpleMessage( "将删除所有配置、设置和本地数据,随后关闭应用。重新打开即可从头开始。", ), "clearDataFailed": m1, "clearSearch": MessageLookupByLibrary.simpleMessage("清除搜索"), + "clientFingerprint": MessageLookupByLibrary.simpleMessage("客户端指纹"), "clipboardExport": MessageLookupByLibrary.simpleMessage("导出剪贴板"), "clipboardImport": MessageLookupByLibrary.simpleMessage("剪贴板导入"), "clipboardWriteFailed": MessageLookupByLibrary.simpleMessage( @@ -328,6 +349,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "confirmExitWindow": MessageLookupByLibrary.simpleMessage("确定要退出当前窗口吗?"), "confirmForceCrashCore": MessageLookupByLibrary.simpleMessage("确定要强制崩溃核心?"), + "congestionControl": MessageLookupByLibrary.simpleMessage("拥塞控制"), "congestionController": MessageLookupByLibrary.simpleMessage("TCP 拥塞控制"), "connected": MessageLookupByLibrary.simpleMessage("已连接"), "connecting": MessageLookupByLibrary.simpleMessage("连接中…"), @@ -379,6 +401,10 @@ class MessageLookup extends MessageLookupByLibrary { "customProfileDesc": MessageLookupByLibrary.simpleMessage( "基于应用代理和其他配置组建策略组与规则", ), + "customProfileResources": MessageLookupByLibrary.simpleMessage("自定义配置资源"), + "customProfileResourcesDesc": MessageLookupByLibrary.simpleMessage( + "代理、规则集、筛选与图标集", + ), "cut": MessageLookupByLibrary.simpleMessage("剪切"), "dark": MessageLookupByLibrary.simpleMessage("深色"), "dashboard": MessageLookupByLibrary.simpleMessage("仪表盘"), @@ -390,6 +416,7 @@ class MessageLookup extends MessageLookupByLibrary { "保存更改失败,已回滚", ), "daysAgo": m16, + "daysAgoShort": m17, "defaultSelected": MessageLookupByLibrary.simpleMessage("默认选中"), "defaultText": MessageLookupByLibrary.simpleMessage("默认"), "definitionNotMap": MessageLookupByLibrary.simpleMessage( @@ -399,15 +426,15 @@ class MessageLookup extends MessageLookupByLibrary { "delayFailed": MessageLookupByLibrary.simpleMessage("失败"), "delayTest": MessageLookupByLibrary.simpleMessage("延迟测试"), "delete": MessageLookupByLibrary.simpleMessage("删除"), - "deleteMultipTip": m17, - "deleteTip": m18, + "deleteMultipTip": m18, + "deleteTip": m19, "desc": MessageLookupByLibrary.simpleMessage( "基于ClashMeta的多平台代理客户端,简单易用,开源无广告。", ), "destination": MessageLookupByLibrary.simpleMessage("目标地址"), "destinationGeoIP": MessageLookupByLibrary.simpleMessage("目标地理定位"), "destinationIPASN": MessageLookupByLibrary.simpleMessage("目标IP ASN"), - "details": m19, + "details": m20, "detectionTip": MessageLookupByLibrary.simpleMessage("依赖第三方api,仅供参考"), "developerMode": MessageLookupByLibrary.simpleMessage("开发者模式"), "developerModeEnableTip": MessageLookupByLibrary.simpleMessage("开发者模式已启用。"), @@ -415,7 +442,7 @@ class MessageLookup extends MessageLookupByLibrary { "dialerProxyDesc": MessageLookupByLibrary.simpleMessage( "拨号代理为连接 NTP 服务器使用的出站。", ), - "dialerProxyLoopStopped": m20, + "dialerProxyLoopStopped": m21, "direct": MessageLookupByLibrary.simpleMessage("直连"), "disableIcmpForwarding": MessageLookupByLibrary.simpleMessage("禁用ICMP转发"), "disableIcmpForwardingDesc": MessageLookupByLibrary.simpleMessage( @@ -516,6 +543,7 @@ class MessageLookup extends MessageLookupByLibrary { "docked": MessageLookupByLibrary.simpleMessage("固定"), "domain": MessageLookupByLibrary.simpleMessage("域名"), "download": MessageLookupByLibrary.simpleMessage("下载"), + "downloadBandwidth": MessageLookupByLibrary.simpleMessage("下行带宽"), "edit": MessageLookupByLibrary.simpleMessage("编辑"), "editGlobalRules": MessageLookupByLibrary.simpleMessage("编辑全局规则"), "editProxy": MessageLookupByLibrary.simpleMessage("编辑代理"), @@ -524,7 +552,7 @@ class MessageLookup extends MessageLookupByLibrary { "editSsid": MessageLookupByLibrary.simpleMessage("编辑SSID"), "editorUnavailable": MessageLookupByLibrary.simpleMessage("编辑器不可用"), "emptyFallback": MessageLookupByLibrary.simpleMessage("空组兜底"), - "emptyTip": m21, + "emptyTip": m22, "en": MessageLookupByLibrary.simpleMessage("英语"), "enabled": MessageLookupByLibrary.simpleMessage("已开启"), "entries": MessageLookupByLibrary.simpleMessage("个条目"), @@ -540,7 +568,7 @@ class MessageLookup extends MessageLookupByLibrary { "excludeSsidsDesc": MessageLookupByLibrary.simpleMessage( "连接到被排除 SSID 的 Wi-Fi 时,将会自动切换应用运行状态。", ), - "existsTip": m22, + "existsTip": m23, "exit": MessageLookupByLibrary.simpleMessage("退出"), "exitFullScreen": MessageLookupByLibrary.simpleMessage("退出全屏"), "exitNodes": MessageLookupByLibrary.simpleMessage("出口节点"), @@ -562,8 +590,8 @@ class MessageLookup extends MessageLookupByLibrary { "externalLink": MessageLookupByLibrary.simpleMessage("外部链接"), "extraLarge": MessageLookupByLibrary.simpleMessage("超大"), "fade": MessageLookupByLibrary.simpleMessage("淡入"), - "failedCount": m23, - "failedItem": m24, + "failedCount": m24, + "failedItem": m25, "fakeipFilter": MessageLookupByLibrary.simpleMessage("Fake-IP过滤"), "fakeipFilterMode": MessageLookupByLibrary.simpleMessage("Fake-IP过滤模式"), "fakeipFilterModeDesc": MessageLookupByLibrary.simpleMessage( @@ -598,8 +626,8 @@ class MessageLookup extends MessageLookupByLibrary { "fruitSaladScheme": MessageLookupByLibrary.simpleMessage("果缤纷"), "general": MessageLookupByLibrary.simpleMessage("常规"), "geoResources": MessageLookupByLibrary.simpleMessage("Geo"), - "geoSkipped": m25, - "geoUpdated": m26, + "geoSkipped": m26, + "geoUpdated": m27, "geodataLoader": MessageLookupByLibrary.simpleMessage("Geo低内存模式"), "global": MessageLookupByLibrary.simpleMessage("全局"), "go": MessageLookupByLibrary.simpleMessage("前往"), @@ -619,26 +647,28 @@ class MessageLookup extends MessageLookupByLibrary { "hideTimeoutProxiesDesc": MessageLookupByLibrary.simpleMessage( "不显示上次延迟测试超时的代理", ), + "hopInterval": MessageLookupByLibrary.simpleMessage("跳跃间隔"), "host": MessageLookupByLibrary.simpleMessage("主机"), "hostname": MessageLookupByLibrary.simpleMessage("主机名"), - "hotkeyConflictWith": m27, + "hotkeyConflictWith": m28, "hotkeyDesc": MessageLookupByLibrary.simpleMessage( "全局快捷键在窗口隐藏时依然生效,点击操作即可录制组合键。", ), "hotkeyManagement": MessageLookupByLibrary.simpleMessage("快捷键管理"), - "hotkeyNeedsModifier": m28, + "hotkeyNeedsModifier": m29, "hotkeyNotSet": MessageLookupByLibrary.simpleMessage("未设置"), "hotkeyUnavailable": MessageLookupByLibrary.simpleMessage( "未能注册,可能已被其他应用占用", ), "hours": MessageLookupByLibrary.simpleMessage("小时"), - "hoursAgo": m29, - "hoursCount": m30, + "hoursAgo": m30, + "hoursAgoShort": m31, + "hoursCount": m32, "icon": MessageLookupByLibrary.simpleMessage("图标"), "iconRecords": MessageLookupByLibrary.simpleMessage("图标记录"), "iconSets": MessageLookupByLibrary.simpleMessage("图标集"), "iconSetsEmptyTip": MessageLookupByLibrary.simpleMessage( - "暂无图标集,可在「进阶配置 → 图标集」中添加", + "暂无图标集,可在「自定义配置资源 → 图标集」中添加", ), "iconStyle": MessageLookupByLibrary.simpleMessage("图标样式"), "iconStyleFilled": MessageLookupByLibrary.simpleMessage("色块"), @@ -688,25 +718,35 @@ class MessageLookup extends MessageLookupByLibrary { "interval": MessageLookupByLibrary.simpleMessage("间隔"), "intranetIP": MessageLookupByLibrary.simpleMessage("内网 IP"), "invalidBackupFile": MessageLookupByLibrary.simpleMessage("无效备份文件"), + "invalidCidrContent": MessageLookupByLibrary.simpleMessage( + "请输入 CIDR 格式的 IP 段,如 192.168.0.0/16", + ), "invalidDscpContent": MessageLookupByLibrary.simpleMessage( "DSCP 标记不能超过 63", ), + "invalidHostContent": MessageLookupByLibrary.simpleMessage("请输入域名或 IP 地址"), "invalidIconSet": MessageLookupByLibrary.simpleMessage("不是有效的图标集"), "invalidLinkTip": MessageLookupByLibrary.simpleMessage("不是有效的订阅链接或代理分享链接"), + "invalidListenContent": MessageLookupByLibrary.simpleMessage( + "请输入地址和端口,如 0.0.0.0:1053", + ), "invalidNetworkContent": MessageLookupByLibrary.simpleMessage( "仅支持 tcp 或 udp", ), - "invalidPolicy": m31, + "invalidPolicy": m33, + "invalidPortRangeContent": MessageLookupByLibrary.simpleMessage( + "请输入端口或范围,如 443 或 8000-9000", + ), "invalidProfileQrcode": MessageLookupByLibrary.simpleMessage( "该二维码不包含配置文件链接", ), - "invalidProxy": m32, - "invalidProxyProvider": m33, + "invalidProxy": m34, + "invalidProxyProvider": m35, "invalidRangeContent": MessageLookupByLibrary.simpleMessage( "请输入数字或范围,如 80 或 8000-9000,多个用 / 分隔", ), - "invalidRuleSet": m34, - "invalidSubRule": m35, + "invalidRuleSet": m36, + "invalidSubRule": m37, "ipAddress": MessageLookupByLibrary.simpleMessage("IP 地址"), "ipAsn": MessageLookupByLibrary.simpleMessage("ASN"), "ipFlagAbuser": MessageLookupByLibrary.simpleMessage("滥用记录"), @@ -737,7 +777,7 @@ class MessageLookup extends MessageLookupByLibrary { "开启 IPv6 后,VPN 允许 IPv6 入站。", ), "ipv6Timeout": MessageLookupByLibrary.simpleMessage("IPv6超时(毫秒)"), - "itemsCount": m36, + "itemsCount": m38, "ja": MessageLookupByLibrary.simpleMessage("日语"), "justNow": MessageLookupByLibrary.simpleMessage("刚刚"), "keepAliveIdle": MessageLookupByLibrary.simpleMessage("TCP 保持活动空闲时间"), @@ -753,7 +793,7 @@ class MessageLookup extends MessageLookupByLibrary { ), "layout": MessageLookupByLibrary.simpleMessage("布局"), "light": MessageLookupByLibrary.simpleMessage("浅色"), - "lineIssueTip": m37, + "lineIssueTip": m39, "lineWrap": MessageLookupByLibrary.simpleMessage("自动换行"), "link": MessageLookupByLibrary.simpleMessage("链接"), "linkDesc": MessageLookupByLibrary.simpleMessage("通过订阅链接或代理分享链接获取配置文件"), @@ -778,7 +818,7 @@ class MessageLookup extends MessageLookupByLibrary { "locationPermissionDesc": MessageLookupByLibrary.simpleMessage( "根据系统要求,获取Wi-Fi名称需要您授予位置权限。Android 上请选择“始终允许”,否则应用在后台时无法获取 Wi-Fi 名称。", ), - "locationPermissionGuide": m38, + "locationPermissionGuide": m40, "locationPermissionRequired": MessageLookupByLibrary.simpleMessage( "需要位置权限", ), @@ -794,7 +834,7 @@ class MessageLookup extends MessageLookupByLibrary { "matchSourceIp": MessageLookupByLibrary.simpleMessage("匹配来源IP"), "matchTarget": MessageLookupByLibrary.simpleMessage("MATCH-TARGET"), "maxFailedTimes": MessageLookupByLibrary.simpleMessage("最大失败次数"), - "maxLengthTip": m39, + "maxLengthTip": m41, "maximize": MessageLookupByLibrary.simpleMessage("最大化"), "memoryAppResident": MessageLookupByLibrary.simpleMessage("常驻内存"), "memoryAppShared": MessageLookupByLibrary.simpleMessage("应用及共享"), @@ -811,23 +851,25 @@ class MessageLookup extends MessageLookupByLibrary { ), "memoryInfo": MessageLookupByLibrary.simpleMessage("内存信息"), "memoryReleased": MessageLookupByLibrary.simpleMessage("内存已释放"), - "memoryReleasedSize": m40, + "memoryReleasedSize": m42, "messageTest": MessageLookupByLibrary.simpleMessage("消息测试"), "messageTestTip": MessageLookupByLibrary.simpleMessage("这是一条消息。"), "min": MessageLookupByLibrary.simpleMessage("最小"), "minimize": MessageLookupByLibrary.simpleMessage("最小化"), "minimizeOnExit": MessageLookupByLibrary.simpleMessage("退出时最小化"), - "minutesAgo": m41, + "minutesAgo": m43, + "minutesAgoShort": m44, "mixedPort": MessageLookupByLibrary.simpleMessage("混合端口"), "mode": MessageLookupByLibrary.simpleMessage("模式"), "monochromeScheme": MessageLookupByLibrary.simpleMessage("单色"), - "monthsAgo": m42, + "monthsAgo": m45, + "monthsAgoShort": m46, "more": MessageLookupByLibrary.simpleMessage("更多"), "name": MessageLookupByLibrary.simpleMessage("名称"), "navigationBarStyle": MessageLookupByLibrary.simpleMessage("底栏"), "network": MessageLookupByLibrary.simpleMessage("网络"), - "networkAccessDeniedError": m43, - "networkBadResponseError": m44, + "networkAccessDeniedError": m47, + "networkBadResponseError": m48, "networkCancelledError": MessageLookupByLibrary.simpleMessage("请求已取消"), "networkConnectionError": MessageLookupByLibrary.simpleMessage( "无法连接到服务器,请检查网络连接或代理设置", @@ -837,13 +879,13 @@ class MessageLookup extends MessageLookupByLibrary { "无法解析服务器地址,请确认链接正确且 DNS 可用", ), "networkName": MessageLookupByLibrary.simpleMessage("网络名称"), - "networkNotFoundError": m45, + "networkNotFoundError": m49, "networkRateLimitedError": MessageLookupByLibrary.simpleMessage( "请求过于频繁(HTTP 429),请稍后再试", ), - "networkRequestFailed": m46, + "networkRequestFailed": m50, "networkSecret": MessageLookupByLibrary.simpleMessage("网络密码"), - "networkServerError": m47, + "networkServerError": m51, "networkSpeed": MessageLookupByLibrary.simpleMessage("网络速度"), "networkTimeoutError": MessageLookupByLibrary.simpleMessage( "请求超时,请检查网络或代理后重试", @@ -873,9 +915,14 @@ class MessageLookup extends MessageLookupByLibrary { "开启 NTP 后从 NTP 服务器获取时间,而非系统时钟。", ), "nullProfileDesc": MessageLookupByLibrary.simpleMessage("添加一个配置文件后即可开始使用"), - "nullTip": m48, - "numberRangeTip": m49, - "numberTip": m50, + "nullTip": m52, + "numberRangeTip": m53, + "numberTip": m54, + "obfs": MessageLookupByLibrary.simpleMessage("混淆"), + "obfsHost": MessageLookupByLibrary.simpleMessage("混淆主机"), + "obfsMode": MessageLookupByLibrary.simpleMessage("混淆模式"), + "obfsParam": MessageLookupByLibrary.simpleMessage("混淆参数"), + "obfsPassword": MessageLookupByLibrary.simpleMessage("混淆密码"), "onDemand": MessageLookupByLibrary.simpleMessage("按需运行"), "onDemandDesc": MessageLookupByLibrary.simpleMessage( "按需运行会在连上排除的 Wi-Fi 时暂停代理。", @@ -908,14 +955,21 @@ class MessageLookup extends MessageLookupByLibrary { "pleaseUploadValidQrcode": MessageLookupByLibrary.simpleMessage( "请上传有效的二维码", ), + "plugin": MessageLookupByLibrary.simpleMessage("插件"), + "pluginHost": MessageLookupByLibrary.simpleMessage("插件主机"), + "pluginMode": MessageLookupByLibrary.simpleMessage("插件模式"), "port": MessageLookupByLibrary.simpleMessage("端口"), "portConflictTip": MessageLookupByLibrary.simpleMessage("请输入不同的端口"), - "portTip": m51, + "portHopping": MessageLookupByLibrary.simpleMessage("端口跳跃"), + "portTip": m55, "ports": MessageLookupByLibrary.simpleMessage("端口"), + "preSharedKey": MessageLookupByLibrary.simpleMessage("预共享密钥"), "prerequisites": MessageLookupByLibrary.simpleMessage("前置条件"), "pressKeyboard": MessageLookupByLibrary.simpleMessage("请按下组合键"), "preview": MessageLookupByLibrary.simpleMessage("预览"), "previousMatch": MessageLookupByLibrary.simpleMessage("上一个匹配"), + "privateKey": MessageLookupByLibrary.simpleMessage("私钥"), + "privateKeyPassphrase": MessageLookupByLibrary.simpleMessage("私钥密码"), "process": MessageLookupByLibrary.simpleMessage("进程"), "profile": MessageLookupByLibrary.simpleMessage("配置"), "profileAutoUpdateIntervalInvalidValidationDesc": @@ -940,25 +994,29 @@ class MessageLookup extends MessageLookupByLibrary { "profiles": MessageLookupByLibrary.simpleMessage("配置"), "profilesSort": MessageLookupByLibrary.simpleMessage("配置排序"), "project": MessageLookupByLibrary.simpleMessage("项目"), - "providerInUse": m52, + "protocol": MessageLookupByLibrary.simpleMessage("协议"), + "protocolParam": MessageLookupByLibrary.simpleMessage("协议参数"), + "protocolVersion": MessageLookupByLibrary.simpleMessage("协议版本"), + "providerInUse": m56, "providerUrlTip": MessageLookupByLibrary.simpleMessage("仅支持远程资源"), "providers": MessageLookupByLibrary.simpleMessage("外部资源"), "proxies": MessageLookupByLibrary.simpleMessage("代理"), - "proxiesCount": m53, + "proxiesCount": m57, "proxiesEmpty": MessageLookupByLibrary.simpleMessage("代理为空"), - "proxiesProfileLabel": m54, - "proxiesRemovedTip": m55, - "proxiesReplacedTip": m56, + "proxiesProfileLabel": m58, + "proxiesRemovedTip": m59, + "proxiesReplacedTip": m60, "proxyChains": MessageLookupByLibrary.simpleMessage("代理链"), "proxyGroup": MessageLookupByLibrary.simpleMessage("策略组"), "proxyGroupEmpty": MessageLookupByLibrary.simpleMessage("策略组为空"), - "proxyGroupInUse": m57, + "proxyGroupInUse": m61, "proxyGroupNameDuplicate": MessageLookupByLibrary.simpleMessage("策略组名称重复"), "proxyProviders": MessageLookupByLibrary.simpleMessage("代理集"), "proxyProvidersEmpty": MessageLookupByLibrary.simpleMessage("代理集为空"), "proxyProvidersNotEmpty": MessageLookupByLibrary.simpleMessage("代理集不能为空"), "proxyType": MessageLookupByLibrary.simpleMessage("代理类型"), "pruneCache": MessageLookupByLibrary.simpleMessage("修剪缓存"), + "publicKey": MessageLookupByLibrary.simpleMessage("公钥"), "pureBlack": MessageLookupByLibrary.simpleMessage("纯黑"), "pureBlackMode": MessageLookupByLibrary.simpleMessage("纯黑模式"), "qrcode": MessageLookupByLibrary.simpleMessage("二维码"), @@ -976,6 +1034,7 @@ class MessageLookup extends MessageLookupByLibrary { "releaseMemoryFailed": MessageLookupByLibrary.simpleMessage("释放内存失败"), "remote": MessageLookupByLibrary.simpleMessage("远程"), "remoteDestination": MessageLookupByLibrary.simpleMessage("远程目标"), + "remoteDnsResolve": MessageLookupByLibrary.simpleMessage("远程 DNS 解析"), "remove": MessageLookupByLibrary.simpleMessage("移除"), "rename": MessageLookupByLibrary.simpleMessage("重命名"), "replace": MessageLookupByLibrary.simpleMessage("替换"), @@ -983,6 +1042,7 @@ class MessageLookup extends MessageLookupByLibrary { "request": MessageLookupByLibrary.simpleMessage("请求"), "requests": MessageLookupByLibrary.simpleMessage("请求"), "requestsAndUpdates": MessageLookupByLibrary.simpleMessage("请求与更新"), + "required": MessageLookupByLibrary.simpleMessage("必填"), "reset": MessageLookupByLibrary.simpleMessage("重置"), "resetPageChangesTip": MessageLookupByLibrary.simpleMessage( "当前页面存在更改,确定重置吗?", @@ -1126,10 +1186,10 @@ class MessageLookup extends MessageLookupByLibrary { "规则必须以受支持的类型开头,例如 DOMAIN,example.com,DIRECT", ), "rules": MessageLookupByLibrary.simpleMessage("规则"), - "rulesCount": m58, + "rulesCount": m62, "runTime": MessageLookupByLibrary.simpleMessage("启动时间"), "safeMode": MessageLookupByLibrary.simpleMessage("安全模式"), - "safeModeAppTitle": m59, + "safeModeAppTitle": m63, "save": MessageLookupByLibrary.simpleMessage("保存"), "saveChanges": MessageLookupByLibrary.simpleMessage("是否保存更改?"), "script": MessageLookupByLibrary.simpleMessage("脚本"), @@ -1137,8 +1197,9 @@ class MessageLookup extends MessageLookupByLibrary { "scrollToSelected": MessageLookupByLibrary.simpleMessage("滚动到已选"), "search": MessageLookupByLibrary.simpleMessage("搜索"), "seconds": MessageLookupByLibrary.simpleMessage("秒"), - "secondsCount": m60, - "sectionsInEffect": m61, + "secondsCount": m64, + "secretKey": MessageLookupByLibrary.simpleMessage("密钥"), + "sectionsInEffect": m65, "selectAll": MessageLookupByLibrary.simpleMessage("全选"), "selectProxies": MessageLookupByLibrary.simpleMessage("选择代理"), "selectProxyProviders": MessageLookupByLibrary.simpleMessage("选择代理集"), @@ -1146,13 +1207,13 @@ class MessageLookup extends MessageLookupByLibrary { "selectSplitStrategy": MessageLookupByLibrary.simpleMessage("请选择分流策略"), "selectSubRule": MessageLookupByLibrary.simpleMessage("请选择子规则"), "selected": MessageLookupByLibrary.simpleMessage("已选择"), - "selectedCountTitle": m62, + "selectedCountTitle": m66, "server": MessageLookupByLibrary.simpleMessage("服务器"), "serviceAvailable": MessageLookupByLibrary.simpleMessage("可用"), "serviceBlocked": MessageLookupByLibrary.simpleMessage("已被封禁"), "serviceCheck": MessageLookupByLibrary.simpleMessage("检测"), "serviceCheckAll": MessageLookupByLibrary.simpleMessage("全部检测"), - "serviceCheckedAt": m63, + "serviceCheckedAt": m67, "serviceComingSoon": MessageLookupByLibrary.simpleMessage("即将上线"), "serviceDisallowedIsp": MessageLookupByLibrary.simpleMessage("不允许的 ISP"), "serviceFailed": MessageLookupByLibrary.simpleMessage("检测失败"), @@ -1165,8 +1226,8 @@ class MessageLookup extends MessageLookupByLibrary { "serviceUnsupportedRegion": MessageLookupByLibrary.simpleMessage("地区不支持"), "settingEntries": MessageLookupByLibrary.simpleMessage("设置项"), "settings": MessageLookupByLibrary.simpleMessage("设置"), - "settingsCount": m64, - "shareLinkUnreadable": m65, + "settingsCount": m68, + "shareLinkUnreadable": m69, "shareLinksInvalid": MessageLookupByLibrary.simpleMessage("无法从链接中读取代理"), "show": MessageLookupByLibrary.simpleMessage("显示"), "showLess": MessageLookupByLibrary.simpleMessage("收起"), @@ -1185,7 +1246,7 @@ class MessageLookup extends MessageLookupByLibrary { "singleShareLinkOnly": MessageLookupByLibrary.simpleMessage( "这里只能填写一条链接,多条请在列表的快速编辑中填写", ), - "singleValueTip": m66, + "singleValueTip": m70, "size": MessageLookupByLibrary.simpleMessage("尺寸"), "skipCertVerify": MessageLookupByLibrary.simpleMessage("跳过证书验证"), "skipDomain": MessageLookupByLibrary.simpleMessage("跳过域名"), @@ -1266,13 +1327,17 @@ class MessageLookup extends MessageLookupByLibrary { "totalTraffic": MessageLookupByLibrary.simpleMessage("总流量"), "tproxyPort": MessageLookupByLibrary.simpleMessage("Tproxy端口"), "trafficUsage": MessageLookupByLibrary.simpleMessage("流量统计"), + "transport": MessageLookupByLibrary.simpleMessage("传输协议"), "tun": MessageLookupByLibrary.simpleMessage("虚拟网卡"), + "tunAuthorizationFailed": MessageLookupByLibrary.simpleMessage( + "虚拟网卡授权失败,已关闭。", + ), "tunDesc": MessageLookupByLibrary.simpleMessage("虚拟网卡仅在管理员模式生效。"), "turnOff": MessageLookupByLibrary.simpleMessage("关闭"), "turnOn": MessageLookupByLibrary.simpleMessage("开启"), + "udpRelayMode": MessageLookupByLibrary.simpleMessage("UDP 中继模式"), "undo": MessageLookupByLibrary.simpleMessage("撤销"), "unifiedDelay": MessageLookupByLibrary.simpleMessage("统一延迟"), - "universal": MessageLookupByLibrary.simpleMessage("通用"), "unknown": MessageLookupByLibrary.simpleMessage("未知"), "unknownNetworkError": MessageLookupByLibrary.simpleMessage("未知网络错误"), "unmaximize": MessageLookupByLibrary.simpleMessage("向下还原"), @@ -1280,8 +1345,9 @@ class MessageLookup extends MessageLookupByLibrary { "unpinWindow": MessageLookupByLibrary.simpleMessage("取消置顶"), "update": MessageLookupByLibrary.simpleMessage("更新"), "upload": MessageLookupByLibrary.simpleMessage("上传"), + "uploadBandwidth": MessageLookupByLibrary.simpleMessage("上行带宽"), "url": MessageLookupByLibrary.simpleMessage("URL"), - "urlTip": m67, + "urlTip": m71, "useHosts": MessageLookupByLibrary.simpleMessage("使用Hosts"), "useSystemHosts": MessageLookupByLibrary.simpleMessage("使用系统Hosts"), "usedTraffic": MessageLookupByLibrary.simpleMessage("已用流量"), @@ -1304,7 +1370,8 @@ class MessageLookup extends MessageLookupByLibrary { "writeToSystemDesc": MessageLookupByLibrary.simpleMessage( "写入系统会同时设置系统时钟,Android 上不生效。", ), - "yearsAgo": m68, + "yearsAgo": m72, + "yearsAgoShort": m73, "yes": MessageLookupByLibrary.simpleMessage("是"), "zhCN": MessageLookupByLibrary.simpleMessage("中文简体"), }; diff --git a/lib/l10n/l10n.dart b/lib/l10n/l10n.dart index fc045540..428a26f7 100644 --- a/lib/l10n/l10n.dart +++ b/lib/l10n/l10n.dart @@ -3375,6 +3375,11 @@ class AppLocalizations { ); } + /// `Can't save` + String get cannotSave { + return Intl.message('Can\'t save', name: 'cannotSave', desc: '', args: []); + } + /// `Bottom bar` String get navigationBarStyle { return Intl.message( @@ -4450,6 +4455,61 @@ class AppLocalizations { return Intl.message('Just now', name: 'justNow', desc: '', args: []); } + /// `{count, plural, other{{count}y ago}}` + String yearsAgoShort(num count) { + return Intl.plural( + count, + other: '${count}y ago', + name: 'yearsAgoShort', + desc: '', + args: [count], + ); + } + + /// `{count, plural, other{{count}mo ago}}` + String monthsAgoShort(num count) { + return Intl.plural( + count, + other: '${count}mo ago', + name: 'monthsAgoShort', + desc: '', + args: [count], + ); + } + + /// `{count, plural, other{{count}d ago}}` + String daysAgoShort(num count) { + return Intl.plural( + count, + other: '${count}d ago', + name: 'daysAgoShort', + desc: '', + args: [count], + ); + } + + /// `{count, plural, other{{count}h ago}}` + String hoursAgoShort(num count) { + return Intl.plural( + count, + other: '${count}h ago', + name: 'hoursAgoShort', + desc: '', + args: [count], + ); + } + + /// `{count, plural, other{{count}m ago}}` + String minutesAgoShort(num count) { + return Intl.plural( + count, + other: '${count}m ago', + name: 'minutesAgoShort', + desc: '', + args: [count], + ); + } + /// `Don't remind me again` String get noLongerRemind { return Intl.message( @@ -4530,6 +4590,26 @@ class AppLocalizations { ); } + /// `Custom profile resources` + String get customProfileResources { + return Intl.message( + 'Custom profile resources', + name: 'customProfileResources', + desc: '', + args: [], + ); + } + + /// `Proxies, rule providers, filters, and icon sets` + String get customProfileResourcesDesc { + return Intl.message( + 'Proxies, rule providers, filters, and icon sets', + name: 'customProfileResourcesDesc', + desc: '', + args: [], + ); + } + /// `Unknown network error` String get unknownNetworkError { return Intl.message( @@ -4865,6 +4945,16 @@ class AppLocalizations { ); } + /// `TUN could not be authorized, so it was turned off.` + String get tunAuthorizationFailed { + return Intl.message( + 'TUN could not be authorized, so it was turned off.', + name: 'tunAuthorizationFailed', + desc: '', + args: [], + ); + } + /// `Enter your password for sudo` String get sudoPasswordTitle { return Intl.message( @@ -5625,6 +5715,46 @@ class AppLocalizations { ); } + /// `Enter an IP range in CIDR form such as 192.168.0.0/16` + String get invalidCidrContent { + return Intl.message( + 'Enter an IP range in CIDR form such as 192.168.0.0/16', + name: 'invalidCidrContent', + desc: '', + args: [], + ); + } + + /// `Enter a port or a range such as 443 or 8000-9000` + String get invalidPortRangeContent { + return Intl.message( + 'Enter a port or a range such as 443 or 8000-9000', + name: 'invalidPortRangeContent', + desc: '', + args: [], + ); + } + + /// `Enter a domain or an IP address` + String get invalidHostContent { + return Intl.message( + 'Enter a domain or an IP address', + name: 'invalidHostContent', + desc: '', + args: [], + ); + } + + /// `Enter an address and port such as 0.0.0.0:1053` + String get invalidListenContent { + return Intl.message( + 'Enter an address and port such as 0.0.0.0:1053', + name: 'invalidListenContent', + desc: '', + args: [], + ); + } + /// `{proxyName} is an invalid proxy` String invalidProxy(Object proxyName) { return Intl.message( @@ -5864,27 +5994,27 @@ class AppLocalizations { ); } - /// `{count, plural, =1{1 proxy} other{{count} proxies}}` - String proxiesCount(num count) { + /// `{count, plural, =1{1 proxy} other{{label} proxies}}` + String proxiesCount(num count, Object label) { return Intl.plural( count, one: '1 proxy', - other: '$count proxies', + other: '$label proxies', name: 'proxiesCount', desc: '', - args: [count], + args: [count, label], ); } - /// `{count, plural, =1{1 rule} other{{count} rules}}` - String rulesCount(num count) { + /// `{count, plural, =1{1 rule} other{{label} rules}}` + String rulesCount(num count, Object label) { return Intl.plural( count, one: '1 rule', - other: '$count rules', + other: '$label rules', name: 'rulesCount', desc: '', - args: [count], + args: [count, label], ); } @@ -6803,11 +6933,6 @@ class AppLocalizations { ); } - /// `General` - String get universal { - return Intl.message('General', name: 'universal', desc: '', args: []); - } - /// `Filters` String get filters { return Intl.message('Filters', name: 'filters', desc: '', args: []); @@ -6853,10 +6978,10 @@ class AppLocalizations { return Intl.message('Local image', name: 'localImage', desc: '', args: []); } - /// `No icon sets yet. Add them in Advanced configuration → Icon sets` + /// `No icon sets yet. Add them in Custom profile resources → Icon sets` String get iconSetsEmptyTip { return Intl.message( - 'No icon sets yet. Add them in Advanced configuration → Icon sets', + 'No icon sets yet. Add them in Custom profile resources → Icon sets', name: 'iconSetsEmptyTip', desc: '', args: [], @@ -7211,6 +7336,256 @@ class AppLocalizations { String get exitNodes { return Intl.message('Exit nodes', name: 'exitNodes', desc: '', args: []); } + + /// `Cipher` + String get cipher { + return Intl.message('Cipher', name: 'cipher', desc: '', args: []); + } + + /// `Protocol` + String get protocol { + return Intl.message('Protocol', name: 'protocol', desc: '', args: []); + } + + /// `Protocol parameter` + String get protocolParam { + return Intl.message( + 'Protocol parameter', + name: 'protocolParam', + desc: '', + args: [], + ); + } + + /// `Obfuscation` + String get obfs { + return Intl.message('Obfuscation', name: 'obfs', desc: '', args: []); + } + + /// `Obfuscation parameter` + String get obfsParam { + return Intl.message( + 'Obfuscation parameter', + name: 'obfsParam', + desc: '', + args: [], + ); + } + + /// `Obfuscation password` + String get obfsPassword { + return Intl.message( + 'Obfuscation password', + name: 'obfsPassword', + desc: '', + args: [], + ); + } + + /// `Obfuscation mode` + String get obfsMode { + return Intl.message( + 'Obfuscation mode', + name: 'obfsMode', + desc: '', + args: [], + ); + } + + /// `Obfuscation host` + String get obfsHost { + return Intl.message( + 'Obfuscation host', + name: 'obfsHost', + desc: '', + args: [], + ); + } + + /// `Plugin` + String get plugin { + return Intl.message('Plugin', name: 'plugin', desc: '', args: []); + } + + /// `Plugin mode` + String get pluginMode { + return Intl.message('Plugin mode', name: 'pluginMode', desc: '', args: []); + } + + /// `Plugin host` + String get pluginHost { + return Intl.message('Plugin host', name: 'pluginHost', desc: '', args: []); + } + + /// `Private key` + String get privateKey { + return Intl.message('Private key', name: 'privateKey', desc: '', args: []); + } + + /// `Public key` + String get publicKey { + return Intl.message('Public key', name: 'publicKey', desc: '', args: []); + } + + /// `Private key passphrase` + String get privateKeyPassphrase { + return Intl.message( + 'Private key passphrase', + name: 'privateKeyPassphrase', + desc: '', + args: [], + ); + } + + /// `Pre-shared key` + String get preSharedKey { + return Intl.message( + 'Pre-shared key', + name: 'preSharedKey', + desc: '', + args: [], + ); + } + + /// `Key` + String get secretKey { + return Intl.message('Key', name: 'secretKey', desc: '', args: []); + } + + /// `Transport` + String get transport { + return Intl.message('Transport', name: 'transport', desc: '', args: []); + } + + /// `Upload bandwidth` + String get uploadBandwidth { + return Intl.message( + 'Upload bandwidth', + name: 'uploadBandwidth', + desc: '', + args: [], + ); + } + + /// `Download bandwidth` + String get downloadBandwidth { + return Intl.message( + 'Download bandwidth', + name: 'downloadBandwidth', + desc: '', + args: [], + ); + } + + /// `Protocol version` + String get protocolVersion { + return Intl.message( + 'Protocol version', + name: 'protocolVersion', + desc: '', + args: [], + ); + } + + /// `Port hopping` + String get portHopping { + return Intl.message( + 'Port hopping', + name: 'portHopping', + desc: '', + args: [], + ); + } + + /// `Hop interval` + String get hopInterval { + return Intl.message( + 'Hop interval', + name: 'hopInterval', + desc: '', + args: [], + ); + } + + /// `Congestion control` + String get congestionControl { + return Intl.message( + 'Congestion control', + name: 'congestionControl', + desc: '', + args: [], + ); + } + + /// `UDP relay mode` + String get udpRelayMode { + return Intl.message( + 'UDP relay mode', + name: 'udpRelayMode', + desc: '', + args: [], + ); + } + + /// `Allowed IPs` + String get allowedIps { + return Intl.message('Allowed IPs', name: 'allowedIps', desc: '', args: []); + } + + /// `Remote DNS resolution` + String get remoteDnsResolve { + return Intl.message( + 'Remote DNS resolution', + name: 'remoteDnsResolve', + desc: '', + args: [], + ); + } + + /// `Client fingerprint` + String get clientFingerprint { + return Intl.message( + 'Client fingerprint', + name: 'clientFingerprint', + desc: '', + args: [], + ); + } + + /// `Required` + String get required { + return Intl.message('Required', name: 'required', desc: '', args: []); + } + + /// `Age secret key` + String get ageSecretKey { + return Intl.message( + 'Age secret key', + name: 'ageSecretKey', + desc: '', + args: [], + ); + } + + /// `Encrypted profile` + String get ageEncryptedProfile { + return Intl.message( + 'Encrypted profile', + name: 'ageEncryptedProfile', + desc: '', + args: [], + ); + } + + /// `This profile is encrypted with age and needs an age secret key to decrypt` + String get ageSecretKeyRequiredTip { + return Intl.message( + 'This profile is encrypted with age and needs an age secret key to decrypt', + name: 'ageSecretKeyRequiredTip', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/manager/theme_manager.dart b/lib/manager/theme_manager.dart index 839ebad5..2e0a4d24 100644 --- a/lib/manager/theme_manager.dart +++ b/lib/manager/theme_manager.dart @@ -49,7 +49,7 @@ class ThemeManager extends ConsumerWidget { ); final double textScaleFactor = max( min( - textScale.enable ? textScale.scale : defaultTextScaleFactor, + textScale.enable ? textScale.scale : systemTextScaleOf(context), maxTextScale, ), minTextScale, diff --git a/lib/models/clash_config.dart b/lib/models/clash_config.dart index f4ee3cc9..3ef2110f 100644 --- a/lib/models/clash_config.dart +++ b/lib/models/clash_config.dart @@ -70,6 +70,7 @@ abstract class ProxyGroup with _$ProxyGroup { required GroupType type, List? proxies, List? use, + @JsonKey(name: 'use-position') Map? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, @@ -91,6 +92,10 @@ abstract class ProxyGroup with _$ProxyGroup { bool? hidden, String? icon, String? order, + + /// Where the members list shows the filters among the proxies, which no + /// config carries, as `ProxyGroupMembersSliver` writes it. + List? memberLayout, }) = _ProxyGroup; factory ProxyGroup.fromJson(Map json) => @@ -106,6 +111,7 @@ extension ProxyGroupExt on ProxyGroup { 'type': type.value, if (proxies?.isNotEmpty == true) 'proxies': proxies, if (use?.isNotEmpty == true) 'use': use, + 'use-position': ?effectiveUsePosition, if (url?.isNotEmpty == true) 'url': url, if (interval != null) 'interval': interval, if (timeout != null) 'timeout': timeout, @@ -137,22 +143,71 @@ extension ProxyGroupExt on ProxyGroup { }; } + /// [usePosition] without the providers the core would list last anyway. + Map? get effectiveUsePosition { + final count = proxies?.length ?? 0; + final positions = { + for (final name in use ?? const []) + if (usePosition?[name] case final position? when position < count) + name: position < 0 ? 0 : position, + }; + return positions.isEmpty ? null : positions; + } + String get definitionYaml => yaml.encode(definition); + /// What [definitionYaml] leaves out, such as an option added without a + /// value, was never there to remove, so it stays. ProxyGroup withDefinitionYaml(String content) { final document = _plainYaml(loadYaml(content)); if (document is! Map || - document['name'] is! String || + (document['name'] is! String && document['name'] is! num) || document['type'] is! String) { throw const FormatException('Not a proxy group mapping'); } - return ProxyGroup.fromJson({ + final shown = definition; + final group = ProxyGroup.fromJson({ + for (final MapEntry(:key, :value) in toJson().entries) + if (!shown.containsKey(key) && key != 'use-position') key: value, ...document, + for (final key in proxyGroupTextKeys) + if (document[key] case final value? when value is! String) + key: value.toString(), + for (final key in const ['proxies', 'use']) + if (document[key] case final List names) + key: [for (final name in names) name.toString()], + if (document.containsKey('use-position')) + 'use-position': usePositionOf(document['use-position']), 'id': id, }).copyWith(profileId: profileId, order: order); + return group.copyWith(usePosition: group.effectiveUsePosition); } } +Map? usePositionOf( + Object? value, [ + Map names = const {}, +]) => switch (value) { + Map() => { + for (final MapEntry(:key, :value) in value.entries) + names['$key'] ?? '$key': ?int.tryParse('$value'), + }, + _ => null, +}; + +const proxyGroupTextKeys = [ + 'name', + 'url', + 'filter', + 'exclude-filter', + 'exclude-type', + 'expected-status', + 'hash-key', + 'default-selected', + 'empty-fallback', + 'icon', +]; + @freezed abstract class Proxy with _$Proxy { const factory Proxy({ @@ -285,6 +340,10 @@ abstract class CustomProxy with _$CustomProxy { @JsonKey(fromJson: Snowflake.buildId) required int id, @Default({}) Map definition, String? order, + + /// Options the form lists with no value yet, which [definition] cannot + /// hold since the core reads every key in it. + @Default({}) Set emptyOptions, }) = _CustomProxy; factory CustomProxy.fromJson(Map json) => @@ -1026,15 +1085,23 @@ abstract class Dns with _$Dns { @JsonKey(name: 'cache-algorithm') DnsCacheAlgorithm cacheAlgorithm, @Default(4096) @JsonKey(name: 'cache-max-size') int cacheMaxSize, - @Default([]) + @Default(['114.114.114.114', '223.5.5.5', '8.8.8.8', '1.0.0.1']) @JsonKey(name: 'default-nameserver') List defaultNameserver, @Default(DnsMode.redirHost) @JsonKey(name: 'enhanced-mode') DnsMode enhancedMode, - @Default('') @JsonKey(name: 'fake-ip-range') String fakeIpRange, + @Default('198.18.0.1/16') + @JsonKey(name: 'fake-ip-range') + String fakeIpRange, @Default('') @JsonKey(name: 'fake-ip-range6') String fakeIpRange6, - @Default([]) @JsonKey(name: 'fake-ip-filter') List fakeIpFilter, + @Default([ + 'dns.msftnsci.com', + 'www.msftnsci.com', + 'www.msftconnecttest.com', + ]) + @JsonKey(name: 'fake-ip-filter') + List fakeIpFilter, @Default(FakeIpFilterMode.blacklist) @JsonKey(name: 'fake-ip-filter-mode') FakeIpFilterMode fakeIpFilterMode, @@ -1042,7 +1109,8 @@ abstract class Dns with _$Dns { @Default({}) @JsonKey(name: 'nameserver-policy') Map nameserverPolicy, - @Default([]) List nameserver, + @Default(['https://doh.pub/dns-query', 'tls://223.5.5.5:853']) + List nameserver, @Default([]) List fallback, @Default(false) @JsonKey(name: 'fallback-lazy-query') @@ -1129,8 +1197,18 @@ extension DnsOverrideExt on Dns { return json; } - Map overrideJson(Set keys) => - _overrideJson(_json, DnsOverrideKey.values, keys); + /// [keepEmpty] writes emptied keys, but the core refuses an empty nameserver. + Map overrideJson( + Set keys, { + bool keepEmpty = false, + }) => _overrideJson( + _json, + DnsOverrideKey.values, + keys, + keepEmpty: keepEmpty + ? keys.difference({DnsOverrideKey.nameserver}) + : const {}, + ); String overrideYaml(Set keys) => yaml.encode(overrideJson(keys)); @@ -1165,8 +1243,9 @@ Object? _plainYaml(Object? node) => switch (node) { Map _overrideJson( Map json, List values, - Set keys, -) { + Set keys, { + Set keepEmpty = const {}, +}) { final result = {}; for (final key in values) { if (!keys.contains(key)) { @@ -1176,7 +1255,7 @@ Map _overrideJson( final value = parent == null ? json[key.path] : (json[parent] as Map)[key.field]; - if (_isUnset(value)) { + if (_isUnset(value) && !keepEmpty.contains(key)) { continue; } if (parent == null) { @@ -1191,8 +1270,6 @@ Map _overrideJson( return result; } -/// A key is added without a value, and writes nothing until it gets one: the -/// core refuses an empty `nameserver` outright. bool _isUnset(Object? value) => switch (value) { String() => value.isEmpty, Iterable() => value.isEmpty, @@ -1299,7 +1376,7 @@ Map mergeDnsOverride( abstract class Ntp with _$Ntp { const factory Ntp({ @Default(false) bool enable, - @Default('') String server, + @Default('time.apple.com') String server, @Default(123) int port, @Default(30) int interval, @Default('') @JsonKey(name: 'dialer-proxy') String dialerProxy, @@ -1457,6 +1534,10 @@ abstract class ProxyProviderOptions with _$ProxyProviderOptions { @Default(ProviderOverride()) @JsonKey(name: 'override') ProviderOverride proxyOverride, + + /// Where the filters list shows the exclude filters among the filters, + /// which no config carries, as `ProxyProviderFiltersSliver` writes it. + List? memberLayout, }) = _ProxyProviderOptions; factory ProxyProviderOptions.fromJson(Map json) => @@ -1465,27 +1546,39 @@ abstract class ProxyProviderOptions with _$ProxyProviderOptions { extension ProxyProviderOptionsExt on ProxyProviderOptions { /// A health check without a url never runs, so one always carries a url. - Map get definition => { - if (healthCheck != const ProviderHealthCheck()) - 'health-check': { - 'enable': true, - 'url': healthCheck.url ?? defaultTestUrl, - 'interval': ?healthCheck.interval, - 'timeout': ?healthCheck.timeout, - 'lazy': ?healthCheck.lazy, - 'expected-status': ?healthCheck.expectedStatus, - }, - 'filter': ?filter, - 'exclude-filter': ?excludeFilter, - if (proxyOverride != const ProviderOverride()) - 'override': { - 'additional-prefix': ?proxyOverride.additionalPrefix, - 'additional-suffix': ?proxyOverride.additionalSuffix, - 'udp': ?proxyOverride.udp, - 'skip-cert-verify': ?proxyOverride.skipCertVerify, - 'ip-version': ?proxyOverride.ipVersion?.value, - }, - }; + /// A text option added but left empty stays in the form and out of here. + Map get definition { + String? text(String? value) => value?.isNotEmpty == true ? value : null; + final healthCheck = this.healthCheck.copyWith( + url: text(this.healthCheck.url), + expectedStatus: text(this.healthCheck.expectedStatus), + ); + final proxyOverride = this.proxyOverride.copyWith( + additionalPrefix: text(this.proxyOverride.additionalPrefix), + additionalSuffix: text(this.proxyOverride.additionalSuffix), + ); + return { + if (healthCheck != const ProviderHealthCheck()) + 'health-check': { + 'enable': true, + 'url': healthCheck.url ?? defaultTestUrl, + 'interval': ?healthCheck.interval, + 'timeout': ?healthCheck.timeout, + 'lazy': ?healthCheck.lazy, + 'expected-status': ?healthCheck.expectedStatus, + }, + 'filter': ?filter, + 'exclude-filter': ?excludeFilter, + if (proxyOverride != const ProviderOverride()) + 'override': { + 'additional-prefix': ?proxyOverride.additionalPrefix, + 'additional-suffix': ?proxyOverride.additionalSuffix, + 'udp': ?proxyOverride.udp, + 'skip-cert-verify': ?proxyOverride.skipCertVerify, + 'ip-version': ?proxyOverride.ipVersion?.value, + }, + }; + } } extension ProfileOverridesExt on ProfileOverrides { diff --git a/lib/models/config.dart b/lib/models/config.dart index 3df1b576..a6118260 100644 --- a/lib/models/config.dart +++ b/lib/models/config.dart @@ -315,7 +315,7 @@ abstract class NetworkProps with _$NetworkProps { const factory NetworkProps({ @Default(true) bool systemProxy, @Default(defaultBypassDomain) List bypassDomain, - @Default(false) bool bypassPrivateRoute, + @Default(true) bool bypassPrivateRoute, @Default(true) bool autoSetSystemDns, @Default(false) bool appendSystemDns, @Default(defaultAuthenticationProps) AuthenticationProps authentication, @@ -448,19 +448,20 @@ Config _withLegacyOverrideDns(Config config, Map json) { Map _withLegacyRouteMode(Map json) { final network = json['networkProps']; if (network is! Map || - network['routeMode'] != 'bypassPrivate' || + !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.from(network), - 'bypassPrivateRoute': true, + 'bypassPrivateRoute': bypassPrivate, }, - if (clash is Map && tun is Map) + if (bypassPrivate && clash is Map && tun is Map) 'patchClashConfig': { ...Map.from(clash), 'tun': {...Map.from(tun), 'route-address': []}, diff --git a/lib/models/generated/clash_config.freezed.dart b/lib/models/generated/clash_config.freezed.dart index f95915b0..bdba1397 100644 --- a/lib/models/generated/clash_config.freezed.dart +++ b/lib/models/generated/clash_config.freezed.dart @@ -16,7 +16,9 @@ T _$identity(T value) => value; /// @nodoc mixin _$ProxyGroup { - int? get profileId;@JsonKey(fromJson: Snowflake.buildId) int get id; String get name; GroupType get type; List? get proxies; List? get use; int? get interval; bool? get lazy;@JsonKey(name: 'disable-udp') bool? get disableUDP; String? get url; int? get timeout;@JsonKey(name: 'max-failed-times') int? get maxFailedTimes; String? get filter;@JsonKey(name: 'exclude-filter') String? get excludeFilter;@JsonKey(name: 'exclude-type') String? get excludeType;@JsonKey(name: 'expected-status') String? get expectedStatus; int? get tolerance; LoadBalanceStrategy? get strategy;@JsonKey(name: 'hash-key') String? get hashKey;@JsonKey(name: 'default-selected') String? get defaultSelected;@JsonKey(name: 'empty-fallback') String? get emptyFallback;@JsonKey(name: 'include-all') bool? get includeAll;@JsonKey(name: 'include-all-proxies') bool? get includeAllProxies;@JsonKey(name: 'include-all-providers') bool? get includeAllProviders; bool? get hidden; String? get icon; String? get order; + int? get profileId;@JsonKey(fromJson: Snowflake.buildId) int get id; String get name; GroupType get type; List? get proxies; List? get use;@JsonKey(name: 'use-position') Map? get usePosition; int? get interval; bool? get lazy;@JsonKey(name: 'disable-udp') bool? get disableUDP; String? get url; int? get timeout;@JsonKey(name: 'max-failed-times') int? get maxFailedTimes; String? get filter;@JsonKey(name: 'exclude-filter') String? get excludeFilter;@JsonKey(name: 'exclude-type') String? get excludeType;@JsonKey(name: 'expected-status') String? get expectedStatus; int? get tolerance; LoadBalanceStrategy? get strategy;@JsonKey(name: 'hash-key') String? get hashKey;@JsonKey(name: 'default-selected') String? get defaultSelected;@JsonKey(name: 'empty-fallback') String? get emptyFallback;@JsonKey(name: 'include-all') bool? get includeAll;@JsonKey(name: 'include-all-proxies') bool? get includeAllProxies;@JsonKey(name: 'include-all-providers') bool? get includeAllProviders; bool? get hidden; String? get icon; String? get order;/// Where the members list shows the filters among the proxies, which no +/// config carries, as `ProxyGroupMembersSliver` writes it. + List? get memberLayout; /// Create a copy of ProxyGroup /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -30,20 +32,20 @@ $ProxyGroupCopyWith get copyWith => _$ProxyGroupCopyWithImpl { factory $ProxyGroupCopyWith(ProxyGroup value, $Res Function(ProxyGroup) _then) = _$ProxyGroupCopyWithImpl; @useResult $Res call({ - int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all') bool? includeAll,@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,@JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order + int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use,@JsonKey(name: 'use-position') Map? usePosition, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all') bool? includeAll,@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,@JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List? memberLayout }); @@ -71,7 +73,7 @@ class _$ProxyGroupCopyWithImpl<$Res> /// Create a copy of ProxyGroup /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAll = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? usePosition = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAll = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,Object? memberLayout = freezed,}) { return _then(ProxyGroup( profileId: freezed == profileId ? _self.profileId : profileId // ignore: cast_nullable_to_non_nullable as int?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable @@ -79,7 +81,8 @@ as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nu as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as GroupType,proxies: freezed == proxies ? _self.proxies : proxies // ignore: cast_nullable_to_non_nullable as List?,use: freezed == use ? _self.use : use // ignore: cast_nullable_to_non_nullable -as List?,interval: freezed == interval ? _self.interval : interval // ignore: cast_nullable_to_non_nullable +as List?,usePosition: freezed == usePosition ? _self.usePosition : usePosition // ignore: cast_nullable_to_non_nullable +as Map?,interval: freezed == interval ? _self.interval : interval // ignore: cast_nullable_to_non_nullable as int?,lazy: freezed == lazy ? _self.lazy : lazy // ignore: cast_nullable_to_non_nullable as bool?,disableUDP: freezed == disableUDP ? _self.disableUDP : disableUDP // ignore: cast_nullable_to_non_nullable as bool?,url: freezed == url ? _self.url : url // ignore: cast_nullable_to_non_nullable @@ -100,7 +103,8 @@ as bool?,includeAllProviders: freezed == includeAllProviders ? _self.includeAllP as bool?,hidden: freezed == hidden ? _self.hidden : hidden // ignore: cast_nullable_to_non_nullable as bool?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable as String?,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable -as String?, +as String?,memberLayout: freezed == memberLayout ? _self.memberLayout : memberLayout // ignore: cast_nullable_to_non_nullable +as List?, )); } @@ -185,10 +189,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, @JsonKey(name: 'use-position') Map? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List? memberLayout)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _ProxyGroup() when $default != null: -return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order);case _: +return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _: return orElse(); } @@ -206,10 +210,10 @@ return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_th /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, @JsonKey(name: 'use-position') Map? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List? memberLayout) $default,) {final _that = this; switch (_that) { case _ProxyGroup(): -return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order);case _: +return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _: throw StateError('Unexpected subclass'); } @@ -226,10 +230,10 @@ return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_th /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, @JsonKey(name: 'use-position') Map? usePosition, int? interval, bool? lazy, @JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout, @JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'exclude-type') String? excludeType, @JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy, @JsonKey(name: 'hash-key') String? hashKey, @JsonKey(name: 'default-selected') String? defaultSelected, @JsonKey(name: 'empty-fallback') String? emptyFallback, @JsonKey(name: 'include-all') bool? includeAll, @JsonKey(name: 'include-all-proxies') bool? includeAllProxies, @JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List? memberLayout)? $default,) {final _that = this; switch (_that) { case _ProxyGroup() when $default != null: -return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order);case _: +return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_that.use,_that.usePosition,_that.interval,_that.lazy,_that.disableUDP,_that.url,_that.timeout,_that.maxFailedTimes,_that.filter,_that.excludeFilter,_that.excludeType,_that.expectedStatus,_that.tolerance,_that.strategy,_that.hashKey,_that.defaultSelected,_that.emptyFallback,_that.includeAll,_that.includeAllProxies,_that.includeAllProviders,_that.hidden,_that.icon,_that.order,_that.memberLayout);case _: return null; } @@ -241,7 +245,7 @@ return $default(_that.profileId,_that.id,_that.name,_that.type,_that.proxies,_th @JsonSerializable() class _ProxyGroup implements ProxyGroup { - const _ProxyGroup({this.profileId, @JsonKey(fromJson: Snowflake.buildId) required this.id, required this.name, required this.type, List? proxies, List? use, this.interval, this.lazy, @JsonKey(name: 'disable-udp') this.disableUDP, this.url, this.timeout, @JsonKey(name: 'max-failed-times') this.maxFailedTimes, this.filter, @JsonKey(name: 'exclude-filter') this.excludeFilter, @JsonKey(name: 'exclude-type') this.excludeType, @JsonKey(name: 'expected-status') this.expectedStatus, this.tolerance, this.strategy, @JsonKey(name: 'hash-key') this.hashKey, @JsonKey(name: 'default-selected') this.defaultSelected, @JsonKey(name: 'empty-fallback') this.emptyFallback, @JsonKey(name: 'include-all') this.includeAll, @JsonKey(name: 'include-all-proxies') this.includeAllProxies, @JsonKey(name: 'include-all-providers') this.includeAllProviders, this.hidden, this.icon, this.order}): _proxies = proxies,_use = use; + const _ProxyGroup({this.profileId, @JsonKey(fromJson: Snowflake.buildId) required this.id, required this.name, required this.type, List? proxies, List? use, @JsonKey(name: 'use-position') Map? usePosition, this.interval, this.lazy, @JsonKey(name: 'disable-udp') this.disableUDP, this.url, this.timeout, @JsonKey(name: 'max-failed-times') this.maxFailedTimes, this.filter, @JsonKey(name: 'exclude-filter') this.excludeFilter, @JsonKey(name: 'exclude-type') this.excludeType, @JsonKey(name: 'expected-status') this.expectedStatus, this.tolerance, this.strategy, @JsonKey(name: 'hash-key') this.hashKey, @JsonKey(name: 'default-selected') this.defaultSelected, @JsonKey(name: 'empty-fallback') this.emptyFallback, @JsonKey(name: 'include-all') this.includeAll, @JsonKey(name: 'include-all-proxies') this.includeAllProxies, @JsonKey(name: 'include-all-providers') this.includeAllProviders, this.hidden, this.icon, this.order, List? memberLayout}): _proxies = proxies,_use = use,_usePosition = usePosition,_memberLayout = memberLayout; factory _ProxyGroup.fromJson(Map json) => _$ProxyGroupFromJson(json); @override final int? profileId; @@ -266,6 +270,15 @@ class _ProxyGroup implements ProxyGroup { return EqualUnmodifiableListView(value); } + final Map? _usePosition; +@override@JsonKey(name: 'use-position') Map? get usePosition { + final value = _usePosition; + if (value == null) return null; + if (_usePosition is EqualUnmodifiableMapView) return _usePosition; + // ignore: implicit_dynamic_type + return EqualUnmodifiableMapView(value); +} + @override final int? interval; @override final bool? lazy; @override@JsonKey(name: 'disable-udp') final bool? disableUDP; @@ -287,6 +300,19 @@ class _ProxyGroup implements ProxyGroup { @override final bool? hidden; @override final String? icon; @override final String? order; +/// Where the members list shows the filters among the proxies, which no +/// config carries, as `ProxyGroupMembersSliver` writes it. + final List? _memberLayout; +/// Where the members list shows the filters among the proxies, which no +/// config carries, as `ProxyGroupMembersSliver` writes it. +@override List? get memberLayout { + final value = _memberLayout; + if (value == null) return null; + if (_memberLayout is EqualUnmodifiableListView) return _memberLayout; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + /// Create a copy of ProxyGroup /// with the given fields replaced by the non-null parameter values. @@ -301,18 +327,18 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyGroup&&(identical(other.profileId, profileId) || other.profileId == profileId)&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.proxies, _proxies)&&const DeepCollectionEquality().equals(other.use, _use)&&(identical(other.interval, interval) || other.interval == interval)&&(identical(other.lazy, lazy) || other.lazy == lazy)&&(identical(other.disableUDP, disableUDP) || other.disableUDP == disableUDP)&&(identical(other.url, url) || other.url == url)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.maxFailedTimes, maxFailedTimes) || other.maxFailedTimes == maxFailedTimes)&&(identical(other.filter, filter) || other.filter == filter)&&(identical(other.excludeFilter, excludeFilter) || other.excludeFilter == excludeFilter)&&(identical(other.excludeType, excludeType) || other.excludeType == excludeType)&&(identical(other.expectedStatus, expectedStatus) || other.expectedStatus == expectedStatus)&&(identical(other.tolerance, tolerance) || other.tolerance == tolerance)&&(identical(other.strategy, strategy) || other.strategy == strategy)&&(identical(other.hashKey, hashKey) || other.hashKey == hashKey)&&(identical(other.defaultSelected, defaultSelected) || other.defaultSelected == defaultSelected)&&(identical(other.emptyFallback, emptyFallback) || other.emptyFallback == emptyFallback)&&(identical(other.includeAll, includeAll) || other.includeAll == includeAll)&&(identical(other.includeAllProxies, includeAllProxies) || other.includeAllProxies == includeAllProxies)&&(identical(other.includeAllProviders, includeAllProviders) || other.includeAllProviders == includeAllProviders)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.order, order) || other.order == order)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyGroup&&(identical(other.profileId, profileId) || other.profileId == profileId)&&(identical(other.id, id) || other.id == id)&&(identical(other.name, name) || other.name == name)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.proxies, _proxies)&&const DeepCollectionEquality().equals(other.use, _use)&&const DeepCollectionEquality().equals(other.usePosition, _usePosition)&&(identical(other.interval, interval) || other.interval == interval)&&(identical(other.lazy, lazy) || other.lazy == lazy)&&(identical(other.disableUDP, disableUDP) || other.disableUDP == disableUDP)&&(identical(other.url, url) || other.url == url)&&(identical(other.timeout, timeout) || other.timeout == timeout)&&(identical(other.maxFailedTimes, maxFailedTimes) || other.maxFailedTimes == maxFailedTimes)&&(identical(other.filter, filter) || other.filter == filter)&&(identical(other.excludeFilter, excludeFilter) || other.excludeFilter == excludeFilter)&&(identical(other.excludeType, excludeType) || other.excludeType == excludeType)&&(identical(other.expectedStatus, expectedStatus) || other.expectedStatus == expectedStatus)&&(identical(other.tolerance, tolerance) || other.tolerance == tolerance)&&(identical(other.strategy, strategy) || other.strategy == strategy)&&(identical(other.hashKey, hashKey) || other.hashKey == hashKey)&&(identical(other.defaultSelected, defaultSelected) || other.defaultSelected == defaultSelected)&&(identical(other.emptyFallback, emptyFallback) || other.emptyFallback == emptyFallback)&&(identical(other.includeAll, includeAll) || other.includeAll == includeAll)&&(identical(other.includeAllProxies, includeAllProxies) || other.includeAllProxies == includeAllProxies)&&(identical(other.includeAllProviders, includeAllProviders) || other.includeAllProviders == includeAllProviders)&&(identical(other.hidden, hidden) || other.hidden == hidden)&&(identical(other.icon, icon) || other.icon == icon)&&(identical(other.order, order) || other.order == order)&&const DeepCollectionEquality().equals(other.memberLayout, _memberLayout)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode { - return Object.hashAll([runtimeType,profileId,id,name,type,const DeepCollectionEquality().hash(_proxies),const DeepCollectionEquality().hash(_use),interval,lazy,disableUDP,url,timeout,maxFailedTimes,filter,excludeFilter,excludeType,expectedStatus,tolerance,strategy,hashKey,defaultSelected,emptyFallback,includeAll,includeAllProxies,includeAllProviders,hidden,icon,order]); + return Object.hashAll([runtimeType,profileId,id,name,type,const DeepCollectionEquality().hash(_proxies),const DeepCollectionEquality().hash(_use),const DeepCollectionEquality().hash(_usePosition),interval,lazy,disableUDP,url,timeout,maxFailedTimes,filter,excludeFilter,excludeType,expectedStatus,tolerance,strategy,hashKey,defaultSelected,emptyFallback,includeAll,includeAllProxies,includeAllProviders,hidden,icon,order,const DeepCollectionEquality().hash(_memberLayout)]); } @override String toString() { - return 'ProxyGroup(profileId: $profileId, id: $id, name: $name, type: $type, proxies: $proxies, use: $use, interval: $interval, lazy: $lazy, disableUDP: $disableUDP, url: $url, timeout: $timeout, maxFailedTimes: $maxFailedTimes, filter: $filter, excludeFilter: $excludeFilter, excludeType: $excludeType, expectedStatus: $expectedStatus, tolerance: $tolerance, strategy: $strategy, hashKey: $hashKey, defaultSelected: $defaultSelected, emptyFallback: $emptyFallback, includeAll: $includeAll, includeAllProxies: $includeAllProxies, includeAllProviders: $includeAllProviders, hidden: $hidden, icon: $icon, order: $order)'; + return 'ProxyGroup(profileId: $profileId, id: $id, name: $name, type: $type, proxies: $proxies, use: $use, usePosition: $usePosition, interval: $interval, lazy: $lazy, disableUDP: $disableUDP, url: $url, timeout: $timeout, maxFailedTimes: $maxFailedTimes, filter: $filter, excludeFilter: $excludeFilter, excludeType: $excludeType, expectedStatus: $expectedStatus, tolerance: $tolerance, strategy: $strategy, hashKey: $hashKey, defaultSelected: $defaultSelected, emptyFallback: $emptyFallback, includeAll: $includeAll, includeAllProxies: $includeAllProxies, includeAllProviders: $includeAllProviders, hidden: $hidden, icon: $icon, order: $order, memberLayout: $memberLayout)'; } @@ -323,7 +349,7 @@ abstract mixin class _$ProxyGroupCopyWith<$Res> implements $ProxyGroupCopyWith<$ factory _$ProxyGroupCopyWith(_ProxyGroup value, $Res Function(_ProxyGroup) _then) = __$ProxyGroupCopyWithImpl; @override @useResult $Res call({ - int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all') bool? includeAll,@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,@JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order + int? profileId,@JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List? proxies, List? use,@JsonKey(name: 'use-position') Map? usePosition, int? interval, bool? lazy,@JsonKey(name: 'disable-udp') bool? disableUDP, String? url, int? timeout,@JsonKey(name: 'max-failed-times') int? maxFailedTimes, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'exclude-type') String? excludeType,@JsonKey(name: 'expected-status') String? expectedStatus, int? tolerance, LoadBalanceStrategy? strategy,@JsonKey(name: 'hash-key') String? hashKey,@JsonKey(name: 'default-selected') String? defaultSelected,@JsonKey(name: 'empty-fallback') String? emptyFallback,@JsonKey(name: 'include-all') bool? includeAll,@JsonKey(name: 'include-all-proxies') bool? includeAllProxies,@JsonKey(name: 'include-all-providers') bool? includeAllProviders, bool? hidden, String? icon, String? order, List? memberLayout }); @@ -340,7 +366,7 @@ class __$ProxyGroupCopyWithImpl<$Res> /// Create a copy of ProxyGroup /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAll = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? profileId = freezed,Object? id = null,Object? name = null,Object? type = null,Object? proxies = freezed,Object? use = freezed,Object? usePosition = freezed,Object? interval = freezed,Object? lazy = freezed,Object? disableUDP = freezed,Object? url = freezed,Object? timeout = freezed,Object? maxFailedTimes = freezed,Object? filter = freezed,Object? excludeFilter = freezed,Object? excludeType = freezed,Object? expectedStatus = freezed,Object? tolerance = freezed,Object? strategy = freezed,Object? hashKey = freezed,Object? defaultSelected = freezed,Object? emptyFallback = freezed,Object? includeAll = freezed,Object? includeAllProxies = freezed,Object? includeAllProviders = freezed,Object? hidden = freezed,Object? icon = freezed,Object? order = freezed,Object? memberLayout = freezed,}) { return _then(_ProxyGroup( profileId: freezed == profileId ? _self.profileId : profileId // ignore: cast_nullable_to_non_nullable as int?,id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable @@ -348,7 +374,8 @@ as int,name: null == name ? _self.name : name // ignore: cast_nullable_to_non_nu as String,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable as GroupType,proxies: freezed == proxies ? _self._proxies : proxies // ignore: cast_nullable_to_non_nullable as List?,use: freezed == use ? _self._use : use // ignore: cast_nullable_to_non_nullable -as List?,interval: freezed == interval ? _self.interval : interval // ignore: cast_nullable_to_non_nullable +as List?,usePosition: freezed == usePosition ? _self._usePosition : usePosition // ignore: cast_nullable_to_non_nullable +as Map?,interval: freezed == interval ? _self.interval : interval // ignore: cast_nullable_to_non_nullable as int?,lazy: freezed == lazy ? _self.lazy : lazy // ignore: cast_nullable_to_non_nullable as bool?,disableUDP: freezed == disableUDP ? _self.disableUDP : disableUDP // ignore: cast_nullable_to_non_nullable as bool?,url: freezed == url ? _self.url : url // ignore: cast_nullable_to_non_nullable @@ -369,7 +396,8 @@ as bool?,includeAllProviders: freezed == includeAllProviders ? _self.includeAllP as bool?,hidden: freezed == hidden ? _self.hidden : hidden // ignore: cast_nullable_to_non_nullable as bool?,icon: freezed == icon ? _self.icon : icon // ignore: cast_nullable_to_non_nullable as String?,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable -as String?, +as String?,memberLayout: freezed == memberLayout ? _self._memberLayout : memberLayout // ignore: cast_nullable_to_non_nullable +as List?, )); } @@ -656,7 +684,9 @@ as String?, /// @nodoc mixin _$CustomProxy { -@JsonKey(fromJson: Snowflake.buildId) int get id; Map get definition; String? get order; +@JsonKey(fromJson: Snowflake.buildId) int get id; Map get definition; String? get order;/// Options the form lists with no value yet, which [definition] cannot +/// hold since the core reads every key in it. + Set get emptyOptions; /// Create a copy of CustomProxy /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -670,20 +700,20 @@ $CustomProxyCopyWith get copyWith => _$CustomProxyCopyWithImpl { factory $CustomProxyCopyWith(CustomProxy value, $Res Function(CustomProxy) _then) = _$CustomProxyCopyWithImpl; @useResult $Res call({ -@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order +@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order, Set emptyOptions }); @@ -711,12 +741,13 @@ class _$CustomProxyCopyWithImpl<$Res> /// Create a copy of CustomProxy /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? definition = null,Object? order = freezed,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? definition = null,Object? order = freezed,Object? emptyOptions = null,}) { return _then(CustomProxy( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as int,definition: null == definition ? _self.definition : definition // ignore: cast_nullable_to_non_nullable as Map,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable -as String?, +as String?,emptyOptions: null == emptyOptions ? _self.emptyOptions : emptyOptions // ignore: cast_nullable_to_non_nullable +as Set, )); } @@ -801,10 +832,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order, Set emptyOptions)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _CustomProxy() when $default != null: -return $default(_that.id,_that.definition,_that.order);case _: +return $default(_that.id,_that.definition,_that.order,_that.emptyOptions);case _: return orElse(); } @@ -822,10 +853,10 @@ return $default(_that.id,_that.definition,_that.order);case _: /// } /// ``` -@optionalTypeArgs TResult when(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order, Set emptyOptions) $default,) {final _that = this; switch (_that) { case _CustomProxy(): -return $default(_that.id,_that.definition,_that.order);case _: +return $default(_that.id,_that.definition,_that.order,_that.emptyOptions);case _: throw StateError('Unexpected subclass'); } @@ -842,10 +873,10 @@ return $default(_that.id,_that.definition,_that.order);case _: /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order, Set emptyOptions)? $default,) {final _that = this; switch (_that) { case _CustomProxy() when $default != null: -return $default(_that.id,_that.definition,_that.order);case _: +return $default(_that.id,_that.definition,_that.order,_that.emptyOptions);case _: return null; } @@ -857,7 +888,7 @@ return $default(_that.id,_that.definition,_that.order);case _: @JsonSerializable() class _CustomProxy implements CustomProxy { - const _CustomProxy({@JsonKey(fromJson: Snowflake.buildId) required this.id, Map definition = const {}, this.order}): _definition = definition; + const _CustomProxy({@JsonKey(fromJson: Snowflake.buildId) required this.id, Map definition = const {}, this.order, Set emptyOptions = const {}}): _definition = definition,_emptyOptions = emptyOptions; factory _CustomProxy.fromJson(Map json) => _$CustomProxyFromJson(json); @override@JsonKey(fromJson: Snowflake.buildId) final int id; @@ -869,6 +900,17 @@ class _CustomProxy implements CustomProxy { } @override final String? order; +/// Options the form lists with no value yet, which [definition] cannot +/// hold since the core reads every key in it. + final Set _emptyOptions; +/// Options the form lists with no value yet, which [definition] cannot +/// hold since the core reads every key in it. +@override@JsonKey() Set get emptyOptions { + if (_emptyOptions is EqualUnmodifiableSetView) return _emptyOptions; + // ignore: implicit_dynamic_type + return EqualUnmodifiableSetView(_emptyOptions); +} + /// Create a copy of CustomProxy /// with the given fields replaced by the non-null parameter values. @@ -883,18 +925,18 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomProxy&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.definition, _definition)&&(identical(other.order, order) || other.order == order)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _CustomProxy&&(identical(other.id, id) || other.id == id)&&const DeepCollectionEquality().equals(other.definition, _definition)&&(identical(other.order, order) || other.order == order)&&const DeepCollectionEquality().equals(other.emptyOptions, _emptyOptions)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode { - return Object.hash(runtimeType,id,const DeepCollectionEquality().hash(_definition),order); + return Object.hash(runtimeType,id,const DeepCollectionEquality().hash(_definition),order,const DeepCollectionEquality().hash(_emptyOptions)); } @override String toString() { - return 'CustomProxy(id: $id, definition: $definition, order: $order)'; + return 'CustomProxy(id: $id, definition: $definition, order: $order, emptyOptions: $emptyOptions)'; } @@ -905,7 +947,7 @@ abstract mixin class _$CustomProxyCopyWith<$Res> implements $CustomProxyCopyWith factory _$CustomProxyCopyWith(_CustomProxy value, $Res Function(_CustomProxy) _then) = __$CustomProxyCopyWithImpl; @override @useResult $Res call({ -@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order +@JsonKey(fromJson: Snowflake.buildId) int id, Map definition, String? order, Set emptyOptions }); @@ -922,12 +964,13 @@ class __$CustomProxyCopyWithImpl<$Res> /// Create a copy of CustomProxy /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? definition = null,Object? order = freezed,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? definition = null,Object? order = freezed,Object? emptyOptions = null,}) { return _then(_CustomProxy( id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable as int,definition: null == definition ? _self._definition : definition // ignore: cast_nullable_to_non_nullable as Map,order: freezed == order ? _self.order : order // ignore: cast_nullable_to_non_nullable -as String?, +as String?,emptyOptions: null == emptyOptions ? _self._emptyOptions : emptyOptions // ignore: cast_nullable_to_non_nullable +as Set, )); } @@ -5103,7 +5146,7 @@ return $default(_that.enable,_that.listen,_that.listenRoutingMark,_that.preferH3 @JsonSerializable() class _Dns implements Dns { - const _Dns({this.enable = false, this.listen = '', @JsonKey(name: 'listen-routing-mark') this.listenRoutingMark = 0, @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 = false, this.ipv6 = false, @JsonKey(name: 'ipv6-timeout') this.ipv6Timeout = 100, @JsonKey(name: 'cache-algorithm') this.cacheAlgorithm = DnsCacheAlgorithm.lru, @JsonKey(name: 'cache-max-size') this.cacheMaxSize = 4096, @JsonKey(name: 'default-nameserver') List defaultNameserver = const [], @JsonKey(name: 'enhanced-mode') this.enhancedMode = DnsMode.redirHost, @JsonKey(name: 'fake-ip-range') this.fakeIpRange = '', @JsonKey(name: 'fake-ip-range6') this.fakeIpRange6 = '', @JsonKey(name: 'fake-ip-filter') List fakeIpFilter = const [], @JsonKey(name: 'fake-ip-filter-mode') this.fakeIpFilterMode = FakeIpFilterMode.blacklist, @JsonKey(name: 'fake-ip-ttl') this.fakeIpTtl = 1, @JsonKey(name: 'nameserver-policy') Map nameserverPolicy = const {}, List nameserver = const [], List fallback = const [], @JsonKey(name: 'fallback-lazy-query') this.fallbackLazyQuery = false, @JsonKey(name: 'proxy-server-nameserver') List proxyServerNameserver = const [], @JsonKey(name: 'proxy-server-nameserver-policy') Map proxyServerNameserverPolicy = const {}, @JsonKey(name: 'direct-nameserver') List directNameserver = const [], @JsonKey(name: 'direct-nameserver-follow-policy') this.directNameserverFollowPolicy = false, @JsonKey(name: 'fallback-filter') this.fallbackFilter = const FallbackFilter()}): _defaultNameserver = defaultNameserver,_fakeIpFilter = fakeIpFilter,_nameserverPolicy = nameserverPolicy,_nameserver = nameserver,_fallback = fallback,_proxyServerNameserver = proxyServerNameserver,_proxyServerNameserverPolicy = proxyServerNameserverPolicy,_directNameserver = directNameserver; + const _Dns({this.enable = false, this.listen = '', @JsonKey(name: 'listen-routing-mark') this.listenRoutingMark = 0, @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 = false, this.ipv6 = false, @JsonKey(name: 'ipv6-timeout') this.ipv6Timeout = 100, @JsonKey(name: 'cache-algorithm') this.cacheAlgorithm = DnsCacheAlgorithm.lru, @JsonKey(name: 'cache-max-size') this.cacheMaxSize = 4096, @JsonKey(name: 'default-nameserver') List defaultNameserver = const ['114.114.114.114', '223.5.5.5', '8.8.8.8', '1.0.0.1'], @JsonKey(name: 'enhanced-mode') this.enhancedMode = DnsMode.redirHost, @JsonKey(name: 'fake-ip-range') this.fakeIpRange = '198.18.0.1/16', @JsonKey(name: 'fake-ip-range6') this.fakeIpRange6 = '', @JsonKey(name: 'fake-ip-filter') List fakeIpFilter = const ['dns.msftnsci.com', 'www.msftnsci.com', 'www.msftconnecttest.com'], @JsonKey(name: 'fake-ip-filter-mode') this.fakeIpFilterMode = FakeIpFilterMode.blacklist, @JsonKey(name: 'fake-ip-ttl') this.fakeIpTtl = 1, @JsonKey(name: 'nameserver-policy') Map nameserverPolicy = const {}, List nameserver = const ['https://doh.pub/dns-query', 'tls://223.5.5.5:853'], List fallback = const [], @JsonKey(name: 'fallback-lazy-query') this.fallbackLazyQuery = false, @JsonKey(name: 'proxy-server-nameserver') List proxyServerNameserver = const [], @JsonKey(name: 'proxy-server-nameserver-policy') Map proxyServerNameserverPolicy = const {}, @JsonKey(name: 'direct-nameserver') List directNameserver = const [], @JsonKey(name: 'direct-nameserver-follow-policy') this.directNameserverFollowPolicy = false, @JsonKey(name: 'fallback-filter') this.fallbackFilter = const FallbackFilter()}): _defaultNameserver = defaultNameserver,_fakeIpFilter = fakeIpFilter,_nameserverPolicy = nameserverPolicy,_nameserver = nameserver,_fallback = fallback,_proxyServerNameserver = proxyServerNameserver,_proxyServerNameserverPolicy = proxyServerNameserverPolicy,_directNameserver = directNameserver; factory _Dns.fromJson(Map json) => _$DnsFromJson(json); @override@JsonKey() final bool enable; @@ -5487,7 +5530,7 @@ return $default(_that.enable,_that.server,_that.port,_that.interval,_that.dialer @JsonSerializable() class _Ntp implements Ntp { - const _Ntp({this.enable = false, this.server = '', this.port = 123, this.interval = 30, @JsonKey(name: 'dialer-proxy') this.dialerProxy = '', @JsonKey(name: 'write-to-system') this.writeToSystem = false}); + const _Ntp({this.enable = false, this.server = 'time.apple.com', this.port = 123, this.interval = 30, @JsonKey(name: 'dialer-proxy') this.dialerProxy = '', @JsonKey(name: 'write-to-system') this.writeToSystem = false}); factory _Ntp.fromJson(Map json) => _$NtpFromJson(json); @override@JsonKey() final bool enable; @@ -6807,7 +6850,9 @@ as IpVersion?, /// @nodoc mixin _$ProxyProviderOptions { -@JsonKey(name: 'health-check') ProviderHealthCheck get healthCheck; String? get filter;@JsonKey(name: 'exclude-filter') String? get excludeFilter;@JsonKey(name: 'override') ProviderOverride get proxyOverride; +@JsonKey(name: 'health-check') ProviderHealthCheck get healthCheck; String? get filter;@JsonKey(name: 'exclude-filter') String? get excludeFilter;@JsonKey(name: 'override') ProviderOverride get proxyOverride;/// Where the filters list shows the exclude filters among the filters, +/// which no config carries, as `ProxyProviderFiltersSliver` writes it. + List? get memberLayout; /// Create a copy of ProxyProviderOptions /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -6821,20 +6866,20 @@ $ProxyProviderOptionsCopyWith get copyWith => _$ProxyProvi @override bool operator ==(Object other) { final _this = this as ProxyProviderOptions; - return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyProviderOptions&&(identical(other.healthCheck, _this.healthCheck) || other.healthCheck == _this.healthCheck)&&(identical(other.filter, _this.filter) || other.filter == _this.filter)&&(identical(other.excludeFilter, _this.excludeFilter) || other.excludeFilter == _this.excludeFilter)&&(identical(other.proxyOverride, _this.proxyOverride) || other.proxyOverride == _this.proxyOverride)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyProviderOptions&&(identical(other.healthCheck, _this.healthCheck) || other.healthCheck == _this.healthCheck)&&(identical(other.filter, _this.filter) || other.filter == _this.filter)&&(identical(other.excludeFilter, _this.excludeFilter) || other.excludeFilter == _this.excludeFilter)&&(identical(other.proxyOverride, _this.proxyOverride) || other.proxyOverride == _this.proxyOverride)&&const DeepCollectionEquality().equals(other.memberLayout, _this.memberLayout)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode { final _this = this as ProxyProviderOptions; - return Object.hash(runtimeType,_this.healthCheck,_this.filter,_this.excludeFilter,_this.proxyOverride); + return Object.hash(runtimeType,_this.healthCheck,_this.filter,_this.excludeFilter,_this.proxyOverride,const DeepCollectionEquality().hash(_this.memberLayout)); } @override String toString() { final _this = this as ProxyProviderOptions; - return 'ProxyProviderOptions(healthCheck: ${_this.healthCheck}, filter: ${_this.filter}, excludeFilter: ${_this.excludeFilter}, proxyOverride: ${_this.proxyOverride})'; + return 'ProxyProviderOptions(healthCheck: ${_this.healthCheck}, filter: ${_this.filter}, excludeFilter: ${_this.excludeFilter}, proxyOverride: ${_this.proxyOverride}, memberLayout: ${_this.memberLayout})'; } @@ -6845,7 +6890,7 @@ abstract mixin class $ProxyProviderOptionsCopyWith<$Res> { factory $ProxyProviderOptionsCopyWith(ProxyProviderOptions value, $Res Function(ProxyProviderOptions) _then) = _$ProxyProviderOptionsCopyWithImpl; @useResult $Res call({ -@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'override') ProviderOverride proxyOverride +@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'override') ProviderOverride proxyOverride, List? memberLayout }); @@ -6862,13 +6907,14 @@ class _$ProxyProviderOptionsCopyWithImpl<$Res> /// Create a copy of ProxyProviderOptions /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? healthCheck = null,Object? filter = freezed,Object? excludeFilter = freezed,Object? proxyOverride = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? healthCheck = null,Object? filter = freezed,Object? excludeFilter = freezed,Object? proxyOverride = null,Object? memberLayout = freezed,}) { return _then(ProxyProviderOptions( healthCheck: null == healthCheck ? _self.healthCheck : healthCheck // ignore: cast_nullable_to_non_nullable as ProviderHealthCheck,filter: freezed == filter ? _self.filter : filter // ignore: cast_nullable_to_non_nullable as String?,excludeFilter: freezed == excludeFilter ? _self.excludeFilter : excludeFilter // ignore: cast_nullable_to_non_nullable as String?,proxyOverride: null == proxyOverride ? _self.proxyOverride : proxyOverride // ignore: cast_nullable_to_non_nullable -as ProviderOverride, +as ProviderOverride,memberLayout: freezed == memberLayout ? _self.memberLayout : memberLayout // ignore: cast_nullable_to_non_nullable +as List?, )); } /// Create a copy of ProxyProviderOptions @@ -6971,10 +7017,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride, List? memberLayout)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _ProxyProviderOptions() when $default != null: -return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOverride);case _: +return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOverride,_that.memberLayout);case _: return orElse(); } @@ -6992,10 +7038,10 @@ return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOv /// } /// ``` -@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride, List? memberLayout) $default,) {final _that = this; switch (_that) { case _ProxyProviderOptions(): -return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOverride);case _: +return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOverride,_that.memberLayout);case _: throw StateError('Unexpected subclass'); } @@ -7012,10 +7058,10 @@ return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOv /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride, List? memberLayout)? $default,) {final _that = this; switch (_that) { case _ProxyProviderOptions() when $default != null: -return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOverride);case _: +return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOverride,_that.memberLayout);case _: return null; } @@ -7027,13 +7073,26 @@ return $default(_that.healthCheck,_that.filter,_that.excludeFilter,_that.proxyOv @JsonSerializable() class _ProxyProviderOptions implements ProxyProviderOptions { - const _ProxyProviderOptions({@JsonKey(name: 'health-check') this.healthCheck = const ProviderHealthCheck(), this.filter, @JsonKey(name: 'exclude-filter') this.excludeFilter, @JsonKey(name: 'override') this.proxyOverride = const ProviderOverride()}); + const _ProxyProviderOptions({@JsonKey(name: 'health-check') this.healthCheck = const ProviderHealthCheck(), this.filter, @JsonKey(name: 'exclude-filter') this.excludeFilter, @JsonKey(name: 'override') this.proxyOverride = const ProviderOverride(), List? memberLayout}): _memberLayout = memberLayout; factory _ProxyProviderOptions.fromJson(Map json) => _$ProxyProviderOptionsFromJson(json); @override@JsonKey(name: 'health-check') final ProviderHealthCheck healthCheck; @override final String? filter; @override@JsonKey(name: 'exclude-filter') final String? excludeFilter; @override@JsonKey(name: 'override') final ProviderOverride proxyOverride; +/// Where the filters list shows the exclude filters among the filters, +/// which no config carries, as `ProxyProviderFiltersSliver` writes it. + final List? _memberLayout; +/// Where the filters list shows the exclude filters among the filters, +/// which no config carries, as `ProxyProviderFiltersSliver` writes it. +@override List? get memberLayout { + final value = _memberLayout; + if (value == null) return null; + if (_memberLayout is EqualUnmodifiableListView) return _memberLayout; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(value); +} + /// Create a copy of ProxyProviderOptions /// with the given fields replaced by the non-null parameter values. @@ -7048,18 +7107,18 @@ Map toJson() { @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyProviderOptions&&(identical(other.healthCheck, healthCheck) || other.healthCheck == healthCheck)&&(identical(other.filter, filter) || other.filter == filter)&&(identical(other.excludeFilter, excludeFilter) || other.excludeFilter == excludeFilter)&&(identical(other.proxyOverride, proxyOverride) || other.proxyOverride == proxyOverride)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _ProxyProviderOptions&&(identical(other.healthCheck, healthCheck) || other.healthCheck == healthCheck)&&(identical(other.filter, filter) || other.filter == filter)&&(identical(other.excludeFilter, excludeFilter) || other.excludeFilter == excludeFilter)&&(identical(other.proxyOverride, proxyOverride) || other.proxyOverride == proxyOverride)&&const DeepCollectionEquality().equals(other.memberLayout, _memberLayout)); } @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode { - return Object.hash(runtimeType,healthCheck,filter,excludeFilter,proxyOverride); + return Object.hash(runtimeType,healthCheck,filter,excludeFilter,proxyOverride,const DeepCollectionEquality().hash(_memberLayout)); } @override String toString() { - return 'ProxyProviderOptions(healthCheck: $healthCheck, filter: $filter, excludeFilter: $excludeFilter, proxyOverride: $proxyOverride)'; + return 'ProxyProviderOptions(healthCheck: $healthCheck, filter: $filter, excludeFilter: $excludeFilter, proxyOverride: $proxyOverride, memberLayout: $memberLayout)'; } @@ -7070,7 +7129,7 @@ abstract mixin class _$ProxyProviderOptionsCopyWith<$Res> implements $ProxyProvi factory _$ProxyProviderOptionsCopyWith(_ProxyProviderOptions value, $Res Function(_ProxyProviderOptions) _then) = __$ProxyProviderOptionsCopyWithImpl; @override @useResult $Res call({ -@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'override') ProviderOverride proxyOverride +@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter,@JsonKey(name: 'exclude-filter') String? excludeFilter,@JsonKey(name: 'override') ProviderOverride proxyOverride, List? memberLayout }); @@ -7087,13 +7146,14 @@ class __$ProxyProviderOptionsCopyWithImpl<$Res> /// Create a copy of ProxyProviderOptions /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? healthCheck = null,Object? filter = freezed,Object? excludeFilter = freezed,Object? proxyOverride = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? healthCheck = null,Object? filter = freezed,Object? excludeFilter = freezed,Object? proxyOverride = null,Object? memberLayout = freezed,}) { return _then(_ProxyProviderOptions( healthCheck: null == healthCheck ? _self.healthCheck : healthCheck // ignore: cast_nullable_to_non_nullable as ProviderHealthCheck,filter: freezed == filter ? _self.filter : filter // ignore: cast_nullable_to_non_nullable as String?,excludeFilter: freezed == excludeFilter ? _self.excludeFilter : excludeFilter // ignore: cast_nullable_to_non_nullable as String?,proxyOverride: null == proxyOverride ? _self.proxyOverride : proxyOverride // ignore: cast_nullable_to_non_nullable -as ProviderOverride, +as ProviderOverride,memberLayout: freezed == memberLayout ? _self._memberLayout : memberLayout // ignore: cast_nullable_to_non_nullable +as List?, )); } diff --git a/lib/models/generated/clash_config.g.dart b/lib/models/generated/clash_config.g.dart index 47d8f7f8..1f48a42d 100644 --- a/lib/models/generated/clash_config.g.dart +++ b/lib/models/generated/clash_config.g.dart @@ -15,6 +15,9 @@ _ProxyGroup _$ProxyGroupFromJson(Map json) => _ProxyGroup( ?.map((e) => e as String) .toList(), use: (json['use'] as List?)?.map((e) => e as String).toList(), + usePosition: (json['use-position'] as Map?)?.map( + (k, e) => MapEntry(k, (e as num).toInt()), + ), interval: (json['interval'] as num?)?.toInt(), lazy: json['lazy'] as bool?, disableUDP: json['disable-udp'] as bool?, @@ -36,6 +39,9 @@ _ProxyGroup _$ProxyGroupFromJson(Map json) => _ProxyGroup( hidden: json['hidden'] as bool?, icon: json['icon'] as String?, order: json['order'] as String?, + memberLayout: (json['memberLayout'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$ProxyGroupToJson(_ProxyGroup instance) => @@ -46,6 +52,7 @@ Map _$ProxyGroupToJson(_ProxyGroup instance) => 'type': _$GroupTypeEnumMap[instance.type]!, 'proxies': instance.proxies, 'use': instance.use, + 'use-position': instance.usePosition, 'interval': instance.interval, 'lazy': instance.lazy, 'disable-udp': instance.disableUDP, @@ -67,6 +74,7 @@ Map _$ProxyGroupToJson(_ProxyGroup instance) => 'hidden': instance.hidden, 'icon': instance.icon, 'order': instance.order, + 'memberLayout': instance.memberLayout, }; const _$GroupTypeEnumMap = { @@ -99,6 +107,11 @@ _CustomProxy _$CustomProxyFromJson(Map json) => _CustomProxy( id: Snowflake.buildId((json['id'] as num?)?.toInt()), definition: json['definition'] as Map? ?? const {}, order: json['order'] as String?, + emptyOptions: + (json['emptyOptions'] as List?) + ?.map((e) => $enumDecode(_$ProxyFieldEnumMap, e)) + .toSet() ?? + const {}, ); Map _$CustomProxyToJson(_CustomProxy instance) => @@ -106,8 +119,73 @@ Map _$CustomProxyToJson(_CustomProxy instance) => 'id': instance.id, 'definition': instance.definition, 'order': instance.order, + 'emptyOptions': instance.emptyOptions + .map((e) => _$ProxyFieldEnumMap[e]!) + .toList(), }; +const _$ProxyFieldEnumMap = { + ProxyField.username: 'username', + ProxyField.password: 'password', + ProxyField.uuid: 'uuid', + ProxyField.psk: 'psk', + ProxyField.key: 'key', + ProxyField.privateKey: 'privateKey', + ProxyField.publicKey: 'publicKey', + ProxyField.privateKeyPassphrase: 'privateKeyPassphrase', + ProxyField.preSharedKey: 'preSharedKey', + ProxyField.authStr: 'authStr', + ProxyField.cipher: 'cipher', + ProxyField.alterId: 'alterId', + ProxyField.protocol: 'protocol', + ProxyField.obfs: 'obfs', + ProxyField.transport: 'transport', + ProxyField.up: 'up', + ProxyField.down: 'down', + ProxyField.network: 'network', + ProxyField.flow: 'flow', + ProxyField.wsPath: 'wsPath', + ProxyField.wsHost: 'wsHost', + ProxyField.grpcServiceName: 'grpcServiceName', + ProxyField.xhttpPath: 'xhttpPath', + ProxyField.xhttpHost: 'xhttpHost', + ProxyField.tls: 'tls', + ProxyField.sni: 'sni', + ProxyField.servername: 'servername', + ProxyField.alpn: 'alpn', + ProxyField.skipCertVerify: 'skipCertVerify', + ProxyField.clientFingerprint: 'clientFingerprint', + ProxyField.realityPublicKey: 'realityPublicKey', + ProxyField.realityShortId: 'realityShortId', + ProxyField.plugin: 'plugin', + ProxyField.pluginMode: 'pluginMode', + ProxyField.pluginHost: 'pluginHost', + ProxyField.udpOverTcp: 'udpOverTcp', + ProxyField.protocolParam: 'protocolParam', + ProxyField.obfsParam: 'obfsParam', + ProxyField.obfsPassword: 'obfsPassword', + ProxyField.obfsSecret: 'obfsSecret', + ProxyField.obfsMode: 'obfsMode', + ProxyField.obfsHost: 'obfsHost', + ProxyField.version: 'version', + ProxyField.ports: 'ports', + ProxyField.hopInterval: 'hopInterval', + ProxyField.congestionController: 'congestionController', + ProxyField.udpRelayMode: 'udpRelayMode', + ProxyField.reduceRtt: 'reduceRtt', + ProxyField.ip: 'ip', + ProxyField.ipv6: 'ipv6', + ProxyField.allowedIps: 'allowedIps', + ProxyField.dns: 'dns', + ProxyField.remoteDnsResolve: 'remoteDnsResolve', + ProxyField.mtu: 'mtu', + ProxyField.tfo: 'tfo', + ProxyField.mptcp: 'mptcp', + ProxyField.interfaceName: 'interfaceName', + ProxyField.routingMark: 'routingMark', + ProxyField.ipVersion: 'ipVersion', +}; + _RuleProvider _$RuleProviderFromJson(Map json) => _RuleProvider(name: json['name'] as String); @@ -274,17 +352,17 @@ _Dns _$DnsFromJson(Map json) => _Dns( (json['default-nameserver'] as List?) ?.map((e) => e as String) .toList() ?? - const [], + const ['114.114.114.114', '223.5.5.5', '8.8.8.8', '1.0.0.1'], enhancedMode: $enumDecodeNullable(_$DnsModeEnumMap, json['enhanced-mode']) ?? DnsMode.redirHost, - fakeIpRange: json['fake-ip-range'] as String? ?? '', + fakeIpRange: json['fake-ip-range'] as String? ?? '198.18.0.1/16', fakeIpRange6: json['fake-ip-range6'] as String? ?? '', fakeIpFilter: (json['fake-ip-filter'] as List?) ?.map((e) => e as String) .toList() ?? - const [], + const ['dns.msftnsci.com', 'www.msftnsci.com', 'www.msftconnecttest.com'], fakeIpFilterMode: $enumDecodeNullable( _$FakeIpFilterModeEnumMap, @@ -301,7 +379,7 @@ _Dns _$DnsFromJson(Map json) => _Dns( (json['nameserver'] as List?) ?.map((e) => e as String) .toList() ?? - const [], + const ['https://doh.pub/dns-query', 'tls://223.5.5.5:853'], fallback: (json['fallback'] as List?)?.map((e) => e as String).toList() ?? const [], @@ -380,7 +458,7 @@ const _$FakeIpFilterModeEnumMap = { _Ntp _$NtpFromJson(Map json) => _Ntp( enable: json['enable'] as bool? ?? false, - server: json['server'] as String? ?? '', + server: json['server'] as String? ?? 'time.apple.com', port: (json['port'] as num?)?.toInt() ?? 123, interval: (json['interval'] as num?)?.toInt() ?? 30, dialerProxy: json['dialer-proxy'] as String? ?? '', @@ -591,6 +669,9 @@ _ProxyProviderOptions _$ProxyProviderOptionsFromJson( proxyOverride: json['override'] == null ? const ProviderOverride() : ProviderOverride.fromJson(json['override'] as Map), + memberLayout: (json['memberLayout'] as List?) + ?.map((e) => e as String) + .toList(), ); Map _$ProxyProviderOptionsToJson( @@ -600,6 +681,7 @@ Map _$ProxyProviderOptionsToJson( 'filter': instance.filter, 'exclude-filter': instance.excludeFilter, 'override': instance.proxyOverride, + 'memberLayout': instance.memberLayout, }; _Rule _$RuleFromJson(Map json) => _Rule( diff --git a/lib/models/generated/config.freezed.dart b/lib/models/generated/config.freezed.dart index 9fff4982..bbf35588 100644 --- a/lib/models/generated/config.freezed.dart +++ b/lib/models/generated/config.freezed.dart @@ -2053,7 +2053,7 @@ return $default(_that.systemProxy,_that.bypassDomain,_that.bypassPrivateRoute,_t @JsonSerializable() class _NetworkProps implements NetworkProps { - const _NetworkProps({this.systemProxy = true, List bypassDomain = defaultBypassDomain, this.bypassPrivateRoute = false, this.autoSetSystemDns = true, this.appendSystemDns = false, this.authentication = defaultAuthenticationProps}): _bypassDomain = bypassDomain; + const _NetworkProps({this.systemProxy = true, List bypassDomain = defaultBypassDomain, this.bypassPrivateRoute = true, this.autoSetSystemDns = true, this.appendSystemDns = false, this.authentication = defaultAuthenticationProps}): _bypassDomain = bypassDomain; factory _NetworkProps.fromJson(Map json) => _$NetworkPropsFromJson(json); @override@JsonKey() final bool systemProxy; diff --git a/lib/models/generated/config.g.dart b/lib/models/generated/config.g.dart index 10307bf1..e2a08828 100644 --- a/lib/models/generated/config.g.dart +++ b/lib/models/generated/config.g.dart @@ -259,7 +259,7 @@ _NetworkProps _$NetworkPropsFromJson(Map json) => ?.map((e) => e as String) .toList() ?? defaultBypassDomain, - bypassPrivateRoute: json['bypassPrivateRoute'] as bool? ?? false, + bypassPrivateRoute: json['bypassPrivateRoute'] as bool? ?? true, autoSetSystemDns: json['autoSetSystemDns'] as bool? ?? true, appendSystemDns: json['appendSystemDns'] as bool? ?? false, authentication: json['authentication'] == null diff --git a/lib/models/generated/state.freezed.dart b/lib/models/generated/state.freezed.dart index 7d624d09..1c992b70 100644 --- a/lib/models/generated/state.freezed.dart +++ b/lib/models/generated/state.freezed.dart @@ -8718,7 +8718,8 @@ $ProxiesDataCopyWith<$Res> get proxiesData { /// @nodoc mixin _$MakeRealProfileState { - String get profilesPath; int get profileId; Map get rawConfig; PatchClashConfig get realPatchConfig; ProfileOverrides get overrides; bool get appendSystemDns; List get proxyGroups; List get rules; List get addedRules; String get defaultUA; List get authentication; Map get injectedProxyProviders; Map get injectedRuleProviders; String? get matchTarget; bool get safeMode; + String get profilesPath; int get profileId; Map get rawConfig; PatchClashConfig get realPatchConfig; ProfileOverrides get overrides;/// Set for a subscription, whose own DNS values an emptied entry replaces. + bool get keepEmptyDnsOverrides; bool get appendSystemDns; List get proxyGroups; List get rules; List get addedRules; String get defaultUA; List get authentication; Map get injectedProxyProviders; Map get injectedRuleProviders; String? get matchTarget; bool get safeMode; /// Create a copy of MakeRealProfileState /// with the given fields replaced by the non-null parameter values. @JsonKey(includeFromJson: false, includeToJson: false) @@ -8730,20 +8731,20 @@ $MakeRealProfileStateCopyWith get copyWith => _$MakeRealPr @override bool operator ==(Object other) { final _this = this as MakeRealProfileState; - return identical(this, other) || (other.runtimeType == runtimeType&&other is MakeRealProfileState&&(identical(other.profilesPath, _this.profilesPath) || other.profilesPath == _this.profilesPath)&&(identical(other.profileId, _this.profileId) || other.profileId == _this.profileId)&&const DeepCollectionEquality().equals(other.rawConfig, _this.rawConfig)&&(identical(other.realPatchConfig, _this.realPatchConfig) || other.realPatchConfig == _this.realPatchConfig)&&(identical(other.overrides, _this.overrides) || other.overrides == _this.overrides)&&(identical(other.appendSystemDns, _this.appendSystemDns) || other.appendSystemDns == _this.appendSystemDns)&&const DeepCollectionEquality().equals(other.proxyGroups, _this.proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _this.rules)&&const DeepCollectionEquality().equals(other.addedRules, _this.addedRules)&&(identical(other.defaultUA, _this.defaultUA) || other.defaultUA == _this.defaultUA)&&const DeepCollectionEquality().equals(other.authentication, _this.authentication)&&const DeepCollectionEquality().equals(other.injectedProxyProviders, _this.injectedProxyProviders)&&const DeepCollectionEquality().equals(other.injectedRuleProviders, _this.injectedRuleProviders)&&(identical(other.matchTarget, _this.matchTarget) || other.matchTarget == _this.matchTarget)&&(identical(other.safeMode, _this.safeMode) || other.safeMode == _this.safeMode)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is MakeRealProfileState&&(identical(other.profilesPath, _this.profilesPath) || other.profilesPath == _this.profilesPath)&&(identical(other.profileId, _this.profileId) || other.profileId == _this.profileId)&&const DeepCollectionEquality().equals(other.rawConfig, _this.rawConfig)&&(identical(other.realPatchConfig, _this.realPatchConfig) || other.realPatchConfig == _this.realPatchConfig)&&(identical(other.overrides, _this.overrides) || other.overrides == _this.overrides)&&(identical(other.keepEmptyDnsOverrides, _this.keepEmptyDnsOverrides) || other.keepEmptyDnsOverrides == _this.keepEmptyDnsOverrides)&&(identical(other.appendSystemDns, _this.appendSystemDns) || other.appendSystemDns == _this.appendSystemDns)&&const DeepCollectionEquality().equals(other.proxyGroups, _this.proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _this.rules)&&const DeepCollectionEquality().equals(other.addedRules, _this.addedRules)&&(identical(other.defaultUA, _this.defaultUA) || other.defaultUA == _this.defaultUA)&&const DeepCollectionEquality().equals(other.authentication, _this.authentication)&&const DeepCollectionEquality().equals(other.injectedProxyProviders, _this.injectedProxyProviders)&&const DeepCollectionEquality().equals(other.injectedRuleProviders, _this.injectedRuleProviders)&&(identical(other.matchTarget, _this.matchTarget) || other.matchTarget == _this.matchTarget)&&(identical(other.safeMode, _this.safeMode) || other.safeMode == _this.safeMode)); } @override int get hashCode { final _this = this as MakeRealProfileState; - return Object.hash(runtimeType,_this.profilesPath,_this.profileId,const DeepCollectionEquality().hash(_this.rawConfig),_this.realPatchConfig,_this.overrides,_this.appendSystemDns,const DeepCollectionEquality().hash(_this.proxyGroups),const DeepCollectionEquality().hash(_this.rules),const DeepCollectionEquality().hash(_this.addedRules),_this.defaultUA,const DeepCollectionEquality().hash(_this.authentication),const DeepCollectionEquality().hash(_this.injectedProxyProviders),const DeepCollectionEquality().hash(_this.injectedRuleProviders),_this.matchTarget,_this.safeMode); + return Object.hash(runtimeType,_this.profilesPath,_this.profileId,const DeepCollectionEquality().hash(_this.rawConfig),_this.realPatchConfig,_this.overrides,_this.keepEmptyDnsOverrides,_this.appendSystemDns,const DeepCollectionEquality().hash(_this.proxyGroups),const DeepCollectionEquality().hash(_this.rules),const DeepCollectionEquality().hash(_this.addedRules),_this.defaultUA,const DeepCollectionEquality().hash(_this.authentication),const DeepCollectionEquality().hash(_this.injectedProxyProviders),const DeepCollectionEquality().hash(_this.injectedRuleProviders),_this.matchTarget,_this.safeMode); } @override String toString() { final _this = this as MakeRealProfileState; - return 'MakeRealProfileState(profilesPath: ${_this.profilesPath}, profileId: ${_this.profileId}, rawConfig: ${_this.rawConfig}, realPatchConfig: ${_this.realPatchConfig}, overrides: ${_this.overrides}, appendSystemDns: ${_this.appendSystemDns}, proxyGroups: ${_this.proxyGroups}, rules: ${_this.rules}, addedRules: ${_this.addedRules}, defaultUA: ${_this.defaultUA}, authentication: ${_this.authentication}, injectedProxyProviders: ${_this.injectedProxyProviders}, injectedRuleProviders: ${_this.injectedRuleProviders}, matchTarget: ${_this.matchTarget}, safeMode: ${_this.safeMode})'; + return 'MakeRealProfileState(profilesPath: ${_this.profilesPath}, profileId: ${_this.profileId}, rawConfig: ${_this.rawConfig}, realPatchConfig: ${_this.realPatchConfig}, overrides: ${_this.overrides}, keepEmptyDnsOverrides: ${_this.keepEmptyDnsOverrides}, appendSystemDns: ${_this.appendSystemDns}, proxyGroups: ${_this.proxyGroups}, rules: ${_this.rules}, addedRules: ${_this.addedRules}, defaultUA: ${_this.defaultUA}, authentication: ${_this.authentication}, injectedProxyProviders: ${_this.injectedProxyProviders}, injectedRuleProviders: ${_this.injectedRuleProviders}, matchTarget: ${_this.matchTarget}, safeMode: ${_this.safeMode})'; } @@ -8754,7 +8755,7 @@ abstract mixin class $MakeRealProfileStateCopyWith<$Res> { factory $MakeRealProfileStateCopyWith(MakeRealProfileState value, $Res Function(MakeRealProfileState) _then) = _$MakeRealProfileStateCopyWithImpl; @useResult $Res call({ - String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode + String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode }); @@ -8771,14 +8772,15 @@ class _$MakeRealProfileStateCopyWithImpl<$Res> /// Create a copy of MakeRealProfileState /// with the given fields replaced by the non-null parameter values. -@pragma('vm:prefer-inline') @override $Res call({Object? profilesPath = null,Object? profileId = null,Object? rawConfig = null,Object? realPatchConfig = null,Object? overrides = null,Object? appendSystemDns = null,Object? proxyGroups = null,Object? rules = null,Object? addedRules = null,Object? defaultUA = null,Object? authentication = null,Object? injectedProxyProviders = null,Object? injectedRuleProviders = null,Object? matchTarget = freezed,Object? safeMode = null,}) { +@pragma('vm:prefer-inline') @override $Res call({Object? profilesPath = null,Object? profileId = null,Object? rawConfig = null,Object? realPatchConfig = null,Object? overrides = null,Object? keepEmptyDnsOverrides = null,Object? appendSystemDns = null,Object? proxyGroups = null,Object? rules = null,Object? addedRules = null,Object? defaultUA = null,Object? authentication = null,Object? injectedProxyProviders = null,Object? injectedRuleProviders = null,Object? matchTarget = freezed,Object? safeMode = null,}) { return _then(MakeRealProfileState( profilesPath: null == profilesPath ? _self.profilesPath : profilesPath // ignore: cast_nullable_to_non_nullable as String,profileId: null == profileId ? _self.profileId : profileId // ignore: cast_nullable_to_non_nullable as int,rawConfig: null == rawConfig ? _self.rawConfig : rawConfig // ignore: cast_nullable_to_non_nullable as Map,realPatchConfig: null == realPatchConfig ? _self.realPatchConfig : realPatchConfig // ignore: cast_nullable_to_non_nullable as PatchClashConfig,overrides: null == overrides ? _self.overrides : overrides // ignore: cast_nullable_to_non_nullable -as ProfileOverrides,appendSystemDns: null == appendSystemDns ? _self.appendSystemDns : appendSystemDns // ignore: cast_nullable_to_non_nullable +as ProfileOverrides,keepEmptyDnsOverrides: null == keepEmptyDnsOverrides ? _self.keepEmptyDnsOverrides : keepEmptyDnsOverrides // ignore: cast_nullable_to_non_nullable +as bool,appendSystemDns: null == appendSystemDns ? _self.appendSystemDns : appendSystemDns // ignore: cast_nullable_to_non_nullable as bool,proxyGroups: null == proxyGroups ? _self.proxyGroups : proxyGroups // ignore: cast_nullable_to_non_nullable as List,rules: null == rules ? _self.rules : rules // ignore: cast_nullable_to_non_nullable as List,addedRules: null == addedRules ? _self.addedRules : addedRules // ignore: cast_nullable_to_non_nullable @@ -8891,10 +8893,10 @@ return $default(_that);case _: /// } /// ``` -@optionalTypeArgs TResult maybeWhen(TResult Function( String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode)? $default,{required TResult orElse(),}) {final _that = this; +@optionalTypeArgs TResult maybeWhen(TResult Function( String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode)? $default,{required TResult orElse(),}) {final _that = this; switch (_that) { case _MakeRealProfileState() when $default != null: -return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPatchConfig,_that.overrides,_that.appendSystemDns,_that.proxyGroups,_that.rules,_that.addedRules,_that.defaultUA,_that.authentication,_that.injectedProxyProviders,_that.injectedRuleProviders,_that.matchTarget,_that.safeMode);case _: +return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPatchConfig,_that.overrides,_that.keepEmptyDnsOverrides,_that.appendSystemDns,_that.proxyGroups,_that.rules,_that.addedRules,_that.defaultUA,_that.authentication,_that.injectedProxyProviders,_that.injectedRuleProviders,_that.matchTarget,_that.safeMode);case _: return orElse(); } @@ -8912,10 +8914,10 @@ return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPat /// } /// ``` -@optionalTypeArgs TResult when(TResult Function( String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode) $default,) {final _that = this; switch (_that) { case _MakeRealProfileState(): -return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPatchConfig,_that.overrides,_that.appendSystemDns,_that.proxyGroups,_that.rules,_that.addedRules,_that.defaultUA,_that.authentication,_that.injectedProxyProviders,_that.injectedRuleProviders,_that.matchTarget,_that.safeMode);case _: +return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPatchConfig,_that.overrides,_that.keepEmptyDnsOverrides,_that.appendSystemDns,_that.proxyGroups,_that.rules,_that.addedRules,_that.defaultUA,_that.authentication,_that.injectedProxyProviders,_that.injectedRuleProviders,_that.matchTarget,_that.safeMode);case _: throw StateError('Unexpected subclass'); } @@ -8932,10 +8934,10 @@ return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPat /// } /// ``` -@optionalTypeArgs TResult? whenOrNull(TResult? Function( String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode)? $default,) {final _that = this; switch (_that) { case _MakeRealProfileState() when $default != null: -return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPatchConfig,_that.overrides,_that.appendSystemDns,_that.proxyGroups,_that.rules,_that.addedRules,_that.defaultUA,_that.authentication,_that.injectedProxyProviders,_that.injectedRuleProviders,_that.matchTarget,_that.safeMode);case _: +return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPatchConfig,_that.overrides,_that.keepEmptyDnsOverrides,_that.appendSystemDns,_that.proxyGroups,_that.rules,_that.addedRules,_that.defaultUA,_that.authentication,_that.injectedProxyProviders,_that.injectedRuleProviders,_that.matchTarget,_that.safeMode);case _: return null; } @@ -8947,7 +8949,7 @@ return $default(_that.profilesPath,_that.profileId,_that.rawConfig,_that.realPat class _MakeRealProfileState implements MakeRealProfileState { - const _MakeRealProfileState({required this.profilesPath, required this.profileId, required Map rawConfig, required this.realPatchConfig, this.overrides = const ProfileOverrides(), required this.appendSystemDns, required List proxyGroups, required List rules, required List addedRules, required this.defaultUA, List authentication = const [], Map injectedProxyProviders = const {}, Map injectedRuleProviders = const {}, this.matchTarget, this.safeMode = false}): _rawConfig = rawConfig,_proxyGroups = proxyGroups,_rules = rules,_addedRules = addedRules,_authentication = authentication,_injectedProxyProviders = injectedProxyProviders,_injectedRuleProviders = injectedRuleProviders; + const _MakeRealProfileState({required this.profilesPath, required this.profileId, required Map rawConfig, required this.realPatchConfig, this.overrides = const ProfileOverrides(), this.keepEmptyDnsOverrides = false, required this.appendSystemDns, required List proxyGroups, required List rules, required List addedRules, required this.defaultUA, List authentication = const [], Map injectedProxyProviders = const {}, Map injectedRuleProviders = const {}, this.matchTarget, this.safeMode = false}): _rawConfig = rawConfig,_proxyGroups = proxyGroups,_rules = rules,_addedRules = addedRules,_authentication = authentication,_injectedProxyProviders = injectedProxyProviders,_injectedRuleProviders = injectedRuleProviders; @override final String profilesPath; @@ -8961,6 +8963,8 @@ class _MakeRealProfileState implements MakeRealProfileState { @override final PatchClashConfig realPatchConfig; @override@JsonKey() final ProfileOverrides overrides; +/// Set for a subscription, whose own DNS values an emptied entry replaces. +@override@JsonKey() final bool keepEmptyDnsOverrides; @override final bool appendSystemDns; final List _proxyGroups; @override List get proxyGroups { @@ -9018,18 +9022,18 @@ _$MakeRealProfileStateCopyWith<_MakeRealProfileState> get copyWith => __$MakeRea @override bool operator ==(Object other) { - return identical(this, other) || (other.runtimeType == runtimeType&&other is _MakeRealProfileState&&(identical(other.profilesPath, profilesPath) || other.profilesPath == profilesPath)&&(identical(other.profileId, profileId) || other.profileId == profileId)&&const DeepCollectionEquality().equals(other.rawConfig, _rawConfig)&&(identical(other.realPatchConfig, realPatchConfig) || other.realPatchConfig == realPatchConfig)&&(identical(other.overrides, overrides) || other.overrides == overrides)&&(identical(other.appendSystemDns, appendSystemDns) || other.appendSystemDns == appendSystemDns)&&const DeepCollectionEquality().equals(other.proxyGroups, _proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _rules)&&const DeepCollectionEquality().equals(other.addedRules, _addedRules)&&(identical(other.defaultUA, defaultUA) || other.defaultUA == defaultUA)&&const DeepCollectionEquality().equals(other.authentication, _authentication)&&const DeepCollectionEquality().equals(other.injectedProxyProviders, _injectedProxyProviders)&&const DeepCollectionEquality().equals(other.injectedRuleProviders, _injectedRuleProviders)&&(identical(other.matchTarget, matchTarget) || other.matchTarget == matchTarget)&&(identical(other.safeMode, safeMode) || other.safeMode == safeMode)); + return identical(this, other) || (other.runtimeType == runtimeType&&other is _MakeRealProfileState&&(identical(other.profilesPath, profilesPath) || other.profilesPath == profilesPath)&&(identical(other.profileId, profileId) || other.profileId == profileId)&&const DeepCollectionEquality().equals(other.rawConfig, _rawConfig)&&(identical(other.realPatchConfig, realPatchConfig) || other.realPatchConfig == realPatchConfig)&&(identical(other.overrides, overrides) || other.overrides == overrides)&&(identical(other.keepEmptyDnsOverrides, keepEmptyDnsOverrides) || other.keepEmptyDnsOverrides == keepEmptyDnsOverrides)&&(identical(other.appendSystemDns, appendSystemDns) || other.appendSystemDns == appendSystemDns)&&const DeepCollectionEquality().equals(other.proxyGroups, _proxyGroups)&&const DeepCollectionEquality().equals(other.rules, _rules)&&const DeepCollectionEquality().equals(other.addedRules, _addedRules)&&(identical(other.defaultUA, defaultUA) || other.defaultUA == defaultUA)&&const DeepCollectionEquality().equals(other.authentication, _authentication)&&const DeepCollectionEquality().equals(other.injectedProxyProviders, _injectedProxyProviders)&&const DeepCollectionEquality().equals(other.injectedRuleProviders, _injectedRuleProviders)&&(identical(other.matchTarget, matchTarget) || other.matchTarget == matchTarget)&&(identical(other.safeMode, safeMode) || other.safeMode == safeMode)); } @override int get hashCode { - return Object.hash(runtimeType,profilesPath,profileId,const DeepCollectionEquality().hash(_rawConfig),realPatchConfig,overrides,appendSystemDns,const DeepCollectionEquality().hash(_proxyGroups),const DeepCollectionEquality().hash(_rules),const DeepCollectionEquality().hash(_addedRules),defaultUA,const DeepCollectionEquality().hash(_authentication),const DeepCollectionEquality().hash(_injectedProxyProviders),const DeepCollectionEquality().hash(_injectedRuleProviders),matchTarget,safeMode); + return Object.hash(runtimeType,profilesPath,profileId,const DeepCollectionEquality().hash(_rawConfig),realPatchConfig,overrides,keepEmptyDnsOverrides,appendSystemDns,const DeepCollectionEquality().hash(_proxyGroups),const DeepCollectionEquality().hash(_rules),const DeepCollectionEquality().hash(_addedRules),defaultUA,const DeepCollectionEquality().hash(_authentication),const DeepCollectionEquality().hash(_injectedProxyProviders),const DeepCollectionEquality().hash(_injectedRuleProviders),matchTarget,safeMode); } @override String toString() { - return 'MakeRealProfileState(profilesPath: $profilesPath, profileId: $profileId, rawConfig: $rawConfig, realPatchConfig: $realPatchConfig, overrides: $overrides, appendSystemDns: $appendSystemDns, proxyGroups: $proxyGroups, rules: $rules, addedRules: $addedRules, defaultUA: $defaultUA, authentication: $authentication, injectedProxyProviders: $injectedProxyProviders, injectedRuleProviders: $injectedRuleProviders, matchTarget: $matchTarget, safeMode: $safeMode)'; + return 'MakeRealProfileState(profilesPath: $profilesPath, profileId: $profileId, rawConfig: $rawConfig, realPatchConfig: $realPatchConfig, overrides: $overrides, keepEmptyDnsOverrides: $keepEmptyDnsOverrides, appendSystemDns: $appendSystemDns, proxyGroups: $proxyGroups, rules: $rules, addedRules: $addedRules, defaultUA: $defaultUA, authentication: $authentication, injectedProxyProviders: $injectedProxyProviders, injectedRuleProviders: $injectedRuleProviders, matchTarget: $matchTarget, safeMode: $safeMode)'; } @@ -9040,7 +9044,7 @@ abstract mixin class _$MakeRealProfileStateCopyWith<$Res> implements $MakeRealPr factory _$MakeRealProfileStateCopyWith(_MakeRealProfileState value, $Res Function(_MakeRealProfileState) _then) = __$MakeRealProfileStateCopyWithImpl; @override @useResult $Res call({ - String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode + String profilesPath, int profileId, Map rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List proxyGroups, List rules, List addedRules, String defaultUA, List authentication, Map injectedProxyProviders, Map injectedRuleProviders, String? matchTarget, bool safeMode }); @@ -9057,14 +9061,15 @@ class __$MakeRealProfileStateCopyWithImpl<$Res> /// Create a copy of MakeRealProfileState /// with the given fields replaced by the non-null parameter values. -@override @pragma('vm:prefer-inline') $Res call({Object? profilesPath = null,Object? profileId = null,Object? rawConfig = null,Object? realPatchConfig = null,Object? overrides = null,Object? appendSystemDns = null,Object? proxyGroups = null,Object? rules = null,Object? addedRules = null,Object? defaultUA = null,Object? authentication = null,Object? injectedProxyProviders = null,Object? injectedRuleProviders = null,Object? matchTarget = freezed,Object? safeMode = null,}) { +@override @pragma('vm:prefer-inline') $Res call({Object? profilesPath = null,Object? profileId = null,Object? rawConfig = null,Object? realPatchConfig = null,Object? overrides = null,Object? keepEmptyDnsOverrides = null,Object? appendSystemDns = null,Object? proxyGroups = null,Object? rules = null,Object? addedRules = null,Object? defaultUA = null,Object? authentication = null,Object? injectedProxyProviders = null,Object? injectedRuleProviders = null,Object? matchTarget = freezed,Object? safeMode = null,}) { return _then(_MakeRealProfileState( profilesPath: null == profilesPath ? _self.profilesPath : profilesPath // ignore: cast_nullable_to_non_nullable as String,profileId: null == profileId ? _self.profileId : profileId // ignore: cast_nullable_to_non_nullable as int,rawConfig: null == rawConfig ? _self._rawConfig : rawConfig // ignore: cast_nullable_to_non_nullable as Map,realPatchConfig: null == realPatchConfig ? _self.realPatchConfig : realPatchConfig // ignore: cast_nullable_to_non_nullable as PatchClashConfig,overrides: null == overrides ? _self.overrides : overrides // ignore: cast_nullable_to_non_nullable -as ProfileOverrides,appendSystemDns: null == appendSystemDns ? _self.appendSystemDns : appendSystemDns // ignore: cast_nullable_to_non_nullable +as ProfileOverrides,keepEmptyDnsOverrides: null == keepEmptyDnsOverrides ? _self.keepEmptyDnsOverrides : keepEmptyDnsOverrides // ignore: cast_nullable_to_non_nullable +as bool,appendSystemDns: null == appendSystemDns ? _self.appendSystemDns : appendSystemDns // ignore: cast_nullable_to_non_nullable as bool,proxyGroups: null == proxyGroups ? _self._proxyGroups : proxyGroups // ignore: cast_nullable_to_non_nullable as List,rules: null == rules ? _self._rules : rules // ignore: cast_nullable_to_non_nullable as List,addedRules: null == addedRules ? _self._addedRules : addedRules // ignore: cast_nullable_to_non_nullable diff --git a/lib/models/state.dart b/lib/models/state.dart index 5e9d1fd2..dc86a53e 100644 --- a/lib/models/state.dart +++ b/lib/models/state.dart @@ -334,6 +334,9 @@ abstract class MakeRealProfileState with _$MakeRealProfileState { required Map rawConfig, required PatchClashConfig realPatchConfig, @Default(ProfileOverrides()) ProfileOverrides overrides, + + /// Set for a subscription, whose own DNS values an emptied entry replaces. + @Default(false) bool keepEmptyDnsOverrides, required bool appendSystemDns, required List proxyGroups, required List rules, diff --git a/lib/pages/error.dart b/lib/pages/error.dart index b13be991..44c8f263 100644 --- a/lib/pages/error.dart +++ b/lib/pages/error.dart @@ -1,126 +1,346 @@ -import 'package:fl_clash/common/color.dart'; -import 'package:fl_clash/common/shape.dart'; +import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/icons/icons.dart'; +import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/widgets/navigation_dock.dart'; +import 'package:flutter/foundation.dart'; import 'package:material_ui/material_ui.dart'; import 'package:flutter/services.dart'; -class InitErrorScreen extends StatelessWidget { +class InitErrorApp extends StatelessWidget { final Object error; final StackTrace stack; + final AsyncCallback onClearData; + final AsyncCallback onExit; - const InitErrorScreen({super.key, required this.error, required this.stack}); + const InitErrorApp({ + super.key, + required this.error, + required this.stack, + required this.onClearData, + required this.onExit, + }); + + ThemeData _theme(Brightness brightness) { + return ThemeData( + useMaterial3: true, + colorScheme: ColorScheme.fromSeed( + seedColor: const Color(defaultPrimaryColor), + brightness: brightness, + ), + ).withAppShapes; + } @override Widget build(BuildContext context) { - final colorScheme = Theme.of(context).colorScheme; - - return Scaffold( - appBar: AppBar( - title: const Text('Init Failed'), - backgroundColor: colorScheme.error, - foregroundColor: colorScheme.onError, - elevation: 0, - ), - body: SafeArea( - child: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - GlyphIcon( - AppGlyphs.warning, - color: colorScheme.error, - size: 32, - ), - const SizedBox(width: 12), - const Expanded( - child: Text( - 'The application encountered a critical error during startup and cannot continue.', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 16, - ), - ), - ), - ], - ), - const SizedBox(height: 24), - _buildSectionLabel('Error Details:'), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: ShapeDecoration( - color: colorScheme.errorContainer.opacity50, - shape: RoundedSuperellipseBorder( - borderRadius: AppRadius.sm, - side: BorderSide(color: colorScheme.error.opacity50), - ), - ), - child: SelectableText( - error.toString(), - style: TextStyle( - color: colorScheme.onErrorContainer, - fontWeight: FontWeight.w600, - ), - ), - ), - const SizedBox(height: 24), - _buildSectionLabel('Stack Trace:'), - Container( - width: double.infinity, - padding: const EdgeInsets.all(12), - decoration: ShapeDecoration( - color: Theme.of(context).brightness == Brightness.dark - ? Colors.grey[900] - : Colors.grey[200], - shape: RoundedSuperellipseBorder( - borderRadius: AppRadius.sm, - side: BorderSide(color: Colors.grey.opacity50), - ), - ), - child: SelectableText( - stack.toString(), - style: const TextStyle(fontFamily: 'monospace', fontSize: 12), - ), - ), - const SizedBox(height: 80), - ], - ), - ), - ), - floatingActionButton: ElasticButton( - child: FloatingActionButton.extended( - onPressed: () => _copyToClipboard(context), - label: const Text('Copy Details'), - icon: const GlyphIcon(AppGlyphs.copy, fill: 1), - backgroundColor: colorScheme.error, - foregroundColor: colorScheme.onError, - ), - ), - ); - } - - Widget _buildSectionLabel(String text) { - return Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - text, - style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), - ), - ); - } - - void _copyToClipboard(BuildContext context) { - final text = '=== ERROR ===\n$error\n\n=== STACK TRACE ===\n$stack'; - Clipboard.setData(ClipboardData(text: text)); - - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Error details copied to clipboard'), - duration: Duration(seconds: 2), + return MaterialApp( + debugShowCheckedModeBanner: false, + title: appName, + theme: _theme(Brightness.light), + darkTheme: _theme(Brightness.dark), + localizationsDelegates: const [ + AppLocalizations.delegate, + ...GlobalMaterialLocalizations.delegates, + ], + supportedLocales: AppLocalizations.delegate.supportedLocales, + home: InitErrorScreen( + error: error, + stack: stack, + onClearData: onClearData, + onExit: onExit, + ), + ); + } +} + +class InitErrorScreen extends StatefulWidget { + final Object error; + final StackTrace stack; + final AsyncCallback onClearData; + final AsyncCallback onExit; + + const InitErrorScreen({ + super.key, + required this.error, + required this.stack, + required this.onClearData, + required this.onExit, + }); + + @override + State createState() => _InitErrorScreenState(); +} + +class _InitErrorScreenState extends State { + static const _maxContentWidth = 720.0; + + bool _isExiting = false; + + void _showSnackBar(String message) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(message), behavior: SnackBarBehavior.floating), + ); + } + + Future _copyDetails() async { + final text = + '=== ERROR ===\n${widget.error}\n\n' + '=== STACK TRACE ===\n${widget.stack}'; + await Clipboard.setData(ClipboardData(text: text)); + if (!mounted) { + return; + } + _showSnackBar(context.appLocalizations.copySuccess); + } + + Future _confirmClearData() async { + final appLocalizations = context.appLocalizations; + final res = await dialogs.showCommonDialog( + context: context, + child: Builder( + builder: (context) { + return AlertDialog( + title: Text(appLocalizations.clearData), + content: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 300), + child: Text(appLocalizations.clearDataAndExitTip), + ), + actionsPadding: const EdgeInsets.fromLTRB(12, 0, 12, 16), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(appLocalizations.cancel), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(appLocalizations.confirm), + ), + ], + ); + }, + ), + ); + return res == true; + } + + Future _clearData() async { + if (!await _confirmClearData() || !mounted) { + return; + } + final appLocalizations = context.appLocalizations; + setState(() { + _isExiting = true; + }); + try { + await widget.onClearData(); + } catch (error) { + if (!mounted) { + return; + } + setState(() { + _isExiting = false; + }); + _showSnackBar(appLocalizations.clearDataFailed('$error')); + return; + } + await widget.onExit(); + } + + Future _exit() async { + setState(() { + _isExiting = true; + }); + await widget.onExit(); + } + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + final colorScheme = context.colorScheme; + final textTheme = context.textTheme; + return Scaffold( + body: SafeArea( + bottom: false, + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(16, 32, 16, 24), + child: Center( + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: _maxContentWidth, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + DecoratedBox( + decoration: ShapeDecoration( + color: colorScheme.errorContainer, + shape: AppShape.full, + ), + child: SizedBox.square( + dimension: 56, + child: Center( + child: GlyphIcon( + AppGlyphs.warning, + size: 28, + color: colorScheme.onErrorContainer, + ), + ), + ), + ), + const SizedBox(height: 20), + Text( + appLocalizations.initFailed, + style: textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + Text( + appLocalizations.initFailedTip, + style: textTheme.bodyMedium?.copyWith( + color: colorScheme.onSurfaceVariant, + height: 1.6, + ), + ), + const SizedBox(height: 28), + _ErrorSection( + label: appLocalizations.errorDetails, + text: widget.error.toString(), + backgroundColor: colorScheme.errorContainer, + style: textTheme.bodyMedium?.copyWith( + color: colorScheme.onErrorContainer, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 20), + _ErrorSection( + label: appLocalizations.stackTrace, + text: widget.stack.toString(), + backgroundColor: colorScheme.surfaceContainerHighest, + style: textTheme.bodySmall?.copyWith( + color: colorScheme.onSurfaceVariant, + fontFamily: 'monospace', + height: 1.5, + ), + ), + ], + ), + ), + ), + ), + ), + _ErrorActionBar( + maxWidth: _maxContentWidth, + children: [ + ElasticButton( + child: TextButton.icon( + onPressed: _isExiting ? null : _clearData, + style: TextButton.styleFrom( + foregroundColor: colorScheme.error, + ), + icon: const GlyphIcon(AppGlyphs.broom, size: 18), + label: Text(appLocalizations.clearData), + ), + ), + ElasticButton( + child: OutlinedButton.icon( + onPressed: _copyDetails, + icon: const GlyphIcon(AppGlyphs.copy, size: 18), + label: Text(appLocalizations.copy), + ), + ), + ElasticButton( + child: FilledButton( + onPressed: _isExiting ? null : _exit, + child: Text(appLocalizations.exit), + ), + ), + ], + ), + ], + ), + ), + ); + } +} + +class _ErrorSection extends StatelessWidget { + final String label; + final String text; + final Color backgroundColor; + final TextStyle? style; + + const _ErrorSection({ + required this.label, + required this.text, + required this.backgroundColor, + required this.style, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text( + label, + style: context.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 8), + DecoratedBox( + decoration: ShapeDecoration( + color: backgroundColor, + shape: AppShape.xl, + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: SelectableText(text, style: style), + ), + ), + ], + ); + } +} + +class _ErrorActionBar extends StatelessWidget { + final double maxWidth; + final List children; + + const _ErrorActionBar({required this.maxWidth, required this.children}); + + @override + Widget build(BuildContext context) { + final colorScheme = context.colorScheme; + return DecoratedBox( + decoration: BoxDecoration( + color: colorScheme.surface, + border: Border(top: BorderSide(color: colorScheme.outlineVariant)), + ), + child: SafeArea( + top: false, + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: maxWidth), + child: Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 12), + child: OverflowBar( + alignment: MainAxisAlignment.end, + spacing: 8, + overflowSpacing: 8, + overflowAlignment: OverflowBarAlignment.end, + children: children, + ), + ), + ), + ), ), ); } diff --git a/lib/providers/actions/setup.dart b/lib/providers/actions/setup.dart index aab6a892..7df987ff 100644 --- a/lib/providers/actions/setup.dart +++ b/lib/providers/actions/setup.dart @@ -453,6 +453,7 @@ class SetupAction extends _$SetupAction { rawConfig: rawConfig, realPatchConfig: realPatchConfig, overrides: setupState.overrides, + keepEmptyDnsOverrides: setupState.profileType != ProfileType.custom, appendSystemDns: appendSystemDns, addedRules: setupState.addedRules, defaultUA: defaultUA, @@ -579,6 +580,14 @@ class SetupAction extends _$SetupAction { authorizationNotifier.value = TunAuthorizationState.authorized; return true; case AuthorizeCode.error: + authorizationNotifier.value = TunAuthorizationState.none; + ref + .read(patchClashConfigProvider.notifier) + .update((state) => state.copyWith.tun(enable: false)); + dialogs.showNotifier( + currentAppLocalizations.tunAuthorizationFailed, + level: MessageLevel.error, + ); return true; } } @@ -610,7 +619,7 @@ class SetupAction extends _$SetupAction { // A refresh failure is surfaced by safeRun; setup keeps the old profile. final nextProfile = await globalState.safeRun( () => profile?.checkAndUpdateAndCopy( - validate: (path) => _core.validateProfile(path), + validate: (path, key) => _core.validateProfile(path, key), ), ); if (nextProfile != null) { @@ -646,7 +655,10 @@ class SetupAction extends _$SetupAction { _appliedInputs = inputs; return _SetupTaskResult.completed; } + // Go answers a config it rejects by running its built-in default, which + // has no proxies, so until this push succeeds no applied config is known. _appliedInputs = null; + globalState.lastConfigMd5 = null; if (system.isAndroid) { await ref.read(vpnOptionsProvider.future); await preferences.saveShareState(ref.read(sharedStateProvider)); @@ -668,6 +680,7 @@ class SetupAction extends _$SetupAction { preloadInvoke: preloadInvoke, ); if (message.isNotEmpty) { + await globalState.safeRun(() => onUpdated?.call()); throw MessageException(message); } } catch (e, s) { diff --git a/lib/providers/generated/action.g.dart b/lib/providers/generated/action.g.dart index cdb869fb..9dd52a72 100644 --- a/lib/providers/generated/action.g.dart +++ b/lib/providers/generated/action.g.dart @@ -91,7 +91,7 @@ final class SetupActionProvider extends $NotifierProvider { } } -String _$setupActionHash() => r'b9b400d5128bcf9e045b397d681109ae9cca3a48'; +String _$setupActionHash() => r'bc66d570ecc9a96fa751970a9f8b5584d9a843ee'; abstract class _$SetupAction extends $Notifier { void build(); @@ -450,7 +450,7 @@ final class ProfilesActionProvider } } -String _$profilesActionHash() => r'4f716e75b986cf6e2873b7fb62acedbc87f94199'; +String _$profilesActionHash() => r'1ed9d382c267699f9705428fcc5f13378a7c825f'; abstract class _$ProfilesAction extends $Notifier { void build(); diff --git a/lib/providers/state/custom.dart b/lib/providers/state/custom.dart index 951447f4..a68c2714 100644 --- a/lib/providers/state/custom.dart +++ b/lib/providers/state/custom.dart @@ -231,6 +231,41 @@ List importedCustomProxies( typedef ShareLinkLine = ({int line, String link, CustomProxy? kept}); +/// [next], read from an edited link, plus what [previous] holds that its own +/// link, read back as [linked], cannot carry, which the user could not remove. +Map withUnlinkedFields( + Map next, { + required Map previous, + required Map? linked, +}) { + if (linked == null || next['type'] != previous['type']) { + return next; + } + return _withUnlinked(next, previous, linked); +} + +Map _withUnlinked( + Map next, + Map previous, + Map linked, +) { + final result = {...next}; + for (final MapEntry(:key, :value) in previous.entries) { + final carried = linked[key]; + final edited = result[key]; + if (!linked.containsKey(key)) { + result.putIfAbsent(key.toString(), () => value); + } else if (value is Map && carried is Map && edited is Map) { + result[key.toString()] = _withUnlinked( + Map.from(edited), + value, + carried, + ); + } + } + return result; +} + /// A line that repeats a proxy's own link keeps that proxy as it is, since a /// link carries fewer fields than the mapping. class ShareLinkEdit { @@ -273,11 +308,22 @@ class ShareLinkEdit { List get pending => lines.where((line) => line.kept == null).toList(); + Map get droppedLinks { + final keptIds = {for (final line in lines) ?line.kept?.id}; + return { + for (final (index, proxy) in previous.indexed) + if (links[index].isNotEmpty && !keptIds.contains(proxy.id)) + proxy.id: links[index], + }; + } + /// A proxy named after one the edit dropped takes over its id, so each - /// profile's dialer for it stays. + /// profile's dialer for it stays, and the options it listed without a + /// value, which no link carries. List apply( Map>> decoded, { required Set groupNames, + Map> linked = const {}, }) { final keptIds = {for (final line in lines) ?line.kept?.id}; final dropped = { @@ -303,11 +349,26 @@ class ShareLinkEdit { final base = linkProxyName(definition); final replaced = taken.contains(base) ? null : dropped.remove(base); final name = freeProxyName(base, taken); + final named = {...definition, 'name': name}; + final proxy = CustomProxy.fromDefinition( + replaced == null + ? named + : withUnlinkedFields( + named, + previous: replaced.definition, + linked: linked[replaced.id], + ), + id: replaced?.id, + ); edited.add( - CustomProxy.fromDefinition({ - ...definition, - 'name': name, - }, id: replaced?.id), + replaced == null + ? proxy + : proxy.copyWith( + emptyOptions: { + for (final field in replaced.emptyOptions) + if (replaced.valueOf(field) == null) field, + }, + ), ); } } diff --git a/lib/providers/state/custom_import.dart b/lib/providers/state/custom_import.dart index 83904b81..d0d86320 100644 --- a/lib/providers/state/custom_import.dart +++ b/lib/providers/state/custom_import.dart @@ -411,33 +411,27 @@ ProxyGroup _importGroup( required Map names, required Map use, }) { - final json = _stringified( - { - // A custom profile has no include-all-providers, so include-all keeps - // only its proxies. - for (final MapEntry(:key, :value) in group.entries) - if (key != 'include-all' && key != 'include-all-providers') key: value, - if (group['include-all'] == true) 'include-all-proxies': true, - 'id': 0, - 'name': name, - if (group.containsKey('proxies')) - 'proxies': _renamedList(group['proxies'], names), - if (group.containsKey('use')) 'use': _renamedList(group['use'], use), - if (group['default-selected'] case final Object selected) - 'default-selected': names[selected.toString()] ?? selected.toString(), - }, - const [ - 'url', - 'filter', - 'exclude-filter', - 'exclude-type', - 'expected-status', - 'hash-key', - 'empty-fallback', - 'icon', - ], + final json = _stringified({ + // A custom profile has no include-all-providers, so include-all keeps + // only its proxies. + for (final MapEntry(:key, :value) in group.entries) + if (key != 'include-all' && key != 'include-all-providers') key: value, + if (group['include-all'] == true) 'include-all-proxies': true, + 'id': 0, + 'name': name, + if (group.containsKey('proxies')) + 'proxies': _renamedList(group['proxies'], names), + if (group.containsKey('use')) 'use': _renamedList(group['use'], use), + if (group.containsKey('use-position')) + 'use-position': usePositionOf(group['use-position'], use), + if (group['default-selected'] case final Object selected) + 'default-selected': names[selected.toString()] ?? selected.toString(), + }, proxyGroupTextKeys); + final imported = _decodeEntry( + _nameOf(group), + () => ProxyGroup.fromJson(json), ); - return _decodeEntry(_nameOf(group), () => ProxyGroup.fromJson(json)); + return imported.copyWith(usePosition: imported.effectiveUsePosition); } Rule _importRule( diff --git a/lib/views/about.dart b/lib/views/about.dart index 6146e842..b386038a 100644 --- a/lib/views/about.dart +++ b/lib/views/about.dart @@ -29,12 +29,15 @@ class AboutView extends ConsumerWidget { Future _checkUpdate(BuildContext context, WidgetRef ref) async { if (ref.read(loadingProvider(LoadingTag.checkUpdate))) return; final commonAction = ref.read(commonActionProvider.notifier); - final data = await globalState.loadingRun?>( - request.checkForUpdate, + final result = await globalState.loadingRun( + () async => (release: await request.checkForUpdate()), title: context.appLocalizations.checkUpdate, tag: LoadingTag.checkUpdate, ); - unawaited(commonAction.checkUpdateResultHandle(data: data, isUser: true)); + if (result == null) return; + unawaited( + commonAction.checkUpdateResultHandle(data: result.release, isUser: true), + ); } Widget _buildLinkItem({ diff --git a/lib/views/access.dart b/lib/views/access.dart index cc557f35..77eeb7cb 100644 --- a/lib/views/access.dart +++ b/lib/views/access.dart @@ -19,7 +19,8 @@ class AccessView extends ConsumerStatefulWidget { ConsumerState createState() => _AccessViewState(); } -class _AccessViewState extends ConsumerState { +class _AccessViewState extends ConsumerState + with RouteSettledMixin { late ScrollController _controller; List? _pinedList; bool _isInit = false; @@ -30,8 +31,7 @@ class _AccessViewState extends ConsumerState { @override void initState() { super.initState(); - _controller = ScrollController(); - _completer.complete(_loadPackages()); + _controller = sheetScrollController(context); final accessControl = ref .read(vpnSettingProvider.select((state) => state.accessControlProps)) .copyWith(); @@ -49,6 +49,9 @@ class _AccessViewState extends ConsumerState { ); } + @override + void didSettleRoute() => _completer.complete(_loadPackages()); + Future _loadPackages() async { final action = ref.read(systemActionProvider.notifier); final packages = await action.getPackages(); @@ -160,7 +163,6 @@ class _AccessViewState extends ConsumerState { Future _handleToSetting() async { await showSheet( context: context, - props: const SheetProps(isScrollControlled: true), builder: (context) { final appLocalizations = context.appLocalizations; return CommonScaffold( @@ -179,9 +181,9 @@ class _AccessViewState extends ConsumerState { }); } - void _handleToggle() { + void _handleEnable(bool enable) { ref.read(accessControlStateProvider.notifier).update((state) { - return state.copyWith(enable: !state.enable); + return state.copyWith(enable: enable); }); } @@ -264,8 +266,19 @@ class _AccessViewState extends ConsumerState { return [ CommonPopupMenuItem( glyph: AppGlyphs.toggle, - label: enable ? appLocalizations.turnOff : appLocalizations.turnOn, - onPressed: _handleToggle, + label: appLocalizations.accessControl, + subItems: [ + CommonPopupMenuItem( + label: appLocalizations.turnOn, + checked: enable, + onPressed: () => _handleEnable(true), + ), + CommonPopupMenuItem( + label: appLocalizations.turnOff, + checked: !enable, + onPressed: () => _handleEnable(false), + ), + ], ), CommonPopupMenuItem( glyph: AppGlyphs.sliders, @@ -319,6 +332,8 @@ class _AccessViewState extends ConsumerState { controller: _controller, child: ListView.builder( controller: _controller, + padding: EdgeInsets.only(bottom: BottomInsetScope.of(context)), + scrollCacheExtent: arrivalScrollCacheExtent(routeSettled), itemCount: packages.length, itemExtent: 72, itemBuilder: (_, index) { @@ -372,7 +387,7 @@ class _AccessViewState extends ConsumerState { CommonMinFilledButtonTheme( child: ElasticButton( child: FilledButton.tonal( - onPressed: _handleToggle, + onPressed: () => _handleEnable(true), child: Text(appLocalizations.turnOn), ), ), diff --git a/lib/views/config/advanced.dart b/lib/views/config/advanced.dart index 1afdeff3..2c1c5128 100644 --- a/lib/views/config/advanced.dart +++ b/lib/views/config/advanced.dart @@ -1,10 +1,7 @@ import 'package:fl_clash/common/common.dart'; -import 'package:fl_clash/enum/enum.dart'; import 'package:fl_clash/icons/icons.dart'; import 'package:fl_clash/views/config/dns.dart'; import 'package:fl_clash/views/config/network.dart'; -import 'package:fl_clash/views/config/ntp.dart'; -import 'package:fl_clash/views/config/providers.dart'; import 'package:fl_clash/views/config/scripts.dart'; import 'package:fl_clash/widgets/list.dart'; import 'package:fl_clash/widgets/scaffold.dart'; @@ -18,7 +15,7 @@ class AdvancedConfigView extends StatelessWidget { @override Widget build(BuildContext context) { final appLocalizations = context.appLocalizations; - final List items = [ + final items = [ ListItem.open( title: Text(appLocalizations.network), leading: const GlyphIcon(AppGlyphs.key), @@ -32,28 +29,11 @@ class AdvancedConfigView extends StatelessWidget { leading: const GlyphIcon(AppGlyphs.dns), widget: const DnsView(), ), - ListItem.open( - title: const Text('NTP'), - leading: const GlyphIcon(AppGlyphs.clock), - widget: const NtpView(), - ), ListItem.open( title: Text(appLocalizations.addedRules), leading: const GlyphIcon(AppGlyphs.rules), widget: const AddedRulesView(), ), - if (feature.customProviders) ...[ - ListItem.open( - title: Text(appLocalizations.proxyProviders), - leading: const GlyphIcon(AppGlyphs.proxies), - widget: const ClashProvidersView(kind: ProviderKind.proxy), - ), - ListItem.open( - title: Text(appLocalizations.ruleProviders), - leading: const GlyphIcon(AppGlyphs.resources), - widget: const ClashProvidersView(kind: ProviderKind.rule), - ), - ], ListItem.open( title: Text(appLocalizations.script), leading: const GlyphIcon(AppGlyphs.code), diff --git a/lib/views/config/custom_resources.dart b/lib/views/config/custom_resources.dart new file mode 100644 index 00000000..a19a8491 --- /dev/null +++ b/lib/views/config/custom_resources.dart @@ -0,0 +1,49 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/icons/icons.dart'; +import 'package:fl_clash/views/config/filters.dart'; +import 'package:fl_clash/views/config/icon_sets.dart'; +import 'package:fl_clash/views/config/providers.dart'; +import 'package:fl_clash/views/profiles/custom/custom_proxies.dart'; +import 'package:fl_clash/widgets/list.dart'; +import 'package:fl_clash/widgets/scaffold.dart'; +import 'package:material_ui/material_ui.dart'; + +class CustomResourcesView extends StatelessWidget { + const CustomResourcesView({super.key}); + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + final items = [ + ListItem.open( + title: Text(appLocalizations.proxies), + leading: const GlyphIcon(AppGlyphs.proxies), + widget: const CustomProxiesView(), + ), + ListItem.open( + title: Text(appLocalizations.ruleProviders), + leading: const GlyphIcon(AppGlyphs.resources), + widget: const ClashProvidersView(), + ), + ListItem.open( + title: Text(appLocalizations.filters), + leading: const GlyphIcon(AppGlyphs.filter), + widget: const FiltersView(), + ), + ListItem.open( + title: Text(appLocalizations.iconSets), + leading: const GlyphIcon(AppGlyphs.photos), + widget: const IconSetsView(), + ), + ]; + return BaseScaffold( + title: appLocalizations.customProfileResources, + body: ListView( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ).copyWith(top: context.contentTopPadding, bottom: 16), + children: [generateSectionV3(items: items)], + ), + ); + } +} diff --git a/lib/views/config/dns.dart b/lib/views/config/dns.dart index c3574208..e5782f4e 100644 --- a/lib/views/config/dns.dart +++ b/lib/views/config/dns.dart @@ -1,32 +1,15 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/features/overwrite/overwrite.dart'; -import 'package:fl_clash/icons/icons.dart'; import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/pages/editor.dart'; -import 'package:fl_clash/providers/config.dart'; +import 'package:fl_clash/providers/state.dart'; +import 'package:fl_clash/views/config/override.dart'; import 'package:fl_clash/widgets/widgets.dart'; +import 'package:flutter/foundation.dart' show mapEquals; import 'package:material_ui/material_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -typedef _DnsUpdate = - PatchClashConfig Function(PatchClashConfig state, T value); - -final _dnsOverrideKeysSelector = patchClashConfigProvider.select( - (state) => state.dnsOverrideKeys, -); - -ProviderListenable _dnsSelector(T Function(Dns dns) select) { - return patchClashConfigProvider.select((state) => select(state.dns)); -} - -ConfigWriter _dnsWriter(_DnsUpdate update) { - return (ref, value) => ref - .read(patchClashConfigProvider.notifier) - .update((state) => update(state, value)); -} - extension on DnsOverrideKey { String label(AppLocalizations l) => switch (this) { DnsOverrideKey.enable => l.status, @@ -71,455 +54,343 @@ extension on DnsOverrideKey { DnsOverrideKey.fakeIpFilterMode => (l) => l.fakeipFilterModeDesc, _ => null, }; + + String section(AppLocalizations l) => switch (this) { + DnsOverrideKey.enable || + DnsOverrideKey.listen || + DnsOverrideKey.listenRoutingMark || + DnsOverrideKey.useHosts || + DnsOverrideKey.useSystemHosts || + DnsOverrideKey.ipv6 || + DnsOverrideKey.ipv6Timeout || + DnsOverrideKey.respectRules || + DnsOverrideKey.preferH3 || + DnsOverrideKey.cacheAlgorithm || + DnsOverrideKey.cacheMaxSize || + DnsOverrideKey.enhancedMode => l.options, + DnsOverrideKey.fakeIpRange || + DnsOverrideKey.fakeIpRange6 || + DnsOverrideKey.fakeIpFilter || + DnsOverrideKey.fakeIpFilterMode || + DnsOverrideKey.fakeIpTtl => 'Fake-IP', + DnsOverrideKey.defaultNameserver || + DnsOverrideKey.nameserverPolicy || + DnsOverrideKey.nameserver || + DnsOverrideKey.fallback || + DnsOverrideKey.fallbackLazyQuery || + DnsOverrideKey.proxyServerNameserver || + DnsOverrideKey.proxyServerNameserverPolicy || + DnsOverrideKey.directNameserver || + DnsOverrideKey.directNameserverFollowPolicy => 'Nameserver', + DnsOverrideKey.fallbackFilterGeoip || + DnsOverrideKey.fallbackFilterGeoipCode || + DnsOverrideKey.fallbackFilterGeosite || + DnsOverrideKey.fallbackFilterIpcidr || + DnsOverrideKey.fallbackFilterDomain => l.fallbackFilter, + }; } // mihomo reports `hosts` in connection metadata but rejects it as an // enhanced-mode. const _enhancedModes = [DnsMode.normal, DnsMode.fakeIp, DnsMode.redirHost]; -List<(String, List)> _sectionsOf( - AppLocalizations l, - Iterable keys, -) { - return [ - (l.options, keys.where((key) => !key.isFallbackFilter).toList()), - (l.fallbackFilter, keys.where((key) => key.isFallbackFilter).toList()), - ]; -} +String _enhancedModeValue(DnsMode mode) => switch (mode) { + DnsMode.normal => 'normal', + DnsMode.fakeIp => 'fake-ip', + DnsMode.redirHost => 'redir-host', + DnsMode.hosts => 'hosts', +}; -class DnsView extends ConsumerWidget { - const DnsView({super.key}); +typedef _DnsState = OverrideState; - Future _handleAdd(BuildContext context, WidgetRef ref) async { - final appLocalizations = context.appLocalizations; - final added = ref.read(_dnsOverrideKeysSelector); - final remaining = DnsOverrideKey.values.where( - (key) => !added.contains(key), - ); - final key = await showSheet( - context: context, - props: const SheetProps(isScrollControlled: true), - builder: (context) => OverwriteSelectionSheet( - title: appLocalizations.addOverrideEntry, - sections: [ - for (final (label, keys) in _sectionsOf(appLocalizations, remaining)) - if (keys.isNotEmpty) - OverwriteSelectionSection( - label: label, - items: keys, - subtitleBuilder: (context, key) => - key.description?.call(context.appLocalizations), - ), - ], - labelBuilder: (key) => key.label(appLocalizations), - selectedOf: (_) => null, - onSelected: (key) => Navigator.of(context).pop(key), - ), - ); - if (key == null || !context.mounted) { - return; - } - ref - .read(patchClashConfigProvider.notifier) - .update( - (state) => - state.copyWith(dnsOverrideKeys: {...state.dnsOverrideKeys, key}), - ); - } +_DnsState _patchDns(PatchClashConfig config) => + (model: config.dns, keys: config.dnsOverrideKeys); - Future _handleQuickEdit(BuildContext context, WidgetRef ref) { - final config = ref.read(patchClashConfigProvider); - final raw = config.dns.overrideYaml(config.dnsOverrideKeys); - return BaseNavigator.push( - context, - EditorPage( - title: 'DNS', - content: raw, - schema: EditorSchema.dns, - readOnly: false, - onPop: (_, _, content) => _handleQuickEditPop(ref, content, raw), - ), - ); - } +PatchClashConfig _setPatchDns(PatchClashConfig config, _DnsState state) => + config.copyWith(dns: state.model, dnsOverrideKeys: state.keys); - Future _handleQuickEditPop( - WidgetRef ref, - String content, - String raw, - ) async { - if (content == raw) { - return true; - } - try { - final result = ref - .read(patchClashConfigProvider) - .dns - .applyOverrideYaml(content); - ref - .read(patchClashConfigProvider.notifier) - .update( - (state) => - state.copyWith(dns: result.dns, dnsOverrideKeys: result.keys), - ); - return true; - } catch (error) { - final res = await dialogs.showMessage( - message: TextSpan( - text: - '${compactError(error)}\n\n' - '${currentAppLocalizations.discardChanges}', +_DnsState _profileDns(ProfileOverrides overrides) => + (model: overrides.dns, keys: overrides.dnsOverrideKeys); + +ProfileOverrides _setProfileDns(ProfileOverrides overrides, _DnsState state) => + overrides.copyWith(dns: state.model, dnsOverrideKeys: state.keys); + +String _patchDnsDescription(AppLocalizations l) => l.dnsOverrideDesc; + +class DnsView extends OverrideView { + /// The keys written over the DNS of every subscription and file profile. + const DnsView({super.key}) + : super( + spec: const _DnsOverrideSpec( + PatchOverrideTarget( + get: _patchDns, + set: _setPatchDns, + description: _patchDnsDescription, + ), + keys: DnsOverrideKey.normalProfileKeys, ), ); - return res == true; - } + + DnsView.profile(int profileId, {super.key}) + : super( + spec: _ProfileDnsOverrideSpec( + ProfileOverrideTarget( + profileId, + get: _profileDns, + set: _setProfileDns, + issues: customProfileIssuesProvider( + profileId, + ).select((state) => state.dns), + ), + ), + ); +} + +class _DnsOverrideSpec extends OverrideSpec { + const _DnsOverrideSpec(this.target, {this.keys}); + + @override + final OverrideTarget target; + + final Set? keys; + + @override + String title(AppLocalizations l) => 'DNS'; + + @override + List get values => + keys == null ? DnsOverrideKey.values : [...?keys]; + + @override + NullStatusIllustration get illustration => NullStatusIllustration.dns; + + @override + Dns get defaults => defaultDns; + + @override + String sectionOf(DnsOverrideKey key, AppLocalizations l) => key.section(l); + + @override + String label(DnsOverrideKey key, AppLocalizations l) => key.label(l); + + @override + ConfigLabel? description(DnsOverrideKey key) => key.description; + + @override + OverrideField fieldOf(DnsOverrideKey key) => switch (key) { + DnsOverrideKey.enable => ToggleOverrideField( + (dns) => dns.enable, + (dns, value) => dns.copyWith(enable: value), + ), + DnsOverrideKey.listen => TextOverrideField( + (dns) => dns.listen, + (dns, value) => dns.copyWith(listen: value), + maxLength: TextInputLimits.dnsListen, + validator: validateListenAddress, + ), + DnsOverrideKey.listenRoutingMark => NumberOverrideField( + (dns) => dns.listenRoutingMark, + (dns, value) => dns.copyWith(listenRoutingMark: value), + ), + DnsOverrideKey.useHosts => ToggleOverrideField( + (dns) => dns.useHosts, + (dns, value) => dns.copyWith(useHosts: value), + ), + DnsOverrideKey.useSystemHosts => ToggleOverrideField( + (dns) => dns.useSystemHosts, + (dns, value) => dns.copyWith(useSystemHosts: value), + ), + DnsOverrideKey.ipv6 => ToggleOverrideField( + (dns) => dns.ipv6, + (dns, value) => dns.copyWith(ipv6: value), + ), + DnsOverrideKey.ipv6Timeout => NumberOverrideField( + (dns) => dns.ipv6Timeout, + (dns, value) => dns.copyWith(ipv6Timeout: value), + ), + DnsOverrideKey.respectRules => ToggleOverrideField( + (dns) => dns.respectRules, + (dns, value) => dns.copyWith(respectRules: value), + ), + DnsOverrideKey.preferH3 => ToggleOverrideField( + (dns) => dns.preferH3, + (dns, value) => dns.copyWith(preferH3: value), + ), + DnsOverrideKey.cacheAlgorithm => OptionsOverrideField( + DnsCacheAlgorithm.values, + (dns) => dns.cacheAlgorithm, + (dns, value) => dns.copyWith(cacheAlgorithm: value), + ), + DnsOverrideKey.cacheMaxSize => NumberOverrideField( + (dns) => dns.cacheMaxSize, + (dns, value) => dns.copyWith(cacheMaxSize: value), + ), + DnsOverrideKey.enhancedMode => OptionsOverrideField( + _enhancedModes, + (dns) => dns.enhancedMode, + (dns, value) => dns.copyWith(enhancedMode: value), + labelOf: _enhancedModeValue, + ), + DnsOverrideKey.fakeIpRange => TextOverrideField( + (dns) => dns.fakeIpRange, + (dns, value) => dns.copyWith(fakeIpRange: value), + maxLength: TextInputLimits.cidr, + validator: validateCidr, + ), + DnsOverrideKey.fakeIpRange6 => TextOverrideField( + (dns) => dns.fakeIpRange6, + (dns, value) => dns.copyWith(fakeIpRange6: value), + maxLength: TextInputLimits.cidr, + validator: validateCidr, + ), + DnsOverrideKey.fakeIpFilter => ListOverrideField( + (dns) => dns.fakeIpFilter, + (dns, value) => dns.copyWith(fakeIpFilter: value), + itemMaxLength: TextInputLimits.domain, + ), + DnsOverrideKey.fakeIpFilterMode => OptionsOverrideField( + FakeIpFilterMode.values, + (dns) => dns.fakeIpFilterMode, + (dns, value) => dns.copyWith(fakeIpFilterMode: value), + ), + DnsOverrideKey.fakeIpTtl => NumberOverrideField( + (dns) => dns.fakeIpTtl, + (dns, value) => dns.copyWith(fakeIpTtl: value), + ), + DnsOverrideKey.defaultNameserver => ListOverrideField( + (dns) => dns.defaultNameserver, + (dns, value) => dns.copyWith(defaultNameserver: value), + itemMaxLength: TextInputLimits.dnsServer, + ), + DnsOverrideKey.nameserverPolicy => _PolicyField( + (dns) => dns.nameserverPolicy, + (dns, value) => dns.copyWith(nameserverPolicy: value), + ), + DnsOverrideKey.nameserver => ListOverrideField( + (dns) => dns.nameserver, + (dns, value) => dns.copyWith(nameserver: value), + itemMaxLength: TextInputLimits.dnsServer, + ), + DnsOverrideKey.fallback => ListOverrideField( + (dns) => dns.fallback, + (dns, value) => dns.copyWith(fallback: value), + itemMaxLength: TextInputLimits.dnsServer, + ), + DnsOverrideKey.fallbackLazyQuery => ToggleOverrideField( + (dns) => dns.fallbackLazyQuery, + (dns, value) => dns.copyWith(fallbackLazyQuery: value), + ), + DnsOverrideKey.proxyServerNameserver => ListOverrideField( + (dns) => dns.proxyServerNameserver, + (dns, value) => dns.copyWith(proxyServerNameserver: value), + itemMaxLength: TextInputLimits.dnsServer, + ), + DnsOverrideKey.proxyServerNameserverPolicy => _PolicyField( + (dns) => dns.proxyServerNameserverPolicy, + (dns, value) => dns.copyWith(proxyServerNameserverPolicy: value), + ), + DnsOverrideKey.directNameserver => ListOverrideField( + (dns) => dns.directNameserver, + (dns, value) => dns.copyWith(directNameserver: value), + itemMaxLength: TextInputLimits.dnsServer, + ), + DnsOverrideKey.directNameserverFollowPolicy => ToggleOverrideField( + (dns) => dns.directNameserverFollowPolicy, + (dns, value) => dns.copyWith(directNameserverFollowPolicy: value), + ), + DnsOverrideKey.fallbackFilterGeoip => ToggleOverrideField( + (dns) => dns.fallbackFilter.geoip, + (dns, value) => dns.copyWith.fallbackFilter(geoip: value), + ), + DnsOverrideKey.fallbackFilterGeoipCode => TextOverrideField( + (dns) => dns.fallbackFilter.geoipCode, + (dns, value) => dns.copyWith.fallbackFilter(geoipCode: value), + maxLength: TextInputLimits.geoIpCode, + ), + DnsOverrideKey.fallbackFilterGeosite => ListOverrideField( + (dns) => dns.fallbackFilter.geosite, + (dns, value) => dns.copyWith.fallbackFilter(geosite: value), + itemMaxLength: TextInputLimits.geoSite, + ), + DnsOverrideKey.fallbackFilterIpcidr => ListOverrideField( + (dns) => dns.fallbackFilter.ipcidr, + (dns, value) => dns.copyWith.fallbackFilter(ipcidr: value), + itemMaxLength: TextInputLimits.cidr, + itemValidator: validateCidr, + ), + DnsOverrideKey.fallbackFilterDomain => ListOverrideField( + (dns) => dns.fallbackFilter.domain, + (dns, value) => dns.copyWith.fallbackFilter(domain: value), + itemMaxLength: TextInputLimits.domain, + ), + }; +} + +class _ProfileDnsOverrideSpec extends _DnsOverrideSpec + with OverrideQuickEdit { + const _ProfileDnsOverrideSpec(super.target); + + @override + String overrideYaml(Dns model, Set keys) => + model.overrideYaml(keys); + + @override + _DnsState applyOverrideYaml(Dns model, String content) { + final result = model.applyOverrideYaml(content); + return (model: result.dns, keys: result.keys); } @override - Widget build(BuildContext context, WidgetRef ref) { - final appLocalizations = context.appLocalizations; - final canAdd = ref.watch( - _dnsOverrideKeysSelector.select( - (keys) => keys.length < DnsOverrideKey.values.length, - ), - ); - return BaseScaffold( - title: 'DNS', - menuItems: [ - CommonPopupMenuItem( - glyph: AppGlyphs.add, - label: appLocalizations.add, - onPressed: canAdd ? () => _handleAdd(context, ref) : null, - ), - CommonPopupMenuItem( - glyph: AppGlyphs.compose, - label: appLocalizations.quickEdit, - onPressed: () => _handleQuickEdit(context, ref), - ), - ], - body: const _OverrideList(), - ); - } + EditorSchema get schema => EditorSchema.dns; } -class _OverrideList extends ConsumerWidget { - const _OverrideList(); +class _PolicyField extends ValueOverrideField> { + const _PolicyField(super.select, super.update); @override - Widget build(BuildContext context, WidgetRef ref) { - final appLocalizations = context.appLocalizations; - final keys = ref.watch(_dnsOverrideKeysSelector); - final entries = DnsOverrideKey.values.where(keys.contains); - return CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16).copyWith( - top: context.contentTopPadding, - bottom: entries.isEmpty ? 0 : 16, - ), - sliver: SliverList.list( - children: [ - generateSectionV3( - items: [ - ConfigToggleItem( - title: (l) => l.overrideDns, - selector: overrideDnsProvider, - onChanged: (ref, value) => - ref.read(overrideDnsProvider.notifier).value = value, - ), - ], - ), - for (final (title, keys) in _sectionsOf( - appLocalizations, - entries, - )) - generateSectionV3( - title: title, - items: [ - for (final key in keys) - _OverrideItem(key: ValueKey(key), overrideKey: key), - ], - ), - ], - ), - ), - if (entries.isEmpty) - SliverFillRemaining( - hasScrollBody: false, - child: NullStatus( - label: appLocalizations.nullTip(appLocalizations.overrideEntries), - illustration: NullStatusIllustration.dns, - ), - ), - ], + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }) { + return _PolicyItem( + model: model, + title: title, + leading: leading, + select: select, + update: update, ); } } -class _OverrideItem extends StatelessWidget { - const _OverrideItem({super.key, required this.overrideKey}); - - final DnsOverrideKey overrideKey; - - @override - Widget build(BuildContext context) { - final leading = _RemoveButton(overrideKey); - final title = overrideKey.label; - - Widget toggle(bool Function(Dns dns) select, _DnsUpdate update) { - return ConfigToggleItem( - leading: leading, - title: title, - subtitle: overrideKey.description, - selector: _dnsSelector(select), - onChanged: _dnsWriter(update), - ); - } - - Widget text( - String Function(Dns dns) select, - _DnsUpdate update, { - required int maxLength, - }) { - return ConfigTextItem( - leading: leading, - title: title, - selector: _dnsSelector(select), - onChanged: _dnsWriter(update), - maxLength: maxLength, - ); - } - - Widget number(int Function(Dns dns) select, _DnsUpdate update) { - return ConfigTextItem( - leading: leading, - title: title, - selector: _dnsSelector((dns) => '${select(dns)}'), - onChanged: _dnsWriter( - (state, value) => update(state, int.parse(value)), - ), - maxLength: TextInputLimits.number, - keyboardType: TextInputType.number, - normalize: (value) => value.trim(), - validator: (value, l) => (int.tryParse(value ?? '') ?? -1) < 0 - ? l.numberTip(title(l)) - : null, - ); - } - - Widget options( - List values, - T Function(Dns dns) select, - _DnsUpdate update, - ) { - return ConfigOptionsItem( - leading: leading, - title: title, - options: values, - textBuilder: (value) => value.name, - selector: _dnsSelector(select), - onChanged: _dnsWriter(update), - ); - } - - Widget policy( - Map Function(Dns dns) select, - _DnsUpdate> update, - ) { - return _PolicyItem( - overrideKey: overrideKey, - leading: leading, - select: select, - update: update, - ); - } - - Widget list( - List Function(Dns dns) select, - _DnsUpdate> update, { - required int itemMaxLength, - }) { - return ConfigListEditItem( - leading: leading, - title: title, - subtitle: overrideKey.description, - selector: _dnsSelector(select), - onChanged: _dnsWriter(update), - itemMaxLength: itemMaxLength, - ); - } - - return switch (overrideKey) { - DnsOverrideKey.enable => toggle( - (dns) => dns.enable, - (state, value) => state.copyWith.dns(enable: value), - ), - DnsOverrideKey.listen => text( - (dns) => dns.listen, - (state, value) => state.copyWith.dns(listen: value), - maxLength: TextInputLimits.dnsListen, - ), - DnsOverrideKey.listenRoutingMark => number( - (dns) => dns.listenRoutingMark, - (state, value) => state.copyWith.dns(listenRoutingMark: value), - ), - DnsOverrideKey.useHosts => toggle( - (dns) => dns.useHosts, - (state, value) => state.copyWith.dns(useHosts: value), - ), - DnsOverrideKey.useSystemHosts => toggle( - (dns) => dns.useSystemHosts, - (state, value) => state.copyWith.dns(useSystemHosts: value), - ), - DnsOverrideKey.ipv6 => toggle( - (dns) => dns.ipv6, - (state, value) => state.copyWith.dns(ipv6: value), - ), - DnsOverrideKey.ipv6Timeout => number( - (dns) => dns.ipv6Timeout, - (state, value) => state.copyWith.dns(ipv6Timeout: value), - ), - DnsOverrideKey.respectRules => toggle( - (dns) => dns.respectRules, - (state, value) => state.copyWith.dns(respectRules: value), - ), - DnsOverrideKey.preferH3 => toggle( - (dns) => dns.preferH3, - (state, value) => state.copyWith.dns(preferH3: value), - ), - DnsOverrideKey.cacheAlgorithm => options( - DnsCacheAlgorithm.values, - (dns) => dns.cacheAlgorithm, - (state, value) => state.copyWith.dns(cacheAlgorithm: value), - ), - DnsOverrideKey.cacheMaxSize => number( - (dns) => dns.cacheMaxSize, - (state, value) => state.copyWith.dns(cacheMaxSize: value), - ), - DnsOverrideKey.enhancedMode => options( - _enhancedModes, - (dns) => dns.enhancedMode, - (state, value) => state.copyWith.dns(enhancedMode: value), - ), - DnsOverrideKey.fakeIpRange => text( - (dns) => dns.fakeIpRange, - (state, value) => state.copyWith.dns(fakeIpRange: value), - maxLength: TextInputLimits.cidr, - ), - DnsOverrideKey.fakeIpRange6 => text( - (dns) => dns.fakeIpRange6, - (state, value) => state.copyWith.dns(fakeIpRange6: value), - maxLength: TextInputLimits.cidr, - ), - DnsOverrideKey.fakeIpFilter => list( - (dns) => dns.fakeIpFilter, - (state, value) => state.copyWith.dns(fakeIpFilter: value), - itemMaxLength: TextInputLimits.domain, - ), - DnsOverrideKey.fakeIpFilterMode => options( - FakeIpFilterMode.values, - (dns) => dns.fakeIpFilterMode, - (state, value) => state.copyWith.dns(fakeIpFilterMode: value), - ), - DnsOverrideKey.fakeIpTtl => number( - (dns) => dns.fakeIpTtl, - (state, value) => state.copyWith.dns(fakeIpTtl: value), - ), - DnsOverrideKey.defaultNameserver => list( - (dns) => dns.defaultNameserver, - (state, value) => state.copyWith.dns(defaultNameserver: value), - itemMaxLength: TextInputLimits.dnsServer, - ), - DnsOverrideKey.nameserverPolicy => policy( - (dns) => dns.nameserverPolicy, - (state, value) => state.copyWith.dns(nameserverPolicy: value), - ), - DnsOverrideKey.nameserver => list( - (dns) => dns.nameserver, - (state, value) => state.copyWith.dns(nameserver: value), - itemMaxLength: TextInputLimits.dnsServer, - ), - DnsOverrideKey.fallback => list( - (dns) => dns.fallback, - (state, value) => state.copyWith.dns(fallback: value), - itemMaxLength: TextInputLimits.dnsServer, - ), - DnsOverrideKey.fallbackLazyQuery => toggle( - (dns) => dns.fallbackLazyQuery, - (state, value) => state.copyWith.dns(fallbackLazyQuery: value), - ), - DnsOverrideKey.proxyServerNameserver => list( - (dns) => dns.proxyServerNameserver, - (state, value) => state.copyWith.dns(proxyServerNameserver: value), - itemMaxLength: TextInputLimits.dnsServer, - ), - DnsOverrideKey.proxyServerNameserverPolicy => policy( - (dns) => dns.proxyServerNameserverPolicy, - (state, value) => - state.copyWith.dns(proxyServerNameserverPolicy: value), - ), - DnsOverrideKey.directNameserver => list( - (dns) => dns.directNameserver, - (state, value) => state.copyWith.dns(directNameserver: value), - itemMaxLength: TextInputLimits.dnsServer, - ), - DnsOverrideKey.directNameserverFollowPolicy => toggle( - (dns) => dns.directNameserverFollowPolicy, - (state, value) => - state.copyWith.dns(directNameserverFollowPolicy: value), - ), - DnsOverrideKey.fallbackFilterGeoip => toggle( - (dns) => dns.fallbackFilter.geoip, - (state, value) => state.copyWith.dns.fallbackFilter(geoip: value), - ), - DnsOverrideKey.fallbackFilterGeoipCode => text( - (dns) => dns.fallbackFilter.geoipCode, - (state, value) => state.copyWith.dns.fallbackFilter(geoipCode: value), - maxLength: TextInputLimits.geoIpCode, - ), - DnsOverrideKey.fallbackFilterGeosite => list( - (dns) => dns.fallbackFilter.geosite, - (state, value) => state.copyWith.dns.fallbackFilter(geosite: value), - itemMaxLength: TextInputLimits.geoSite, - ), - DnsOverrideKey.fallbackFilterIpcidr => list( - (dns) => dns.fallbackFilter.ipcidr, - (state, value) => state.copyWith.dns.fallbackFilter(ipcidr: value), - itemMaxLength: TextInputLimits.cidr, - ), - DnsOverrideKey.fallbackFilterDomain => list( - (dns) => dns.fallbackFilter.domain, - (state, value) => state.copyWith.dns.fallbackFilter(domain: value), - itemMaxLength: TextInputLimits.domain, - ), - }; - } -} - class _PolicyItem extends ConsumerWidget { const _PolicyItem({ - required this.overrideKey, + required this.model, + required this.title, required this.leading, required this.select, required this.update, }); - final DnsOverrideKey overrideKey; + final OverrideModel model; + final ConfigLabel title; final Widget leading; final Map Function(Dns dns) select; - final _DnsUpdate> update; + final Dns Function(Dns dns, Map value) update; @override Widget build(BuildContext context, WidgetRef ref) { final appLocalizations = context.appLocalizations; - final title = overrideKey.label(appLocalizations); - final description = overrideKey.description; - final policy = ref.watch(_dnsSelector(select)); + final title = this.title(appLocalizations); + final policy = ref.watch(model.selectModel(select)); return ListItem.open( leading: leading, title: Text(title), - subtitle: description == null - ? null - : Text(description(appLocalizations)), + subtitle: Text( + policy.isEmpty + ? appLocalizations.none + : appLocalizations.itemsCount(policy.length), + ), widget: MapEditView( title: title, entries: policy, @@ -529,37 +400,14 @@ class _PolicyItem extends ConsumerWidget { subtitleBuilder: (item) => Text(item.value), ), onChanged: (value) { - if (value is Map) { - _dnsWriter(update)(ref, Map.from(value)); + if (value is! Map) { + return; + } + final next = Map.from(value); + if (!mapEquals(next, policy)) { + model.modelWriter(update)(ref, next); } }, ); } } - -class _RemoveButton extends ConsumerWidget { - const _RemoveButton(this.overrideKey); - - final DnsOverrideKey overrideKey; - - @override - Widget build(BuildContext context, WidgetRef ref) { - return CommonMinIconButtonTheme( - child: ElasticButton( - child: IconButton.filledTonal( - tooltip: context.appLocalizations.remove, - onPressed: () => ref - .read(patchClashConfigProvider.notifier) - .update( - (state) => state.copyWith( - dnsOverrideKeys: {...state.dnsOverrideKeys} - ..remove(overrideKey), - ), - ), - icon: const GlyphIcon(AppGlyphs.remove, size: 18, fill: 1), - padding: EdgeInsets.zero, - ), - ), - ); - } -} diff --git a/lib/views/config/filters.dart b/lib/views/config/filters.dart new file mode 100644 index 00000000..c74c0e51 --- /dev/null +++ b/lib/views/config/filters.dart @@ -0,0 +1,280 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/features/form/form.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/providers/providers.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class FiltersView extends ConsumerStatefulWidget { + const FiltersView({super.key}); + + @override + ConsumerState createState() => _FiltersViewState(); +} + +class _FiltersViewState extends ConsumerState { + List get _filters => ref.read(appSettingProvider).filters; + + void _save(List filters) { + ref + .read(appSettingProvider.notifier) + .update((state) => state.copyWith(filters: filters)); + } + + Future _handleAddOrUpdate([Filter? filter]) async { + final res = await dialogs.showCommonDialog( + child: _FilterDialog( + filter: filter, + checkRegex: _checkRegex, + reservedLabels: { + for (final item in _filters) + if (item.label != filter?.label) item.label, + }, + ), + ); + if (res == null || !mounted) { + return; + } + final filters = _filters; + _save( + filter == null + ? [...filters, res] + : [ + for (final item in filters) + item.label == filter.label ? res : item, + ], + ); + } + + Future _checkRegex(String regex) async { + final errors = await checkFilters(ref, [regex]); + final error = errors?.single ?? ''; + return error.isEmpty ? null : error; + } + + void _handleDelete(Set labels) { + _save(_filters.where((item) => !labels.contains(item.label)).toList()); + } + + void _handleReorder(int oldIndex, int newIndex) { + _save(_filters.copyAndReorder(oldIndex, newIndex)); + } + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + return ListEditorPage( + title: appLocalizations.filters, + selectionEnabled: true, + idOf: (filter) => filter.label, + itemsOf: (ref) => + ref.watch(appSettingProvider.select((state) => state.filters)), + itemBuilder: + ( + context, + ref, + filter, + index, + isEditing, + isSelected, + onToggleSelected, + ) { + return _FilterItem( + filter: filter, + isEditing: isEditing, + isSelected: isSelected, + onSelected: onToggleSelected, + onPressed: () { + _handleAddOrUpdate(filter); + }, + ); + }, + onReorder: _handleReorder, + onAdd: _handleAddOrUpdate, + onDelete: _handleDelete, + searchFieldsOf: (filter) => [filter.label, filter.regex], + emptyLabel: appLocalizations.nullTip(appLocalizations.filters), + ); + } +} + +class _FilterItem extends StatelessWidget { + final Filter filter; + final bool isEditing; + final bool isSelected; + final VoidCallback onSelected; + final VoidCallback onPressed; + + const _FilterItem({ + required this.filter, + required this.isEditing, + required this.isSelected, + required this.onSelected, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return DecorationListItem( + isSelected: isSelected, + onPressed: isEditing ? onSelected : onPressed, + contentPadding: const EdgeInsets.only(left: 16), + title: TooltipText( + text: Text(filter.label, maxLines: 1, overflow: TextOverflow.ellipsis), + ), + subtitle: FilterRegexText(filter.regex), + trailing: CommonCheckBox( + value: isSelected, + isCircle: true, + onChanged: (_) => onSelected(), + ), + ); + } +} + +class _FilterDialog extends StatefulWidget { + final Filter? filter; + final Future Function(String regex) checkRegex; + final Set reservedLabels; + + const _FilterDialog({ + required this.filter, + required this.checkRegex, + required this.reservedLabels, + }); + + @override + State<_FilterDialog> createState() => _FilterDialogState(); +} + +class _FilterDialogState extends State<_FilterDialog> { + final _formKey = GlobalKey(); + late final TextEditingController _labelController; + late final TextEditingController _regexController; + String? _regexError; + bool _checking = false; + + @override + void initState() { + super.initState(); + _labelController = TextEditingController(text: widget.filter?.label); + _regexController = TextEditingController(text: widget.filter?.regex); + } + + @override + void dispose() { + _labelController.dispose(); + _regexController.dispose(); + super.dispose(); + } + + String? _validateLabel(String? value) { + final appLocalizations = context.appLocalizations; + final label = value?.trim() ?? ''; + if (label.isEmpty) { + return appLocalizations.emptyTip(appLocalizations.label); + } + if (widget.reservedLabels.contains(label)) { + return appLocalizations.existsTip(appLocalizations.label); + } + return null; + } + + String? _validateRegex(String? value) { + final appLocalizations = context.appLocalizations; + if (value == null || value.trim().isEmpty) { + return appLocalizations.emptyTip(appLocalizations.regex); + } + return null; + } + + Future _handleSubmit() async { + if (_checking || _formKey.currentState?.validate() == false) { + return; + } + final filter = Filter( + label: _labelController.text.trim(), + regex: _regexController.text.trim(), + ); + setState(() { + _checking = true; + }); + String? error; + try { + error = await widget.checkRegex(filter.regex); + } finally { + if (mounted) { + setState(() { + _checking = false; + _regexError = error; + }); + } + } + if (error == null && mounted) { + Navigator.of(context).pop(filter); + } + } + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + return CommonDialog( + title: widget.filter == null + ? appLocalizations.add + : appLocalizations.edit, + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(appLocalizations.cancel), + ), + TextButton( + onPressed: _checking ? null : _handleSubmit, + child: Text(appLocalizations.confirm), + ), + ], + child: Form( + key: _formKey, + autovalidateMode: AutovalidateMode.onUserInteraction, + child: Wrap( + runSpacing: 16, + children: [ + TextFormField( + controller: _labelController, + autofocus: widget.filter == null, + validator: _validateLabel, + inputFormatters: TextInputLimits.limit(TextInputLimits.name), + decoration: InputDecoration(labelText: appLocalizations.label), + ), + TextFormField( + controller: _regexController, + validator: _validateRegex, + forceErrorText: _regexError, + onChanged: (_) { + if (_regexError != null) { + setState(() { + _regexError = null; + }); + } + }, + minLines: 1, + maxLines: 3, + autocorrect: false, + enableSuggestions: false, + textInputAction: TextInputAction.done, + inputFormatters: TextInputLimits.limit(TextInputLimits.filter), + onFieldSubmitted: (_) => _handleSubmit(), + decoration: InputDecoration( + labelText: appLocalizations.regex, + hintText: r'(?i)hk|hong\s*kong', + errorMaxLines: 3, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/config/general.dart b/lib/views/config/general.dart index a6dc66ab..4b830976 100644 --- a/lib/views/config/general.dart +++ b/lib/views/config/general.dart @@ -45,46 +45,6 @@ class UaItem extends ConsumerWidget { } } -class KeepAliveIntervalItem extends ConsumerWidget { - const KeepAliveIntervalItem({super.key}); - - @override - Widget build(BuildContext context, ref) { - final appLocalizations = context.appLocalizations; - final keepAliveInterval = ref.watch( - patchClashConfigProvider.select((state) => state.keepAliveInterval), - ); - return ListItem.input( - title: Text(appLocalizations.keepAliveIntervalDesc), - subtitle: Text(appLocalizations.secondsCount(keepAliveInterval)), - dialogTitle: appLocalizations.keepAliveIntervalDesc, - suffixText: appLocalizations.seconds, - resetValue: '$defaultKeepAliveInterval', - value: '$keepAliveInterval', - maxLength: TextInputLimits.interval, - validator: (String? value) { - if (value == null || value.isEmpty) { - return appLocalizations.emptyTip(appLocalizations.interval); - } - final intValue = int.tryParse(value); - if (intValue == null) { - return appLocalizations.numberTip(appLocalizations.interval); - } - return null; - }, - onChanged: (String? value) { - if (value == null) { - return; - } - final intValue = int.parse(value); - ref - .read(patchClashConfigProvider.notifier) - .update((state) => state.copyWith(keepAliveInterval: intValue)); - }, - ); - } -} - class TestUrlItem extends ConsumerWidget { const TestUrlItem({super.key}); @@ -150,11 +110,17 @@ class HostsItem extends ConsumerWidget { @override Widget build(BuildContext context, ref) { + final appLocalizations = context.appLocalizations; final hosts = ref.watch( patchClashConfigProvider.select((state) => state.hosts), ); return ListItem.open( title: const Text('Hosts'), + subtitle: Text( + hosts.isEmpty + ? appLocalizations.none + : appLocalizations.itemsCount(hosts.length), + ), widget: MapEditView( title: 'Hosts', entries: hosts, @@ -179,7 +145,6 @@ class AuthenticationItem extends ConsumerWidget { Widget build(BuildContext context, ref) { return ConfigToggleItem( title: (l) => l.authentication, - subtitle: (l) => l.authenticationDesc, selector: networkSettingProvider.select( (state) => state.authentication.enable, ), @@ -257,13 +222,11 @@ class ExternalControllerSecretItem extends ConsumerWidget { ConfigToggleItem _clashToggle({ required ConfigLabel title, - ConfigLabel? subtitle, required bool Function(PatchClashConfig state) select, required PatchClashConfig Function(PatchClashConfig state, bool value) update, }) { return ConfigToggleItem( title: title, - subtitle: subtitle, selector: patchClashConfigProvider.select(select), onChanged: (ref, value) => ref .read(patchClashConfigProvider.notifier) @@ -273,13 +236,11 @@ ConfigToggleItem _clashToggle({ ConfigToggleItem _appSettingToggle({ required ConfigLabel title, - ConfigLabel? subtitle, required bool Function(AppSettingProps state) select, required AppSettingProps Function(AppSettingProps state, bool value) update, }) { return ConfigToggleItem( title: title, - subtitle: subtitle, selector: appSettingProvider.select(select), onChanged: (ref, value) => ref .read(appSettingProvider.notifier) @@ -295,26 +256,22 @@ class GeneralView extends ConsumerWidget { if (system.isDesktop) ...[ _appSettingToggle( title: (l) => l.autoLaunch, - subtitle: (l) => l.autoLaunchDesc, select: (state) => state.autoLaunch, update: (state, value) => state.copyWith(autoLaunch: value), ), _appSettingToggle( title: (l) => l.silentLaunch, - subtitle: (l) => l.silentLaunchDesc, select: (state) => state.silentLaunch, update: (state, value) => state.copyWith(silentLaunch: value), ), ], _appSettingToggle( title: (l) => l.autoRun, - subtitle: (l) => l.autoRunDesc, select: (state) => state.autoRun, update: (state, value) => state.copyWith(autoRun: value), ), ListItem.open( title: Text(appLocalizations.onDemand), - subtitle: Text(appLocalizations.onDemandDesc), widget: const OnDemandView(), ), _appSettingToggle( @@ -325,7 +282,6 @@ class GeneralView extends ConsumerWidget { if (system.isAndroid) ...[ _appSettingToggle( title: (l) => l.exclude, - subtitle: (l) => l.excludeDesc, select: (state) => state.hidden, update: (state, value) => state.copyWith(hidden: value), ), @@ -352,7 +308,6 @@ class GeneralView extends ConsumerWidget { ), _clashToggle( title: (l) => l.externalController, - subtitle: (l) => l.externalControllerDesc, select: (state) => state.externalController == ExternalControllerStatus.open, update: (state, value) => state @@ -385,7 +340,6 @@ class GeneralView extends ConsumerWidget { select: (state) => state.tcpConcurrent, update: (state, value) => state.copyWith(tcpConcurrent: value), ), - if (system.isDesktop) const KeepAliveIntervalItem(), _clashToggle( title: (l) => l.findProcessMode, select: (state) => state.findProcessMode == FindProcessMode.always, @@ -395,7 +349,6 @@ class GeneralView extends ConsumerWidget { ), _appSettingToggle( title: (l) => l.autoCloseConnections, - subtitle: (l) => l.autoCloseConnectionsDesc, select: (state) => state.closeConnections, update: (state, value) => state.copyWith(closeConnections: value), ), @@ -412,7 +365,6 @@ class GeneralView extends ConsumerWidget { const UaItem(), _appSettingToggle( title: (l) => l.checkCertificate, - subtitle: (l) => l.checkCertificateDesc, select: (state) => state.checkCertificate, update: (state, value) => state.copyWith(checkCertificate: value), ), @@ -428,7 +380,6 @@ class GeneralView extends ConsumerWidget { return [ _clashToggle( title: (l) => 'IPv6', - subtitle: (l) => l.ipv6Desc, select: (state) => state.ipv6, update: (state, value) => state.copyWith(ipv6: value), ), @@ -459,14 +410,12 @@ class GeneralView extends ConsumerWidget { const LogLevelItem(), _appSettingToggle( title: (l) => l.logcat, - subtitle: (l) => l.logcatDesc, select: (state) => state.openLogs, update: (state, value) => state.copyWith(openLogs: value), ), if (system.isAndroid) _appSettingToggle( title: (l) => l.crashlytics, - subtitle: (l) => l.crashlyticsTip, select: (state) => state.crashlytics, update: (state, value) => state.copyWith(crashlytics: value), ), @@ -494,10 +443,16 @@ class GeneralView extends ConsumerWidget { generateSectionV3( title: appLocalizations.startupAndBackground, items: _startupItems(appLocalizations), + footer: [ + if (system.isDesktop) appLocalizations.silentLaunchDesc, + appLocalizations.autoRunDesc, + appLocalizations.onDemandDesc, + ].join('\n'), ), generateSectionV3( title: appLocalizations.requestsAndUpdates, items: _requestItems(), + footer: appLocalizations.checkCertificateDesc, ), generateSectionV3( title: appLocalizations.inbound, @@ -505,15 +460,28 @@ class GeneralView extends ConsumerWidget { authentication: authentication, externalController: externalController, ), + footer: [ + appLocalizations.externalControllerDesc, + appLocalizations.authenticationDesc, + ].join('\n'), ), generateSectionV3( title: appLocalizations.connection, items: _connectionItems(), + footer: appLocalizations.autoCloseConnectionsDesc, + ), + generateSectionV3( + title: appLocalizations.core, + items: _coreItems(), + footer: appLocalizations.ipv6Desc, ), - generateSectionV3(title: appLocalizations.core, items: _coreItems()), generateSectionV3( title: appLocalizations.logsAndDiagnostics, items: _logItems(), + footer: [ + appLocalizations.logcatDesc, + if (system.isAndroid) appLocalizations.crashlyticsTip, + ].join('\n'), ), ], ), diff --git a/lib/views/config/icon_sets.dart b/lib/views/config/icon_sets.dart new file mode 100644 index 00000000..2b612cdf --- /dev/null +++ b/lib/views/config/icon_sets.dart @@ -0,0 +1,314 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/icons/icons.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/providers/providers.dart'; +import 'package:fl_clash/state.dart'; +import 'package:fl_clash/views/profiles/custom/icon.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class IconSetsView extends ConsumerStatefulWidget { + const IconSetsView({super.key}); + + @override + ConsumerState createState() => _IconSetsViewState(); +} + +class _IconSetsViewState extends ConsumerState { + late final IconSetsAction _iconSetsAction; + + @override + void initState() { + super.initState(); + _iconSetsAction = ref.read(iconSetsActionProvider.notifier); + } + + Future _run(FutureOr Function() task, {String? title}) { + return globalState.loadingRun(task, title: title, tag: LoadingTag.iconSets); + } + + Future _handleImportFromUrl() async { + final res = await dialogs.showNamedUrlInput( + title: context.appLocalizations.importUrl, + ); + if (res == null || !mounted) { + return; + } + await _run(() => _iconSetsAction.importUrl(res.url, name: res.label)); + } + + Future _handleImportFromFile() async { + final file = await globalState.safeRun(picker.pickerFile); + if (file == null) { + return; + } + final bytes = await file.readBytes(); + if (!mounted) { + return; + } + await _run( + () => _iconSetsAction.importContent( + file.name, + utf8.decode(bytes, allowMalformed: true), + ), + ); + } + + Future _handleEditOptions(IconSet iconSet) async { + final appLocalizations = context.appLocalizations; + if (!iconSet.isRemote) { + final name = await dialogs.showCommonDialog( + child: InputDialog( + title: appLocalizations.options, + value: iconSet.name, + labelText: appLocalizations.name, + inputFormatters: TextInputLimits.limit(TextInputLimits.name), + validator: (value) => value?.trim().isNotEmpty == true + ? null + : appLocalizations.emptyTip(appLocalizations.name), + ), + ); + if (name != null) { + ref + .read(iconSetsProvider.notifier) + .put(iconSet.copyWith(name: name.trim())); + } + return; + } + final res = await dialogs.showNamedUrlInput( + title: appLocalizations.options, + label: iconSet.name, + url: iconSet.url, + ); + if (res == null || !mounted) { + return; + } + final next = iconSet.copyWith( + name: res.label.isNotEmpty ? res.label : iconSet.name, + url: res.url, + ); + if (next.url == iconSet.url) { + ref.read(iconSetsProvider.notifier).put(next); + return; + } + await _run(() => _iconSetsAction.sync(next), title: next.name); + } + + Future _handleDelete(IconSet iconSet) async { + final appLocalizations = context.appLocalizations; + final res = await dialogs.showMessage( + title: appLocalizations.tip, + message: TextSpan(text: appLocalizations.deleteTip(iconSet.name)), + ); + if (res == true) { + ref.read(iconSetsProvider.notifier).del(iconSet.id); + } + } + + void _handlePreview(IconSet iconSet) { + unawaited(BaseNavigator.push(context, _IconSetPreviewView(iconSet.id))); + } + + List _buildMenuItems(IconSet iconSet) { + final appLocalizations = context.appLocalizations; + return [ + if (iconSet.isRemote) + CommonPopupMenuItem( + glyph: AppGlyphs.sync, + label: appLocalizations.sync, + onPressed: () { + _run(() => _iconSetsAction.sync(iconSet), title: iconSet.name); + }, + ), + CommonPopupMenuItem( + glyph: AppGlyphs.settings, + label: appLocalizations.options, + onPressed: () { + _handleEditOptions(iconSet); + }, + ), + CommonPopupMenuItem( + danger: true, + glyph: AppGlyphs.delete, + label: appLocalizations.delete, + onPressed: () { + _handleDelete(iconSet); + }, + ), + ]; + } + + Widget _buildItem(List iconSets, int index) { + final appLocalizations = context.appLocalizations; + final iconSet = iconSets[index]; + return SortableItem( + key: ValueKey(iconSet.id), + index: index, + child: ContextMenuRegion( + menuItems: _buildMenuItems(iconSet), + child: ItemPositionProvider( + position: ItemPosition.get(index, iconSets.length), + child: DecorationListItem( + contentPadding: const EdgeInsets.only(left: 16), + leading: SizedBox.square( + dimension: 32, + child: IconTheme.merge( + data: const IconThemeData(size: 32), + child: CommonTargetIcon(src: iconSet.cover), + ), + ), + title: ChipTitle( + title: iconSet.name, + chip: iconSet.icons.length.compact, + ), + subtitle: Text( + iconSet.isRemote + ? appLocalizations.remote + : appLocalizations.local, + style: context.listCaptionStyle, + ), + trailing: const DisclosureIndicator(), + onPressed: () { + _handlePreview(iconSet); + }, + ), + ), + ), + ); + } + + Widget _buildContent(List iconSets, {required bool loading}) { + final appLocalizations = context.appLocalizations; + return NullStatusSwitcher( + isLoading: loading, + isEmpty: iconSets.isEmpty, + nullStatus: NullStatus( + label: appLocalizations.nullTip(appLocalizations.iconSets), + illustration: NullStatusIllustration.icons, + ), + child: ReorderableListView.builder( + padding: const EdgeInsets.all( + 16, + ).copyWith(top: context.contentTopPadding), + buildDefaultDragHandles: false, + itemCount: iconSets.length, + itemBuilder: (_, index) => _buildItem(iconSets, index), + proxyDecorator: (_, index, animation) { + return commonProxyDecorator( + _buildItem(iconSets, index), + index, + animation, + ); + }, + onReorderItem: ref.read(iconSetsProvider.notifier).order, + ), + ); + } + + @override + Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; + final state = ref.watch(iconSetsProvider); + final iconSets = state.value ?? []; + return CommonScaffold( + canSort: iconSets.length > 1, + isLoading: ref.watch(loadingProvider(LoadingTag.iconSets)), + actions: [ + CommonPopupBox( + targetBuilder: (open) { + return IconButton( + tooltip: appLocalizations.add, + onPressed: () { + final isMobile = ref.read(isMobileViewProvider); + open(offset: Offset(0, isMobile ? 0 : 20)); + }, + icon: const GlyphIcon(AppGlyphs.addCircle), + ); + }, + popupBuilder: (_) => CommonPopupMenu( + items: [ + CommonPopupMenuItem( + glyph: AppGlyphs.cloudDownload, + label: appLocalizations.importUrl, + onPressed: _handleImportFromUrl, + ), + CommonPopupMenuItem( + glyph: AppGlyphs.importFile, + label: appLocalizations.importFile, + onPressed: _handleImportFromFile, + ), + ], + ), + ), + ], + body: _buildContent(iconSets, loading: state.isLoading), + title: appLocalizations.iconSets, + ); + } +} + +class _IconSetPreviewView extends ConsumerStatefulWidget { + const _IconSetPreviewView(this.iconSetId); + + final int iconSetId; + + @override + ConsumerState<_IconSetPreviewView> createState() => + _IconSetPreviewViewState(); +} + +class _IconSetPreviewViewState extends ConsumerState<_IconSetPreviewView> { + var _query = SearchQuery(''); + + @override + Widget build(BuildContext context) { + final iconSet = ref.watch( + iconSetsProvider.select( + (state) => state.value + ?.where((item) => item.id == widget.iconSetId) + .firstOrNull, + ), + ); + final icons = (iconSet?.icons ?? const []) + .whereMatches(_query, (icon) => [icon.label]) + .toList(); + return CommonScaffold( + title: iconSet?.name ?? context.appLocalizations.iconSets, + searchState: AppBarSearchState( + onSearch: (query) { + setState(() { + _query = SearchQuery(query); + }); + }, + ), + body: NullStatusSwitcher( + isEmpty: icons.isEmpty, + isSearching: _query.isNotEmpty, + nullStatus: NullStatus( + label: context.appLocalizations.nullTip( + context.appLocalizations.icon, + ), + illustration: NullStatusIllustration.icons, + ), + child: Builder( + builder: (context) => IconGridScrollView( + slivers: [ + SliverToBoxAdapter( + child: SizedBox(height: context.contentTopPadding), + ), + SliverIconGrid(icons: icons), + SliverToBoxAdapter( + child: SizedBox(height: 20 + BottomInsetScope.of(context)), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/views/config/network.dart b/lib/views/config/network.dart index 2ba45541..39c7cc30 100644 --- a/lib/views/config/network.dart +++ b/lib/views/config/network.dart @@ -38,11 +38,9 @@ ConfigToggleItem _vpnToggle({ required ConfigLabel title, required bool Function(VpnProps state) select, required _VpnUpdate update, - ConfigLabel? subtitle, }) { return ConfigToggleItem( title: title, - subtitle: subtitle, selector: vpnSettingProvider.select(select), onChanged: _vpnWriter(update), ); @@ -52,11 +50,9 @@ ConfigToggleItem _networkToggle({ required ConfigLabel title, required bool Function(NetworkProps state) select, required _NetworkUpdate update, - ConfigLabel? subtitle, }) { return ConfigToggleItem( title: title, - subtitle: subtitle, selector: networkSettingProvider.select(select), onChanged: _networkWriter(update), ); @@ -69,7 +65,6 @@ class VPNItem extends ConsumerWidget { Widget build(BuildContext context, ref) { return _vpnToggle( title: (l) => 'VPN', - subtitle: (l) => l.vpnEnableDesc, select: (state) => state.enable, update: (state, value) => state.copyWith(enable: value), ); @@ -83,7 +78,6 @@ class TUNItem extends ConsumerWidget { Widget build(BuildContext context, ref) { return ConfigToggleItem( title: (l) => l.tun, - subtitle: (l) => l.tunDesc, selector: patchClashConfigProvider.select((state) => state.tun.enable), onChanged: _tunWriter( (state, value) => state.copyWith.tun(enable: value), @@ -110,14 +104,8 @@ class VpnSystemProxyItem extends ConsumerWidget { @override Widget build(BuildContext context, ref) { - final authenticationEnable = ref.watch( - networkSettingProvider.select((state) => state.authentication.enable), - ); return _vpnToggle( title: (l) => l.systemProxy, - subtitle: authenticationEnable - ? (l) => l.authenticationSystemProxyDesc - : null, select: (state) => state.systemProxy, update: (state, value) => state.copyWith(systemProxy: value), ); @@ -144,7 +132,6 @@ class Ipv6Item extends ConsumerWidget { Widget build(BuildContext context, ref) { return _vpnToggle( title: (l) => 'IPv6', - subtitle: (l) => l.ipv6InboundDesc, select: (state) => state.ipv6, update: (state, value) => state.copyWith(ipv6: value), ); @@ -223,7 +210,6 @@ class InterfaceNameItem extends ConsumerWidget { Widget build(BuildContext context, ref) { return ConfigTextItem( title: (l) => l.interfaceName, - subtitle: (l) => l.interfaceNameDesc, maxLength: TextInputLimits.name, selector: patchClashConfigProvider.select((state) => state.interfaceName), onChanged: _tunWriter( @@ -233,19 +219,15 @@ class InterfaceNameItem extends ConsumerWidget { } } -class RouteModeItem extends ConsumerWidget { - const RouteModeItem({super.key}); +class BypassPrivateRouteItem extends ConsumerWidget { + const BypassPrivateRouteItem({super.key}); @override Widget build(BuildContext context, ref) { - return ConfigOptionsItem( - title: (l) => l.routeMode, - options: RouteMode.values, - textBuilder: (mode) => mode.label, - selector: networkSettingProvider.select((state) => state.routeMode), - onChanged: _networkWriter( - (state, value) => state.copyWith(routeMode: value), - ), + return _networkToggle( + title: (l) => l.bypassPrivateRoute, + select: (state) => state.bypassPrivateRoute, + update: (state, value) => state.copyWith(bypassPrivateRoute: value), ); } } @@ -257,7 +239,6 @@ class BypassDomainItem extends ConsumerWidget { Widget build(BuildContext context, ref) { return ConfigListEditItem( title: (l) => l.bypassDomain, - subtitle: (l) => l.bypassDomainDesc, itemMaxLength: TextInputLimits.domain, selector: networkSettingProvider.select((state) => state.bypassDomain), onChanged: _networkWriter( @@ -275,6 +256,7 @@ class RouteAddressItem extends ConsumerWidget { return ConfigListEditItem( title: (l) => l.routeAddress, itemMaxLength: TextInputLimits.cidr, + itemValidator: validateCidr, selector: patchClashConfigProvider.select( (state) => state.tun.routeAddress, ), @@ -302,11 +284,188 @@ class LoopbackItem extends StatelessWidget { } } +const _maxUint32 = 0xFFFFFFFF; + +// The congestion controls mipstack registers, cubic first as its default. +const _congestionControllers = [ + defaultCongestionController, + 'reno', + 'bbr', + 'bbr3', +]; + +class _NumberItem extends ConsumerWidget { + const _NumberItem({ + required this.title, + required this.select, + required this.update, + this.isSeconds = false, + this.min = 0, + this.max = _maxUint32, + }); + + final ConfigLabel title; + final int Function(PatchClashConfig state) select; + final _TunUpdate update; + final bool isSeconds; + final int min; + final int max; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appLocalizations = context.appLocalizations; + final label = title(appLocalizations); + final value = ref.watch(patchClashConfigProvider.select(select)); + return ListItem.input( + title: Text(label), + subtitle: Text( + isSeconds ? appLocalizations.secondsCount(value) : '$value', + ), + dialogTitle: label, + value: '$value', + resetValue: '${select(defaultClashConfig)}', + suffixText: isSeconds ? appLocalizations.seconds : null, + maxLength: TextInputLimits.number, + keyboardType: TextInputType.number, + validator: (value) { + final number = int.tryParse(value?.trim() ?? ''); + if (number == null) { + return appLocalizations.numberTip(label); + } + return number < min || number > max + ? appLocalizations.numberRangeTip(label, '$min', '$max') + : null; + }, + onChanged: (value) { + if (value == null) { + return; + } + ref + .read(patchClashConfigProvider.notifier) + .update((state) => update(state, int.parse(value.trim()))); + }, + ); + } +} + +List tunItems({ + required bool isLinux, + required bool isWindows, + required bool isMipsStack, +}) { + return [ + _NumberItem( + title: (l) => 'MTU', + min: minTunMtu, + max: maxTunMtu, + select: (state) => state.tun.mtu, + update: (state, value) => state.copyWith.tun(mtu: value), + ), + if (isMipsStack) + ConfigOptionsItem( + title: (l) => l.congestionController, + options: _congestionControllers, + textBuilder: (value) => value, + selector: patchClashConfigProvider.select( + (state) => state.tun.congestionController, + ), + onChanged: _tunWriter( + (state, value) => state.copyWith.tun(congestionController: value), + ), + ), + if (isLinux || isWindows) + ConfigToggleItem( + title: (l) => l.strictRoute, + selector: patchClashConfigProvider.select( + (state) => state.tun.strictRoute, + ), + onChanged: _tunWriter( + (state, value) => state.copyWith.tun(strictRoute: value), + ), + ), + const BypassPrivateRouteItem(), + const RouteAddressItem(), + ConfigListEditItem( + title: (l) => l.routeExcludeAddress, + itemMaxLength: TextInputLimits.cidr, + itemValidator: validateCidr, + selector: patchClashConfigProvider.select( + (state) => state.tun.routeExcludeAddress, + ), + onChanged: _tunWriter( + (state, value) => state.copyWith.tun(routeExcludeAddress: value), + ), + ), + ]; +} + +class TunSection extends ConsumerWidget { + const TunSection({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isMipsStack = ref.watch( + patchClashConfigProvider.select( + (state) => state.tun.stack == TunStack.mips, + ), + ); + final appLocalizations = context.appLocalizations; + return generateSectionV3( + title: appLocalizations.tun, + items: tunItems( + isLinux: system.isLinux, + isWindows: system.isWindows, + isMipsStack: isMipsStack, + ), + footer: [ + if (system.isLinux || system.isWindows) + appLocalizations.strictRouteDesc, + appLocalizations.bypassPrivateRouteDesc, + ].join('\n'), + ); + } +} + +// mihomo keeps TCP keep-alive off on Android whatever the config says +// (component/keepalive), and only Linux honours a routing mark. +List outboundItems({required bool isDesktop, required bool isLinux}) { + return [ + if (isDesktop) ...[ + ConfigToggleItem( + title: (l) => l.disableKeepAlive, + selector: patchClashConfigProvider.select( + (state) => state.disableKeepAlive, + ), + onChanged: _tunWriter( + (state, value) => state.copyWith(disableKeepAlive: value), + ), + ), + _NumberItem( + title: (l) => l.keepAliveIdle, + isSeconds: true, + select: (state) => state.keepAliveIdle, + update: (state, value) => state.copyWith(keepAliveIdle: value), + ), + _NumberItem( + title: (l) => l.keepAliveIntervalDesc, + isSeconds: true, + select: (state) => state.keepAliveInterval, + update: (state, value) => state.copyWith(keepAliveInterval: value), + ), + ], + if (isLinux) + _NumberItem( + title: (l) => l.routingMark, + select: (state) => state.routingMark, + update: (state, value) => state.copyWith(routingMark: value), + ), + ]; +} + List networkOptionsItems({ required bool isDesktop, required bool isMacOS, required bool isCustomInterfaceName, - required bool isBypassPrivateRoute, }) { return [ if (isDesktop) const TUNItem(), @@ -319,21 +478,24 @@ List networkOptionsItems({ const InterfaceNameModeItem(), if (isCustomInterfaceName) const InterfaceNameItem(), ], - if (!isDesktop) ...[ - const RouteModeItem(), - if (!isBypassPrivateRoute) const RouteAddressItem(), - ], ]; } -class VpnSections extends StatelessWidget { +class VpnSections extends ConsumerWidget { const VpnSections({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final appLocalizations = context.appLocalizations; + final authenticationEnable = ref.watch( + networkSettingProvider.select((state) => state.authentication.enable), + ); return Column( children: [ - generateSectionV3(items: const [VPNItem()]), + generateSectionV3( + items: const [VPNItem()], + footer: appLocalizations.vpnEnableDesc, + ), generateSectionV3( title: 'VPN', items: const [ @@ -343,6 +505,12 @@ class VpnSections extends StatelessWidget { Ipv6Item(), DNSHijackingItem(), ], + footer: [ + if (authenticationEnable) + appLocalizations.authenticationSystemProxyDesc, + appLocalizations.bypassDomainDesc, + appLocalizations.ipv6InboundDesc, + ].join('\n'), ), ], ); @@ -354,13 +522,15 @@ class SystemProxySection extends StatelessWidget { @override Widget build(BuildContext context) { + final appLocalizations = context.appLocalizations; return generateSectionV3( - title: context.appLocalizations.system, + title: appLocalizations.system, items: [ const SystemProxyItem(), const BypassDomainItem(), if (system.isWindows) const LoopbackItem(), ], + footer: appLocalizations.bypassDomainDesc, ); } } @@ -375,19 +545,21 @@ class NetworkOptionsSection extends ConsumerWidget { (state) => state.interfaceNameMode == InterfaceNameMode.custom, ), ); - final isBypassPrivateRoute = ref.watch( - networkSettingProvider.select( - (state) => state.routeMode == RouteMode.bypassPrivate, - ), - ); + final appLocalizations = context.appLocalizations; + final footer = [ + if (system.isDesktop) ...[ + appLocalizations.tunDesc, + if (isCustomInterfaceName) appLocalizations.interfaceNameDesc, + ], + ]; return generateSectionV3( - title: context.appLocalizations.options, + title: appLocalizations.options, items: networkOptionsItems( isDesktop: system.isDesktop, isMacOS: system.isMacOS, isCustomInterfaceName: isCustomInterfaceName, - isBypassPrivateRoute: isBypassPrivateRoute, ), + footer: footer.isEmpty ? null : footer.join('\n'), ); } } @@ -405,6 +577,14 @@ class NetworkListView extends StatelessWidget { if (system.isAndroid) const VpnSections(), if (system.isDesktop) const SystemProxySection(), const NetworkOptionsSection(), + const TunSection(), + generateSectionV3( + title: context.appLocalizations.outbound, + items: outboundItems( + isDesktop: system.isDesktop, + isLinux: system.isLinux, + ), + ), ], ); } diff --git a/lib/views/config/ntp.dart b/lib/views/config/ntp.dart index 2b1d073e..77a01ad4 100644 --- a/lib/views/config/ntp.dart +++ b/lib/views/config/ntp.dart @@ -1,32 +1,13 @@ import 'package:fl_clash/common/common.dart'; import 'package:fl_clash/enum/enum.dart'; -import 'package:fl_clash/features/overwrite/overwrite.dart'; -import 'package:fl_clash/icons/icons.dart'; import 'package:fl_clash/l10n/l10n.dart'; import 'package:fl_clash/models/models.dart'; import 'package:fl_clash/pages/editor.dart'; -import 'package:fl_clash/providers/config.dart'; +import 'package:fl_clash/providers/state.dart'; +import 'package:fl_clash/views/config/override.dart'; import 'package:fl_clash/widgets/widgets.dart'; -import 'package:material_ui/material_ui.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -typedef _NtpUpdate = - PatchClashConfig Function(PatchClashConfig state, T value); - -final _ntpOverrideKeysSelector = patchClashConfigProvider.select( - (state) => state.ntpOverrideKeys, -); - -ProviderListenable _ntpSelector(T Function(Ntp ntp) select) { - return patchClashConfigProvider.select((state) => select(state.ntp)); -} - -ConfigWriter _ntpWriter(_NtpUpdate update) { - return (ref, value) => ref - .read(patchClashConfigProvider.notifier) - .update((state) => update(state, value)); -} - extension on NtpOverrideKey { String label(AppLocalizations l) => switch (this) { NtpOverrideKey.enable => l.status, @@ -45,286 +26,100 @@ extension on NtpOverrideKey { }; } -class NtpView extends ConsumerWidget { - const NtpView({super.key}); +typedef _NtpState = OverrideState; - Future _handleAdd(BuildContext context, WidgetRef ref) async { - final appLocalizations = context.appLocalizations; - final added = ref.read(_ntpOverrideKeysSelector); - final remaining = NtpOverrideKey.values - .where((key) => !added.contains(key)) - .toList(); - final key = await showSheet( - context: context, - props: const SheetProps(isScrollControlled: true), - builder: (context) => OverwriteSelectionSheet( - title: appLocalizations.addOverrideEntry, - sections: [ - OverwriteSelectionSection( - label: appLocalizations.options, - items: remaining, - subtitleBuilder: (context, key) => - key.description?.call(context.appLocalizations), +_NtpState _profileNtp(ProfileOverrides overrides) => + (model: overrides.ntp, keys: overrides.ntpOverrideKeys); + +ProfileOverrides _setProfileNtp(ProfileOverrides overrides, _NtpState state) => + overrides.copyWith(ntp: state.model, ntpOverrideKeys: state.keys); + +class NtpView extends OverrideView { + NtpView(int profileId, {super.key}) + : super( + spec: _NtpOverrideSpec( + ProfileOverrideTarget( + profileId, + get: _profileNtp, + set: _setProfileNtp, + issues: customProfileIssuesProvider( + profileId, + ).select((state) => state.ntp), ), - ], - labelBuilder: (key) => key.label(appLocalizations), - selectedOf: (_) => null, - onSelected: (key) => Navigator.of(context).pop(key), - ), - ); - if (key == null || !context.mounted) { - return; - } - ref - .read(patchClashConfigProvider.notifier) - .update( - (state) => - state.copyWith(ntpOverrideKeys: {...state.ntpOverrideKeys, key}), - ); - } - - Future _handleQuickEdit(BuildContext context, WidgetRef ref) { - final config = ref.read(patchClashConfigProvider); - final raw = config.ntp.overrideYaml(config.ntpOverrideKeys); - return BaseNavigator.push( - context, - EditorPage( - title: 'NTP', - content: raw, - schema: EditorSchema.ntp, - readOnly: false, - onPop: (_, _, content) => _handleQuickEditPop(ref, content, raw), - ), - ); - } - - Future _handleQuickEditPop( - WidgetRef ref, - String content, - String raw, - ) async { - if (content == raw) { - return true; - } - try { - final result = ref - .read(patchClashConfigProvider) - .ntp - .applyOverrideYaml(content); - ref - .read(patchClashConfigProvider.notifier) - .update( - (state) => - state.copyWith(ntp: result.ntp, ntpOverrideKeys: result.keys), - ); - return true; - } catch (error) { - final res = await dialogs.showMessage( - message: TextSpan( - text: - '${compactError(error)}\n\n' - '${currentAppLocalizations.discardChanges}', ), ); - return res == true; - } +} + +class _NtpOverrideSpec extends OverrideSpec + with OverrideQuickEdit { + const _NtpOverrideSpec(this.target); + + @override + final OverrideTarget target; + + @override + String title(AppLocalizations l) => 'NTP'; + + @override + List get values => NtpOverrideKey.values; + + @override + String overrideYaml(Ntp model, Set keys) => + model.overrideYaml(keys); + + @override + _NtpState applyOverrideYaml(Ntp model, String content) { + final result = model.applyOverrideYaml(content); + return (model: result.ntp, keys: result.keys); } @override - Widget build(BuildContext context, WidgetRef ref) { - final appLocalizations = context.appLocalizations; - final canAdd = ref.watch( - _ntpOverrideKeysSelector.select( - (keys) => keys.length < NtpOverrideKey.values.length, - ), - ); - return BaseScaffold( - title: 'NTP', - menuItems: [ - CommonPopupMenuItem( - glyph: AppGlyphs.add, - label: appLocalizations.add, - onPressed: canAdd ? () => _handleAdd(context, ref) : null, - ), - CommonPopupMenuItem( - glyph: AppGlyphs.compose, - label: appLocalizations.quickEdit, - onPressed: () => _handleQuickEdit(context, ref), - ), - ], - body: const _OverrideList(), - ); - } -} - -class _OverrideList extends ConsumerWidget { - const _OverrideList(); + EditorSchema get schema => EditorSchema.ntp; @override - Widget build(BuildContext context, WidgetRef ref) { - final appLocalizations = context.appLocalizations; - final keys = ref.watch(_ntpOverrideKeysSelector); - final entries = NtpOverrideKey.values.where(keys.contains); - return CustomScrollView( - slivers: [ - SliverPadding( - padding: const EdgeInsets.symmetric(horizontal: 16).copyWith( - top: context.contentTopPadding, - bottom: entries.isEmpty ? 0 : 16, - ), - sliver: SliverList.list( - children: [ - generateSectionV3( - items: [ - ConfigToggleItem( - title: (l) => l.overrideNtp, - selector: overrideNtpProvider, - onChanged: (ref, value) => - ref.read(overrideNtpProvider.notifier).value = value, - ), - ], - ), - generateSectionV3( - title: appLocalizations.options, - items: [ - for (final key in entries) - _OverrideItem(key: ValueKey(key), overrideKey: key), - ], - ), - ], - ), - ), - if (entries.isEmpty) - SliverFillRemaining( - hasScrollBody: false, - child: NullStatus( - label: appLocalizations.nullTip(appLocalizations.overrideEntries), - illustration: NullStatusIllustration.ntp, - ), - ), - ], - ); - } -} - -class _OverrideItem extends StatelessWidget { - const _OverrideItem({super.key, required this.overrideKey}); - - final NtpOverrideKey overrideKey; + NullStatusIllustration get illustration => NullStatusIllustration.ntp; @override - Widget build(BuildContext context) { - final leading = _RemoveButton(overrideKey); - final title = overrideKey.label; - - Widget toggle(bool Function(Ntp ntp) select, _NtpUpdate update) { - return ConfigToggleItem( - leading: leading, - title: title, - subtitle: overrideKey.description, - selector: _ntpSelector(select), - onChanged: _ntpWriter(update), - ); - } - - Widget text( - String Function(Ntp ntp) select, - _NtpUpdate update, { - required int maxLength, - }) { - return ConfigTextItem( - leading: leading, - title: title, - selector: _ntpSelector(select), - onChanged: _ntpWriter(update), - maxLength: maxLength, - ); - } - - Widget number( - int Function(Ntp ntp) select, - _NtpUpdate update, { - int maxLength = TextInputLimits.number, - int min = 0, - int max = 0x7fffffff, - }) { - return ConfigTextItem( - leading: leading, - title: title, - selector: _ntpSelector((ntp) => '${select(ntp)}'), - onChanged: _ntpWriter( - (state, value) => update(state, int.parse(value)), - ), - maxLength: maxLength, - keyboardType: TextInputType.number, - normalize: (value) => value.trim(), - validator: (value, l) { - final number = int.tryParse(value ?? ''); - return number == null || number < min || number > max - ? l.numberTip(title(l)) - : null; - }, - ); - } - - return switch (overrideKey) { - NtpOverrideKey.enable => toggle( - (ntp) => ntp.enable, - (state, value) => state.copyWith.ntp(enable: value), - ), - NtpOverrideKey.server => text( - (ntp) => ntp.server, - (state, value) => state.copyWith.ntp(server: value), - maxLength: TextInputLimits.domain, - ), - NtpOverrideKey.port => number( - (ntp) => ntp.port, - (state, value) => state.copyWith.ntp(port: value), - maxLength: TextInputLimits.port, - min: 1, - max: 65535, - ), - NtpOverrideKey.interval => number( - (ntp) => ntp.interval, - (state, value) => state.copyWith.ntp(interval: value), - min: 1, - ), - NtpOverrideKey.dialerProxy => text( - (ntp) => ntp.dialerProxy, - (state, value) => state.copyWith.ntp(dialerProxy: value), - maxLength: TextInputLimits.groupName, - ), - NtpOverrideKey.writeToSystem => toggle( - (ntp) => ntp.writeToSystem, - (state, value) => state.copyWith.ntp(writeToSystem: value), - ), - }; - } -} - -class _RemoveButton extends ConsumerWidget { - const _RemoveButton(this.overrideKey); - - final NtpOverrideKey overrideKey; + Ntp get defaults => defaultNtp; @override - Widget build(BuildContext context, WidgetRef ref) { - return CommonMinIconButtonTheme( - child: ElasticButton( - child: IconButton.filledTonal( - tooltip: context.appLocalizations.remove, - onPressed: () => ref - .read(patchClashConfigProvider.notifier) - .update( - (state) => state.copyWith( - ntpOverrideKeys: {...state.ntpOverrideKeys} - ..remove(overrideKey), - ), - ), - icon: const GlyphIcon(AppGlyphs.remove, size: 18, fill: 1), - padding: EdgeInsets.zero, - ), - ), - ); - } + String label(NtpOverrideKey key, AppLocalizations l) => key.label(l); + + @override + ConfigLabel? description(NtpOverrideKey key) => key.description; + + @override + OverrideField fieldOf(NtpOverrideKey key) => switch (key) { + NtpOverrideKey.enable => ToggleOverrideField( + (ntp) => ntp.enable, + (ntp, value) => ntp.copyWith(enable: value), + ), + NtpOverrideKey.server => TextOverrideField( + (ntp) => ntp.server, + (ntp, value) => ntp.copyWith(server: value), + maxLength: TextInputLimits.domain, + validator: validateHost, + ), + NtpOverrideKey.port => NumberOverrideField( + (ntp) => ntp.port, + (ntp, value) => ntp.copyWith(port: value), + maxLength: TextInputLimits.port, + min: 1, + max: 65535, + ), + NtpOverrideKey.interval => NumberOverrideField( + (ntp) => ntp.interval, + (ntp, value) => ntp.copyWith(interval: value), + min: 1, + ), + NtpOverrideKey.dialerProxy => TextOverrideField( + (ntp) => ntp.dialerProxy, + (ntp, value) => ntp.copyWith(dialerProxy: value), + maxLength: TextInputLimits.groupName, + ), + NtpOverrideKey.writeToSystem => ToggleOverrideField( + (ntp) => ntp.writeToSystem, + (ntp, value) => ntp.copyWith(writeToSystem: value), + ), + }; } diff --git a/lib/views/config/override.dart b/lib/views/config/override.dart new file mode 100644 index 00000000..7e965d1f --- /dev/null +++ b/lib/views/config/override.dart @@ -0,0 +1,667 @@ +import 'package:fl_clash/common/common.dart'; +import 'package:fl_clash/enum/enum.dart'; +import 'package:fl_clash/features/form/form.dart'; +import 'package:fl_clash/icons/icons.dart'; +import 'package:fl_clash/l10n/l10n.dart'; +import 'package:fl_clash/models/models.dart'; +import 'package:fl_clash/pages/editor.dart'; +import 'package:fl_clash/providers/config.dart'; +import 'package:fl_clash/providers/database.dart'; +import 'package:fl_clash/providers/state.dart'; +import 'package:fl_clash/widgets/widgets.dart'; +import 'package:material_ui/material_ui.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +typedef OverrideState = ({M model, Set keys}); + +abstract interface class OverrideModel { + ProviderListenable selectModel(T Function(M model) select); + + ConfigWriter modelWriter(M Function(M model, T value) update); + + bool get emptyCancels; +} + +/// Where an override page keeps its model and the keys picked from it. +abstract class OverrideTarget + implements OverrideModel { + const OverrideTarget(); + + ConfigLabel get description; + + ConfigLabel get entries; + + ConfigLabel get addEntry; + + ProviderListenable>? get issues => null; + + ProviderListenable select(T Function(OverrideState state) select); + + OverrideState read(WidgetRef ref); + + void update( + WidgetRef ref, + OverrideState Function(OverrideState state) update, + ); + + @override + ProviderListenable selectModel(T Function(M model) select) => + this.select((state) => select(state.model)); + + @override + ConfigWriter modelWriter(M Function(M model, T value) update) => + (ref, value) => this.update( + ref, + (state) => (model: update(state.model, value), keys: state.keys), + ); + + @override + bool get emptyCancels => true; +} + +class PatchOverrideTarget + extends OverrideTarget { + const PatchOverrideTarget({ + required this.get, + required this.set, + required this.description, + }); + + final OverrideState Function(PatchClashConfig config) get; + final PatchClashConfig Function( + PatchClashConfig config, + OverrideState state, + ) + set; + + @override + final ConfigLabel description; + + @override + ConfigLabel get entries => + (l) => l.overrideEntries; + + @override + ConfigLabel get addEntry => + (l) => l.addOverrideEntry; + + /// An emptied entry overrides the profile's own value with nothing. + @override + bool get emptyCancels => false; + + @override + ProviderListenable select( + T Function(OverrideState state) select, + ) => patchClashConfigProvider.select((config) => select(get(config))); + + @override + OverrideState read(WidgetRef ref) => + get(ref.read(patchClashConfigProvider)); + + @override + void update( + WidgetRef ref, + OverrideState Function(OverrideState state) update, + ) { + ref + .read(patchClashConfigProvider.notifier) + .update((config) => set(config, update(get(config)))); + } +} + +class ProfileOverrideTarget + extends OverrideTarget { + const ProfileOverrideTarget( + this.profileId, { + required this.get, + required this.set, + this.issues, + }); + + final int profileId; + + @override + final ProviderListenable>? issues; + final OverrideState Function(ProfileOverrides overrides) get; + final ProfileOverrides Function( + ProfileOverrides overrides, + OverrideState state, + ) + set; + + @override + ConfigLabel get description => + (l) => l.profileSettingsDesc; + + @override + ConfigLabel get entries => + (l) => l.settingEntries; + + @override + ConfigLabel get addEntry => + (l) => l.addSettingEntry; + + ProfileOverrides _of(Profile? profile) => + profile?.overrides ?? const ProfileOverrides(); + + @override + ProviderListenable select( + T Function(OverrideState state) select, + ) => + profileProvider(profileId).select((profile) => select(get(_of(profile)))); + + @override + OverrideState read(WidgetRef ref) => + get(_of(ref.read(profileProvider(profileId)))); + + @override + void update( + WidgetRef ref, + OverrideState Function(OverrideState state) update, + ) { + ref + .read(profilesProvider.notifier) + .updateProfile( + profileId, + (profile) => profile.copyWith( + overrides: set(profile.overrides, update(get(profile.overrides))), + ), + ); + } +} + +abstract class OverrideField { + const OverrideField(); + + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }); + + M reset(M model, M defaults); +} + +abstract class ValueOverrideField extends OverrideField { + const ValueOverrideField(this.select, this.update); + + final T Function(M model) select; + final M Function(M model, T value) update; + + @override + M reset(M model, M defaults) => update(model, select(defaults)); +} + +class ToggleOverrideField extends ValueOverrideField { + const ToggleOverrideField(super.select, super.update); + + @override + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }) { + return ConfigToggleItem( + leading: leading, + title: title, + selector: model.selectModel(select), + onChanged: model.modelWriter(update), + ); + } +} + +class OptionsOverrideField extends ValueOverrideField { + const OptionsOverrideField( + this.options, + super.select, + super.update, { + this.labelOf = _enumName, + }); + + final List options; + final String Function(T value) labelOf; + + static String _enumName(Enum value) => value.name; + + @override + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }) { + return ConfigOptionsItem( + leading: leading, + title: title, + options: options, + textBuilder: labelOf, + selector: model.selectModel(select), + onChanged: model.modelWriter(update), + ); + } +} + +class NumberOverrideField extends ValueOverrideField { + const NumberOverrideField( + super.select, + super.update, { + this.min = 0, + this.max = 0x7fffffff, + this.maxLength = TextInputLimits.number, + }); + + final int min; + final int max; + final int maxLength; + + String? _validate(String? value, ConfigLabel title) { + final number = int.tryParse(value?.trim() ?? ''); + return number == null || number < min || number > max + ? currentAppLocalizations.numberTip(title(currentAppLocalizations)) + : null; + } + + @override + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }) { + return ConfigTextItem( + leading: leading, + title: title, + selector: model.selectModel((model) => '${select(model)}'), + onChanged: model.modelWriter( + (model, value) => update(model, int.parse(value)), + ), + maxLength: maxLength, + keyboardType: TextInputType.number, + normalize: (value) => value.trim(), + validator: (value, _) => _validate(value, title), + allowEmpty: model.emptyCancels, + ); + } +} + +class TextOverrideField extends ValueOverrideField { + const TextOverrideField( + super.select, + super.update, { + required this.maxLength, + this.validator, + }); + + final int maxLength; + final ConfigValidator? validator; + + @override + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }) { + return ConfigTextItem( + leading: leading, + title: title, + selector: model.selectModel(select), + onChanged: model.modelWriter(update), + maxLength: maxLength, + normalize: (value) => value.trim(), + validator: validator, + allowEmpty: true, + ); + } +} + +class ListOverrideField extends ValueOverrideField> { + const ListOverrideField( + super.select, + super.update, { + required this.itemMaxLength, + this.itemValidator, + }); + + final int itemMaxLength; + final ConfigValidator? itemValidator; + + @override + Widget build( + OverrideModel model, { + required ConfigLabel title, + required Widget leading, + }) { + return ConfigListEditItem( + leading: leading, + title: title, + selector: model.selectModel(select), + onChanged: model.modelWriter(update), + itemMaxLength: itemMaxLength, + itemValidator: itemValidator, + ); + } +} + +abstract class OverrideSpec { + const OverrideSpec(); + + OverrideTarget get target; + + String title(AppLocalizations l); + + List get values; + + NullStatusIllustration get illustration; + + /// What a newly added key starts from. + M get defaults; + + String sectionOf(K key, AppLocalizations l) => l.options; + + String label(K key, AppLocalizations l); + + ConfigLabel? description(K key); + + OverrideField fieldOf(K key); + + /// Also resets the values of [keys], so a profile with no keys is untouched. + OverrideState withoutKeys(OverrideState state, Set keys) { + var model = state.model; + for (final key in keys) { + model = fieldOf(key).reset(model, defaults); + } + return (model: model, keys: state.keys.difference(keys)); + } + + List<(String, List)> sectionsOf(AppLocalizations l, Iterable keys) { + final sections = >{}; + for (final key in keys) { + sections.putIfAbsent(sectionOf(key, l), () => []).add(key); + } + return [ + for (final MapEntry(:key, :value) in sections.entries) (key, value), + ]; + } +} + +mixin OverrideQuickEdit on OverrideSpec { + String overrideYaml(M model, Set keys); + + /// Throws when [content] names a key outside [values] or a value the model + /// cannot hold. + OverrideState applyOverrideYaml(M model, String content); + + EditorSchema get schema; +} + +class OverrideView extends ConsumerWidget { + const OverrideView({super.key, required this.spec}); + + final OverrideSpec spec; + + void _handleAdd(BuildContext context, WidgetRef ref) { + final appLocalizations = context.appLocalizations; + final added = spec.target.read(ref).keys; + final remaining = spec.values.where((key) => !added.contains(key)); + showSheet( + context: context, + builder: (_) => SelectionSheet( + title: spec.target.addEntry(appLocalizations), + sections: [ + for (final (label, keys) in spec.sectionsOf( + appLocalizations, + remaining, + )) + SelectionSection( + label: label, + items: keys, + subtitleBuilder: (context, key) => + spec.description(key)?.call(context.appLocalizations), + ), + ], + labelBuilder: (key) => spec.label(key, appLocalizations), + selectedOf: (_) => null, + removeOnSelect: true, + onSelected: (key) { + if (!context.mounted) { + return; + } + spec.target.update( + ref, + (state) => ( + model: spec.fieldOf(key).reset(state.model, spec.defaults), + keys: {...state.keys, key}, + ), + ); + }, + ), + ); + } + + Future _handleQuickEdit( + BuildContext context, + WidgetRef ref, + OverrideQuickEdit quickEdit, + ) { + final state = spec.target.read(ref); + final raw = quickEdit.overrideYaml(state.model, state.keys); + final page = EditorPage( + title: spec.title(context.appLocalizations), + content: raw, + schema: quickEdit.schema, + readOnly: false, + onPop: (_, _, content) => + _handleQuickEditPop(ref, quickEdit, content, raw), + ); + return sheetNavigatorOf(context).push( + context.isMobileView + ? CommonRoute(builder: (_) => page) + : CommonDesktopRoute(builder: (_) => page), + ); + } + + Future _handleQuickEditPop( + WidgetRef ref, + OverrideQuickEdit quickEdit, + String content, + String raw, + ) async { + if (content == raw) { + return true; + } + try { + spec.target.update(ref, (state) { + final listed = quickEdit.applyOverrideYaml(state.model, raw).keys; + final next = readRelaxed( + content, + quickEdit.schema, + (content) => quickEdit.applyOverrideYaml(state.model, content), + ); + final stillSet = quickEdit + .applyOverrideYaml( + next.model, + quickEdit.overrideYaml(next.model, next.keys), + ) + .keys; + return spec.withoutKeys(( + model: next.model, + keys: {...next.keys, ...state.keys.difference(listed)}, + ), listed.difference(stillSet)); + }); + return true; + } catch (error) { + final res = await dialogs.showMessage( + message: TextSpan( + text: + '${compactError(error)}\n\n' + '${currentAppLocalizations.discardChanges}', + ), + ); + return res == true; + } + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appLocalizations = context.appLocalizations; + final spec = this.spec; + final target = spec.target; + final (:isEmpty, :canAdd) = ref.watch( + target.select( + (state) => ( + isEmpty: !spec.values.any(state.keys.contains), + canAdd: spec.values.any((key) => !state.keys.contains(key)), + ), + ), + ); + final addEntry = target.addEntry(appLocalizations); + return CommonScaffold( + title: spec.title(appLocalizations), + iconActions: [ + if (!isEmpty) + IconButtonData( + glyph: AppGlyphs.addCircle, + onPressed: canAdd ? () => _handleAdd(context, ref) : null, + tooltip: addEntry, + ), + if (spec is OverrideQuickEdit) + IconButtonData( + glyph: AppGlyphs.compose, + onPressed: () => _handleQuickEdit(context, ref, spec), + tooltip: appLocalizations.quickEdit, + ), + ], + body: NullStatusSwitcher( + isEmpty: isEmpty, + nullStatus: NullStatus( + label: appLocalizations.nullTip(target.entries(appLocalizations)), + description: target.description(appLocalizations), + illustration: spec.illustration, + action: ElasticButton( + child: FilledButton.tonalIcon( + onPressed: () => _handleAdd(context, ref), + icon: const GlyphIcon(AppGlyphs.addCircle, fill: 1), + label: Text(addEntry), + ), + ), + ), + child: _OverrideList(spec: spec), + ), + ); + } +} + +class _OverrideList extends ConsumerWidget { + const _OverrideList({required this.spec}); + + final OverrideSpec spec; + + String? _footerOf(List keys, AppLocalizations appLocalizations) { + final lines = [ + for (final key in keys) ?spec.description(key)?.call(appLocalizations), + ]; + return lines.isEmpty ? null : lines.join('\n'); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final appLocalizations = context.appLocalizations; + final keys = ref.watch(spec.target.select((state) => state.keys)); + final entries = spec.values.where(keys.contains); + return ListView( + padding: const EdgeInsets.symmetric( + horizontal: 16, + ).copyWith(top: context.contentTopPadding, bottom: 16), + children: [ + if (spec.target.issues case final issues?) + CustomIssuesBanner(issues: ref.watch(issues)), + AnimatedEntries( + children: [ + for (final (title, keys) in spec.sectionsOf( + appLocalizations, + entries, + )) + KeyedSubtree( + key: ValueKey(title), + child: generateAnimatedSection( + title: title, + footer: _footerOf(keys, appLocalizations), + items: [ + for (final key in keys) + KeyedSubtree( + key: ValueKey(key), + child: spec + .fieldOf(key) + .build( + spec.fieldOf(key) is ValueOverrideField && + spec.target.emptyCancels + ? _ListedValue(spec, key) + : spec.target, + title: (l) => spec.label(key, l), + leading: _RemoveButton( + spec: spec, + overrideKey: key, + ), + ), + ), + ], + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 16.mAp), + child: ListFooter(text: spec.target.description(appLocalizations)), + ), + ], + ); + } +} + +/// A key's own value; emptying it takes the key off the list. +class _ListedValue implements OverrideModel { + const _ListedValue(this.spec, this.overrideKey); + + final OverrideSpec spec; + final K overrideKey; + + OverrideTarget get target => spec.target; + + @override + bool get emptyCancels => true; + + static bool _isEmpty(Object? value) => switch (value) { + String() => value.isEmpty, + Iterable() => value.isEmpty, + Map() => value.isEmpty, + _ => false, + }; + + @override + ProviderListenable selectModel(T Function(M model) select) => + target.selectModel(select); + + @override + ConfigWriter modelWriter(M Function(M model, T value) update) => + (ref, value) => target.update( + ref, + (state) => _isEmpty(value) + ? spec.withoutKeys(state, {overrideKey}) + : (model: update(state.model, value), keys: state.keys), + ); +} + +class _RemoveButton extends ConsumerWidget { + const _RemoveButton({required this.spec, required this.overrideKey}); + + final OverrideSpec spec; + final K overrideKey; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return EntryButton.remove( + onPressed: () => spec.target.update( + ref, + (state) => spec.withoutKeys(state, {overrideKey}), + ), + ); + } +} diff --git a/lib/views/config/providers.dart b/lib/views/config/providers.dart index f050771d..dd494fee 100644 --- a/lib/views/config/providers.dart +++ b/lib/views/config/providers.dart @@ -14,9 +14,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; class ClashProvidersView extends ConsumerStatefulWidget { - const ClashProvidersView({super.key, required this.kind}); - - final ProviderKind kind; + const ClashProvidersView({super.key}); @override ConsumerState createState() => _ClashProvidersViewState(); @@ -25,36 +23,17 @@ class ClashProvidersView extends ConsumerStatefulWidget { class _ClashProvidersViewState extends ConsumerState { late final ClashProvidersAction _providersAction; - ProviderKind get _kind => widget.kind; - @override void initState() { super.initState(); _providersAction = ref.read(clashProvidersActionProvider.notifier); } - String get _template => switch (_kind) { - ProviderKind.proxy => proxyProviderTemplate, - ProviderKind.rule => ruleProviderTemplate, - }; - - String _sectionLabel(BuildContext context) { - final appLocalizations = context.appLocalizations; - return switch (_kind) { - ProviderKind.proxy => appLocalizations.proxyProviders, - ProviderKind.rule => appLocalizations.ruleProviders, - }; - } - - List get _providers => - ref.read(clashProvidersProvider(_kind)).value ?? []; - Set _reservedLabels({ClashProvider? except}) { return { - for (final item in _providers) + for (final item + in ref.read(clashProvidersProvider).value ?? const []) if (item.id != except?.id) item.label, - if (_kind == ProviderKind.proxy) - ...ref.read(profileProvidersProvider).keys, }; } @@ -74,12 +53,11 @@ class _ClashProvidersViewState extends ConsumerState { return null; } - String _uniqueLabel(String name) { - final reserved = _reservedLabels(); + String _uniqueLabel(String name, [Set? reserved]) { return uniqueLabelFor( name, - fallback: _sectionLabel(context), - taken: reserved.contains, + fallback: context.appLocalizations.ruleProviders, + taken: (reserved ?? _reservedLabels()).contains, ); } @@ -87,80 +65,58 @@ class _ClashProvidersViewState extends ConsumerState { ClashProvider provider, { ClashProvider? previous, List? content, + bool refresh = false, }) async { - final appLocalizations = context.appLocalizations; - final conflicts = await _providersAction.putProvider( - provider, - previous: previous, - content: content, - ); - if (conflicts.isEmpty) { - return true; - } - await dialogs.showMessage( - title: appLocalizations.tip, - message: TextSpan( - text: appLocalizations.providerRenameShadowed( - conflicts.map((profile) => profile.realLabel).join(', '), - provider.label, - ), - ), - cancelable: false, - ); - return false; - } - - /// The core has no default for behavior and refuses the whole config without - /// one, so a new rule set names it before anything is written. - Future _resolveBehavior(ClashProvider provider) async { - if (provider.kind != ProviderKind.rule) { - return provider; - } - final res = await _showBehaviorDialog(provider); - return res == null ? null : provider.copyWith(behavior: res); - } - - Future _showBehaviorDialog( - ClashProvider provider, - ) async { - final options = provider.format.behaviors; - return dialogs.showCommonDialog( - child: OptionsDialog( - title: context.appLocalizations.behavior, - options: options, - textBuilder: (item) => item.name, - value: options.contains(provider.behavior) - ? provider.behavior! - : options.first, + final saved = await globalState.loadingRun( + () => _providersAction.putProvider( + provider, + previous: previous, + content: content, + refresh: refresh, ), + title: provider.label, + tag: LoadingTag.ruleProviders, ); + return saved != null; } Future _handleImportFromUrl() async { final appLocalizations = context.appLocalizations; - final res = await dialogs.showNamedUrlInput( + final res = await dialogs.showNamedUrlsInput( title: appLocalizations.importUrl, labelValidator: (value) => _validateLabel(value, optional: true), urlValidator: _validateProviderUrl, + existingUrls: { + for (final item + in ref.read(clashProvidersProvider).value ?? + const []) + if (item.isRemote) item.url, + }, ); if (res == null || !mounted) { return; } - final fileName = res.url.urlFileName; - final label = res.label.isNotEmpty - ? res.label - : _uniqueLabel(fileName.fileStem); - final provider = await _resolveBehavior( - ClashProvider.create( - kind: _kind, - label: label, - url: res.url, - ).withFileFormat(fileName), - ); - if (provider == null) { + if (res case [final single]) { + final label = single.label.isNotEmpty + ? single.label + : _uniqueLabel(single.url.urlFileName.fileStem); + await _put(ClashProvider.create(label: label, url: single.url)); return; } - await _put(provider); + final reserved = _reservedLabels(); + final providers = []; + for (final item in res) { + final label = _uniqueLabel(item.url.urlFileName.fileStem, reserved); + reserved.add(label); + providers.add(ClashProvider.create(label: label, url: item.url)); + } + await globalState.batchRun( + providers, + _providersAction.putProvider, + label: (provider) => provider.url, + concurrency: maxConcurrentImports, + tag: LoadingTag.ruleProviders, + ); } Future _handleImportFromFile() async { @@ -172,16 +128,10 @@ class _ClashProvidersViewState extends ConsumerState { if (!mounted) { return; } - final provider = await _resolveBehavior( - ClashProvider.create( - kind: _kind, - label: _uniqueLabel(file.name.fileStem), - ).withFileFormat(file.name), + await _put( + ClashProvider.create(label: _uniqueLabel(file.name.fileStem)), + content: bytes, ); - if (provider == null) { - return; - } - await _put(provider, content: bytes); } Future _showNameDialog({ClashProvider? except}) async { @@ -222,15 +172,8 @@ class _ClashProvidersViewState extends ConsumerState { ); return; } - var next = provider?.copyWith(label: label); - if (next == null) { - next = await _resolveBehavior( - ClashProvider.create(kind: _kind, label: label), - ); - if (next == null) { - return; - } - } + final next = + provider?.copyWith(label: label) ?? ClashProvider.create(label: label); if (!await _put(next, previous: provider, content: utf8.encode(content))) { return; } @@ -269,7 +212,7 @@ class _ClashProvidersViewState extends ConsumerState { EditorPage( titleEditable: true, title: provider?.label ?? '', - load: () async => raw = (await provider?.content) ?? _template, + load: () async => raw = (await provider?.content) ?? '', schema: EditorSchema.provider, onSave: (_, title, content) => _handleEditorSave(title, content, provider: provider), @@ -287,132 +230,120 @@ class _ClashProvidersViewState extends ConsumerState { context, EditorPage( title: provider.label, - load: () async => core.dumpRuleSet(await provider.path), + load: () async => provider.isTextContent + ? (await provider.content) ?? '' + : core.dumpRuleSet(await provider.path), schema: EditorSchema.provider, ), ), ); } - Future _handleEditOptions(ClashProvider provider) async { - final res = await dialogs.showCommonDialog( - child: _ProviderDialog( - provider: provider, - reservedLabels: _reservedLabels(except: provider), - ), - ); - if (res == null) { - return; - } - await _put(res, previous: provider); - } + Future _handleEditOptions(ClashProvider provider) => + _showClashProviderOptions(ref, provider); Future _handleDelete(ClashProvider provider) async { final appLocalizations = context.appLocalizations; - var users = await _providersAction.profilesUsing(provider); - if (users.isEmpty) { - final res = await dialogs.showMessage( - title: appLocalizations.tip, - message: TextSpan(text: appLocalizations.deleteTip(provider.label)), - ); - if (res != true) { - return; - } - users = await _providersAction.delProvider(provider); - if (users.isEmpty) { - return; - } - } - await dialogs.showMessage( + final users = await _providersAction.profilesUsing(provider); + final res = await dialogs.showMessage( title: appLocalizations.tip, message: TextSpan( - text: appLocalizations.providerInUse( - provider.label, - users.map((profile) => profile.realLabel).join(', '), - ), + text: users.isEmpty + ? appLocalizations.deleteTip(provider.label) + : appLocalizations.providerInUse( + provider.label, + users.map((profile) => profile.realLabel).join(', '), + ), ), - cancelable: false, ); + if (res != true) { + return; + } + _providersAction.delProvider(provider); } bool _isEditable(ClashProvider provider) => !provider.isRemote && provider.isTextContent; - Widget _buildItem(ClashProvider provider, int index, int length) { + List _buildMenuItems(ClashProvider provider) { final appLocalizations = context.appLocalizations; final editable = _isEditable(provider); - return ItemPositionProvider( - position: ItemPosition.get(index, length), - child: DecorationListItem( - contentPadding: const EdgeInsets.only(left: 16, right: 6), - title: Text( - provider.label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: Text( - provider.isRemote ? provider.url : appLocalizations.file, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - trailing: CommonPopupBox( - popupBuilder: (_) => CommonPopupMenu( - items: [ - if (editable) - CommonPopupMenuItem( - glyph: AppGlyphs.compose, - label: appLocalizations.edit, - onPressed: () { - _handleToEditor(provider); - }, - ), - if (!provider.isTextContent) - CommonPopupMenuItem( - glyph: AppGlyphs.eye, - label: appLocalizations.preview, - onPressed: () { - _handlePreview(provider); - }, - ), - CommonPopupMenuItem( - glyph: AppGlyphs.settings, - label: appLocalizations.options, - onPressed: () { - _handleEditOptions(provider); - }, - ), - CommonPopupMenuItem( - danger: true, - glyph: AppGlyphs.delete, - label: appLocalizations.delete, - onPressed: () { - _handleDelete(provider); - }, - ), - ], - ), - targetBuilder: (open) { - return IconButton( - style: IconButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: VisualDensity.standard, - ), - tooltip: appLocalizations.more, - onPressed: () { - open(); - }, - icon: const GlyphIcon(AppGlyphs.more), - ); + return [ + if (editable) + CommonPopupMenuItem( + glyph: AppGlyphs.compose, + label: appLocalizations.edit, + onPressed: () { + _handleToEditor(provider); }, ), + if (!provider.isTextContent) + CommonPopupMenuItem( + glyph: AppGlyphs.eye, + label: appLocalizations.preview, + onPressed: () { + _handlePreview(provider); + }, + ), + if (provider.isRemote) + CommonPopupMenuItem( + glyph: AppGlyphs.sync, + label: appLocalizations.sync, + onPressed: () { + _put(provider, previous: provider, refresh: true); + }, + ), + CommonPopupMenuItem( + glyph: AppGlyphs.settings, + label: appLocalizations.options, onPressed: () { - if (editable) { - _handleToEditor(provider); - } else { - _handleEditOptions(provider); - } + _handleEditOptions(provider); }, ), + CommonPopupMenuItem( + danger: true, + glyph: AppGlyphs.delete, + label: appLocalizations.delete, + onPressed: () { + _handleDelete(provider); + }, + ), + ]; + } + + Widget _buildItem(List providers, int index) { + final provider = providers[index]; + final editable = _isEditable(provider); + return SortableItem( + key: ValueKey(provider.id), + index: index, + child: ContextMenuRegion( + menuItems: _buildMenuItems(provider), + child: ItemPositionProvider( + position: ItemPosition.get(index, providers.length), + child: DecorationListItem( + contentPadding: const EdgeInsets.only(left: 16), + title: ChipTitle( + title: provider.label, + chip: provider.behavior.name, + ), + subtitle: Text( + provider.isRemote + ? context.appLocalizations.remote + : context.appLocalizations.local, + style: context.listCaptionStyle, + ), + trailing: _RuleSetUpdateTime(provider: provider), + onPressed: () { + if (editable) { + _handleToEditor(provider); + } else { + _handlePreview(provider); + } + }, + ), + ), + ), ); } @@ -422,11 +353,8 @@ class _ClashProvidersViewState extends ConsumerState { isLoading: loading, isEmpty: providers.isEmpty, nullStatus: NullStatus( - illustration: switch (_kind) { - ProviderKind.proxy => NullStatusIllustration.proxies, - ProviderKind.rule => NullStatusIllustration.rules, - }, - label: appLocalizations.nullTip(_sectionLabel(context)), + illustration: NullStatusIllustration.rules, + label: appLocalizations.nullTip(appLocalizations.ruleProviders), ), child: ReorderableListView.builder( padding: const EdgeInsets.all( @@ -434,21 +362,15 @@ class _ClashProvidersViewState extends ConsumerState { ).copyWith(top: context.contentTopPadding), buildDefaultDragHandles: false, itemCount: providers.length, - itemBuilder: (_, index) { - return ReorderableDelayedDragStartListener( - key: ValueKey(providers[index].id), - index: index, - child: _buildItem(providers[index], index, providers.length), - ); - }, + itemBuilder: (_, index) => _buildItem(providers, index), proxyDecorator: (_, index, animation) { return commonProxyDecorator( - _buildItem(providers[index], index, providers.length), + _buildItem(providers, index), index, animation, ); }, - onReorderItem: ref.read(clashProvidersProvider(_kind).notifier).order, + onReorderItem: ref.read(clashProvidersProvider.notifier).order, ), ); } @@ -456,17 +378,21 @@ class _ClashProvidersViewState extends ConsumerState { @override Widget build(BuildContext context) { final appLocalizations = context.appLocalizations; - final state = ref.watch(clashProvidersProvider(_kind)); + final state = ref.watch(clashProvidersProvider); + final providers = state.value ?? []; return CommonScaffold( + canSort: providers.length > 1, + isLoading: ref.watch(loadingProvider(LoadingTag.ruleProviders)), actions: [ CommonPopupBox( targetBuilder: (open) { - return FilledButton.tonal( + return IconButton( + tooltip: appLocalizations.add, onPressed: () { final isMobile = ref.read(isMobileViewProvider); open(offset: Offset(0, isMobile ? 0 : 20)); }, - child: Text(appLocalizations.add), + icon: const GlyphIcon(AppGlyphs.addCircle), ); }, popupBuilder: (_) => CommonPopupMenu( @@ -490,12 +416,85 @@ class _ClashProvidersViewState extends ConsumerState { ), ), ], - body: _buildContent(state.value ?? [], loading: state.isLoading), - title: _sectionLabel(context), + body: _buildContent(providers, loading: state.isLoading), + title: appLocalizations.ruleProviders, ); } } +/// The update time is the cached file's, so it is read again whenever a save or +/// sync on this page finishes. +class _RuleSetUpdateTime extends ConsumerStatefulWidget { + final ClashProvider provider; + + const _RuleSetUpdateTime({required this.provider}); + + @override + ConsumerState<_RuleSetUpdateTime> createState() => _RuleSetUpdateTimeState(); +} + +class _RuleSetUpdateTimeState extends ConsumerState<_RuleSetUpdateTime> { + late Future _fileInfo = widget.provider.fileInfo; + + @override + void didUpdateWidget(covariant _RuleSetUpdateTime oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.provider != widget.provider) { + _fileInfo = widget.provider.fileInfo; + } + } + + @override + Widget build(BuildContext context) { + ref.listen(loadingProvider(LoadingTag.ruleProviders), (previous, next) { + if (previous == true && !next) { + setState(() { + _fileInfo = widget.provider.fileInfo; + }); + } + }); + // Inset here, not in the row, so the sort handle that replaces it sits flush. + return Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: FutureBuilder( + future: _fileInfo, + builder: (context, snapshot) => LastUpdateTimeText( + lastUpdateDate: snapshot.data?.lastModified, + compact: true, + style: context.listCaptionStyle, + ), + ), + ); + } +} + +Future _showClashProviderOptions( + WidgetRef ref, + ClashProvider provider, +) async { + final res = await dialogs.showCommonDialog( + child: _ProviderDialog( + provider: provider, + reservedLabels: { + for (final item + in ref.read(clashProvidersProvider).value ?? + const []) + if (item.id != provider.id) item.label, + }, + ), + ); + if (res == null) { + return; + } + await globalState.loadingRun( + () => ref + .read(clashProvidersActionProvider.notifier) + .putProvider(res, previous: provider), + title: res.label, + tag: LoadingTag.ruleProviders, + ); +} + String? _validateProviderUrl(String? value) { final appLocalizations = currentAppLocalizations; final url = value?.trim() ?? ''; @@ -523,19 +522,14 @@ class _ProviderDialogState extends State<_ProviderDialog> { final _formKey = GlobalKey(); late final TextEditingController _labelController; late final TextEditingController _urlController; - late ClashProvider _draft; ClashProvider get _provider => widget.provider; - ProviderKind get _kind => _provider.kind; - @override void initState() { super.initState(); _labelController = TextEditingController(text: _provider.label); - _urlController = TextEditingController(text: _provider.url) - ..addListener(_followUrlFormat); - _draft = _provider.withFormat(_provider.format ?? RuleProviderFormat.yaml); + _urlController = TextEditingController(text: _provider.url); } @override @@ -545,23 +539,6 @@ class _ProviderDialogState extends State<_ProviderDialog> { super.dispose(); } - void _setFormat(RuleProviderFormat format) { - if (format == _draft.format) { - return; - } - setState(() { - _draft = _draft.withFormat(format); - }); - } - - /// A url whose extension names a format follows it, as an import does. - void _followUrlFormat() { - final format = ruleProviderFormatOf(_urlController.text.trim().urlFileName); - if (format != null) { - _setFormat(format); - } - } - String? _validateLabel(String? value) { final appLocalizations = context.appLocalizations; final label = value?.trim() ?? ''; @@ -579,47 +556,13 @@ class _ProviderDialogState extends State<_ProviderDialog> { return; } Navigator.of(context).pop( - _draft.copyWith( + _provider.copyWith( label: _labelController.text.trim(), url: _urlController.text.trim(), ), ); } - Future _handleSelectFormat() async { - final res = await dialogs.showCommonDialog( - context: context, - child: OptionsDialog( - title: context.appLocalizations.format, - options: RuleProviderFormat.values, - textBuilder: (item) => item.name, - value: _draft.format!, - ), - ); - if (res == null) { - return; - } - _setFormat(res); - } - - Future _handleSelectBehavior() async { - final res = await dialogs.showCommonDialog( - context: context, - child: OptionsDialog( - title: context.appLocalizations.behavior, - options: _draft.format.behaviors, - textBuilder: (item) => item.name, - value: _draft.behavior!, - ), - ); - if (res == null) { - return; - } - setState(() { - _draft = _draft.copyWith(behavior: res); - }); - } - Widget _buildField({ required TextEditingController controller, required String label, @@ -672,28 +615,6 @@ class _ProviderDialogState extends State<_ProviderDialog> { validator: _validateProviderUrl, keyboardType: TextInputType.url, ), - if (_kind == ProviderKind.rule) ...[ - ListItem( - padding: EdgeInsets.zero, - title: Text(appLocalizations.format), - trailing: ElasticButton( - child: FilledButton( - onPressed: _handleSelectFormat, - child: Text(_draft.format!.name), - ), - ), - ), - ListItem( - padding: EdgeInsets.zero, - title: Text(appLocalizations.behavior), - trailing: ElasticButton( - child: FilledButton( - onPressed: _handleSelectBehavior, - child: Text(_draft.behavior!.name), - ), - ), - ), - ], ], ), ), diff --git a/lib/views/config/rules.dart b/lib/views/config/rules.dart index 3e577503..6e5cb0fb 100644 --- a/lib/views/config/rules.dart +++ b/lib/views/config/rules.dart @@ -44,10 +44,9 @@ class _AddedRulesViewState extends ConsumerState { ref.read(globalRulesProvider.notifier).put(res); } - void _handleQuickAdd() { + void _handleQuickActions() { showSheet( context: context, - props: const SheetProps(isScrollControlled: true), builder: (_) => RulePresetSheet(onAdd: ref.read(globalRulesProvider.notifier).putAll), ); @@ -126,19 +125,18 @@ class _AddedRulesViewState extends ConsumerState { searchState: AppBarSearchState(onSearch: _handleSearch), actions: [ if (!isSelecting) - FilledButton.tonal( - onPressed: () { - _handleAddOrUpdate(); - }, - child: Text(appLocalizations.add), + IconButton( + tooltip: appLocalizations.add, + onPressed: _handleAddOrUpdate, + icon: const GlyphIcon(AppGlyphs.addCircle), ), ], iconActions: [ if (!isSelecting) IconButtonData( glyph: AppGlyphs.bolt, - onPressed: _handleQuickAdd, - tooltip: appLocalizations.quickAdd, + onPressed: _handleQuickActions, + tooltip: appLocalizations.quickActions, ), ], selectionActions: isSelecting ? selectionActions : const [], @@ -150,33 +148,45 @@ class _AddedRulesViewState extends ConsumerState { label: appLocalizations.nullTip(appLocalizations.rule), illustration: NullStatusIllustration.rules, ), - child: isSearching - ? ListView.builder( - padding: _listPadding, - itemBuilder: (_, index) => - _buildRuleItem(rules, index, selectedRules), - itemExtent: ruleItemHeight, - itemCount: rules.length, - ) - : ReorderableList( - padding: _listPadding, - itemBuilder: (_, index) => - ReorderableDelayedDragStartListener( - key: ObjectKey(rules[index]), - index: index, - child: _buildRuleItem(rules, index, selectedRules), - ), - itemExtent: ruleItemHeight, - itemCount: rules.length, - onReorderItem: ref.read(globalRulesProvider.notifier).order, - ), + child: Builder( + builder: (context) => isSearching + ? ListView.builder( + padding: _listPadding(context), + itemBuilder: (_, index) => + _buildRuleItem(rules, index, selectedRules), + itemExtent: ruleItemHeight, + itemCount: rules.length, + ) + : ReorderableList( + padding: _listPadding(context), + itemBuilder: (_, index) => + ReorderableDelayedDragStartListener( + key: ObjectKey(rules[index]), + index: index, + child: _buildRuleItem(rules, index, selectedRules), + ), + proxyDecorator: (child, index, animation) => + commonProxyDecorator( + _buildRuleItem(rules, index, selectedRules), + index, + animation, + ), + itemExtent: ruleItemHeight, + itemCount: rules.length, + onReorderItem: ref.read(globalRulesProvider.notifier).order, + ), + ), ), ), ); } - EdgeInsets get _listPadding => - const EdgeInsets.all(16).copyWith(top: context.contentTopPadding); + EdgeInsets _listPadding(BuildContext context) => EdgeInsets.fromLTRB( + 16, + context.contentTopPadding, + 16, + 16 + BottomInsetScope.of(context), + ); Widget _buildRuleItem(List rules, int index, Set selected) { final rule = rules[index]; diff --git a/lib/views/config/scripts.dart b/lib/views/config/scripts.dart index 10933752..682625de 100644 --- a/lib/views/config/scripts.dart +++ b/lib/views/config/scripts.dart @@ -67,17 +67,10 @@ class _ScriptsViewState extends ConsumerState { ).copyWith(top: context.contentTopPadding), buildDefaultDragHandles: false, itemCount: scripts.length, - itemBuilder: (_, index) { - final script = scripts[index]; - return ReorderableDelayedDragStartListener( - key: ValueKey(script.id), - index: index, - child: _buildItem(script, index, scripts.length), - ); - }, + itemBuilder: (_, index) => _buildItem(scripts, index), proxyDecorator: (_, index, animation) { return commonProxyDecorator( - _buildItem(scripts[index], index, scripts.length), + _buildItem(scripts, index), index, animation, ); @@ -87,32 +80,77 @@ class _ScriptsViewState extends ConsumerState { ); } - Widget _buildItem(Script script, int index, int length) { - return ItemPositionProvider( - position: ItemPosition.get(index, length), - child: DecorationListItem( - minVerticalPadding: 0, - contentPadding: const EdgeInsets.only(left: 16, right: 6), - title: _ScriptItemTitle(script: script), - trailing: _ScriptItemMenu( - script: script, - onEdit: () { - _handleToEditor(script); - }, - onEditUrl: () { - _handleEditUrl(script); - }, - onUpdate: () { - _handleUpdate(script); - }, - onDelete: () { - _handleDelete(script); - }, - ), + List _buildMenuItems(Script script) { + final appLocalizations = context.appLocalizations; + return [ + CommonPopupMenuItem( + glyph: AppGlyphs.edit, + label: appLocalizations.edit, onPressed: () { _handleToEditor(script); }, ), + if (script.url != null) ...[ + CommonPopupMenuItem( + glyph: AppGlyphs.link, + label: appLocalizations.url, + onPressed: () { + _handleEditUrl(script); + }, + ), + CommonPopupMenuItem( + glyph: AppGlyphs.sync, + label: appLocalizations.sync, + onPressed: () { + _handleUpdate(script); + }, + ), + ], + CommonPopupMenuItem( + danger: true, + glyph: AppGlyphs.delete, + label: appLocalizations.delete, + onPressed: () { + _handleDelete(script); + }, + ), + ]; + } + + Widget _buildItem(List