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 @@
-
\ No newline at end of file
+
\ 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 @@
-
+
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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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 @@
-
\ No newline at end of file
+
\ 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