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
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 秘密鍵が必要です"
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 私钥才能解密"
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bilibili</title><path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bilibili</title><path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m12 2-.2 6.8M17 3.4l-3 6.2M21 7l-6.3 3.4M22 12l-7 .2M20.6 17l-6.2-3M17 21l-3.4-6.3M12 22l-.2-7M7 20.6l3-6.2M3 17l6.3-3.4M2 12l7-.2M3.4 7l6.2 3M7 3l3.4 6.3"/></g><path d="m12 8 3 2 1 3-3 3-3-1-2-3 1-3z"/></svg>
|
||||
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="m12 2-.2 6.8M17 3.4l-3 6.2M21 7l-6.3 3.4M22 12l-7 .2M20.6 17l-6.2-3M17 21l-3.4-6.3M12 22l-.2-7M7 20.6l3-6.2M3 17l6.3-3.4M2 12l7-.2M3.4 7l6.2 3M7 3l3.4 6.3"/></g><path d="m12 8 3 2 1 3-3 3-3-1-2-3 1-3z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 356 B After Width: | Height: | Size: 376 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Disney+</title><path d="M3.6 4.2h10.8a2.4 2.4 0 0 1 2.4 2.4v10.8a2.4 2.4 0 0 1-2.4 2.4H3.6a2.4 2.4 0 0 1-2.4-2.4V6.6a2.4 2.4 0 0 1 2.4-2.4Zm3 4.2v7.2l6-3.6-6-3.6ZM19.2 8.4h1.8v2.7h2.7v1.8h-2.7v2.7h-1.8v-2.7h-2.7v-1.8h2.7V8.4Z"/></svg>
|
||||
<svg fill="currentColor" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><title>Disney+</title><path d="M3.6 4.2h10.8a2.4 2.4 0 0 1 2.4 2.4v10.8a2.4 2.4 0 0 1-2.4 2.4H3.6a2.4 2.4 0 0 1-2.4-2.4V6.6a2.4 2.4 0 0 1 2.4-2.4Zm3 4.2v7.2l6-3.6-6-3.6ZM19.2 8.4h1.8v2.7h2.7v1.8h-2.7v2.7h-1.8v-2.7h-2.7v-1.8h2.7V8.4Z"/></svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 302 B After Width: | Height: | Size: 322 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>
|
||||
|
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 421 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>
|
||||
|
Before Width: | Height: | Size: 822 B After Width: | Height: | Size: 842 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>
|
||||
|
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 477 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Netflix</title><path d="M5.398 0v.006c3.028 8.556 5.37 15.175 8.348 23.596 2.344.058 4.85.398 4.854.398-2.8-7.924-5.923-16.747-8.487-24zm8.489 0v9.63L18.6 22.951c-.043-7.86-.004-15.913.002-22.95zM5.398 1.05V24c1.873-.225 2.81-.312 4.715-.398v-9.22z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Netflix</title><path d="M5.398 0v.006c3.028 8.556 5.37 15.175 8.348 23.596 2.344.058 4.85.398 4.854.398-2.8-7.924-5.923-16.747-8.487-24zm8.489 0v9.63L18.6 22.951c-.043-7.86-.004-15.913.002-22.95zM5.398 1.05V24c1.873-.225 2.81-.312 4.715-.398v-9.22z"/></svg>
|
||||
|
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 355 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spotify</title><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spotify</title><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>
|
||||
|
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 717 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TikTok</title><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TikTok</title><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>
|
||||
|
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 727 B |
@@ -1 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
<svg fill="currentColor" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>
|
||||
|
Before Width: | Height: | Size: 459 B After Width: | Height: | Size: 479 B |
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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, Map<_HanVariant, List<String>>>{
|
||||
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<Locale> 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<String> appFontFamilyFallback({
|
||||
required TargetPlatform platform,
|
||||
required Iterable<Locale> locales,
|
||||
String? fontFamily,
|
||||
}) {
|
||||
final base = Typography.material2021(platform: platform).black.bodyMedium!;
|
||||
return [
|
||||
if (fontFamily != null) ?base.fontFamily,
|
||||
...?base.fontFamilyFallback,
|
||||
...?_hanFamilies[platform]?[_hanVariantOf(locales)],
|
||||
];
|
||||
}
|
||||
@@ -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<String> parseListInput(
|
||||
String text, {
|
||||
Set<String> existing = const {},
|
||||
int? maxLength,
|
||||
bool Function(String value)? isValid,
|
||||
}) {
|
||||
final entries = <String>[];
|
||||
final issues = <InputIssue>[];
|
||||
@@ -105,6 +112,16 @@ ParsedInput<String> parseListInput(
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (isValid != null && !isValid(value)) {
|
||||
issues.add(
|
||||
InputIssue(
|
||||
line: index + 1,
|
||||
raw: value,
|
||||
kind: InputIssueKind.invalidValue,
|
||||
),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
if (!seen.add(value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -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<List<String>> 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<List<String>> loadSystemFontFamilies() async {
|
||||
try {
|
||||
return await systemFontFamiliesReader();
|
||||
} catch (e) {
|
||||
commonPrint.log('loadSystemFontFamilies: $e', logLevel: LogLevel.warning);
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _readSystemFontFamilies() {
|
||||
return sortFontFamilies(switch (Platform.operatingSystem) {
|
||||
'macos' => _coreTextFamilies(),
|
||||
'windows' => _directWriteFamilies(),
|
||||
'linux' => _fontconfigFamilies(),
|
||||
'android' => androidFontFamilies(
|
||||
File('/system/etc/fonts.xml').readAsStringSync(),
|
||||
),
|
||||
_ => const <String>[],
|
||||
});
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
List<String> sortFontFamilies(Iterable<String> families) {
|
||||
final unique = <String, String>{};
|
||||
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<String> androidFontFamilies(String fontsXml) => [
|
||||
for (final match in RegExp(
|
||||
r'<family\b[^>]*\bname="([^"]+)"',
|
||||
).allMatches(fontsXml))
|
||||
match.group(1)!,
|
||||
];
|
||||
|
||||
final class _CFRange extends Struct {
|
||||
@IntPtr()
|
||||
external int location;
|
||||
|
||||
@IntPtr()
|
||||
external int length;
|
||||
}
|
||||
|
||||
List<String> _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<Pointer<Void> Function(), Pointer<Void> Function()>(
|
||||
'CTFontManagerCopyAvailableFontFamilyNames',
|
||||
);
|
||||
final arrayCount = coreFoundation
|
||||
.lookupFunction<
|
||||
IntPtr Function(Pointer<Void>),
|
||||
int Function(Pointer<Void>)
|
||||
>('CFArrayGetCount');
|
||||
final arrayValue = coreFoundation
|
||||
.lookupFunction<
|
||||
Pointer<Void> Function(Pointer<Void>, IntPtr),
|
||||
Pointer<Void> Function(Pointer<Void>, int)
|
||||
>('CFArrayGetValueAtIndex');
|
||||
final stringLength = coreFoundation
|
||||
.lookupFunction<
|
||||
IntPtr Function(Pointer<Void>),
|
||||
int Function(Pointer<Void>)
|
||||
>('CFStringGetLength');
|
||||
final stringCharacters = coreFoundation
|
||||
.lookupFunction<
|
||||
Void Function(Pointer<Void>, _CFRange, Pointer<Uint16>),
|
||||
void Function(Pointer<Void>, _CFRange, Pointer<Uint16>)
|
||||
>('CFStringGetCharacters');
|
||||
final release = coreFoundation
|
||||
.lookupFunction<
|
||||
Void Function(Pointer<Void>),
|
||||
void Function(Pointer<Void>)
|
||||
>('CFRelease');
|
||||
|
||||
String read(Pointer<Void> string, Arena arena) {
|
||||
final length = stringLength(string);
|
||||
final range = Struct.create<_CFRange>()
|
||||
..location = 0
|
||||
..length = length;
|
||||
final buffer = arena<Uint16>(length);
|
||||
stringCharacters(string, range, buffer);
|
||||
return buffer.cast<Utf16>().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<Void> pointer) {
|
||||
Pointer<NativeFunction<T>> method<T extends Function>(int index) =>
|
||||
pointer.cast<Pointer<Pointer<NativeFunction<T>>>>().value[index];
|
||||
|
||||
void release() => method<Uint32 Function(Pointer<Void>)>(
|
||||
2,
|
||||
).asFunction<int Function(Pointer<Void>)>()(pointer);
|
||||
}
|
||||
|
||||
typedef _GetObjectNative =
|
||||
Int32 Function(Pointer<Void>, Pointer<Pointer<Void>>);
|
||||
typedef _GetObject = int Function(Pointer<Void>, Pointer<Pointer<Void>>);
|
||||
typedef _GetIndexedNative =
|
||||
Int32 Function(Pointer<Void>, Uint32, Pointer<Pointer<Void>>);
|
||||
typedef _GetIndexed = int Function(Pointer<Void>, int, Pointer<Pointer<Void>>);
|
||||
|
||||
const _dwriteFactoryTypeShared = 0;
|
||||
|
||||
const _iidDWriteFactory = '{B859EE5A-D838-4B5B-A2E8-1ADC7D93DB48}';
|
||||
|
||||
void _check(int hresult, String call) {
|
||||
if (hresult < 0) {
|
||||
throw OSError(call, hresult);
|
||||
}
|
||||
}
|
||||
|
||||
List<String> _directWriteFamilies() => using((arena) {
|
||||
final createFactory = DynamicLibrary.open('dwrite.dll')
|
||||
.lookupFunction<
|
||||
Int32 Function(Int32, Pointer<GUID>, Pointer<Pointer<Void>>),
|
||||
int Function(int, Pointer<GUID>, Pointer<Pointer<Void>>)
|
||||
>('DWriteCreateFactory');
|
||||
final out = arena<Pointer<Void>>();
|
||||
_check(
|
||||
createFactory(
|
||||
_dwriteFactoryTypeShared,
|
||||
GUID(_iidDWriteFactory).toNative(allocator: arena),
|
||||
out,
|
||||
),
|
||||
'DWriteCreateFactory',
|
||||
);
|
||||
final factory = _ComObject(out.value);
|
||||
try {
|
||||
_check(
|
||||
factory
|
||||
.method<Int32 Function(Pointer<Void>, Pointer<Pointer<Void>>, Int32)>(
|
||||
3,
|
||||
)
|
||||
.asFunction<
|
||||
int Function(Pointer<Void>, Pointer<Pointer<Void>>, int)
|
||||
>()(factory.pointer, out, 0),
|
||||
'GetSystemFontCollection',
|
||||
);
|
||||
final collection = _ComObject(out.value);
|
||||
try {
|
||||
final count = collection
|
||||
.method<Uint32 Function(Pointer<Void>)>(3)
|
||||
.asFunction<int Function(Pointer<Void>)>()(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<Pointer<Void>>();
|
||||
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<Uint32>();
|
||||
final exists = arena<Int32>();
|
||||
final findLocale = names
|
||||
.method<
|
||||
Int32 Function(
|
||||
Pointer<Void>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<Uint32>,
|
||||
Pointer<Int32>,
|
||||
)
|
||||
>(4)
|
||||
.asFunction<
|
||||
int Function(
|
||||
Pointer<Void>,
|
||||
Pointer<Utf16>,
|
||||
Pointer<Uint32>,
|
||||
Pointer<Int32>,
|
||||
)
|
||||
>();
|
||||
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<Uint32>();
|
||||
final stringLength = names
|
||||
.method<Int32 Function(Pointer<Void>, Uint32, Pointer<Uint32>)>(7)
|
||||
.asFunction<int Function(Pointer<Void>, int, Pointer<Uint32>)>();
|
||||
if (stringLength(names.pointer, at, length) < 0) {
|
||||
return null;
|
||||
}
|
||||
final size = length.value + 1;
|
||||
final buffer = arena<Uint16>(size);
|
||||
final string = names
|
||||
.method<
|
||||
Int32 Function(Pointer<Void>, Uint32, Pointer<Uint16>, Uint32)
|
||||
>(8)
|
||||
.asFunction<int Function(Pointer<Void>, int, Pointer<Uint16>, int)>();
|
||||
if (string(names.pointer, at, buffer, size) < 0) {
|
||||
return null;
|
||||
}
|
||||
return buffer.cast<Utf16>().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<Pointer<Void>> fonts;
|
||||
}
|
||||
|
||||
List<String> _fontconfigFamilies() => using((arena) {
|
||||
final fontconfig = DynamicLibrary.open('libfontconfig.so.1');
|
||||
final createPattern = fontconfig
|
||||
.lookupFunction<Pointer<Void> Function(), Pointer<Void> Function()>(
|
||||
'FcPatternCreate',
|
||||
);
|
||||
final addBool = fontconfig
|
||||
.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Utf8>, Int32),
|
||||
int Function(Pointer<Void>, Pointer<Utf8>, int)
|
||||
>('FcPatternAddBool');
|
||||
final createObjectSet = fontconfig
|
||||
.lookupFunction<Pointer<Void> Function(), Pointer<Void> Function()>(
|
||||
'FcObjectSetCreate',
|
||||
);
|
||||
final addObject = fontconfig
|
||||
.lookupFunction<
|
||||
Int32 Function(Pointer<Void>, Pointer<Utf8>),
|
||||
int Function(Pointer<Void>, Pointer<Utf8>)
|
||||
>('FcObjectSetAdd');
|
||||
final listFonts = fontconfig
|
||||
.lookupFunction<
|
||||
Pointer<_FcFontSet> Function(
|
||||
Pointer<Void>,
|
||||
Pointer<Void>,
|
||||
Pointer<Void>,
|
||||
),
|
||||
Pointer<_FcFontSet> Function(
|
||||
Pointer<Void>,
|
||||
Pointer<Void>,
|
||||
Pointer<Void>,
|
||||
)
|
||||
>('FcFontList');
|
||||
final getString = fontconfig
|
||||
.lookupFunction<
|
||||
Int32 Function(
|
||||
Pointer<Void>,
|
||||
Pointer<Utf8>,
|
||||
Int32,
|
||||
Pointer<Pointer<Utf8>>,
|
||||
),
|
||||
int Function(Pointer<Void>, Pointer<Utf8>, int, Pointer<Pointer<Utf8>>)
|
||||
>('FcPatternGetString');
|
||||
final destroyFontSet = fontconfig
|
||||
.lookupFunction<
|
||||
Void Function(Pointer<_FcFontSet>),
|
||||
void Function(Pointer<_FcFontSet>)
|
||||
>('FcFontSetDestroy');
|
||||
final destroyObjectSet = fontconfig
|
||||
.lookupFunction<
|
||||
Void Function(Pointer<Void>),
|
||||
void Function(Pointer<Void>)
|
||||
>('FcObjectSetDestroy');
|
||||
final destroyPattern = fontconfig
|
||||
.lookupFunction<
|
||||
Void Function(Pointer<Void>),
|
||||
void Function(Pointer<Void>)
|
||||
>('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 <String>[];
|
||||
}
|
||||
try {
|
||||
final value = arena<Pointer<Utf8>>();
|
||||
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);
|
||||
}
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -11,6 +11,9 @@ class CustomProxies extends Table {
|
||||
|
||||
TextColumn get order => text().nullable()();
|
||||
|
||||
TextColumn get emptyOptions =>
|
||||
text().map(const StringSetConverter()).nullable()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
@@ -59,7 +62,15 @@ class CustomProxiesDao extends DatabaseAccessor<Database>
|
||||
|
||||
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}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -3793,7 +3793,16 @@ class $CustomProxiesTable extends CustomProxies
|
||||
requiredDuringInsert: false,
|
||||
);
|
||||
@override
|
||||
List<GeneratedColumn> get $columns => [id, definition, order];
|
||||
late final GeneratedColumnWithTypeConverter<Set<String>?, String>
|
||||
emptyOptions = GeneratedColumn<String>(
|
||||
'empty_options',
|
||||
aliasedName,
|
||||
true,
|
||||
type: DriftSqlType.string,
|
||||
requiredDuringInsert: false,
|
||||
).withConverter<Set<String>?>($CustomProxiesTable.$converteremptyOptionsn);
|
||||
@override
|
||||
List<GeneratedColumn> 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<Map<String, dynamic>, String> $converterdefinition =
|
||||
const JsonMapConverter();
|
||||
static TypeConverter<Set<String>, String> $converteremptyOptions =
|
||||
const StringSetConverter();
|
||||
static TypeConverter<Set<String>?, String?> $converteremptyOptionsn =
|
||||
NullAwareTypeConverter.wrap($converteremptyOptions);
|
||||
}
|
||||
|
||||
class RawCustomProxy extends DataClass implements Insertable<RawCustomProxy> {
|
||||
final int id;
|
||||
final Map<String, dynamic> definition;
|
||||
final String? order;
|
||||
final Set<String>? emptyOptions;
|
||||
const RawCustomProxy({
|
||||
required this.id,
|
||||
required this.definition,
|
||||
this.order,
|
||||
this.emptyOptions,
|
||||
});
|
||||
@override
|
||||
Map<String, Expression> toColumns(bool nullToAbsent) {
|
||||
@@ -3871,6 +3892,11 @@ class RawCustomProxy extends DataClass implements Insertable<RawCustomProxy> {
|
||||
if (!nullToAbsent || order != null) {
|
||||
map['order'] = Variable<String>(order);
|
||||
}
|
||||
if (!nullToAbsent || emptyOptions != null) {
|
||||
map['empty_options'] = Variable<String>(
|
||||
$CustomProxiesTable.$converteremptyOptionsn.toSql(emptyOptions),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -3881,6 +3907,9 @@ class RawCustomProxy extends DataClass implements Insertable<RawCustomProxy> {
|
||||
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<RawCustomProxy> {
|
||||
id: serializer.fromJson<int>(json['id']),
|
||||
definition: serializer.fromJson<Map<String, dynamic>>(json['definition']),
|
||||
order: serializer.fromJson<String?>(json['order']),
|
||||
emptyOptions: serializer.fromJson<Set<String>?>(json['emptyOptions']),
|
||||
);
|
||||
}
|
||||
@override
|
||||
@@ -3902,6 +3932,7 @@ class RawCustomProxy extends DataClass implements Insertable<RawCustomProxy> {
|
||||
'id': serializer.toJson<int>(id),
|
||||
'definition': serializer.toJson<Map<String, dynamic>>(definition),
|
||||
'order': serializer.toJson<String?>(order),
|
||||
'emptyOptions': serializer.toJson<Set<String>?>(emptyOptions),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -3909,10 +3940,12 @@ class RawCustomProxy extends DataClass implements Insertable<RawCustomProxy> {
|
||||
int? id,
|
||||
Map<String, dynamic>? definition,
|
||||
Value<String?> order = const Value.absent(),
|
||||
Value<Set<String>?> 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<RawCustomProxy> {
|
||||
? 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<RawCustomProxy> {
|
||||
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<RawCustomProxy> {
|
||||
final Value<int> id;
|
||||
final Value<Map<String, dynamic>> definition;
|
||||
final Value<String?> order;
|
||||
final Value<Set<String>?> 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<String, dynamic> definition,
|
||||
this.order = const Value.absent(),
|
||||
this.emptyOptions = const Value.absent(),
|
||||
}) : definition = Value(definition);
|
||||
static Insertable<RawCustomProxy> custom({
|
||||
Expression<int>? id,
|
||||
Expression<String>? definition,
|
||||
Expression<String>? order,
|
||||
Expression<String>? 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<RawCustomProxy> {
|
||||
Value<int>? id,
|
||||
Value<Map<String, dynamic>>? definition,
|
||||
Value<String?>? order,
|
||||
Value<Set<String>?>? 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<RawCustomProxy> {
|
||||
if (order.present) {
|
||||
map['order'] = Variable<String>(order.value);
|
||||
}
|
||||
if (emptyOptions.present) {
|
||||
map['empty_options'] = Variable<String>(
|
||||
$CustomProxiesTable.$converteremptyOptionsn.toSql(emptyOptions.value),
|
||||
);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
@@ -4005,7 +4055,8 @@ class CustomProxiesCompanion extends UpdateCompanion<RawCustomProxy> {
|
||||
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<int> id,
|
||||
required Map<String, dynamic> definition,
|
||||
Value<String?> order,
|
||||
Value<Set<String>?> emptyOptions,
|
||||
});
|
||||
typedef $$CustomProxiesTableUpdateCompanionBuilder =
|
||||
CustomProxiesCompanion Function({
|
||||
Value<int> id,
|
||||
Value<Map<String, dynamic>> definition,
|
||||
Value<String?> order,
|
||||
Value<Set<String>?> emptyOptions,
|
||||
});
|
||||
|
||||
final class $$CustomProxiesTableReferences
|
||||
@@ -7355,6 +7408,12 @@ class $$CustomProxiesTableFilterComposer
|
||||
builder: (column) => ColumnFilters(column),
|
||||
);
|
||||
|
||||
ColumnWithTypeConverterFilters<Set<String>?, Set<String>, String>
|
||||
get emptyOptions => $composableBuilder(
|
||||
column: $table.emptyOptions,
|
||||
builder: (column) => ColumnWithTypeConverterFilters(column),
|
||||
);
|
||||
|
||||
Expression<bool> proxyDialersRefs(
|
||||
Expression<bool> Function($$ProxyDialersTableFilterComposer f) f,
|
||||
) {
|
||||
@@ -7404,6 +7463,11 @@ class $$CustomProxiesTableOrderingComposer
|
||||
column: $table.order,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
|
||||
ColumnOrderings<String> get emptyOptions => $composableBuilder(
|
||||
column: $table.emptyOptions,
|
||||
builder: (column) => ColumnOrderings(column),
|
||||
);
|
||||
}
|
||||
|
||||
class $$CustomProxiesTableAnnotationComposer
|
||||
@@ -7427,6 +7491,12 @@ class $$CustomProxiesTableAnnotationComposer
|
||||
GeneratedColumn<String> get order =>
|
||||
$composableBuilder(column: $table.order, builder: (column) => column);
|
||||
|
||||
GeneratedColumnWithTypeConverter<Set<String>?, String> get emptyOptions =>
|
||||
$composableBuilder(
|
||||
column: $table.emptyOptions,
|
||||
builder: (column) => column,
|
||||
);
|
||||
|
||||
Expression<T> proxyDialersRefs<T extends Object>(
|
||||
Expression<T> Function($$ProxyDialersTableAnnotationComposer a) f,
|
||||
) {
|
||||
@@ -7484,20 +7554,24 @@ class $$CustomProxiesTableTableManager
|
||||
Value<int> id = const Value.absent(),
|
||||
Value<Map<String, dynamic>> definition = const Value.absent(),
|
||||
Value<String?> order = const Value.absent(),
|
||||
Value<Set<String>?> emptyOptions = const Value.absent(),
|
||||
}) => CustomProxiesCompanion(
|
||||
id: id,
|
||||
definition: definition,
|
||||
order: order,
|
||||
emptyOptions: emptyOptions,
|
||||
),
|
||||
createCompanionCallback:
|
||||
({
|
||||
Value<int> id = const Value.absent(),
|
||||
required Map<String, dynamic> definition,
|
||||
Value<String?> order = const Value.absent(),
|
||||
Value<Set<String>?> emptyOptions = const Value.absent(),
|
||||
}) => CustomProxiesCompanion.insert(
|
||||
id: id,
|
||||
definition: definition,
|
||||
order: order,
|
||||
emptyOptions: emptyOptions,
|
||||
),
|
||||
withReferenceMapper: (p0) => p0
|
||||
.map(
|
||||
|
||||
@@ -113,11 +113,23 @@ class ProxyGroupsDao extends DatabaseAccessor<Database>
|
||||
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<String, Object?>,
|
||||
'use-position': {
|
||||
for (final MapEntry(:key, :value) in usePosition!.entries)
|
||||
key == oldName ? newName : key: value,
|
||||
},
|
||||
}),
|
||||
)
|
||||
: const Value.absent(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ class TrackerInfoItem extends ConsumerWidget {
|
||||
);
|
||||
return RecordListItem(
|
||||
onTap: () {
|
||||
showExtend(
|
||||
showInSheet<void>(
|
||||
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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"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"),
|
||||
};
|
||||
|
||||
@@ -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<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
@@ -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("簡体字中国語"),
|
||||
};
|
||||
|
||||
@@ -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<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
"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("Упрощённый китайский"),
|
||||
};
|
||||
|
||||
@@ -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<String, Function> _notInlinedMessages(_) => <String, Function>{
|
||||
@@ -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("中文简体"),
|
||||
};
|
||||
|
||||
@@ -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<AppLocalizations> {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -70,6 +70,7 @@ abstract class ProxyGroup with _$ProxyGroup {
|
||||
required GroupType type,
|
||||
List<String>? proxies,
|
||||
List<String>? use,
|
||||
@JsonKey(name: 'use-position') Map<String, int>? 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<String>? memberLayout,
|
||||
}) = _ProxyGroup;
|
||||
|
||||
factory ProxyGroup.fromJson(Map<String, Object?> 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<String, int>? get effectiveUsePosition {
|
||||
final count = proxies?.length ?? 0;
|
||||
final positions = {
|
||||
for (final name in use ?? const <String>[])
|
||||
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<String, Object?> ||
|
||||
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<Object?> 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<String, int>? usePositionOf(
|
||||
Object? value, [
|
||||
Map<String, String> 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<String, dynamic> 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<ProxyField> emptyOptions,
|
||||
}) = _CustomProxy;
|
||||
|
||||
factory CustomProxy.fromJson(Map<String, Object?> 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<String> 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<String> fakeIpFilter,
|
||||
@Default([
|
||||
'dns.msftnsci.com',
|
||||
'www.msftnsci.com',
|
||||
'www.msftconnecttest.com',
|
||||
])
|
||||
@JsonKey(name: 'fake-ip-filter')
|
||||
List<String> 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<String, String> nameserverPolicy,
|
||||
@Default([]) List<String> nameserver,
|
||||
@Default(['https://doh.pub/dns-query', 'tls://223.5.5.5:853'])
|
||||
List<String> nameserver,
|
||||
@Default([]) List<String> fallback,
|
||||
@Default(false)
|
||||
@JsonKey(name: 'fallback-lazy-query')
|
||||
@@ -1129,8 +1197,18 @@ extension DnsOverrideExt on Dns {
|
||||
return json;
|
||||
}
|
||||
|
||||
Map<String, Object?> overrideJson(Set<DnsOverrideKey> keys) =>
|
||||
_overrideJson(_json, DnsOverrideKey.values, keys);
|
||||
/// [keepEmpty] writes emptied keys, but the core refuses an empty nameserver.
|
||||
Map<String, Object?> overrideJson(
|
||||
Set<DnsOverrideKey> keys, {
|
||||
bool keepEmpty = false,
|
||||
}) => _overrideJson(
|
||||
_json,
|
||||
DnsOverrideKey.values,
|
||||
keys,
|
||||
keepEmpty: keepEmpty
|
||||
? keys.difference({DnsOverrideKey.nameserver})
|
||||
: const <DnsOverrideKey>{},
|
||||
);
|
||||
|
||||
String overrideYaml(Set<DnsOverrideKey> keys) =>
|
||||
yaml.encode(overrideJson(keys));
|
||||
@@ -1165,8 +1243,9 @@ Object? _plainYaml(Object? node) => switch (node) {
|
||||
Map<String, Object?> _overrideJson<K extends OverrideKey>(
|
||||
Map<String, Object?> json,
|
||||
List<K> values,
|
||||
Set<K> keys,
|
||||
) {
|
||||
Set<K> keys, {
|
||||
Set<K> keepEmpty = const {},
|
||||
}) {
|
||||
final result = <String, Object?>{};
|
||||
for (final key in values) {
|
||||
if (!keys.contains(key)) {
|
||||
@@ -1176,7 +1255,7 @@ Map<String, Object?> _overrideJson<K extends OverrideKey>(
|
||||
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<String, Object?> _overrideJson<K extends OverrideKey>(
|
||||
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<String, dynamic> 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<String>? memberLayout,
|
||||
}) = _ProxyProviderOptions;
|
||||
|
||||
factory ProxyProviderOptions.fromJson(Map<String, Object?> 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<String, Object?> 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<String, Object?> 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 {
|
||||
|
||||
@@ -315,7 +315,7 @@ abstract class NetworkProps with _$NetworkProps {
|
||||
const factory NetworkProps({
|
||||
@Default(true) bool systemProxy,
|
||||
@Default(defaultBypassDomain) List<String> 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<String, Object?> json) {
|
||||
Map<String, Object?> _withLegacyRouteMode(Map<String, Object?> 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<String, Object?>.from(network),
|
||||
'bypassPrivateRoute': true,
|
||||
'bypassPrivateRoute': bypassPrivate,
|
||||
},
|
||||
if (clash is Map && tun is Map)
|
||||
if (bypassPrivate && clash is Map && tun is Map)
|
||||
'patchClashConfig': {
|
||||
...Map<String, Object?>.from(clash),
|
||||
'tun': {...Map<String, Object?>.from(tun), 'route-address': <String>[]},
|
||||
|
||||
@@ -16,7 +16,9 @@ T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ProxyGroup {
|
||||
|
||||
int? get profileId;@JsonKey(fromJson: Snowflake.buildId) int get id; String get name; GroupType get type; List<String>? get proxies; List<String>? 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<String>? get proxies; List<String>? get use;@JsonKey(name: 'use-position') Map<String, int>? 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<String>? 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<ProxyGroup> get copyWith => _$ProxyGroupCopyWithImpl<ProxyGr
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
final _this = this as ProxyGroup;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyGroup&&(identical(other.profileId, _this.profileId) || other.profileId == _this.profileId)&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.name, _this.name) || other.name == _this.name)&&(identical(other.type, _this.type) || other.type == _this.type)&&const DeepCollectionEquality().equals(other.proxies, _this.proxies)&&const DeepCollectionEquality().equals(other.use, _this.use)&&(identical(other.interval, _this.interval) || other.interval == _this.interval)&&(identical(other.lazy, _this.lazy) || other.lazy == _this.lazy)&&(identical(other.disableUDP, _this.disableUDP) || other.disableUDP == _this.disableUDP)&&(identical(other.url, _this.url) || other.url == _this.url)&&(identical(other.timeout, _this.timeout) || other.timeout == _this.timeout)&&(identical(other.maxFailedTimes, _this.maxFailedTimes) || other.maxFailedTimes == _this.maxFailedTimes)&&(identical(other.filter, _this.filter) || other.filter == _this.filter)&&(identical(other.excludeFilter, _this.excludeFilter) || other.excludeFilter == _this.excludeFilter)&&(identical(other.excludeType, _this.excludeType) || other.excludeType == _this.excludeType)&&(identical(other.expectedStatus, _this.expectedStatus) || other.expectedStatus == _this.expectedStatus)&&(identical(other.tolerance, _this.tolerance) || other.tolerance == _this.tolerance)&&(identical(other.strategy, _this.strategy) || other.strategy == _this.strategy)&&(identical(other.hashKey, _this.hashKey) || other.hashKey == _this.hashKey)&&(identical(other.defaultSelected, _this.defaultSelected) || other.defaultSelected == _this.defaultSelected)&&(identical(other.emptyFallback, _this.emptyFallback) || other.emptyFallback == _this.emptyFallback)&&(identical(other.includeAll, _this.includeAll) || other.includeAll == _this.includeAll)&&(identical(other.includeAllProxies, _this.includeAllProxies) || other.includeAllProxies == _this.includeAllProxies)&&(identical(other.includeAllProviders, _this.includeAllProviders) || other.includeAllProviders == _this.includeAllProviders)&&(identical(other.hidden, _this.hidden) || other.hidden == _this.hidden)&&(identical(other.icon, _this.icon) || other.icon == _this.icon)&&(identical(other.order, _this.order) || other.order == _this.order));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ProxyGroup&&(identical(other.profileId, _this.profileId) || other.profileId == _this.profileId)&&(identical(other.id, _this.id) || other.id == _this.id)&&(identical(other.name, _this.name) || other.name == _this.name)&&(identical(other.type, _this.type) || other.type == _this.type)&&const DeepCollectionEquality().equals(other.proxies, _this.proxies)&&const DeepCollectionEquality().equals(other.use, _this.use)&&const DeepCollectionEquality().equals(other.usePosition, _this.usePosition)&&(identical(other.interval, _this.interval) || other.interval == _this.interval)&&(identical(other.lazy, _this.lazy) || other.lazy == _this.lazy)&&(identical(other.disableUDP, _this.disableUDP) || other.disableUDP == _this.disableUDP)&&(identical(other.url, _this.url) || other.url == _this.url)&&(identical(other.timeout, _this.timeout) || other.timeout == _this.timeout)&&(identical(other.maxFailedTimes, _this.maxFailedTimes) || other.maxFailedTimes == _this.maxFailedTimes)&&(identical(other.filter, _this.filter) || other.filter == _this.filter)&&(identical(other.excludeFilter, _this.excludeFilter) || other.excludeFilter == _this.excludeFilter)&&(identical(other.excludeType, _this.excludeType) || other.excludeType == _this.excludeType)&&(identical(other.expectedStatus, _this.expectedStatus) || other.expectedStatus == _this.expectedStatus)&&(identical(other.tolerance, _this.tolerance) || other.tolerance == _this.tolerance)&&(identical(other.strategy, _this.strategy) || other.strategy == _this.strategy)&&(identical(other.hashKey, _this.hashKey) || other.hashKey == _this.hashKey)&&(identical(other.defaultSelected, _this.defaultSelected) || other.defaultSelected == _this.defaultSelected)&&(identical(other.emptyFallback, _this.emptyFallback) || other.emptyFallback == _this.emptyFallback)&&(identical(other.includeAll, _this.includeAll) || other.includeAll == _this.includeAll)&&(identical(other.includeAllProxies, _this.includeAllProxies) || other.includeAllProxies == _this.includeAllProxies)&&(identical(other.includeAllProviders, _this.includeAllProviders) || other.includeAllProviders == _this.includeAllProviders)&&(identical(other.hidden, _this.hidden) || other.hidden == _this.hidden)&&(identical(other.icon, _this.icon) || other.icon == _this.icon)&&(identical(other.order, _this.order) || other.order == _this.order)&&const DeepCollectionEquality().equals(other.memberLayout, _this.memberLayout));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode {
|
||||
final _this = this as ProxyGroup;
|
||||
return Object.hashAll([runtimeType,_this.profileId,_this.id,_this.name,_this.type,const DeepCollectionEquality().hash(_this.proxies),const DeepCollectionEquality().hash(_this.use),_this.interval,_this.lazy,_this.disableUDP,_this.url,_this.timeout,_this.maxFailedTimes,_this.filter,_this.excludeFilter,_this.excludeType,_this.expectedStatus,_this.tolerance,_this.strategy,_this.hashKey,_this.defaultSelected,_this.emptyFallback,_this.includeAll,_this.includeAllProxies,_this.includeAllProviders,_this.hidden,_this.icon,_this.order]);
|
||||
return Object.hashAll([runtimeType,_this.profileId,_this.id,_this.name,_this.type,const DeepCollectionEquality().hash(_this.proxies),const DeepCollectionEquality().hash(_this.use),const DeepCollectionEquality().hash(_this.usePosition),_this.interval,_this.lazy,_this.disableUDP,_this.url,_this.timeout,_this.maxFailedTimes,_this.filter,_this.excludeFilter,_this.excludeType,_this.expectedStatus,_this.tolerance,_this.strategy,_this.hashKey,_this.defaultSelected,_this.emptyFallback,_this.includeAll,_this.includeAllProxies,_this.includeAllProviders,_this.hidden,_this.icon,_this.order,const DeepCollectionEquality().hash(_this.memberLayout)]);
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final _this = this as ProxyGroup;
|
||||
return 'ProxyGroup(profileId: ${_this.profileId}, id: ${_this.id}, name: ${_this.name}, type: ${_this.type}, proxies: ${_this.proxies}, use: ${_this.use}, interval: ${_this.interval}, lazy: ${_this.lazy}, disableUDP: ${_this.disableUDP}, url: ${_this.url}, timeout: ${_this.timeout}, maxFailedTimes: ${_this.maxFailedTimes}, filter: ${_this.filter}, excludeFilter: ${_this.excludeFilter}, excludeType: ${_this.excludeType}, expectedStatus: ${_this.expectedStatus}, tolerance: ${_this.tolerance}, strategy: ${_this.strategy}, hashKey: ${_this.hashKey}, defaultSelected: ${_this.defaultSelected}, emptyFallback: ${_this.emptyFallback}, includeAll: ${_this.includeAll}, includeAllProxies: ${_this.includeAllProxies}, includeAllProviders: ${_this.includeAllProviders}, hidden: ${_this.hidden}, icon: ${_this.icon}, order: ${_this.order})';
|
||||
return 'ProxyGroup(profileId: ${_this.profileId}, id: ${_this.id}, name: ${_this.name}, type: ${_this.type}, proxies: ${_this.proxies}, use: ${_this.use}, usePosition: ${_this.usePosition}, interval: ${_this.interval}, lazy: ${_this.lazy}, disableUDP: ${_this.disableUDP}, url: ${_this.url}, timeout: ${_this.timeout}, maxFailedTimes: ${_this.maxFailedTimes}, filter: ${_this.filter}, excludeFilter: ${_this.excludeFilter}, excludeType: ${_this.excludeType}, expectedStatus: ${_this.expectedStatus}, tolerance: ${_this.tolerance}, strategy: ${_this.strategy}, hashKey: ${_this.hashKey}, defaultSelected: ${_this.defaultSelected}, emptyFallback: ${_this.emptyFallback}, includeAll: ${_this.includeAll}, includeAllProxies: ${_this.includeAllProxies}, includeAllProviders: ${_this.includeAllProviders}, hidden: ${_this.hidden}, icon: ${_this.icon}, order: ${_this.order}, memberLayout: ${_this.memberLayout})';
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +56,7 @@ abstract mixin class $ProxyGroupCopyWith<$Res> {
|
||||
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<String>? proxies, List<String>? 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<String>? proxies, List<String>? use,@JsonKey(name: 'use-position') Map<String, int>? 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<String>? 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<String>?,use: freezed == use ? _self.use : use // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,interval: freezed == interval ? _self.interval : interval // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,usePosition: freezed == usePosition ? _self.usePosition : usePosition // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>?,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<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -185,10 +189,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? 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 extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? 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<String>? 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 extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? 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 extends Object?>(TResult Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? 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<String>? 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 extends Object?>(TResult? Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? 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 extends Object?>(TResult? Function( int? profileId, @JsonKey(fromJson: Snowflake.buildId) int id, String name, GroupType type, List<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? 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<String>? 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<String>? proxies, List<String>? 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<String>? proxies, List<String>? use, @JsonKey(name: 'use-position') Map<String, int>? 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<String>? memberLayout}): _proxies = proxies,_use = use,_usePosition = usePosition,_memberLayout = memberLayout;
|
||||
factory _ProxyGroup.fromJson(Map<String, dynamic> json) => _$ProxyGroupFromJson(json);
|
||||
|
||||
@override final int? profileId;
|
||||
@@ -266,6 +270,15 @@ class _ProxyGroup implements ProxyGroup {
|
||||
return EqualUnmodifiableListView(value);
|
||||
}
|
||||
|
||||
final Map<String, int>? _usePosition;
|
||||
@override@JsonKey(name: 'use-position') Map<String, int>? 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<String>? _memberLayout;
|
||||
/// Where the members list shows the filters among the proxies, which no
|
||||
/// config carries, as `ProxyGroupMembersSliver` writes it.
|
||||
@override List<String>? 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<String, dynamic> 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<String>? proxies, List<String>? 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<String>? proxies, List<String>? use,@JsonKey(name: 'use-position') Map<String, int>? 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<String>? 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<String>?,use: freezed == use ? _self._use : use // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,interval: freezed == interval ? _self.interval : interval // ignore: cast_nullable_to_non_nullable
|
||||
as List<String>?,usePosition: freezed == usePosition ? _self._usePosition : usePosition // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, int>?,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<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -656,7 +684,9 @@ as String?,
|
||||
/// @nodoc
|
||||
mixin _$CustomProxy {
|
||||
|
||||
@JsonKey(fromJson: Snowflake.buildId) int get id; Map<String, dynamic> get definition; String? get order;
|
||||
@JsonKey(fromJson: Snowflake.buildId) int get id; Map<String, dynamic> 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<ProxyField> 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<CustomProxy> get copyWith => _$CustomProxyCopyWithImpl<Cust
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
final _this = this as CustomProxy;
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomProxy&&(identical(other.id, _this.id) || other.id == _this.id)&&const DeepCollectionEquality().equals(other.definition, _this.definition)&&(identical(other.order, _this.order) || other.order == _this.order));
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is CustomProxy&&(identical(other.id, _this.id) || other.id == _this.id)&&const DeepCollectionEquality().equals(other.definition, _this.definition)&&(identical(other.order, _this.order) || other.order == _this.order)&&const DeepCollectionEquality().equals(other.emptyOptions, _this.emptyOptions));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode {
|
||||
final _this = this as CustomProxy;
|
||||
return Object.hash(runtimeType,_this.id,const DeepCollectionEquality().hash(_this.definition),_this.order);
|
||||
return Object.hash(runtimeType,_this.id,const DeepCollectionEquality().hash(_this.definition),_this.order,const DeepCollectionEquality().hash(_this.emptyOptions));
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final _this = this as CustomProxy;
|
||||
return 'CustomProxy(id: ${_this.id}, definition: ${_this.definition}, order: ${_this.order})';
|
||||
return 'CustomProxy(id: ${_this.id}, definition: ${_this.definition}, order: ${_this.order}, emptyOptions: ${_this.emptyOptions})';
|
||||
}
|
||||
|
||||
|
||||
@@ -694,7 +724,7 @@ abstract mixin class $CustomProxyCopyWith<$Res> {
|
||||
factory $CustomProxyCopyWith(CustomProxy value, $Res Function(CustomProxy) _then) = _$CustomProxyCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order
|
||||
@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order, Set<ProxyField> 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<String, dynamic>,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<ProxyField>,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -801,10 +832,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order, Set<ProxyField> 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 extends Object?>(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order, Set<ProxyField> 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 extends Object?>(TResult? Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order, Set<ProxyField> 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<String, dynamic> definition = const {}, this.order}): _definition = definition;
|
||||
const _CustomProxy({@JsonKey(fromJson: Snowflake.buildId) required this.id, Map<String, dynamic> definition = const {}, this.order, Set<ProxyField> emptyOptions = const {}}): _definition = definition,_emptyOptions = emptyOptions;
|
||||
factory _CustomProxy.fromJson(Map<String, dynamic> 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<ProxyField> _emptyOptions;
|
||||
/// Options the form lists with no value yet, which [definition] cannot
|
||||
/// hold since the core reads every key in it.
|
||||
@override@JsonKey() Set<ProxyField> 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<String, dynamic> 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<String, dynamic> definition, String? order
|
||||
@JsonKey(fromJson: Snowflake.buildId) int id, Map<String, dynamic> definition, String? order, Set<ProxyField> 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<String, dynamic>,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<ProxyField>,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -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<String> 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<String> 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<String, String> nameserverPolicy = const {}, List<String> nameserver = const [], List<String> fallback = const [], @JsonKey(name: 'fallback-lazy-query') this.fallbackLazyQuery = false, @JsonKey(name: 'proxy-server-nameserver') List<String> proxyServerNameserver = const [], @JsonKey(name: 'proxy-server-nameserver-policy') Map<String, String> proxyServerNameserverPolicy = const {}, @JsonKey(name: 'direct-nameserver') List<String> 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<String> 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<String> 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<String, String> nameserverPolicy = const {}, List<String> nameserver = const ['https://doh.pub/dns-query', 'tls://223.5.5.5:853'], List<String> fallback = const [], @JsonKey(name: 'fallback-lazy-query') this.fallbackLazyQuery = false, @JsonKey(name: 'proxy-server-nameserver') List<String> proxyServerNameserver = const [], @JsonKey(name: 'proxy-server-nameserver-policy') Map<String, String> proxyServerNameserverPolicy = const {}, @JsonKey(name: 'direct-nameserver') List<String> 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<String, dynamic> 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<String, dynamic> 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<String>? 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<ProxyProviderOptions> 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<String>? 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<String>?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of ProxyProviderOptions
|
||||
@@ -6971,10 +7017,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(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 extends Object?>(TResult Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride, List<String>? 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 extends Object?>(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 extends Object?>(TResult Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride, List<String>? 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 extends Object?>(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 extends Object?>(TResult? Function(@JsonKey(name: 'health-check') ProviderHealthCheck healthCheck, String? filter, @JsonKey(name: 'exclude-filter') String? excludeFilter, @JsonKey(name: 'override') ProviderOverride proxyOverride, List<String>? 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<String>? memberLayout}): _memberLayout = memberLayout;
|
||||
factory _ProxyProviderOptions.fromJson(Map<String, dynamic> 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<String>? _memberLayout;
|
||||
/// Where the filters list shows the exclude filters among the filters,
|
||||
/// which no config carries, as `ProxyProviderFiltersSliver` writes it.
|
||||
@override List<String>? 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<String, dynamic> 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<String>? 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<String>?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,9 @@ _ProxyGroup _$ProxyGroupFromJson(Map<String, dynamic> json) => _ProxyGroup(
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
use: (json['use'] as List<dynamic>?)?.map((e) => e as String).toList(),
|
||||
usePosition: (json['use-position'] as Map<String, dynamic>?)?.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<String, dynamic> json) => _ProxyGroup(
|
||||
hidden: json['hidden'] as bool?,
|
||||
icon: json['icon'] as String?,
|
||||
order: json['order'] as String?,
|
||||
memberLayout: (json['memberLayout'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProxyGroupToJson(_ProxyGroup instance) =>
|
||||
@@ -46,6 +52,7 @@ Map<String, dynamic> _$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<String, dynamic> _$ProxyGroupToJson(_ProxyGroup instance) =>
|
||||
'hidden': instance.hidden,
|
||||
'icon': instance.icon,
|
||||
'order': instance.order,
|
||||
'memberLayout': instance.memberLayout,
|
||||
};
|
||||
|
||||
const _$GroupTypeEnumMap = {
|
||||
@@ -99,6 +107,11 @@ _CustomProxy _$CustomProxyFromJson(Map<String, dynamic> json) => _CustomProxy(
|
||||
id: Snowflake.buildId((json['id'] as num?)?.toInt()),
|
||||
definition: json['definition'] as Map<String, dynamic>? ?? const {},
|
||||
order: json['order'] as String?,
|
||||
emptyOptions:
|
||||
(json['emptyOptions'] as List<dynamic>?)
|
||||
?.map((e) => $enumDecode(_$ProxyFieldEnumMap, e))
|
||||
.toSet() ??
|
||||
const {},
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$CustomProxyToJson(_CustomProxy instance) =>
|
||||
@@ -106,8 +119,73 @@ Map<String, dynamic> _$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<String, dynamic> json) =>
|
||||
_RuleProvider(name: json['name'] as String);
|
||||
|
||||
@@ -274,17 +352,17 @@ _Dns _$DnsFromJson(Map<String, dynamic> json) => _Dns(
|
||||
(json['default-nameserver'] as List<dynamic>?)
|
||||
?.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<dynamic>?)
|
||||
?.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<String, dynamic> json) => _Dns(
|
||||
(json['nameserver'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList() ??
|
||||
const [],
|
||||
const ['https://doh.pub/dns-query', 'tls://223.5.5.5:853'],
|
||||
fallback:
|
||||
(json['fallback'] as List<dynamic>?)?.map((e) => e as String).toList() ??
|
||||
const [],
|
||||
@@ -380,7 +458,7 @@ const _$FakeIpFilterModeEnumMap = {
|
||||
|
||||
_Ntp _$NtpFromJson(Map<String, dynamic> 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<String, dynamic>),
|
||||
memberLayout: (json['memberLayout'] as List<dynamic>?)
|
||||
?.map((e) => e as String)
|
||||
.toList(),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ProxyProviderOptionsToJson(
|
||||
@@ -600,6 +681,7 @@ Map<String, dynamic> _$ProxyProviderOptionsToJson(
|
||||
'filter': instance.filter,
|
||||
'exclude-filter': instance.excludeFilter,
|
||||
'override': instance.proxyOverride,
|
||||
'memberLayout': instance.memberLayout,
|
||||
};
|
||||
|
||||
_Rule _$RuleFromJson(Map<String, dynamic> json) => _Rule(
|
||||
|
||||
@@ -2053,7 +2053,7 @@ return $default(_that.systemProxy,_that.bypassDomain,_that.bypassPrivateRoute,_t
|
||||
@JsonSerializable()
|
||||
|
||||
class _NetworkProps implements NetworkProps {
|
||||
const _NetworkProps({this.systemProxy = true, List<String> bypassDomain = defaultBypassDomain, this.bypassPrivateRoute = false, this.autoSetSystemDns = true, this.appendSystemDns = false, this.authentication = defaultAuthenticationProps}): _bypassDomain = bypassDomain;
|
||||
const _NetworkProps({this.systemProxy = true, List<String> bypassDomain = defaultBypassDomain, this.bypassPrivateRoute = true, this.autoSetSystemDns = true, this.appendSystemDns = false, this.authentication = defaultAuthenticationProps}): _bypassDomain = bypassDomain;
|
||||
factory _NetworkProps.fromJson(Map<String, dynamic> json) => _$NetworkPropsFromJson(json);
|
||||
|
||||
@override@JsonKey() final bool systemProxy;
|
||||
|
||||
@@ -259,7 +259,7 @@ _NetworkProps _$NetworkPropsFromJson(Map<String, dynamic> 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
|
||||
|
||||
@@ -8718,7 +8718,8 @@ $ProxiesDataCopyWith<$Res> get proxiesData {
|
||||
/// @nodoc
|
||||
mixin _$MakeRealProfileState {
|
||||
|
||||
String get profilesPath; int get profileId; Map<String, dynamic> get rawConfig; PatchClashConfig get realPatchConfig; ProfileOverrides get overrides; bool get appendSystemDns; List<ProxyGroup> get proxyGroups; List<Rule> get rules; List<Rule> get addedRules; String get defaultUA; List<String> get authentication; Map<String, dynamic> get injectedProxyProviders; Map<String, dynamic> get injectedRuleProviders; String? get matchTarget; bool get safeMode;
|
||||
String get profilesPath; int get profileId; Map<String, dynamic> 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<ProxyGroup> get proxyGroups; List<Rule> get rules; List<Rule> get addedRules; String get defaultUA; List<String> get authentication; Map<String, dynamic> get injectedProxyProviders; Map<String, dynamic> 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<MakeRealProfileState> 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<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> injectedRuleProviders, String? matchTarget, bool safeMode
|
||||
String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> 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<String, dynamic>,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<ProxyGroup>,rules: null == rules ? _self.rules : rules // ignore: cast_nullable_to_non_nullable
|
||||
as List<Rule>,addedRules: null == addedRules ? _self.addedRules : addedRules // ignore: cast_nullable_to_non_nullable
|
||||
@@ -8891,10 +8893,10 @@ return $default(_that);case _:
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> injectedRuleProviders, String? matchTarget, bool safeMode)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> 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 extends Object?>(TResult Function( String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> injectedRuleProviders, String? matchTarget, bool safeMode) $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> 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 extends Object?>(TResult? Function( String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> injectedRuleProviders, String? matchTarget, bool safeMode)? $default,) {final _that = this;
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> 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<String, dynamic> rawConfig, required this.realPatchConfig, this.overrides = const ProfileOverrides(), required this.appendSystemDns, required List<ProxyGroup> proxyGroups, required List<Rule> rules, required List<Rule> addedRules, required this.defaultUA, List<String> authentication = const [], Map<String, dynamic> injectedProxyProviders = const {}, Map<String, dynamic> 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<String, dynamic> rawConfig, required this.realPatchConfig, this.overrides = const ProfileOverrides(), this.keepEmptyDnsOverrides = false, required this.appendSystemDns, required List<ProxyGroup> proxyGroups, required List<Rule> rules, required List<Rule> addedRules, required this.defaultUA, List<String> authentication = const [], Map<String, dynamic> injectedProxyProviders = const {}, Map<String, dynamic> 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<ProxyGroup> _proxyGroups;
|
||||
@override List<ProxyGroup> 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<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> injectedRuleProviders, String? matchTarget, bool safeMode
|
||||
String profilesPath, int profileId, Map<String, dynamic> rawConfig, PatchClashConfig realPatchConfig, ProfileOverrides overrides, bool keepEmptyDnsOverrides, bool appendSystemDns, List<ProxyGroup> proxyGroups, List<Rule> rules, List<Rule> addedRules, String defaultUA, List<String> authentication, Map<String, dynamic> injectedProxyProviders, Map<String, dynamic> 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<String, dynamic>,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<ProxyGroup>,rules: null == rules ? _self._rules : rules // ignore: cast_nullable_to_non_nullable
|
||||
as List<Rule>,addedRules: null == addedRules ? _self._addedRules : addedRules // ignore: cast_nullable_to_non_nullable
|
||||
|
||||
@@ -334,6 +334,9 @@ abstract class MakeRealProfileState with _$MakeRealProfileState {
|
||||
required Map<String, dynamic> 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<ProxyGroup> proxyGroups,
|
||||
required List<Rule> rules,
|
||||
|
||||
@@ -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<InitErrorScreen> createState() => _InitErrorScreenState();
|
||||
}
|
||||
|
||||
class _InitErrorScreenState extends State<InitErrorScreen> {
|
||||
static const _maxContentWidth = 720.0;
|
||||
|
||||
bool _isExiting = false;
|
||||
|
||||
void _showSnackBar(String message) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message), behavior: SnackBarBehavior.floating),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _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<bool> _confirmClearData() async {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final res = await dialogs.showCommonDialog<bool>(
|
||||
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<void> _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<void> _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<Widget> 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -91,7 +91,7 @@ final class SetupActionProvider extends $NotifierProvider<SetupAction, void> {
|
||||
}
|
||||
}
|
||||
|
||||
String _$setupActionHash() => r'b9b400d5128bcf9e045b397d681109ae9cca3a48';
|
||||
String _$setupActionHash() => r'bc66d570ecc9a96fa751970a9f8b5584d9a843ee';
|
||||
|
||||
abstract class _$SetupAction extends $Notifier<void> {
|
||||
void build();
|
||||
@@ -450,7 +450,7 @@ final class ProfilesActionProvider
|
||||
}
|
||||
}
|
||||
|
||||
String _$profilesActionHash() => r'4f716e75b986cf6e2873b7fb62acedbc87f94199';
|
||||
String _$profilesActionHash() => r'1ed9d382c267699f9705428fcc5f13378a7c825f';
|
||||
|
||||
abstract class _$ProfilesAction extends $Notifier<void> {
|
||||
void build();
|
||||
|
||||
@@ -231,6 +231,41 @@ List<CustomProxy> 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<String, dynamic> withUnlinkedFields(
|
||||
Map<String, dynamic> next, {
|
||||
required Map<String, dynamic> previous,
|
||||
required Map<String, dynamic>? linked,
|
||||
}) {
|
||||
if (linked == null || next['type'] != previous['type']) {
|
||||
return next;
|
||||
}
|
||||
return _withUnlinked(next, previous, linked);
|
||||
}
|
||||
|
||||
Map<String, dynamic> _withUnlinked(
|
||||
Map<String, dynamic> next,
|
||||
Map<dynamic, dynamic> previous,
|
||||
Map<dynamic, dynamic> 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<String, dynamic>.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<ShareLinkLine> get pending =>
|
||||
lines.where((line) => line.kept == null).toList();
|
||||
|
||||
Map<int, String> 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<CustomProxy> apply(
|
||||
Map<int, List<Map<String, dynamic>>> decoded, {
|
||||
required Set<String> groupNames,
|
||||
Map<int, Map<String, dynamic>> linked = const {},
|
||||
}) {
|
||||
final keptIds = {for (final line in lines) ?line.kept?.id};
|
||||
final dropped = <String, CustomProxy>{
|
||||
@@ -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 = <String, dynamic>{...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,
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,33 +411,27 @@ ProxyGroup _importGroup(
|
||||
required Map<String, String> names,
|
||||
required Map<String, String> 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(
|
||||
|
||||
@@ -29,12 +29,15 @@ class AboutView extends ConsumerWidget {
|
||||
Future<void> _checkUpdate(BuildContext context, WidgetRef ref) async {
|
||||
if (ref.read(loadingProvider(LoadingTag.checkUpdate))) return;
|
||||
final commonAction = ref.read(commonActionProvider.notifier);
|
||||
final data = await globalState.loadingRun<Map<String, dynamic>?>(
|
||||
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({
|
||||
|
||||
@@ -19,7 +19,8 @@ class AccessView extends ConsumerStatefulWidget {
|
||||
ConsumerState<AccessView> createState() => _AccessViewState();
|
||||
}
|
||||
|
||||
class _AccessViewState extends ConsumerState<AccessView> {
|
||||
class _AccessViewState extends ConsumerState<AccessView>
|
||||
with RouteSettledMixin<AccessView> {
|
||||
late ScrollController _controller;
|
||||
List<String>? _pinedList;
|
||||
bool _isInit = false;
|
||||
@@ -30,8 +31,7 @@ class _AccessViewState extends ConsumerState<AccessView> {
|
||||
@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<AccessView> {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void didSettleRoute() => _completer.complete(_loadPackages());
|
||||
|
||||
Future<void> _loadPackages() async {
|
||||
final action = ref.read(systemActionProvider.notifier);
|
||||
final packages = await action.getPackages();
|
||||
@@ -160,7 +163,6 @@ class _AccessViewState extends ConsumerState<AccessView> {
|
||||
Future<void> _handleToSetting() async {
|
||||
await showSheet<int>(
|
||||
context: context,
|
||||
props: const SheetProps(isScrollControlled: true),
|
||||
builder: (context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return CommonScaffold(
|
||||
@@ -179,9 +181,9 @@ class _AccessViewState extends ConsumerState<AccessView> {
|
||||
});
|
||||
}
|
||||
|
||||
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<AccessView> {
|
||||
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<AccessView> {
|
||||
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<AccessView> {
|
||||
CommonMinFilledButtonTheme(
|
||||
child: ElasticButton(
|
||||
child: FilledButton.tonal(
|
||||
onPressed: _handleToggle,
|
||||
onPressed: () => _handleEnable(true),
|
||||
child: Text(appLocalizations.turnOn),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<Widget> 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),
|
||||
|
||||
@@ -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)],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<T> =
|
||||
PatchClashConfig Function(PatchClashConfig state, T value);
|
||||
|
||||
final _dnsOverrideKeysSelector = patchClashConfigProvider.select(
|
||||
(state) => state.dnsOverrideKeys,
|
||||
);
|
||||
|
||||
ProviderListenable<T> _dnsSelector<T>(T Function(Dns dns) select) {
|
||||
return patchClashConfigProvider.select((state) => select(state.dns));
|
||||
}
|
||||
|
||||
ConfigWriter<T> _dnsWriter<T>(_DnsUpdate<T> 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<DnsOverrideKey>)> _sectionsOf(
|
||||
AppLocalizations l,
|
||||
Iterable<DnsOverrideKey> 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<DnsOverrideKey, Dns>;
|
||||
|
||||
Future<void> _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<DnsOverrideKey>(
|
||||
context: context,
|
||||
props: const SheetProps(isScrollControlled: true),
|
||||
builder: (context) => OverwriteSelectionSheet<DnsOverrideKey>(
|
||||
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<void> _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<bool> _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<DnsOverrideKey, Dns> {
|
||||
/// 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<DnsOverrideKey, Dns> {
|
||||
const _DnsOverrideSpec(this.target, {this.keys});
|
||||
|
||||
@override
|
||||
final OverrideTarget<DnsOverrideKey, Dns> target;
|
||||
|
||||
final Set<DnsOverrideKey>? keys;
|
||||
|
||||
@override
|
||||
String title(AppLocalizations l) => 'DNS';
|
||||
|
||||
@override
|
||||
List<DnsOverrideKey> 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<Dns> 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<DnsOverrideKey, Dns> {
|
||||
const _ProfileDnsOverrideSpec(super.target);
|
||||
|
||||
@override
|
||||
String overrideYaml(Dns model, Set<DnsOverrideKey> 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<Dns, Map<String, String>> {
|
||||
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<Dns> 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<bool> update) {
|
||||
return ConfigToggleItem(
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: overrideKey.description,
|
||||
selector: _dnsSelector(select),
|
||||
onChanged: _dnsWriter(update),
|
||||
);
|
||||
}
|
||||
|
||||
Widget text(
|
||||
String Function(Dns dns) select,
|
||||
_DnsUpdate<String> 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<int> update) {
|
||||
return ConfigTextItem(
|
||||
leading: leading,
|
||||
title: title,
|
||||
selector: _dnsSelector((dns) => '${select(dns)}'),
|
||||
onChanged: _dnsWriter<String>(
|
||||
(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<T extends Enum>(
|
||||
List<T> values,
|
||||
T Function(Dns dns) select,
|
||||
_DnsUpdate<T> update,
|
||||
) {
|
||||
return ConfigOptionsItem<T>(
|
||||
leading: leading,
|
||||
title: title,
|
||||
options: values,
|
||||
textBuilder: (value) => value.name,
|
||||
selector: _dnsSelector(select),
|
||||
onChanged: _dnsWriter(update),
|
||||
);
|
||||
}
|
||||
|
||||
Widget policy(
|
||||
Map<String, String> Function(Dns dns) select,
|
||||
_DnsUpdate<Map<String, String>> update,
|
||||
) {
|
||||
return _PolicyItem(
|
||||
overrideKey: overrideKey,
|
||||
leading: leading,
|
||||
select: select,
|
||||
update: update,
|
||||
);
|
||||
}
|
||||
|
||||
Widget list(
|
||||
List<String> Function(Dns dns) select,
|
||||
_DnsUpdate<List<String>> 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<Dns> model;
|
||||
final ConfigLabel title;
|
||||
final Widget leading;
|
||||
final Map<String, String> Function(Dns dns) select;
|
||||
final _DnsUpdate<Map<String, String>> update;
|
||||
final Dns Function(Dns dns, Map<String, String> 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<String, String>.from(value));
|
||||
if (value is! Map) {
|
||||
return;
|
||||
}
|
||||
final next = Map<String, String>.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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<FiltersView> createState() => _FiltersViewState();
|
||||
}
|
||||
|
||||
class _FiltersViewState extends ConsumerState<FiltersView> {
|
||||
List<Filter> get _filters => ref.read(appSettingProvider).filters;
|
||||
|
||||
void _save(List<Filter> filters) {
|
||||
ref
|
||||
.read(appSettingProvider.notifier)
|
||||
.update((state) => state.copyWith(filters: filters));
|
||||
}
|
||||
|
||||
Future<void> _handleAddOrUpdate([Filter? filter]) async {
|
||||
final res = await dialogs.showCommonDialog<Filter>(
|
||||
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<String?> _checkRegex(String regex) async {
|
||||
final errors = await checkFilters(ref, [regex]);
|
||||
final error = errors?.single ?? '';
|
||||
return error.isEmpty ? null : error;
|
||||
}
|
||||
|
||||
void _handleDelete(Set<String> 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<Filter, String>(
|
||||
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<String?> Function(String regex) checkRegex;
|
||||
final Set<String> 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<FormState>();
|
||||
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<void> _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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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'),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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<IconSetsView> createState() => _IconSetsViewState();
|
||||
}
|
||||
|
||||
class _IconSetsViewState extends ConsumerState<IconSetsView> {
|
||||
late final IconSetsAction _iconSetsAction;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_iconSetsAction = ref.read(iconSetsActionProvider.notifier);
|
||||
}
|
||||
|
||||
Future<void> _run(FutureOr<IconSet> Function() task, {String? title}) {
|
||||
return globalState.loadingRun(task, title: title, tag: LoadingTag.iconSets);
|
||||
}
|
||||
|
||||
Future<void> _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<void> _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<void> _handleEditOptions(IconSet iconSet) async {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
if (!iconSet.isRemote) {
|
||||
final name = await dialogs.showCommonDialog<String>(
|
||||
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<void> _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<CommonPopupMenuItem> _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<IconSet> 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<IconSet> 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 <IconSetIcon>[])
|
||||
.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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -38,11 +38,9 @@ ConfigToggleItem _vpnToggle({
|
||||
required ConfigLabel title,
|
||||
required bool Function(VpnProps state) select,
|
||||
required _VpnUpdate<bool> 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<bool> 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<RouteMode>(
|
||||
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<int> 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<Widget> 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<String>(
|
||||
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<Widget> 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<Widget> networkOptionsItems({
|
||||
required bool isDesktop,
|
||||
required bool isMacOS,
|
||||
required bool isCustomInterfaceName,
|
||||
required bool isBypassPrivateRoute,
|
||||
}) {
|
||||
return [
|
||||
if (isDesktop) const TUNItem(),
|
||||
@@ -319,21 +478,24 @@ List<Widget> 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<T> =
|
||||
PatchClashConfig Function(PatchClashConfig state, T value);
|
||||
|
||||
final _ntpOverrideKeysSelector = patchClashConfigProvider.select(
|
||||
(state) => state.ntpOverrideKeys,
|
||||
);
|
||||
|
||||
ProviderListenable<T> _ntpSelector<T>(T Function(Ntp ntp) select) {
|
||||
return patchClashConfigProvider.select((state) => select(state.ntp));
|
||||
}
|
||||
|
||||
ConfigWriter<T> _ntpWriter<T>(_NtpUpdate<T> 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<NtpOverrideKey, Ntp>;
|
||||
|
||||
Future<void> _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<NtpOverrideKey>(
|
||||
context: context,
|
||||
props: const SheetProps(isScrollControlled: true),
|
||||
builder: (context) => OverwriteSelectionSheet<NtpOverrideKey>(
|
||||
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<NtpOverrideKey, Ntp> {
|
||||
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<void> _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<bool> _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<NtpOverrideKey, Ntp>
|
||||
with OverrideQuickEdit<NtpOverrideKey, Ntp> {
|
||||
const _NtpOverrideSpec(this.target);
|
||||
|
||||
@override
|
||||
final OverrideTarget<NtpOverrideKey, Ntp> target;
|
||||
|
||||
@override
|
||||
String title(AppLocalizations l) => 'NTP';
|
||||
|
||||
@override
|
||||
List<NtpOverrideKey> get values => NtpOverrideKey.values;
|
||||
|
||||
@override
|
||||
String overrideYaml(Ntp model, Set<NtpOverrideKey> 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<bool> update) {
|
||||
return ConfigToggleItem(
|
||||
leading: leading,
|
||||
title: title,
|
||||
subtitle: overrideKey.description,
|
||||
selector: _ntpSelector(select),
|
||||
onChanged: _ntpWriter(update),
|
||||
);
|
||||
}
|
||||
|
||||
Widget text(
|
||||
String Function(Ntp ntp) select,
|
||||
_NtpUpdate<String> 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<int> update, {
|
||||
int maxLength = TextInputLimits.number,
|
||||
int min = 0,
|
||||
int max = 0x7fffffff,
|
||||
}) {
|
||||
return ConfigTextItem(
|
||||
leading: leading,
|
||||
title: title,
|
||||
selector: _ntpSelector((ntp) => '${select(ntp)}'),
|
||||
onChanged: _ntpWriter<String>(
|
||||
(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<Ntp> 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),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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<K extends OverrideKey, M> = ({M model, Set<K> keys});
|
||||
|
||||
abstract interface class OverrideModel<M> {
|
||||
ProviderListenable<T> selectModel<T>(T Function(M model) select);
|
||||
|
||||
ConfigWriter<T> modelWriter<T>(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<K extends OverrideKey, M>
|
||||
implements OverrideModel<M> {
|
||||
const OverrideTarget();
|
||||
|
||||
ConfigLabel get description;
|
||||
|
||||
ConfigLabel get entries;
|
||||
|
||||
ConfigLabel get addEntry;
|
||||
|
||||
ProviderListenable<List<CustomIssue>>? get issues => null;
|
||||
|
||||
ProviderListenable<T> select<T>(T Function(OverrideState<K, M> state) select);
|
||||
|
||||
OverrideState<K, M> read(WidgetRef ref);
|
||||
|
||||
void update(
|
||||
WidgetRef ref,
|
||||
OverrideState<K, M> Function(OverrideState<K, M> state) update,
|
||||
);
|
||||
|
||||
@override
|
||||
ProviderListenable<T> selectModel<T>(T Function(M model) select) =>
|
||||
this.select((state) => select(state.model));
|
||||
|
||||
@override
|
||||
ConfigWriter<T> modelWriter<T>(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<K extends OverrideKey, M>
|
||||
extends OverrideTarget<K, M> {
|
||||
const PatchOverrideTarget({
|
||||
required this.get,
|
||||
required this.set,
|
||||
required this.description,
|
||||
});
|
||||
|
||||
final OverrideState<K, M> Function(PatchClashConfig config) get;
|
||||
final PatchClashConfig Function(
|
||||
PatchClashConfig config,
|
||||
OverrideState<K, M> 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<T> select<T>(
|
||||
T Function(OverrideState<K, M> state) select,
|
||||
) => patchClashConfigProvider.select((config) => select(get(config)));
|
||||
|
||||
@override
|
||||
OverrideState<K, M> read(WidgetRef ref) =>
|
||||
get(ref.read(patchClashConfigProvider));
|
||||
|
||||
@override
|
||||
void update(
|
||||
WidgetRef ref,
|
||||
OverrideState<K, M> Function(OverrideState<K, M> state) update,
|
||||
) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.update((config) => set(config, update(get(config))));
|
||||
}
|
||||
}
|
||||
|
||||
class ProfileOverrideTarget<K extends OverrideKey, M>
|
||||
extends OverrideTarget<K, M> {
|
||||
const ProfileOverrideTarget(
|
||||
this.profileId, {
|
||||
required this.get,
|
||||
required this.set,
|
||||
this.issues,
|
||||
});
|
||||
|
||||
final int profileId;
|
||||
|
||||
@override
|
||||
final ProviderListenable<List<CustomIssue>>? issues;
|
||||
final OverrideState<K, M> Function(ProfileOverrides overrides) get;
|
||||
final ProfileOverrides Function(
|
||||
ProfileOverrides overrides,
|
||||
OverrideState<K, M> 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<T> select<T>(
|
||||
T Function(OverrideState<K, M> state) select,
|
||||
) =>
|
||||
profileProvider(profileId).select((profile) => select(get(_of(profile))));
|
||||
|
||||
@override
|
||||
OverrideState<K, M> read(WidgetRef ref) =>
|
||||
get(_of(ref.read(profileProvider(profileId))));
|
||||
|
||||
@override
|
||||
void update(
|
||||
WidgetRef ref,
|
||||
OverrideState<K, M> Function(OverrideState<K, M> state) update,
|
||||
) {
|
||||
ref
|
||||
.read(profilesProvider.notifier)
|
||||
.updateProfile(
|
||||
profileId,
|
||||
(profile) => profile.copyWith(
|
||||
overrides: set(profile.overrides, update(get(profile.overrides))),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
abstract class OverrideField<M> {
|
||||
const OverrideField();
|
||||
|
||||
Widget build(
|
||||
OverrideModel<M> model, {
|
||||
required ConfigLabel title,
|
||||
required Widget leading,
|
||||
});
|
||||
|
||||
M reset(M model, M defaults);
|
||||
}
|
||||
|
||||
abstract class ValueOverrideField<M, T> extends OverrideField<M> {
|
||||
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<M> extends ValueOverrideField<M, bool> {
|
||||
const ToggleOverrideField(super.select, super.update);
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
OverrideModel<M> model, {
|
||||
required ConfigLabel title,
|
||||
required Widget leading,
|
||||
}) {
|
||||
return ConfigToggleItem(
|
||||
leading: leading,
|
||||
title: title,
|
||||
selector: model.selectModel(select),
|
||||
onChanged: model.modelWriter(update),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OptionsOverrideField<M, T extends Enum> extends ValueOverrideField<M, T> {
|
||||
const OptionsOverrideField(
|
||||
this.options,
|
||||
super.select,
|
||||
super.update, {
|
||||
this.labelOf = _enumName,
|
||||
});
|
||||
|
||||
final List<T> options;
|
||||
final String Function(T value) labelOf;
|
||||
|
||||
static String _enumName(Enum value) => value.name;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
OverrideModel<M> model, {
|
||||
required ConfigLabel title,
|
||||
required Widget leading,
|
||||
}) {
|
||||
return ConfigOptionsItem<T>(
|
||||
leading: leading,
|
||||
title: title,
|
||||
options: options,
|
||||
textBuilder: labelOf,
|
||||
selector: model.selectModel(select),
|
||||
onChanged: model.modelWriter(update),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NumberOverrideField<M> extends ValueOverrideField<M, int> {
|
||||
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<M> model, {
|
||||
required ConfigLabel title,
|
||||
required Widget leading,
|
||||
}) {
|
||||
return ConfigTextItem(
|
||||
leading: leading,
|
||||
title: title,
|
||||
selector: model.selectModel((model) => '${select(model)}'),
|
||||
onChanged: model.modelWriter<String>(
|
||||
(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<M> extends ValueOverrideField<M, String> {
|
||||
const TextOverrideField(
|
||||
super.select,
|
||||
super.update, {
|
||||
required this.maxLength,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final int maxLength;
|
||||
final ConfigValidator? validator;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
OverrideModel<M> 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<M> extends ValueOverrideField<M, List<String>> {
|
||||
const ListOverrideField(
|
||||
super.select,
|
||||
super.update, {
|
||||
required this.itemMaxLength,
|
||||
this.itemValidator,
|
||||
});
|
||||
|
||||
final int itemMaxLength;
|
||||
final ConfigValidator? itemValidator;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
OverrideModel<M> 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<K extends OverrideKey, M> {
|
||||
const OverrideSpec();
|
||||
|
||||
OverrideTarget<K, M> get target;
|
||||
|
||||
String title(AppLocalizations l);
|
||||
|
||||
List<K> 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<M> fieldOf(K key);
|
||||
|
||||
/// Also resets the values of [keys], so a profile with no keys is untouched.
|
||||
OverrideState<K, M> withoutKeys(OverrideState<K, M> state, Set<K> 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<K>)> sectionsOf(AppLocalizations l, Iterable<K> keys) {
|
||||
final sections = <String, List<K>>{};
|
||||
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<K extends OverrideKey, M> on OverrideSpec<K, M> {
|
||||
String overrideYaml(M model, Set<K> keys);
|
||||
|
||||
/// Throws when [content] names a key outside [values] or a value the model
|
||||
/// cannot hold.
|
||||
OverrideState<K, M> applyOverrideYaml(M model, String content);
|
||||
|
||||
EditorSchema get schema;
|
||||
}
|
||||
|
||||
class OverrideView<K extends OverrideKey, M> extends ConsumerWidget {
|
||||
const OverrideView({super.key, required this.spec});
|
||||
|
||||
final OverrideSpec<K, M> 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<void>(
|
||||
context: context,
|
||||
builder: (_) => SelectionSheet<K>(
|
||||
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<void> _handleQuickEdit(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
OverrideQuickEdit<K, M> 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<bool> _handleQuickEditPop(
|
||||
WidgetRef ref,
|
||||
OverrideQuickEdit<K, M> 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<K, M>)
|
||||
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<K extends OverrideKey, M> extends ConsumerWidget {
|
||||
const _OverrideList({required this.spec});
|
||||
|
||||
final OverrideSpec<K, M> spec;
|
||||
|
||||
String? _footerOf(List<K> 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<K extends OverrideKey, M> implements OverrideModel<M> {
|
||||
const _ListedValue(this.spec, this.overrideKey);
|
||||
|
||||
final OverrideSpec<K, M> spec;
|
||||
final K overrideKey;
|
||||
|
||||
OverrideTarget<K, M> 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<T> selectModel<T>(T Function(M model) select) =>
|
||||
target.selectModel(select);
|
||||
|
||||
@override
|
||||
ConfigWriter<T> modelWriter<T>(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<K extends OverrideKey, M> extends ConsumerWidget {
|
||||
const _RemoveButton({required this.spec, required this.overrideKey});
|
||||
|
||||
final OverrideSpec<K, M> spec;
|
||||
final K overrideKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return EntryButton.remove(
|
||||
onPressed: () => spec.target.update(
|
||||
ref,
|
||||
(state) => spec.withoutKeys(state, {overrideKey}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<ClashProvidersView> createState() => _ClashProvidersViewState();
|
||||
@@ -25,36 +23,17 @@ class ClashProvidersView extends ConsumerStatefulWidget {
|
||||
class _ClashProvidersViewState extends ConsumerState<ClashProvidersView> {
|
||||
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<ClashProvider> get _providers =>
|
||||
ref.read(clashProvidersProvider(_kind)).value ?? [];
|
||||
|
||||
Set<String> _reservedLabels({ClashProvider? except}) {
|
||||
return {
|
||||
for (final item in _providers)
|
||||
for (final item
|
||||
in ref.read(clashProvidersProvider).value ?? const <ClashProvider>[])
|
||||
if (item.id != except?.id) item.label,
|
||||
if (_kind == ProviderKind.proxy)
|
||||
...ref.read(profileProvidersProvider).keys,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -74,12 +53,11 @@ class _ClashProvidersViewState extends ConsumerState<ClashProvidersView> {
|
||||
return null;
|
||||
}
|
||||
|
||||
String _uniqueLabel(String name) {
|
||||
final reserved = _reservedLabels();
|
||||
String _uniqueLabel(String name, [Set<String>? 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<ClashProvidersView> {
|
||||
ClashProvider provider, {
|
||||
ClashProvider? previous,
|
||||
List<int>? 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<ClashProvider?> _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<RuleProviderBehavior?> _showBehaviorDialog(
|
||||
ClashProvider provider,
|
||||
) async {
|
||||
final options = provider.format.behaviors;
|
||||
return dialogs.showCommonDialog<RuleProviderBehavior>(
|
||||
child: OptionsDialog<RuleProviderBehavior>(
|
||||
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<void> _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 <ClashProvider>[])
|
||||
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 = <ClashProvider>[];
|
||||
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<void> _handleImportFromFile() async {
|
||||
@@ -172,16 +128,10 @@ class _ClashProvidersViewState extends ConsumerState<ClashProvidersView> {
|
||||
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<String?> _showNameDialog({ClashProvider? except}) async {
|
||||
@@ -222,15 +172,8 @@ class _ClashProvidersViewState extends ConsumerState<ClashProvidersView> {
|
||||
);
|
||||
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<ClashProvidersView> {
|
||||
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<ClashProvidersView> {
|
||||
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<void> _handleEditOptions(ClashProvider provider) async {
|
||||
final res = await dialogs.showCommonDialog<ClashProvider>(
|
||||
child: _ProviderDialog(
|
||||
provider: provider,
|
||||
reservedLabels: _reservedLabels(except: provider),
|
||||
),
|
||||
);
|
||||
if (res == null) {
|
||||
return;
|
||||
}
|
||||
await _put(res, previous: provider);
|
||||
}
|
||||
Future<void> _handleEditOptions(ClashProvider provider) =>
|
||||
_showClashProviderOptions(ref, provider);
|
||||
|
||||
Future<void> _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<CommonPopupMenuItem> _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<ClashProvider> 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<ClashProvidersView> {
|
||||
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<ClashProvidersView> {
|
||||
).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<ClashProvidersView> {
|
||||
@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<ClashProvidersView> {
|
||||
),
|
||||
),
|
||||
],
|
||||
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?> _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<FileInfo?>(
|
||||
future: _fileInfo,
|
||||
builder: (context, snapshot) => LastUpdateTimeText(
|
||||
lastUpdateDate: snapshot.data?.lastModified,
|
||||
compact: true,
|
||||
style: context.listCaptionStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showClashProviderOptions(
|
||||
WidgetRef ref,
|
||||
ClashProvider provider,
|
||||
) async {
|
||||
final res = await dialogs.showCommonDialog<ClashProvider>(
|
||||
child: _ProviderDialog(
|
||||
provider: provider,
|
||||
reservedLabels: {
|
||||
for (final item
|
||||
in ref.read(clashProvidersProvider).value ??
|
||||
const <ClashProvider>[])
|
||||
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<FormState>();
|
||||
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<void> _handleSelectFormat() async {
|
||||
final res = await dialogs.showCommonDialog<RuleProviderFormat>(
|
||||
context: context,
|
||||
child: OptionsDialog<RuleProviderFormat>(
|
||||
title: context.appLocalizations.format,
|
||||
options: RuleProviderFormat.values,
|
||||
textBuilder: (item) => item.name,
|
||||
value: _draft.format!,
|
||||
),
|
||||
);
|
||||
if (res == null) {
|
||||
return;
|
||||
}
|
||||
_setFormat(res);
|
||||
}
|
||||
|
||||
Future<void> _handleSelectBehavior() async {
|
||||
final res = await dialogs.showCommonDialog<RuleProviderBehavior>(
|
||||
context: context,
|
||||
child: OptionsDialog<RuleProviderBehavior>(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -44,10 +44,9 @@ class _AddedRulesViewState extends ConsumerState<AddedRulesView> {
|
||||
ref.read(globalRulesProvider.notifier).put(res);
|
||||
}
|
||||
|
||||
void _handleQuickAdd() {
|
||||
void _handleQuickActions() {
|
||||
showSheet<void>(
|
||||
context: context,
|
||||
props: const SheetProps(isScrollControlled: true),
|
||||
builder: (_) =>
|
||||
RulePresetSheet(onAdd: ref.read(globalRulesProvider.notifier).putAll),
|
||||
);
|
||||
@@ -126,19 +125,18 @@ class _AddedRulesViewState extends ConsumerState<AddedRulesView> {
|
||||
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<AddedRulesView> {
|
||||
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<Rule> rules, int index, Set<dynamic> selected) {
|
||||
final rule = rules[index];
|
||||
|
||||
@@ -67,17 +67,10 @@ class _ScriptsViewState extends ConsumerState<ScriptsView> {
|
||||
).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<ScriptsView> {
|
||||
);
|
||||
}
|
||||
|
||||
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<CommonPopupMenuItem> _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<Script> scripts, int index) {
|
||||
final script = scripts[index];
|
||||
return SortableItem(
|
||||
key: ValueKey(script.id),
|
||||
index: index,
|
||||
child: Consumer(
|
||||
builder: (_, ref, child) => ContextMenuRegion(
|
||||
menuItems: ref.watch(isUpdatingProvider(script.updatingKey))
|
||||
? null
|
||||
: _buildMenuItems(script),
|
||||
child: child!,
|
||||
),
|
||||
child: ItemPositionProvider(
|
||||
position: ItemPosition.get(index, scripts.length),
|
||||
child: DecorationListItem(
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
title: Text(
|
||||
script.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(
|
||||
script.url != null
|
||||
? context.appLocalizations.remote
|
||||
: context.appLocalizations.local,
|
||||
style: context.listCaptionStyle,
|
||||
),
|
||||
trailing: _ScriptTrailing(script: script),
|
||||
onPressed: () {
|
||||
_handleToEditor(script);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -303,16 +341,18 @@ class _ScriptsViewState extends ConsumerState<ScriptsView> {
|
||||
final scripts = scriptsState.value ?? [];
|
||||
final isLoading = ref.watch(loadingProvider(LoadingTag.scripts));
|
||||
return CommonScaffold(
|
||||
canSort: scripts.length > 1,
|
||||
isLoading: isLoading,
|
||||
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(
|
||||
@@ -342,114 +382,34 @@ class _ScriptsViewState extends ConsumerState<ScriptsView> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ScriptItemTitle extends StatelessWidget {
|
||||
const _ScriptItemTitle({required this.script});
|
||||
class _ScriptTrailing extends ConsumerWidget {
|
||||
const _ScriptTrailing({required this.script});
|
||||
|
||||
final Script script;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final style = DefaultTextStyle.of(context).style;
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
script.label,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: style.copyWith(
|
||||
fontSize: context.textTheme.bodyLarge?.fontSize,
|
||||
),
|
||||
),
|
||||
if (script.url != null)
|
||||
LastUpdateTimeText(
|
||||
lastUpdateDate: script.lastUpdateTime,
|
||||
style: style.copyWith(
|
||||
fontSize: context.textTheme.bodySmall?.fontSize,
|
||||
color: style.color?.opacity60,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScriptItemMenu extends ConsumerWidget {
|
||||
const _ScriptItemMenu({
|
||||
required this.script,
|
||||
required this.onEdit,
|
||||
required this.onEditUrl,
|
||||
required this.onUpdate,
|
||||
required this.onDelete,
|
||||
});
|
||||
|
||||
final Script script;
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onEditUrl;
|
||||
final VoidCallback onUpdate;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final isUpdating = ref.watch(isUpdatingProvider(script.updatingKey));
|
||||
return SizedBox.square(
|
||||
dimension: 40,
|
||||
child: FadeThroughBox(
|
||||
alignment: Alignment.center,
|
||||
// Inset here, not in the row, so the sort handle that replaces it sits flush.
|
||||
return Padding(
|
||||
padding: const EdgeInsetsDirectional.only(end: 16),
|
||||
child: AnimatedSwitcher(
|
||||
duration: context.motionDuration(commonDuration),
|
||||
layoutBuilder: (current, previous) => Stack(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
children: [...previous, ?current],
|
||||
),
|
||||
child: isUpdating
|
||||
? const Padding(
|
||||
? const SizedBox.square(
|
||||
key: ValueKey('loading'),
|
||||
padding: EdgeInsets.all(8),
|
||||
dimension: 24,
|
||||
child: CommonCircleLoading(),
|
||||
)
|
||||
: CommonPopupBox(
|
||||
key: const ValueKey('menu'),
|
||||
popupBuilder: (_) => CommonPopupMenu(
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.edit,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: onEdit,
|
||||
),
|
||||
if (script.url != null) ...[
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.link,
|
||||
label: appLocalizations.url,
|
||||
onPressed: onEditUrl,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.sync,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: onUpdate,
|
||||
),
|
||||
],
|
||||
CommonPopupMenuItem(
|
||||
danger: true,
|
||||
glyph: AppGlyphs.delete,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
targetBuilder: (open) {
|
||||
return IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.standard,
|
||||
),
|
||||
tooltip: appLocalizations.more,
|
||||
onPressed: () {
|
||||
open();
|
||||
},
|
||||
icon: const GlyphIcon(AppGlyphs.more),
|
||||
);
|
||||
},
|
||||
: LastUpdateTimeText(
|
||||
key: const ValueKey('time'),
|
||||
lastUpdateDate: script.lastUpdateTime,
|
||||
compact: true,
|
||||
style: context.listCaptionStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,285 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.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/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';
|
||||
|
||||
extension on SnifferOverrideKey {
|
||||
String label(AppLocalizations l) => switch (this) {
|
||||
SnifferOverrideKey.enable => l.status,
|
||||
SnifferOverrideKey.overrideDest => l.overrideDestination,
|
||||
SnifferOverrideKey.forceDnsMapping => l.forceDnsMapping,
|
||||
SnifferOverrideKey.parsePureIp => l.parsePureIp,
|
||||
SnifferOverrideKey.forceDomain => l.forceDomain,
|
||||
SnifferOverrideKey.skipDomain => l.skipDomain,
|
||||
SnifferOverrideKey.skipSrcAddress => l.skipSrcAddress,
|
||||
SnifferOverrideKey.skipDstAddress => l.skipDstAddress,
|
||||
SnifferOverrideKey.sniffHttp ||
|
||||
SnifferOverrideKey.sniffTls ||
|
||||
SnifferOverrideKey.sniffQuic => field,
|
||||
};
|
||||
|
||||
ConfigLabel? get description => switch (this) {
|
||||
SnifferOverrideKey.enable => (l) => l.snifferStatusDesc,
|
||||
SnifferOverrideKey.overrideDest => (l) => l.overrideDestinationDesc,
|
||||
SnifferOverrideKey.forceDnsMapping => (l) => l.forceDnsMappingDesc,
|
||||
SnifferOverrideKey.parsePureIp => (l) => l.parsePureIpDesc,
|
||||
SnifferOverrideKey.forceDomain => (l) => l.forceDomainDesc,
|
||||
SnifferOverrideKey.skipDomain => (l) => l.skipDomainDesc,
|
||||
_ => null,
|
||||
};
|
||||
}
|
||||
|
||||
typedef _SnifferState = OverrideState<SnifferOverrideKey, Sniffer>;
|
||||
|
||||
_SnifferState _profileSniffer(ProfileOverrides overrides) =>
|
||||
(model: overrides.sniffer, keys: overrides.snifferOverrideKeys);
|
||||
|
||||
ProfileOverrides _setProfileSniffer(
|
||||
ProfileOverrides overrides,
|
||||
_SnifferState state,
|
||||
) => overrides.copyWith(sniffer: state.model, snifferOverrideKeys: state.keys);
|
||||
|
||||
class SnifferView extends OverrideView<SnifferOverrideKey, Sniffer> {
|
||||
SnifferView(int profileId, {super.key})
|
||||
: super(
|
||||
spec: _SnifferOverrideSpec(
|
||||
ProfileOverrideTarget(
|
||||
profileId,
|
||||
get: _profileSniffer,
|
||||
set: _setProfileSniffer,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _SnifferOverrideSpec extends OverrideSpec<SnifferOverrideKey, Sniffer>
|
||||
with OverrideQuickEdit<SnifferOverrideKey, Sniffer> {
|
||||
const _SnifferOverrideSpec(this.target);
|
||||
|
||||
@override
|
||||
final OverrideTarget<SnifferOverrideKey, Sniffer> target;
|
||||
|
||||
@override
|
||||
String title(AppLocalizations l) => l.sniffer;
|
||||
|
||||
@override
|
||||
List<SnifferOverrideKey> get values => SnifferOverrideKey.values;
|
||||
|
||||
@override
|
||||
String overrideYaml(Sniffer model, Set<SnifferOverrideKey> keys) =>
|
||||
model.overrideYaml(keys);
|
||||
|
||||
@override
|
||||
_SnifferState applyOverrideYaml(Sniffer model, String content) {
|
||||
final result = model.applyOverrideYaml(content);
|
||||
return (model: result.sniffer, keys: result.keys);
|
||||
}
|
||||
|
||||
@override
|
||||
EditorSchema get schema => EditorSchema.sniffer;
|
||||
|
||||
@override
|
||||
NullStatusIllustration get illustration => NullStatusIllustration.sniffer;
|
||||
|
||||
@override
|
||||
Sniffer get defaults => defaultSniffer;
|
||||
|
||||
@override
|
||||
String sectionOf(SnifferOverrideKey key, AppLocalizations l) =>
|
||||
key.parent == null ? l.options : l.sniffProtocols;
|
||||
|
||||
@override
|
||||
String label(SnifferOverrideKey key, AppLocalizations l) => key.label(l);
|
||||
|
||||
@override
|
||||
ConfigLabel? description(SnifferOverrideKey key) => key.description;
|
||||
|
||||
@override
|
||||
OverrideField<Sniffer> fieldOf(SnifferOverrideKey key) => switch (key) {
|
||||
SnifferOverrideKey.enable => ToggleOverrideField(
|
||||
(sniffer) => sniffer.enable,
|
||||
(sniffer, value) => sniffer.copyWith(enable: value),
|
||||
),
|
||||
SnifferOverrideKey.overrideDest => ToggleOverrideField(
|
||||
(sniffer) => sniffer.overrideDest,
|
||||
(sniffer, value) => sniffer.copyWith(overrideDest: value),
|
||||
),
|
||||
SnifferOverrideKey.forceDnsMapping => ToggleOverrideField(
|
||||
(sniffer) => sniffer.forceDnsMapping,
|
||||
(sniffer, value) => sniffer.copyWith(forceDnsMapping: value),
|
||||
),
|
||||
SnifferOverrideKey.parsePureIp => ToggleOverrideField(
|
||||
(sniffer) => sniffer.parsePureIp,
|
||||
(sniffer, value) => sniffer.copyWith(parsePureIp: value),
|
||||
),
|
||||
SnifferOverrideKey.forceDomain => ListOverrideField(
|
||||
(sniffer) => sniffer.forceDomain,
|
||||
(sniffer, value) => sniffer.copyWith(forceDomain: value),
|
||||
itemMaxLength: TextInputLimits.domain,
|
||||
),
|
||||
SnifferOverrideKey.skipDomain => ListOverrideField(
|
||||
(sniffer) => sniffer.skipDomain,
|
||||
(sniffer, value) => sniffer.copyWith(skipDomain: value),
|
||||
itemMaxLength: TextInputLimits.domain,
|
||||
),
|
||||
SnifferOverrideKey.skipSrcAddress => ListOverrideField(
|
||||
(sniffer) => sniffer.skipSrcAddress,
|
||||
(sniffer, value) => sniffer.copyWith(skipSrcAddress: value),
|
||||
itemMaxLength: TextInputLimits.cidr,
|
||||
itemValidator: validateIpMatcher,
|
||||
),
|
||||
SnifferOverrideKey.skipDstAddress => ListOverrideField(
|
||||
(sniffer) => sniffer.skipDstAddress,
|
||||
(sniffer, value) => sniffer.copyWith(skipDstAddress: value),
|
||||
itemMaxLength: TextInputLimits.cidr,
|
||||
itemValidator: validateIpMatcher,
|
||||
),
|
||||
SnifferOverrideKey.sniffHttp ||
|
||||
SnifferOverrideKey.sniffTls ||
|
||||
SnifferOverrideKey.sniffQuic => _SniffField(key),
|
||||
};
|
||||
}
|
||||
|
||||
class _SniffField extends OverrideField<Sniffer> {
|
||||
const _SniffField(this.overrideKey);
|
||||
|
||||
final SnifferOverrideKey overrideKey;
|
||||
|
||||
@override
|
||||
Widget build(
|
||||
OverrideModel<Sniffer> model, {
|
||||
required ConfigLabel title,
|
||||
required Widget leading,
|
||||
}) {
|
||||
return _SniffItem(model: model, overrideKey: overrideKey, leading: leading);
|
||||
}
|
||||
|
||||
@override
|
||||
Sniffer reset(Sniffer model, Sniffer defaults) =>
|
||||
model.withSniff(overrideKey, defaults.sniffOf(overrideKey));
|
||||
}
|
||||
|
||||
/// mihomo sniffs a protocol listed without ports on its standard port.
|
||||
String _portsText(AppLocalizations l, List<String> ports) {
|
||||
return ports.isEmpty ? l.defaultText : l.itemsCount(ports.length);
|
||||
}
|
||||
|
||||
class _SniffItem extends ConsumerWidget {
|
||||
const _SniffItem({
|
||||
required this.model,
|
||||
required this.overrideKey,
|
||||
required this.leading,
|
||||
});
|
||||
|
||||
final OverrideModel<Sniffer> model;
|
||||
final SnifferOverrideKey overrideKey;
|
||||
final Widget leading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final ports = ref.watch(
|
||||
model.selectModel((sniffer) => sniffer.sniffOf(overrideKey).ports),
|
||||
);
|
||||
return ListItem.open(
|
||||
leading: leading,
|
||||
title: Text(overrideKey.label(appLocalizations)),
|
||||
subtitle: Text(
|
||||
_portsText(appLocalizations, ports),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
widget: _SniffView(model: model, overrideKey: overrideKey),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// A protocol without its own `override-destination` follows the sniffer's.
|
||||
enum _SniffDestination {
|
||||
inherit(null),
|
||||
enabled(true),
|
||||
disabled(false);
|
||||
|
||||
const _SniffDestination(this.value);
|
||||
|
||||
final bool? value;
|
||||
|
||||
static _SniffDestination of(bool? value) =>
|
||||
values.firstWhere((item) => item.value == value);
|
||||
|
||||
String label(AppLocalizations l) => switch (this) {
|
||||
_SniffDestination.inherit => l.defaultText,
|
||||
_SniffDestination.enabled => l.enabled,
|
||||
_SniffDestination.disabled => l.disabled,
|
||||
};
|
||||
}
|
||||
|
||||
class _SniffView extends ConsumerWidget {
|
||||
const _SniffView({required this.model, required this.overrideKey});
|
||||
|
||||
final OverrideModel<Sniffer> model;
|
||||
final SnifferOverrideKey overrideKey;
|
||||
|
||||
ProviderListenable<T> _sniffSelector<T>(
|
||||
T Function(SnifferConfig sniff) select,
|
||||
) {
|
||||
return model.selectModel((sniffer) => select(sniffer.sniffOf(overrideKey)));
|
||||
}
|
||||
|
||||
ConfigWriter<T> _sniffWriter<T>(
|
||||
SnifferConfig Function(SnifferConfig sniff, T value) update,
|
||||
) {
|
||||
return model.modelWriter<T>(
|
||||
(sniffer, value) => sniffer.withSniff(
|
||||
overrideKey,
|
||||
update(sniffer.sniffOf(overrideKey), value),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final ports = ref.watch(_sniffSelector((sniff) => sniff.ports));
|
||||
return BaseScaffold(
|
||||
title: overrideKey.label(appLocalizations),
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
).copyWith(top: context.contentTopPadding, bottom: 16),
|
||||
children: [
|
||||
generateSectionV3(
|
||||
items: [
|
||||
ConfigListEditItem(
|
||||
title: (l) => l.ports,
|
||||
subtitle: (l) => _portsText(l, ports),
|
||||
selector: _sniffSelector((sniff) => sniff.ports),
|
||||
onChanged: _sniffWriter(
|
||||
(sniff, value) => sniff.copyWith(ports: value),
|
||||
),
|
||||
itemMaxLength: TextInputLimits.portRange,
|
||||
itemValidator: validatePortRange,
|
||||
),
|
||||
ConfigOptionsItem<_SniffDestination>(
|
||||
title: (l) => l.overrideDestination,
|
||||
options: _SniffDestination.values,
|
||||
textBuilder: (value) => value.label(appLocalizations),
|
||||
selector: _sniffSelector(
|
||||
(sniff) => _SniffDestination.of(sniff.overrideDest),
|
||||
),
|
||||
onChanged: _sniffWriter(
|
||||
(sniff, value) => sniff.copyWith(overrideDest: value.value),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/l10n/l10n.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/views/config/override.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
|
||||
extension on TunOverrideKey {
|
||||
String label(AppLocalizations l) => switch (this) {
|
||||
TunOverrideKey.disableIcmpForwarding => l.disableIcmpForwarding,
|
||||
TunOverrideKey.excludeInterface => l.excludeInterface,
|
||||
};
|
||||
|
||||
ConfigLabel get description => switch (this) {
|
||||
TunOverrideKey.disableIcmpForwarding => (l) => l.disableIcmpForwardingDesc,
|
||||
TunOverrideKey.excludeInterface => (l) => l.excludeInterfaceDesc,
|
||||
};
|
||||
}
|
||||
|
||||
typedef _TunState = OverrideState<TunOverrideKey, ProfileTun>;
|
||||
|
||||
_TunState _profileTun(ProfileOverrides overrides) =>
|
||||
(model: overrides.tun, keys: overrides.tunOverrideKeys);
|
||||
|
||||
ProfileOverrides _setProfileTun(ProfileOverrides overrides, _TunState state) =>
|
||||
overrides.copyWith(tun: state.model, tunOverrideKeys: state.keys);
|
||||
|
||||
class TunView extends OverrideView<TunOverrideKey, ProfileTun> {
|
||||
TunView(int profileId, {super.key})
|
||||
: super(
|
||||
spec: _TunOverrideSpec(
|
||||
ProfileOverrideTarget(
|
||||
profileId,
|
||||
get: _profileTun,
|
||||
set: _setProfileTun,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _TunOverrideSpec extends OverrideSpec<TunOverrideKey, ProfileTun> {
|
||||
const _TunOverrideSpec(this.target);
|
||||
|
||||
@override
|
||||
final OverrideTarget<TunOverrideKey, ProfileTun> target;
|
||||
|
||||
@override
|
||||
String title(AppLocalizations l) => l.tun;
|
||||
|
||||
@override
|
||||
List<TunOverrideKey> get values => TunOverrideKey.values;
|
||||
|
||||
@override
|
||||
NullStatusIllustration get illustration => NullStatusIllustration.tun;
|
||||
|
||||
@override
|
||||
ProfileTun get defaults => defaultProfileTun;
|
||||
|
||||
@override
|
||||
String label(TunOverrideKey key, AppLocalizations l) => key.label(l);
|
||||
|
||||
@override
|
||||
ConfigLabel description(TunOverrideKey key) => key.description;
|
||||
|
||||
@override
|
||||
OverrideField<ProfileTun> fieldOf(TunOverrideKey key) => switch (key) {
|
||||
TunOverrideKey.disableIcmpForwarding => ToggleOverrideField(
|
||||
(tun) => tun.disableIcmpForwarding,
|
||||
(tun, value) => tun.copyWith(disableIcmpForwarding: value),
|
||||
),
|
||||
TunOverrideKey.excludeInterface => ListOverrideField(
|
||||
(tun) => tun.excludeInterface,
|
||||
(tun, value) => tun.copyWith(excludeInterface: value),
|
||||
itemMaxLength: TextInputLimits.name,
|
||||
),
|
||||
};
|
||||
}
|
||||
@@ -113,24 +113,52 @@ class _UserAgentsViewState extends ConsumerState<UserAgentsView> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildItem(List<String> entries, int index, String? selected) {
|
||||
final userAgent = entries[index];
|
||||
return ItemPositionProvider(
|
||||
position: ItemPosition.get(index + 1, entries.length + 1),
|
||||
child: _UserAgentItem(
|
||||
value: userAgent,
|
||||
label: userAgent,
|
||||
isSelected: userAgent == selected,
|
||||
onSelected: () {
|
||||
_select(userAgent);
|
||||
List<CommonPopupMenuItem> _buildMenuItems(String userAgent) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return [
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.edit,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_handleEdit(userAgent);
|
||||
},
|
||||
trailing: _UserAgentItemMenu(
|
||||
onEdit: () {
|
||||
_handleEdit(userAgent);
|
||||
},
|
||||
onDelete: () {
|
||||
_handleDelete(userAgent);
|
||||
},
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
danger: true,
|
||||
glyph: AppGlyphs.delete,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: () {
|
||||
_handleDelete(userAgent);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildItem(List<String> entries, int index, String? selected) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final userAgent = entries[index];
|
||||
return SortableItem(
|
||||
key: ValueKey(userAgent),
|
||||
index: index,
|
||||
child: ContextMenuRegion(
|
||||
menuItems: _buildMenuItems(userAgent),
|
||||
child: ItemPositionProvider(
|
||||
position: ItemPosition.get(index + 1, entries.length + 1),
|
||||
child: _UserAgentItem(
|
||||
value: userAgent,
|
||||
label: userAgent,
|
||||
isSelected: userAgent == selected,
|
||||
onSelected: () {
|
||||
_select(userAgent);
|
||||
},
|
||||
trailing: DetailButton(
|
||||
glyph: AppGlyphs.edit,
|
||||
tooltip: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_handleEdit(userAgent);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -146,12 +174,14 @@ class _UserAgentsViewState extends ConsumerState<UserAgentsView> {
|
||||
patchClashConfigProvider.select((state) => state.globalUa),
|
||||
);
|
||||
final entries = _visibleUserAgents(userAgents, selected);
|
||||
return BaseScaffold(
|
||||
return CommonScaffold(
|
||||
canSort: entries.length > 1,
|
||||
title: appLocalizations.userAgent,
|
||||
actions: [
|
||||
FilledButton.tonal(
|
||||
IconButton(
|
||||
tooltip: appLocalizations.add,
|
||||
onPressed: _handleAdd,
|
||||
child: Text(appLocalizations.add),
|
||||
icon: const GlyphIcon(AppGlyphs.addCircle),
|
||||
),
|
||||
],
|
||||
body: _buildRadioGroup(
|
||||
@@ -173,13 +203,7 @@ class _UserAgentsViewState extends ConsumerState<UserAgentsView> {
|
||||
),
|
||||
),
|
||||
itemCount: entries.length,
|
||||
itemBuilder: (_, index) {
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ValueKey(entries[index]),
|
||||
index: index,
|
||||
child: _buildItem(entries, index, selected),
|
||||
);
|
||||
},
|
||||
itemBuilder: (_, index) => _buildItem(entries, index, selected),
|
||||
// The drag proxy is built in the navigator overlay, outside the
|
||||
// page's RadioGroup.
|
||||
proxyDecorator: (_, index, animation) {
|
||||
@@ -220,7 +244,7 @@ class _UserAgentItem extends StatelessWidget {
|
||||
horizontalTitleGap: 8,
|
||||
contentPadding: EdgeInsets.only(
|
||||
left: 14,
|
||||
right: trailing == null ? 16 : 6,
|
||||
right: SortableItem.maybeOf(context) == null ? 16 : 0,
|
||||
),
|
||||
leading: SizedBox.square(
|
||||
dimension: 24,
|
||||
@@ -238,45 +262,3 @@ class _UserAgentItem extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _UserAgentItemMenu extends StatelessWidget {
|
||||
const _UserAgentItemMenu({required this.onEdit, required this.onDelete});
|
||||
|
||||
final VoidCallback onEdit;
|
||||
final VoidCallback onDelete;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return CommonPopupBox(
|
||||
popupBuilder: (_) => CommonPopupMenu(
|
||||
items: [
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.edit,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: onEdit,
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
danger: true,
|
||||
glyph: AppGlyphs.delete,
|
||||
label: appLocalizations.delete,
|
||||
onPressed: onDelete,
|
||||
),
|
||||
],
|
||||
),
|
||||
targetBuilder: (open) {
|
||||
return IconButton(
|
||||
style: IconButton.styleFrom(
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
visualDensity: VisualDensity.standard,
|
||||
),
|
||||
tooltip: appLocalizations.more,
|
||||
onPressed: () {
|
||||
open();
|
||||
},
|
||||
icon: const GlyphIcon(AppGlyphs.more),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,8 @@ class _ConnectionsViewState extends ConsumerState<ConnectionsView>
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = widget.scrollController ?? ScrollController();
|
||||
_scrollController =
|
||||
widget.scrollController ?? sheetScrollController(context);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -129,6 +130,7 @@ class _ConnectionsViewState extends ConsumerState<ConnectionsView>
|
||||
return NullStatusSwitcher(
|
||||
isEmpty: connections.isEmpty,
|
||||
isSearching: state.isSearching,
|
||||
holdsArrival: true,
|
||||
nullStatus: NullStatus(
|
||||
label: appLocalizations.nullTip(appLocalizations.connections),
|
||||
illustration: NullStatusIllustration.connections,
|
||||
|
||||
@@ -20,13 +20,16 @@ class _RequestsViewState extends ConsumerState<RequestsView>
|
||||
with RouteMotionHoldMixin<RequestsView> {
|
||||
final _listController = TrackerInfoListController();
|
||||
late final ScrollController _scrollController;
|
||||
late final _physics = FollowEndScrollPhysics(
|
||||
isFollowing: () => _listController.value.autoScrollToEnd,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController =
|
||||
widget.scrollController ??
|
||||
ScrollController(initialScrollOffset: double.maxFinite);
|
||||
sheetScrollController(context, initialScrollOffset: double.maxFinite);
|
||||
_listController.setTrackerInfos(ref.read(requestsProvider).list);
|
||||
ref.listenManual(requestsProvider.select((state) => state.revision), (
|
||||
_,
|
||||
@@ -84,7 +87,6 @@ class _RequestsViewState extends ConsumerState<RequestsView>
|
||||
},
|
||||
child: ScrollToEndBox(
|
||||
controller: _scrollController,
|
||||
dataSource: requests,
|
||||
enable: state.autoScrollToEnd,
|
||||
onCancelToEnd: () {
|
||||
_listController.setAutoScrollToEnd(false);
|
||||
@@ -97,7 +99,7 @@ class _RequestsViewState extends ConsumerState<RequestsView>
|
||||
child: TrackerInfoList(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: const NextClampingScrollPhysics(),
|
||||
physics: _physics,
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.only(
|
||||
top: context.contentTopPadding,
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/core/core.dart';
|
||||
import 'package:fl_clash/enum/enum.dart';
|
||||
@@ -24,13 +21,10 @@ class DashboardView extends ConsumerStatefulWidget {
|
||||
ConsumerState<DashboardView> createState() => _DashboardViewState();
|
||||
}
|
||||
|
||||
class _DashboardViewState extends ConsumerState<DashboardView>
|
||||
with SingleTickerProviderStateMixin {
|
||||
class _DashboardViewState extends ConsumerState<DashboardView> {
|
||||
final key = GlobalKey<SuperGridState>();
|
||||
final _isEditNotifier = ValueNotifier<bool>(false);
|
||||
final _addedWidgetsNotifier = ValueNotifier<List<GridItem>>([]);
|
||||
late final _addSheetController = SnapSheetController(vsync: this);
|
||||
int _landingCount = 0;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -59,7 +53,6 @@ class _DashboardViewState extends ConsumerState<DashboardView>
|
||||
void dispose() {
|
||||
_isEditNotifier.dispose();
|
||||
_addedWidgetsNotifier.dispose();
|
||||
_addSheetController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@@ -120,8 +113,6 @@ class _DashboardViewState extends ConsumerState<DashboardView>
|
||||
showSnapSheet(
|
||||
context,
|
||||
detents: const [0.85],
|
||||
collapsedDetent: 0.2,
|
||||
controller: _addSheetController,
|
||||
builder: (sheetContext, scrollController) {
|
||||
return ValueListenableBuilder(
|
||||
valueListenable: _addedWidgetsNotifier,
|
||||
@@ -146,21 +137,8 @@ class _DashboardViewState extends ConsumerState<DashboardView>
|
||||
if (grid == null) {
|
||||
return;
|
||||
}
|
||||
final isLast = _addedWidgetsNotifier.value.length <= 1;
|
||||
if (isLast || !_addSheetController.isAttached) {
|
||||
Navigator.of(sheetContext).pop();
|
||||
grid.addItem(item, from: from);
|
||||
return;
|
||||
}
|
||||
_addSheetController.collapse();
|
||||
_landingCount++;
|
||||
unawaited(
|
||||
grid.addItem(item, from: from).whenComplete(() {
|
||||
if (--_landingCount == 0 && mounted) {
|
||||
_addSheetController.restore();
|
||||
}
|
||||
}),
|
||||
);
|
||||
Navigator.of(sheetContext).pop();
|
||||
grid.addItem(item, from: from);
|
||||
}
|
||||
|
||||
void _handleEnterEdit() {
|
||||
@@ -197,18 +175,17 @@ class _DashboardViewState extends ConsumerState<DashboardView>
|
||||
title: context.appLocalizations.dashboard,
|
||||
actions: _buildActions(isEdit),
|
||||
floatingActionButton: hasProfile ? const StartButton() : null,
|
||||
body: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
// SingleChildScrollView snaps a bounce back to its edge whenever a
|
||||
// card's refresh relays it out; a sliver viewport keeps the overscroll.
|
||||
child: ValueListenableBuilder(
|
||||
valueListenable: _addSheetController,
|
||||
builder: (context, sheetHeight, _) {
|
||||
final padding = const EdgeInsets.all(16).copyWith(
|
||||
top: context.contentTopPadding,
|
||||
bottom: 16 + max(BottomInsetScope.of(context), sheetHeight),
|
||||
);
|
||||
return CustomScrollView(
|
||||
body: Builder(
|
||||
builder: (context) {
|
||||
final padding = const EdgeInsets.all(16).copyWith(
|
||||
top: context.contentTopPadding,
|
||||
bottom: 16 + BottomInsetScope.of(context),
|
||||
);
|
||||
return Align(
|
||||
alignment: Alignment.topCenter,
|
||||
// SingleChildScrollView snaps a bounce back to its edge whenever a
|
||||
// card's refresh relays it out; a sliver viewport keeps the overscroll.
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: padding,
|
||||
@@ -256,9 +233,9 @@ class _DashboardViewState extends ConsumerState<DashboardView>
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -333,16 +310,10 @@ class _AddedContainer extends StatelessWidget {
|
||||
right: -8,
|
||||
child: DeferPointer(
|
||||
child: ElasticButton(
|
||||
child: SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: IconButton.filled(
|
||||
tooltip: context.appLocalizations.add,
|
||||
iconSize: 16,
|
||||
padding: const EdgeInsets.all(4),
|
||||
onPressed: () => _handleAdd(context),
|
||||
icon: const GlyphIcon(AppGlyphs.add, fill: 1),
|
||||
),
|
||||
child: CornerBadgeButton(
|
||||
glyph: AppGlyphs.addCircle,
|
||||
tooltip: context.appLocalizations.add,
|
||||
onPressed: () => _handleAdd(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -70,16 +70,6 @@ extension DashboardWidgetView on DashboardWidget {
|
||||
crossAxisCellCount: 4,
|
||||
child: ConnectionsCard(),
|
||||
),
|
||||
DashboardWidget.overrideDnsButton => const GridItem(
|
||||
key: ValueKey(DashboardWidget.overrideDnsButton),
|
||||
crossAxisCellCount: 4,
|
||||
child: OverrideDnsButton(),
|
||||
),
|
||||
DashboardWidget.overrideNtpButton => const GridItem(
|
||||
key: ValueKey(DashboardWidget.overrideNtpButton),
|
||||
crossAxisCellCount: 4,
|
||||
child: OverrideNtpButton(),
|
||||
),
|
||||
DashboardWidget.runTime => const GridItem(
|
||||
key: ValueKey(DashboardWidget.runTime),
|
||||
crossAxisCellCount: 4,
|
||||
|
||||
@@ -65,7 +65,9 @@ class _ConnectionsCardState extends ConsumerState<ConnectionsCard>
|
||||
void _openConnections(BuildContext context) {
|
||||
showSnapSheet(
|
||||
context,
|
||||
builder: (_, controller) => ConnectionsView(scrollController: controller),
|
||||
builder: (_, controller) => NestedPagedSheet(
|
||||
builder: (_) => ConnectionsView(scrollController: controller),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -15,7 +15,9 @@ class DnsQueriesCard extends StatelessWidget {
|
||||
showSnapSheet(
|
||||
context,
|
||||
initialScrollOffset: double.maxFinite,
|
||||
builder: (_, controller) => DnsQueriesView(scrollController: controller),
|
||||
builder: (_, controller) => NestedPagedSheet(
|
||||
builder: (_) => DnsQueriesView(scrollController: controller),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,10 +66,12 @@ class FeedCount extends StatelessWidget {
|
||||
return Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Text(
|
||||
'$count',
|
||||
count.compact,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.bodyMedium?.toLight.adjustSize(1),
|
||||
style: context.textTheme.bodyMedium?.toLight
|
||||
.adjustSize(1)
|
||||
.copyWith(fontFeatures: const [FontFeature.tabularFigures()]),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -47,9 +47,6 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection>
|
||||
fontFamily: FontFamily.twEmoji.value,
|
||||
);
|
||||
final titleTextStyle = context.colorScheme.onSurfaceVariant;
|
||||
final descTextStyle = context.textTheme.titleSmall?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
);
|
||||
final textScale = DashboardWidgetMetrics.textScaleOf(context);
|
||||
return SizedBox(
|
||||
height: DashboardWidgetMetrics.heightOf(context, 1),
|
||||
@@ -84,7 +81,7 @@ class _NetworkDetectionState extends ConsumerState<NetworkDetection>
|
||||
appLocalizations.networkDetection,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: descTextStyle,
|
||||
style: context.sectionHeaderStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -8,6 +8,7 @@ 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/preview.dart';
|
||||
import 'package:fl_clash/views/proxies/providers.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
@@ -30,7 +31,7 @@ typedef ConfigCounts = ({int groups, int proxies, int rules});
|
||||
|
||||
typedef ProfileStats = ({int groups, int proxies, int rules, int providers});
|
||||
|
||||
/// Counts what the Core was handed, overwrites included, rather than what the
|
||||
/// Counts what the Core was handed, extensions included, rather than what the
|
||||
/// profile file declares.
|
||||
ConfigCounts configCountsOf(Map<String, dynamic> config) {
|
||||
int lengthOf(Object? value) => value is List ? value.length : 0;
|
||||
@@ -157,7 +158,7 @@ class _ProfileDetailSheetState extends ConsumerState<ProfileDetailSheet> {
|
||||
),
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.eye,
|
||||
tooltip: appLocalizations.preview,
|
||||
tooltip: appLocalizations.finalConfig,
|
||||
onPressed: () => _handlePreview(profile),
|
||||
),
|
||||
],
|
||||
@@ -193,10 +194,11 @@ class _ProfileDetailSheetState extends ConsumerState<ProfileDetailSheet> {
|
||||
lastUpdateDate: profile.lastUpdateDate,
|
||||
),
|
||||
),
|
||||
_InfoRow(
|
||||
label: appLocalizations.overrideMode,
|
||||
value: Text(_overwriteLabel(context, profile.overwriteType)),
|
||||
),
|
||||
if (profile.type != ProfileType.custom)
|
||||
_InfoRow(
|
||||
label: appLocalizations.extendMode,
|
||||
value: Text(_extendLabel(context, profile.extendType)),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (providers.isNotEmpty)
|
||||
@@ -204,7 +206,7 @@ class _ProfileDetailSheetState extends ConsumerState<ProfileDetailSheet> {
|
||||
title: appLocalizations.providers,
|
||||
items: [
|
||||
for (final provider in providers)
|
||||
_ProviderRow(provider: provider),
|
||||
ExternalProviderItem(provider: provider),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -213,11 +215,10 @@ class _ProfileDetailSheetState extends ConsumerState<ProfileDetailSheet> {
|
||||
}
|
||||
}
|
||||
|
||||
String _overwriteLabel(BuildContext context, OverwriteType type) {
|
||||
String _extendLabel(BuildContext context, ExtendType type) {
|
||||
return switch (type) {
|
||||
OverwriteType.standard => context.appLocalizations.standard,
|
||||
OverwriteType.script => context.appLocalizations.script,
|
||||
OverwriteType.custom => context.appLocalizations.overwriteTypeCustom,
|
||||
ExtendType.standard => context.appLocalizations.standard,
|
||||
ExtendType.script => context.appLocalizations.script,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -233,7 +234,7 @@ class _StatsGrid extends StatelessWidget {
|
||||
final stats = this.stats;
|
||||
final tiles = [
|
||||
(appLocalizations.proxyGroup, stats?.groups),
|
||||
(appLocalizations.proxyNode, stats?.proxies),
|
||||
(appLocalizations.proxies, stats?.proxies),
|
||||
(appLocalizations.rules, stats?.rules),
|
||||
(appLocalizations.providers, stats?.providers),
|
||||
];
|
||||
@@ -322,32 +323,3 @@ class _InfoRow extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProviderRow extends StatelessWidget {
|
||||
const _ProviderRow({required this.provider});
|
||||
|
||||
final ExternalProvider provider;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final count = switch (provider.type) {
|
||||
'Proxy' => appLocalizations.proxiesCount(provider.count),
|
||||
_ => appLocalizations.rulesCount(provider.count),
|
||||
};
|
||||
return ListItem(
|
||||
title: EmojiText(
|
||||
provider.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
subtitle: Text(count),
|
||||
trailing: Text(
|
||||
provider.updateAt.getLastUpdateTimeDesc(context),
|
||||
style: context.textTheme.bodySmall?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/views/dashboard/widget_metrics.dart';
|
||||
import 'package:fl_clash/views/profiles/add.dart';
|
||||
import 'package:fl_clash/views/profiles/custom/custom.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -32,9 +33,12 @@ class ProfilesCard extends ConsumerWidget {
|
||||
height: DashboardWidgetMetrics.heightOf(context, 1),
|
||||
child: CommonCard(
|
||||
radius: DashboardWidgetMetrics.radiusOf(context),
|
||||
onPressed: profile == null
|
||||
? showAddProfilePage
|
||||
: () => showProfileDetailSheet(context),
|
||||
onPressed: switch (profile) {
|
||||
null => () => showAddProfilePage(context),
|
||||
Profile(type: ProfileType.custom, :final id) =>
|
||||
() => showCustomProfileSheet(context, id),
|
||||
_ => () => showProfileDetailSheet(context),
|
||||
},
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
@@ -54,9 +58,7 @@ class ProfilesCard extends ConsumerWidget {
|
||||
appLocalizations.profiles,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: context.textTheme.titleSmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
style: context.sectionHeaderStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -171,33 +173,30 @@ class _ProfilePickerSheet extends ConsumerWidget {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: ref.sheetHeight(context, 0.7)),
|
||||
child: CommonScaffold(
|
||||
title: context.appLocalizations.switchProfile,
|
||||
body: RadioGroup<int>(
|
||||
groupValue: currentId,
|
||||
onChanged: select,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.fromLTRB(16, context.contentTopPadding, 16, 16),
|
||||
itemCount: profiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final profile = profiles[index];
|
||||
return ItemPositionProvider(
|
||||
position: ItemPosition.get(index, profiles.length),
|
||||
child: ListItem.radio(
|
||||
value: profile.id,
|
||||
onTap: () => select(profile.id),
|
||||
title: EmojiText(
|
||||
profile.realLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
return CommonScaffold(
|
||||
title: context.appLocalizations.switchProfile,
|
||||
body: RadioGroup<int>(
|
||||
groupValue: currentId,
|
||||
onChanged: select,
|
||||
child: ListView.builder(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.fromLTRB(16, context.contentTopPadding, 16, 16),
|
||||
itemCount: profiles.length,
|
||||
itemBuilder: (context, index) {
|
||||
final profile = profiles[index];
|
||||
return ItemPositionProvider(
|
||||
position: ItemPosition.get(index, profiles.length),
|
||||
child: ListItem.radio(
|
||||
value: profile.id,
|
||||
onTap: () => select(profile.id),
|
||||
title: EmojiText(
|
||||
profile.realLabel,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:animations/animations.dart';
|
||||
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/dashboard/widget_metrics.dart';
|
||||
import 'package:fl_clash/views/proxies/card.dart';
|
||||
import 'package:fl_clash/views/proxies/common.dart';
|
||||
import 'package:fl_clash/views/proxies/tab.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
@@ -15,61 +16,27 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'row_card.dart';
|
||||
|
||||
const _summaryGap = 8.0;
|
||||
const _headerInkInset = EdgeInsets.symmetric(horizontal: 6, vertical: 2);
|
||||
|
||||
class ProxyGroupsCard extends ConsumerStatefulWidget {
|
||||
class ProxyGroupsCard extends ConsumerWidget {
|
||||
const ProxyGroupsCard({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<ProxyGroupsCard> createState() => _ProxyGroupsCardState();
|
||||
}
|
||||
|
||||
class _ProxyGroupsCardState extends ConsumerState<ProxyGroupsCard> {
|
||||
String? _groupName;
|
||||
bool _reverse = false;
|
||||
|
||||
void _show(String? groupName) {
|
||||
setState(() {
|
||||
_reverse = groupName == null;
|
||||
_groupName = groupName;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final groups = ref.watch(visibleGroupsStateProvider).value;
|
||||
final group = _groupName == null ? null : groups.getGroup(_groupName!);
|
||||
return RowCardFrame(
|
||||
child: PageTransitionSwitcher(
|
||||
reverse: _reverse,
|
||||
duration: context.motionDuration(commonDuration),
|
||||
transitionBuilder: (child, animation, secondaryAnimation) {
|
||||
return SharedAxisTransition(
|
||||
animation: animation,
|
||||
secondaryAnimation: secondaryAnimation,
|
||||
transitionType: SharedAxisTransitionType.horizontal,
|
||||
fillColor: Colors.transparent,
|
||||
child: child,
|
||||
);
|
||||
},
|
||||
child: group == null
|
||||
? _GroupsPane(
|
||||
key: const ValueKey('proxy-groups'),
|
||||
groups: groups,
|
||||
onSelect: (group) => _show(group.name),
|
||||
)
|
||||
: _ProxiesPane(
|
||||
key: ValueKey(group.name),
|
||||
group: group,
|
||||
onBack: () => _show(null),
|
||||
),
|
||||
child: _GroupsPane(
|
||||
groups: groups,
|
||||
onSelect: (group) => showSheet<void>(
|
||||
context: context,
|
||||
builder: (_) => _ProxyGroupSheet(groupName: group.name),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GroupsPane extends StatefulWidget {
|
||||
const _GroupsPane({super.key, required this.groups, required this.onSelect});
|
||||
const _GroupsPane({required this.groups, required this.onSelect});
|
||||
|
||||
final List<Group> groups;
|
||||
final void Function(Group group) onSelect;
|
||||
@@ -135,147 +102,118 @@ class _GroupsPaneState extends State<_GroupsPane> {
|
||||
}
|
||||
}
|
||||
|
||||
class _ProxiesPane extends ConsumerStatefulWidget {
|
||||
const _ProxiesPane({super.key, required this.group, required this.onBack});
|
||||
class _ProxyGroupSheet extends ConsumerStatefulWidget {
|
||||
const _ProxyGroupSheet({required this.groupName});
|
||||
|
||||
final Group group;
|
||||
final VoidCallback onBack;
|
||||
final String groupName;
|
||||
|
||||
@override
|
||||
ConsumerState<_ProxiesPane> createState() => _ProxiesPaneState();
|
||||
ConsumerState<_ProxyGroupSheet> createState() => _ProxyGroupSheetState();
|
||||
}
|
||||
|
||||
class _ProxiesPaneState extends ConsumerState<_ProxiesPane> {
|
||||
final _controller = RowSnapScrollController();
|
||||
class _ProxyGroupSheetState extends ConsumerState<_ProxyGroupSheet> {
|
||||
late final ScrollController _controller;
|
||||
var _revealedSelected = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = sheetScrollController(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _revealSelected(double itemExtent) {
|
||||
void _revealSelected(Group group, int columns, ProxyCardType cardType) {
|
||||
if (_revealedSelected) {
|
||||
return;
|
||||
}
|
||||
_revealedSelected = true;
|
||||
final selected = ref.read(selectedProxyNameProvider(widget.group.name));
|
||||
final index = widget.group.all.indexWhere(
|
||||
(proxy) => proxy.name == selected,
|
||||
final offset = selectedRowOffset(
|
||||
proxies: group.all,
|
||||
selectedProxyName: ref.read(selectedProxyNameProvider(group.name)),
|
||||
columns: columns,
|
||||
rowExtent: getRowExtent(cardType),
|
||||
);
|
||||
if (index <= 0) {
|
||||
if (offset == null || offset == 0) {
|
||||
return;
|
||||
}
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (!_controller.hasClients) {
|
||||
return;
|
||||
if (_controller.hasClients) {
|
||||
_controller.jumpTo(min(offset, _controller.position.maxScrollExtent));
|
||||
}
|
||||
_controller.jumpTo(
|
||||
min(index * itemExtent, _controller.position.maxScrollExtent),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final group = widget.group;
|
||||
final selected = ref.watch(selectedProxyNameProvider(group.name));
|
||||
final colorScheme = context.colorScheme;
|
||||
final headerPadding = DashboardWidgetMetrics.paddingOf(
|
||||
context,
|
||||
).copyWith(bottom: 0);
|
||||
return RowCardPane(
|
||||
headerOverhang: _headerInkInset.bottom,
|
||||
header: Padding(
|
||||
padding: headerPadding.copyWith(
|
||||
left: headerPadding.left - _headerInkInset.left,
|
||||
top: headerPadding.top - _headerInkInset.top,
|
||||
right: headerPadding.right - _headerInkInset.right,
|
||||
final groupName = widget.groupName;
|
||||
final group = ref.watch(
|
||||
visibleGroupsStateProvider.select(
|
||||
(state) => state.value.getGroup(groupName),
|
||||
),
|
||||
);
|
||||
final isDelayTesting = ref.watch(
|
||||
delayTestingGroupsProvider.select((state) => state.contains(groupName)),
|
||||
);
|
||||
final (cardType, layout) = ref.watch(
|
||||
proxiesStyleSettingProvider.select(
|
||||
(state) => (state.cardType, state.layout),
|
||||
),
|
||||
);
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return CommonScaffold(
|
||||
title: groupName,
|
||||
iconActions: [
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.bolt,
|
||||
tooltip: appLocalizations.delayTest,
|
||||
isLoading: isDelayTesting,
|
||||
onPressed: () => ref
|
||||
.read(proxiesActionProvider.notifier)
|
||||
.delayTestPageGroup(groupName, matchSearch: false),
|
||||
),
|
||||
child: Align(
|
||||
alignment: AlignmentDirectional.centerStart,
|
||||
child: Material(
|
||||
type: MaterialType.transparency,
|
||||
child: InkWell(
|
||||
customBorder: AppShape.full,
|
||||
onTap: widget.onBack,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
label: context.appLocalizations.back,
|
||||
child: InfoHeader(
|
||||
padding: _headerInkInset,
|
||||
info: Info(label: group.name, glyph: AppGlyphs.chevronBack),
|
||||
),
|
||||
),
|
||||
],
|
||||
body: group == null
|
||||
? NullStatus(
|
||||
illustration: NullStatusIllustration.proxies,
|
||||
label: appLocalizations.nullTip(appLocalizations.proxyGroup),
|
||||
)
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final columns = getProxiesColumns(
|
||||
max(constraints.maxWidth - 32, 0),
|
||||
layout,
|
||||
);
|
||||
_revealSelected(group, columns, cardType);
|
||||
return ProxyGroupView(
|
||||
group: group,
|
||||
controller: _controller,
|
||||
columns: columns,
|
||||
cardType: cardType,
|
||||
shrinkWrap: true,
|
||||
topPadding: context.contentTopPadding,
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
body: RowCardSlots(
|
||||
rowExtent: rowCardLineExtentOf(context),
|
||||
builder: (_, rowHeight, spacing) {
|
||||
final itemExtent = rowHeight + spacing;
|
||||
_controller.itemExtent = itemExtent;
|
||||
_revealSelected(itemExtent);
|
||||
return ListView.builder(
|
||||
controller: _controller,
|
||||
padding: EdgeInsets.zero,
|
||||
physics: const RowSnapScrollPhysics(),
|
||||
itemExtent: itemExtent,
|
||||
itemCount: group.all.length,
|
||||
itemBuilder: (_, index) {
|
||||
final proxy = group.all[index];
|
||||
final isSelected = proxy.name == selected;
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(bottom: spacing),
|
||||
child: RowCardPill(
|
||||
color: isSelected ? colorScheme.secondaryContainer : null,
|
||||
onTap: () => selectGroupProxy(
|
||||
ref,
|
||||
groupName: group.name,
|
||||
groupType: group.type,
|
||||
proxyName: proxy.name,
|
||||
),
|
||||
child: Row(
|
||||
spacing: _summaryGap,
|
||||
children: [
|
||||
Expanded(
|
||||
child: _RowLabel(
|
||||
text: proxy.name,
|
||||
color: isSelected
|
||||
? colorScheme.onSecondaryContainer
|
||||
: null,
|
||||
),
|
||||
),
|
||||
_ProxyDelay(
|
||||
proxyName: proxy.name,
|
||||
testUrl: group.testUrl,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RowLabel extends StatelessWidget {
|
||||
const _RowLabel({required this.text, this.color});
|
||||
const _RowLabel({required this.text});
|
||||
|
||||
final String text;
|
||||
final Color? color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return OverflowTooltipText(
|
||||
text: text,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: color ?? context.colorScheme.onSurface,
|
||||
color: context.colorScheme.onSurface,
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -329,7 +267,7 @@ class _ProxyDelay extends ConsumerWidget {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
return Text(
|
||||
delay > 0 ? '$delay' : 'Timeout',
|
||||
delay > 0 ? '$delay' : delayFailureText(delay, context.appLocalizations),
|
||||
maxLines: 1,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
color: context.colorScheme.delayColor(delay),
|
||||
|
||||
@@ -4,9 +4,7 @@ import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/icons/icons.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
import 'package:fl_clash/state.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/dashboard/widget_metrics.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
@@ -184,37 +182,3 @@ class VpnButton extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OverrideDnsButton extends StatelessWidget {
|
||||
const OverrideDnsButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _QuickSwitchCard(
|
||||
label: context.appLocalizations.overrideDns,
|
||||
glyph: AppGlyphs.dns,
|
||||
sheetBuilder: (_) => const DnsView(),
|
||||
selector: overrideDnsProvider,
|
||||
onChanged: (ref, value) {
|
||||
ref.read(overrideDnsProvider.notifier).value = value;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class OverrideNtpButton extends StatelessWidget {
|
||||
const OverrideNtpButton({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _QuickSwitchCard(
|
||||
label: context.appLocalizations.overrideNtp,
|
||||
glyph: AppGlyphs.clock,
|
||||
sheetBuilder: (_) => const NtpView(),
|
||||
selector: overrideNtpProvider,
|
||||
onChanged: (ref, value) {
|
||||
ref.read(overrideNtpProvider.notifier).value = value;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,9 @@ class RequestsCard extends StatelessWidget {
|
||||
showSnapSheet(
|
||||
context,
|
||||
initialScrollOffset: double.maxFinite,
|
||||
builder: (_, controller) => RequestsView(scrollController: controller),
|
||||
builder: (_, controller) => NestedPagedSheet(
|
||||
builder: (_) => RequestsView(scrollController: controller),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -46,19 +46,22 @@ class RowCardFrame extends StatelessWidget {
|
||||
side: BorderSide(color: colorScheme.surfaceContainerHighest),
|
||||
),
|
||||
),
|
||||
// The page's safe-area padding would otherwise inset the rows'
|
||||
// scrollbar track as if the card spanned the window.
|
||||
// The page's safe-area padding and bar would otherwise inset the
|
||||
// rows' scrollbar track as if the card spanned the window.
|
||||
child: MediaQuery.removePadding(
|
||||
context: context,
|
||||
removeLeft: true,
|
||||
removeTop: true,
|
||||
removeRight: true,
|
||||
removeBottom: true,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const HiddenBarScrollBehavior(),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: commonCardIconSize),
|
||||
child: child,
|
||||
child: FloatingBarScope(
|
||||
inset: 0,
|
||||
child: ScrollConfiguration(
|
||||
behavior: const HiddenBarScrollBehavior(),
|
||||
child: IconTheme.merge(
|
||||
data: const IconThemeData(size: commonCardIconSize),
|
||||
child: child,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -779,9 +779,10 @@ class _ServiceBadge extends StatelessWidget {
|
||||
width: size * _glyphShare,
|
||||
height: size * _glyphShare,
|
||||
excludeFromSemantics: true,
|
||||
colorFilter: ColorFilter.mode(
|
||||
enabled ? colorScheme.onSecondaryContainer : colorScheme.outline,
|
||||
BlendMode.srcIn,
|
||||
theme: SvgTheme(
|
||||
currentColor: enabled
|
||||
? colorScheme.onSecondaryContainer
|
||||
: colorScheme.outline,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -818,15 +819,17 @@ class _BadgePainter extends CustomPainter {
|
||||
return;
|
||||
}
|
||||
final center = size.bottomRight(Offset(-dotRadius, -dotRadius));
|
||||
final gap = Path()
|
||||
..addOval(Rect.fromCircle(center: center, radius: dotRadius + ring));
|
||||
canvas
|
||||
..saveLayer(bounds, Paint())
|
||||
..drawRSuperellipse(tile, Paint()..color = color)
|
||||
..drawCircle(
|
||||
center,
|
||||
dotRadius + ring,
|
||||
Paint()..blendMode = BlendMode.clear,
|
||||
..drawPath(
|
||||
Path.combine(
|
||||
PathOperation.difference,
|
||||
Path()..addRSuperellipse(tile),
|
||||
gap,
|
||||
),
|
||||
Paint()..color = color,
|
||||
)
|
||||
..restore()
|
||||
..drawCircle(center, dotRadius, Paint()..color = dot);
|
||||
}
|
||||
|
||||
@@ -865,52 +868,49 @@ class ServiceStatusSheet extends ConsumerWidget {
|
||||
final current = targets.contains(saved) ? saved : targets.first;
|
||||
final services = ref.read(serviceStatusProvider.notifier);
|
||||
final localizations = context.appLocalizations;
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: ref.sheetHeight(context, 0.7)),
|
||||
child: CommonScaffold(
|
||||
title: localizations.serviceStatus,
|
||||
iconActions: [
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.bolt,
|
||||
tooltip: localizations.serviceCheckAll,
|
||||
isLoading: targets.any(state.isLoading),
|
||||
onPressed: () => services.refresh(targets),
|
||||
return CommonScaffold(
|
||||
title: localizations.serviceStatus,
|
||||
iconActions: [
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.bolt,
|
||||
tooltip: localizations.serviceCheckAll,
|
||||
isLoading: targets.any(state.isLoading),
|
||||
onPressed: () => services.refresh(targets),
|
||||
),
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.sliders,
|
||||
tooltip: localizations.serviceManage,
|
||||
onPressed: () => Navigator.of(
|
||||
context,
|
||||
).push(PagedSheetRoute(builder: (_) => const ServiceManageView())),
|
||||
),
|
||||
],
|
||||
body: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: context.contentTopPadding),
|
||||
),
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.sliders,
|
||||
tooltip: localizations.serviceManage,
|
||||
onPressed: () => Navigator.of(
|
||||
context,
|
||||
).push(PagedSheetRoute(builder: (_) => const ServiceManageView())),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
sliver: SliverList.builder(
|
||||
itemCount: targets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final target = targets[index];
|
||||
return ItemPositionProvider(
|
||||
position: ItemPosition.get(index, targets.length),
|
||||
child: _ServiceRow(
|
||||
target: target,
|
||||
entry: state.entryOf(target),
|
||||
selected: target == current,
|
||||
onTap: () => context.safeNestedPop(target),
|
||||
onCheck: () => services.refresh([target]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
body: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: context.contentTopPadding),
|
||||
),
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
sliver: SliverList.builder(
|
||||
itemCount: targets.length,
|
||||
itemBuilder: (context, index) {
|
||||
final target = targets[index];
|
||||
return ItemPositionProvider(
|
||||
position: ItemPosition.get(index, targets.length),
|
||||
child: _ServiceRow(
|
||||
target: target,
|
||||
entry: state.entryOf(target),
|
||||
selected: target == current,
|
||||
onTap: () => context.safeNestedPop(target),
|
||||
onCheck: () => services.refresh([target]),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -962,26 +962,23 @@ class ServiceManageView extends ConsumerWidget {
|
||||
);
|
||||
}
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: ref.sheetHeight(context, 0.8)),
|
||||
child: CommonScaffold(
|
||||
title: context.appLocalizations.serviceManage,
|
||||
body: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: context.contentTopPadding),
|
||||
),
|
||||
SliverReorderableList(
|
||||
itemBuilder: (_, index) => itemAt(index),
|
||||
itemCount: targets.length,
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
commonProxyDecorator(itemAt(index), index, animation),
|
||||
onReorderItem: reorder,
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
],
|
||||
),
|
||||
return CommonScaffold(
|
||||
title: context.appLocalizations.serviceManage,
|
||||
body: CustomScrollView(
|
||||
shrinkWrap: true,
|
||||
slivers: [
|
||||
SliverToBoxAdapter(
|
||||
child: SizedBox(height: context.contentTopPadding),
|
||||
),
|
||||
SliverReorderableList(
|
||||
itemBuilder: (_, index) => itemAt(index),
|
||||
itemCount: targets.length,
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
commonProxyDecorator(itemAt(index), index, animation),
|
||||
onReorderItem: reorder,
|
||||
),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1011,39 +1008,32 @@ class _ServiceManageItem extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final onChanged = this.onChanged;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ItemPositionProvider(
|
||||
position: position,
|
||||
child: DecorationListItem(
|
||||
minVerticalPadding: _rowBadgeInset,
|
||||
contentPadding: const EdgeInsets.only(left: _rowBadgeInset, right: 0),
|
||||
onPressed: onChanged == null ? null : () => onChanged(!enabled),
|
||||
leading: _ServiceBadge(
|
||||
target: target,
|
||||
size: _rowBadgeSize,
|
||||
radius: _rowBadgeRadius,
|
||||
enabled: enabled,
|
||||
),
|
||||
title: Text(
|
||||
target.label,
|
||||
style: enabled
|
||||
? null
|
||||
: TextStyle(color: context.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Switch(value: enabled, onChanged: onChanged),
|
||||
ReorderableDelayedDragStartListener(
|
||||
index: index,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: const GlyphIcon(AppGlyphs.dragHandle),
|
||||
),
|
||||
),
|
||||
],
|
||||
return ReorderableDelayedDragStartListener(
|
||||
index: index,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: ItemPositionProvider(
|
||||
position: position,
|
||||
child: DecorationListItem(
|
||||
minVerticalPadding: _rowBadgeInset,
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: _rowBadgeInset,
|
||||
right: 16,
|
||||
),
|
||||
onPressed: onChanged == null ? null : () => onChanged(!enabled),
|
||||
leading: _ServiceBadge(
|
||||
target: target,
|
||||
size: _rowBadgeSize,
|
||||
radius: _rowBadgeRadius,
|
||||
enabled: enabled,
|
||||
),
|
||||
title: Text(
|
||||
target.label,
|
||||
style: enabled
|
||||
? null
|
||||
: TextStyle(color: context.colorScheme.onSurfaceVariant),
|
||||
),
|
||||
trailing: Switch(value: enabled, onChanged: onChanged),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/icons/icons.dart';
|
||||
import 'package:fl_clash/providers/providers.dart';
|
||||
@@ -194,14 +192,11 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final suspended = isStart && suspend;
|
||||
if (NavigationDock.isDocked(context)) {
|
||||
return BreathingFill(
|
||||
active: isStart && !suspend,
|
||||
child: FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: suspended ? appLocalizations.suspended : null,
|
||||
onPressed: handleSwitchStart,
|
||||
child: _buildIcon(isStart, suspended: suspended),
|
||||
),
|
||||
return FloatingActionButton(
|
||||
heroTag: null,
|
||||
tooltip: suspended ? appLocalizations.suspended : null,
|
||||
onPressed: handleSwitchStart,
|
||||
child: _buildIcon(isStart, suspended: suspended),
|
||||
);
|
||||
}
|
||||
final hasThreeDigitHours =
|
||||
@@ -277,128 +272,3 @@ class _StartButtonState extends ConsumerState<StartButton>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class BreathingFill extends StatefulWidget {
|
||||
const BreathingFill({super.key, required this.active, required this.child});
|
||||
|
||||
final bool active;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<BreathingFill> createState() => _BreathingFillState();
|
||||
}
|
||||
|
||||
class _BreathingFillState extends State<BreathingFill> {
|
||||
static const _breathDuration = Duration(milliseconds: 1400);
|
||||
// A ticker would redraw the screen on every vsync for as long as the core runs.
|
||||
static const _breathStep = Duration(milliseconds: 66);
|
||||
static const _fadeDuration = Duration(milliseconds: 300);
|
||||
|
||||
final _breath = ValueNotifier<double>(0);
|
||||
late final AppLifecycleListener _lifecycle;
|
||||
Timer? _timer;
|
||||
int _steps = 0;
|
||||
bool _canAnimate = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_lifecycle = AppLifecycleListener(onStateChange: (_) => _sync());
|
||||
}
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
_canAnimate =
|
||||
!MediaQuery.disableAnimationsOf(context) &&
|
||||
TickerMode.valuesOf(context).enabled;
|
||||
_sync();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(BreathingFill oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
_sync();
|
||||
}
|
||||
|
||||
bool get _isForeground => switch (WidgetsBinding.instance.lifecycleState) {
|
||||
null || AppLifecycleState.resumed || AppLifecycleState.inactive => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
void _sync() {
|
||||
if (!widget.active || !_canAnimate || !_isForeground) {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
if (widget.active) _breath.value = 1;
|
||||
return;
|
||||
}
|
||||
_timer ??= Timer.periodic(_breathStep, (_) => _step());
|
||||
}
|
||||
|
||||
void _step() {
|
||||
final period = _breathDuration.inMicroseconds / _breathStep.inMicroseconds;
|
||||
final phase = ++_steps % (2 * period) / period;
|
||||
_breath.value = Curves.easeInOut.transform(phase <= 1 ? phase : 2 - phase);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
_lifecycle.dispose();
|
||||
_breath.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
return Stack(
|
||||
children: [
|
||||
widget.child,
|
||||
Positioned.fill(
|
||||
child: IgnorePointer(
|
||||
child: AnimatedOpacity(
|
||||
opacity: widget.active ? 1 : 0,
|
||||
duration: _fadeDuration,
|
||||
child: RepaintBoundary(
|
||||
child: CustomPaint(
|
||||
painter: _BreathingFillPainter(
|
||||
breath: _breath,
|
||||
color: theme.colorScheme.onPrimaryContainer,
|
||||
shape:
|
||||
theme.floatingActionButtonTheme.shape ?? AppShape.full,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _BreathingFillPainter extends CustomPainter {
|
||||
_BreathingFillPainter({
|
||||
required this.breath,
|
||||
required this.color,
|
||||
required this.shape,
|
||||
}) : super(repaint: breath);
|
||||
|
||||
final ValueNotifier<double> breath;
|
||||
final Color color;
|
||||
final ShapeBorder shape;
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Size size) {
|
||||
canvas.drawPath(
|
||||
shape.getOuterPath(Offset.zero & size),
|
||||
Paint()..color = color.withValues(alpha: 0.14 * breath.value),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool shouldRepaint(_BreathingFillPainter oldDelegate) =>
|
||||
color != oldDelegate.color || shape != oldDelegate.shape;
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
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/providers/app.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// Resolves to true only on "Agree"; leaving the page any other way declines.
|
||||
/// True only after "Agree" and the restatement; any other way out declines.
|
||||
Future<bool> requestDisclaimerConsent() async {
|
||||
return await BaseNavigator.push<bool>(
|
||||
rootNavigatorKey.currentContext!,
|
||||
@@ -24,7 +26,11 @@ class DisclaimerView extends StatelessWidget {
|
||||
static const _maxContentWidth = 720.0;
|
||||
|
||||
List<({String title, String content})> _leadingTerms(AppLocalizations l) => [
|
||||
(title: l.disclaimerSoftwareTitle, content: l.disclaimerSoftwareContent),
|
||||
(
|
||||
title: l.disclaimerSoftwareTitle,
|
||||
content:
|
||||
'${l.disclaimerSoftwareContent}\n\n${l.disclaimerNoServiceStatement}',
|
||||
),
|
||||
(title: l.disclaimerUsageTitle, content: l.disclaimerUsageContent),
|
||||
(
|
||||
title: l.disclaimerResponsibilityTitle,
|
||||
@@ -44,87 +50,140 @@ class DisclaimerView extends StatelessWidget {
|
||||
(title: l.disclaimerAcceptTitle, content: l.disclaimerAcceptContent),
|
||||
];
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final leadingTerms = _leadingTerms(appLocalizations);
|
||||
final trailingTerms = _trailingTerms(appLocalizations);
|
||||
final privacyIndex = leadingTerms.length + 1;
|
||||
final body = ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
).copyWith(top: context.contentTopPadding, bottom: 32),
|
||||
static const _mobileGutter = 16.0;
|
||||
static const _desktopGutter = 24.0;
|
||||
static const _desktopTopPadding = 48.0;
|
||||
|
||||
ListView _buildBody({
|
||||
required EdgeInsets padding,
|
||||
required List<Widget> children,
|
||||
}) {
|
||||
return ListView(
|
||||
padding: padding,
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: _maxContentWidth),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
_DisclaimerIntro(text: appLocalizations.disclaimerDesc),
|
||||
for (final (index, term) in leadingTerms.indexed)
|
||||
_DisclaimerTerm(
|
||||
index: index + 1,
|
||||
title: term.title,
|
||||
content: term.content,
|
||||
),
|
||||
_DisclaimerTerm(
|
||||
index: privacyIndex,
|
||||
title: appLocalizations.disclaimerPrivacyTitle,
|
||||
content: appLocalizations.disclaimerPrivacyContent,
|
||||
children: [
|
||||
_DataServiceCard(
|
||||
glyph: AppGlyphs.error,
|
||||
title: appLocalizations.disclaimerCrashlyticsTitle,
|
||||
content: appLocalizations.disclaimerCrashlyticsContent,
|
||||
),
|
||||
_DataServiceCard(
|
||||
glyph: AppGlyphs.dataUsage,
|
||||
title: appLocalizations.disclaimerAnalyticsTitle,
|
||||
content: appLocalizations.disclaimerAnalyticsContent,
|
||||
),
|
||||
_TermParagraphs(
|
||||
appLocalizations.disclaimerDataProcessingContent,
|
||||
),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_PolicyLink(
|
||||
label: appLocalizations.disclaimerGooglePrivacy,
|
||||
url: _googlePrivacyUrl,
|
||||
),
|
||||
_PolicyLink(
|
||||
label: appLocalizations.disclaimerFirebasePrivacy,
|
||||
url: _firebasePrivacyUrl,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
for (final (index, term) in trailingTerms.indexed)
|
||||
_DisclaimerTerm(
|
||||
index: privacyIndex + index + 1,
|
||||
title: term.title,
|
||||
content: term.content,
|
||||
),
|
||||
],
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
return CommonScaffold(
|
||||
title: appLocalizations.disclaimer,
|
||||
body: requireConsent
|
||||
? Column(
|
||||
children: [
|
||||
Expanded(child: body),
|
||||
const _ConsentBar(maxWidth: _maxContentWidth),
|
||||
],
|
||||
)
|
||||
: body,
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final terms = _buildTerms(appLocalizations);
|
||||
final body = _buildBody(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: _mobileGutter,
|
||||
).copyWith(top: context.contentTopPadding, bottom: 32),
|
||||
children: terms,
|
||||
);
|
||||
if (!requireConsent) {
|
||||
return CommonScaffold(title: appLocalizations.disclaimer, body: body);
|
||||
}
|
||||
return Consumer(
|
||||
builder: (context, ref, _) {
|
||||
if (ref.watch(isMobileViewProvider)) {
|
||||
return CommonScaffold(
|
||||
title: appLocalizations.disclaimer,
|
||||
body: _ConsentLayout(
|
||||
maxWidth: _maxContentWidth,
|
||||
gutter: _mobileGutter,
|
||||
compact: true,
|
||||
child: body,
|
||||
),
|
||||
);
|
||||
}
|
||||
return Scaffold(
|
||||
body: SafeArea(
|
||||
child: _ConsentLayout(
|
||||
maxWidth: _maxContentWidth,
|
||||
gutter: _desktopGutter,
|
||||
compact: false,
|
||||
child: _buildBody(
|
||||
padding: const EdgeInsets.fromLTRB(
|
||||
_desktopGutter,
|
||||
_desktopTopPadding,
|
||||
_desktopGutter,
|
||||
32,
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: Text(
|
||||
appLocalizations.disclaimer,
|
||||
style: context.textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
...terms,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildTerms(AppLocalizations appLocalizations) {
|
||||
final leadingTerms = _leadingTerms(appLocalizations);
|
||||
final trailingTerms = _trailingTerms(appLocalizations);
|
||||
final privacyIndex = leadingTerms.length + 1;
|
||||
return [
|
||||
_DisclaimerIntro(text: appLocalizations.disclaimerDesc),
|
||||
for (final (index, term) in leadingTerms.indexed)
|
||||
_DisclaimerTerm(
|
||||
index: index + 1,
|
||||
title: term.title,
|
||||
content: term.content,
|
||||
),
|
||||
_DisclaimerTerm(
|
||||
index: privacyIndex,
|
||||
title: appLocalizations.disclaimerPrivacyTitle,
|
||||
content: appLocalizations.disclaimerPrivacyContent,
|
||||
children: [
|
||||
_DataServiceCard(
|
||||
glyph: AppGlyphs.error,
|
||||
title: appLocalizations.disclaimerCrashlyticsTitle,
|
||||
content: appLocalizations.disclaimerCrashlyticsContent,
|
||||
),
|
||||
_DataServiceCard(
|
||||
glyph: AppGlyphs.dataUsage,
|
||||
title: appLocalizations.disclaimerAnalyticsTitle,
|
||||
content: appLocalizations.disclaimerAnalyticsContent,
|
||||
),
|
||||
_TermParagraphs(appLocalizations.disclaimerDataProcessingContent),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_PolicyLink(
|
||||
label: appLocalizations.disclaimerGooglePrivacy,
|
||||
url: _googlePrivacyUrl,
|
||||
),
|
||||
_PolicyLink(
|
||||
label: appLocalizations.disclaimerFirebasePrivacy,
|
||||
url: _firebasePrivacyUrl,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
for (final (index, term) in trailingTerms.indexed)
|
||||
_DisclaimerTerm(
|
||||
index: privacyIndex + index + 1,
|
||||
title: term.title,
|
||||
content: term.content,
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -346,14 +405,152 @@ class _PolicyLink extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsentBar extends StatelessWidget {
|
||||
class _ConsentLayout extends StatefulWidget {
|
||||
final double maxWidth;
|
||||
final double gutter;
|
||||
final bool compact;
|
||||
final Widget child;
|
||||
|
||||
const _ConsentBar({required this.maxWidth});
|
||||
const _ConsentLayout({
|
||||
required this.maxWidth,
|
||||
required this.gutter,
|
||||
required this.compact,
|
||||
required this.child,
|
||||
});
|
||||
|
||||
@override
|
||||
State<_ConsentLayout> createState() => _ConsentLayoutState();
|
||||
}
|
||||
|
||||
class _ConsentLayoutState extends State<_ConsentLayout> {
|
||||
static const _endTolerance = 1.0;
|
||||
|
||||
bool _readToEnd = false;
|
||||
|
||||
bool _handleNotification(Notification notification) {
|
||||
if (_readToEnd) {
|
||||
return false;
|
||||
}
|
||||
final metrics = switch (notification) {
|
||||
ScrollNotification(depth: 0, :final metrics) ||
|
||||
ScrollMetricsNotification(depth: 0, :final metrics) => metrics,
|
||||
_ => null,
|
||||
};
|
||||
if (metrics != null && metrics.extentAfter <= _endTolerance) {
|
||||
setState(() {
|
||||
_readToEnd = true;
|
||||
});
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: NotificationListener<Notification>(
|
||||
onNotification: _handleNotification,
|
||||
child: widget.child,
|
||||
),
|
||||
),
|
||||
_ConsentBar(
|
||||
maxWidth: widget.maxWidth,
|
||||
gutter: widget.gutter,
|
||||
compact: widget.compact,
|
||||
readToEnd: _readToEnd,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConsentBar extends StatelessWidget {
|
||||
final double maxWidth;
|
||||
final double gutter;
|
||||
final bool compact;
|
||||
final bool readToEnd;
|
||||
|
||||
const _ConsentBar({
|
||||
required this.maxWidth,
|
||||
required this.gutter,
|
||||
required this.compact,
|
||||
required this.readToEnd,
|
||||
});
|
||||
|
||||
Widget _buildExitButton(BuildContext context) {
|
||||
return ElasticButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(context.appLocalizations.exit),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _handleAgree(BuildContext context) async {
|
||||
final restated = await dialogs.showCommonDialog<bool>(
|
||||
context: context,
|
||||
dismissible: false,
|
||||
child: const _RestateDialog(),
|
||||
);
|
||||
if (restated == true && context.mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _buildAgreeButton(BuildContext context, {required String label}) {
|
||||
return ElasticButton(
|
||||
child: FilledButton(
|
||||
onPressed: readToEnd ? () => _handleAgree(context) : null,
|
||||
child: Text(label),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCompactActions(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(child: _buildExitButton(context)),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildAgreeButton(
|
||||
context,
|
||||
label: readToEnd
|
||||
? appLocalizations.agree
|
||||
: appLocalizations.disclaimerReadToEnd,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActions(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: readToEnd
|
||||
? const SizedBox.shrink()
|
||||
: Text(
|
||||
appLocalizations.disclaimerReadToEnd,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
_buildExitButton(context),
|
||||
const SizedBox(width: 12),
|
||||
_buildAgreeButton(context, label: appLocalizations.agree),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: context.colorScheme.surface,
|
||||
@@ -365,34 +562,12 @@ class _ConsentBar extends StatelessWidget {
|
||||
top: false,
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth),
|
||||
constraints: BoxConstraints(maxWidth: maxWidth + gutter * 2),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 12),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElasticButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(appLocalizations.exit),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElasticButton(
|
||||
child: FilledButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(true);
|
||||
},
|
||||
child: Text(appLocalizations.agree),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: EdgeInsets.symmetric(horizontal: gutter, vertical: 12),
|
||||
child: compact
|
||||
? _buildCompactActions(context)
|
||||
: _buildActions(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -400,3 +575,110 @@ class _ConsentBar extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
enum _Restatement {
|
||||
partial,
|
||||
diverged,
|
||||
complete;
|
||||
|
||||
static _Restatement of(TextEditingValue value, String statement) {
|
||||
if (value.text.trim() == statement) {
|
||||
return complete;
|
||||
}
|
||||
// Pinyin or kana still being composed is not part of the restatement yet.
|
||||
final composing = value.composing;
|
||||
final committed = value.isComposingRangeValid
|
||||
? composing.textBefore(value.text) + composing.textAfter(value.text)
|
||||
: value.text;
|
||||
return statement.startsWith(committed.trimLeft()) ? partial : diverged;
|
||||
}
|
||||
}
|
||||
|
||||
class _RestateDialog extends StatefulWidget {
|
||||
const _RestateDialog();
|
||||
|
||||
@override
|
||||
State<_RestateDialog> createState() => _RestateDialogState();
|
||||
}
|
||||
|
||||
class _RestateDialogState extends State<_RestateDialog> {
|
||||
final _controller = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Widget _buildStatement(String statement) {
|
||||
return DecoratedBox(
|
||||
decoration: ShapeDecoration(
|
||||
color: context.colorScheme.surfaceContainerHighest,
|
||||
shape: AppShape.lg,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: SelectableText(
|
||||
statement,
|
||||
style: context.textTheme.bodyMedium?.copyWith(height: 1.6),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final statement = appLocalizations.disclaimerNoServiceStatement;
|
||||
return ValueListenableBuilder<TextEditingValue>(
|
||||
valueListenable: _controller,
|
||||
builder: (context, value, _) {
|
||||
final restatement = _Restatement.of(value, statement);
|
||||
return CommonDialog(
|
||||
title: appLocalizations.disclaimerRestateTitle,
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop(false);
|
||||
},
|
||||
child: Text(appLocalizations.cancel),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: restatement == _Restatement.complete
|
||||
? () {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
: null,
|
||||
child: Text(appLocalizations.confirm),
|
||||
),
|
||||
],
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
appLocalizations.disclaimerRestateTip,
|
||||
style: context.textTheme.bodyMedium?.copyWith(
|
||||
color: context.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildStatement(statement),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: _controller,
|
||||
minLines: 3,
|
||||
maxLines: 6,
|
||||
decoration: InputDecoration(
|
||||
hintText: appLocalizations.disclaimerRestateHint,
|
||||
errorText: restatement == _Restatement.diverged
|
||||
? appLocalizations.disclaimerRestateMismatch
|
||||
: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,13 +56,16 @@ class _DnsQueriesViewState extends ConsumerState<DnsQueriesView>
|
||||
with RouteMotionHoldMixin<DnsQueriesView> {
|
||||
final _listController = DnsQueryListController();
|
||||
late final ScrollController _scrollController;
|
||||
late final _physics = FollowEndScrollPhysics(
|
||||
isFollowing: () => _listController.value.autoScrollToEnd,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController =
|
||||
widget.scrollController ??
|
||||
ScrollController(initialScrollOffset: double.maxFinite);
|
||||
sheetScrollController(context, initialScrollOffset: double.maxFinite);
|
||||
_listController.setDnsQueries(ref.read(dnsQueriesProvider).list);
|
||||
ref.listenManual(dnsQueriesProvider.select((state) => state.revision), (
|
||||
_,
|
||||
@@ -120,7 +123,6 @@ class _DnsQueriesViewState extends ConsumerState<DnsQueriesView>
|
||||
},
|
||||
child: ScrollToEndBox(
|
||||
controller: _scrollController,
|
||||
dataSource: dnsQueries,
|
||||
enable: state.autoScrollToEnd,
|
||||
onCancelToEnd: () {
|
||||
_listController.setAutoScrollToEnd(false);
|
||||
@@ -133,13 +135,14 @@ class _DnsQueriesViewState extends ConsumerState<DnsQueriesView>
|
||||
child: SuperListView.separated(
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
physics: const NextClampingScrollPhysics(),
|
||||
physics: _physics,
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.only(
|
||||
top: context.contentTopPadding,
|
||||
bottom: 16 + BottomInsetScope.of(context),
|
||||
),
|
||||
itemCount: dnsQueries.length,
|
||||
extentEstimation: zeroDividerExtentEstimation,
|
||||
separatorBuilder: (_, _) => const Divider(height: 0),
|
||||
itemBuilder: (_, index) {
|
||||
return DnsQueryItem(
|
||||
@@ -167,7 +170,7 @@ class DnsQueryItem extends StatelessWidget {
|
||||
const DnsQueryItem({super.key, required this.dnsQuery, this.onClickKeyword});
|
||||
|
||||
void _showDetail(BuildContext context) {
|
||||
showExtend(
|
||||
showInSheet<void>(
|
||||
context,
|
||||
builder: (_) {
|
||||
return CommonScaffold(
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
|
||||
import 'package:fl_clash/common/common.dart';
|
||||
import 'package:fl_clash/features/form/form.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/scheduler.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
|
||||
sealed class _FontChoice {
|
||||
const _FontChoice();
|
||||
}
|
||||
|
||||
final class _DefaultFont extends _FontChoice {
|
||||
const _DefaultFont();
|
||||
}
|
||||
|
||||
final class _InstalledFont extends _FontChoice {
|
||||
const _InstalledFont(this.family);
|
||||
|
||||
final String family;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) =>
|
||||
other is _InstalledFont && other.family == family;
|
||||
|
||||
@override
|
||||
int get hashCode => family.hashCode;
|
||||
}
|
||||
|
||||
class FontFamilyView extends ConsumerStatefulWidget {
|
||||
const FontFamilyView({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<FontFamilyView> createState() => _FontFamilyViewState();
|
||||
}
|
||||
|
||||
class _FontFamilyViewState extends ConsumerState<FontFamilyView> {
|
||||
List<_InstalledFont>? _installed;
|
||||
var _loadStarted = false;
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (_loadStarted) {
|
||||
return;
|
||||
}
|
||||
_loadStarted = true;
|
||||
unawaited(_load());
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
await whenRouteSettled(context);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final families = await loadSystemFontFamilies();
|
||||
if (mounted) {
|
||||
setState(() => _installed = families.map(_InstalledFont.new).toList());
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final installed = _installed ?? const <_InstalledFont>[];
|
||||
return SelectionSheet<_FontChoice>(
|
||||
title: appLocalizations.fontFamily,
|
||||
loadingStatus: _installed == null
|
||||
? const NullStatus(illustration: NullStatusIllustration.fonts)
|
||||
: null,
|
||||
sections: [
|
||||
const SelectionSection(items: [_DefaultFont()]),
|
||||
if (installed.isNotEmpty)
|
||||
SelectionSection(
|
||||
label: appLocalizations.installedFonts,
|
||||
items: installed,
|
||||
),
|
||||
],
|
||||
labelBuilder: (choice) => switch (choice) {
|
||||
_DefaultFont() => appLocalizations.defaultText,
|
||||
_InstalledFont(:final family) => family,
|
||||
},
|
||||
titleBuilder: (choice, title) => switch (choice) {
|
||||
_DefaultFont() => title,
|
||||
_InstalledFont(:final family) => _FontLabel(
|
||||
family: family,
|
||||
child: title,
|
||||
),
|
||||
},
|
||||
selectedOf: (ref) => switch (ref.watch(
|
||||
themeSettingProvider.select((state) => state.fontFamily),
|
||||
)) {
|
||||
final family? => _InstalledFont(family),
|
||||
null => const _DefaultFont(),
|
||||
},
|
||||
onSelected: (choice) {
|
||||
final String? family;
|
||||
switch (choice) {
|
||||
case _DefaultFont():
|
||||
family = null;
|
||||
case _InstalledFont():
|
||||
family = choice.family;
|
||||
}
|
||||
ref
|
||||
.read(themeSettingProvider.notifier)
|
||||
.update((state) => state.copyWith(fontFamily: family));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Laying out a family for the first time loads it on the UI thread, a few
|
||||
/// milliseconds each, so each name switches to its family on a frame of its
|
||||
/// own.
|
||||
class _FontLabel extends StatefulWidget {
|
||||
const _FontLabel({required this.family, required this.child});
|
||||
|
||||
final String family;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<_FontLabel> createState() => _FontLabelState();
|
||||
}
|
||||
|
||||
class _FontLabelState extends State<_FontLabel> {
|
||||
static final _shown = <String>{};
|
||||
static final _waiting = Queue<_FontLabelState>();
|
||||
static var _scheduled = false;
|
||||
|
||||
bool get _isShown => _shown.contains(widget.family);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_request();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(_FontLabel oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.family != widget.family) {
|
||||
_request();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_waiting.remove(this);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _request() {
|
||||
if (_isShown || _waiting.contains(this)) {
|
||||
return;
|
||||
}
|
||||
_waiting.add(this);
|
||||
_schedule();
|
||||
}
|
||||
|
||||
static void _schedule() {
|
||||
if (_scheduled || _waiting.isEmpty) {
|
||||
return;
|
||||
}
|
||||
_scheduled = true;
|
||||
SchedulerBinding.instance
|
||||
..addPostFrameCallback((_) {
|
||||
_scheduled = false;
|
||||
while (_waiting.isNotEmpty) {
|
||||
final next = _waiting.removeFirst();
|
||||
final family = next.widget.family;
|
||||
if (!next.mounted || next._isShown) {
|
||||
continue;
|
||||
}
|
||||
_shown.add(family);
|
||||
next.setState(() {});
|
||||
break;
|
||||
}
|
||||
_schedule();
|
||||
})
|
||||
..ensureVisualUpdate();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (!_isShown) {
|
||||
return widget.child;
|
||||
}
|
||||
return DefaultTextStyle.merge(
|
||||
style: TextStyle(fontFamily: widget.family),
|
||||
child: widget.child,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -53,11 +53,17 @@ class _LogsViewState extends ConsumerState<LogsView>
|
||||
with RouteMotionHoldMixin<LogsView> {
|
||||
final _listController = LogListController();
|
||||
late final ScrollController _scrollController;
|
||||
late final _physics = FollowEndScrollPhysics(
|
||||
isFollowing: () => _listController.value.autoScrollToEnd,
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController(initialScrollOffset: double.maxFinite);
|
||||
_scrollController = sheetScrollController(
|
||||
context,
|
||||
initialScrollOffset: double.maxFinite,
|
||||
);
|
||||
_listController.setLogs(ref.read(logsProvider).list);
|
||||
ref.listenManual(logsProvider.select((state) => state.revision), (_, _) {
|
||||
updateLogsThrottler();
|
||||
@@ -144,9 +150,8 @@ class _LogsViewState extends ConsumerState<LogsView>
|
||||
},
|
||||
controller: _scrollController,
|
||||
enable: state.autoScrollToEnd,
|
||||
dataSource: logs,
|
||||
child: SuperListView.separated(
|
||||
physics: const NextClampingScrollPhysics(),
|
||||
physics: _physics,
|
||||
reverse: true,
|
||||
shrinkWrap: true,
|
||||
controller: _scrollController,
|
||||
@@ -155,6 +160,7 @@ class _LogsViewState extends ConsumerState<LogsView>
|
||||
bottom: 16 + BottomInsetScope.of(context),
|
||||
),
|
||||
itemCount: logs.length,
|
||||
extentEstimation: zeroDividerExtentEstimation,
|
||||
separatorBuilder: (_, _) => const Divider(height: 0),
|
||||
itemBuilder: (_, index) {
|
||||
final log = logs[index];
|
||||
|
||||
@@ -497,7 +497,10 @@ class _MoreSectionsView extends ConsumerWidget {
|
||||
appLocalizations.dialerProxy,
|
||||
Text(
|
||||
dialerCount > 0
|
||||
? appLocalizations.proxiesCount(dialerCount)
|
||||
? appLocalizations.proxiesCount(
|
||||
dialerCount,
|
||||
dialerCount.compact,
|
||||
)
|
||||
: appLocalizations.none,
|
||||
style: hasDialerIssues
|
||||
? TextStyle(color: context.colorScheme.error)
|
||||
|
||||
@@ -182,12 +182,17 @@ class _CustomProxiesViewState extends ConsumerState<CustomProxiesView> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
if (!customProxiesEquality.equals(_opened, _latest)) {
|
||||
if (!customProxiesEquality.equals(_applied(_opened), _applied(_latest))) {
|
||||
unawaited(_providersAction.applyIfProxiesNamed(_seenNames));
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
static List<CustomProxy> _applied(List<CustomProxy>? proxies) => [
|
||||
for (final proxy in proxies ?? const <CustomProxy>[])
|
||||
proxy.copyWith(emptyOptions: const {}),
|
||||
];
|
||||
|
||||
void _handleReorder(int oldIndex, int newIndex) {
|
||||
ref.read(customProxiesProvider.notifier).order(oldIndex, newIndex);
|
||||
}
|
||||
@@ -263,9 +268,17 @@ class _CustomProxiesViewState extends ConsumerState<CustomProxiesView> {
|
||||
links: links,
|
||||
);
|
||||
final decoded = await _decodeShareLinks(core, edit.pending);
|
||||
final dropped = edit.droppedLinks;
|
||||
final linked = dropped.isEmpty
|
||||
? const <List<Map<String, dynamic>>>[]
|
||||
: await core.decodeShareLinks(dropped.values.toList());
|
||||
final proxies = edit.apply(
|
||||
decoded,
|
||||
groupNames: await ref.read(customGroupNamesProvider.future),
|
||||
linked: {
|
||||
for (final (index, id) in dropped.keys.indexed)
|
||||
if (linked[index] case [final definition]) id: definition,
|
||||
},
|
||||
);
|
||||
if (!await _confirmReplaced(previous, proxies)) {
|
||||
throw const QuickEditCancelled();
|
||||
@@ -755,6 +768,9 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
var _revealed = <String>{};
|
||||
late Set<ProxyField> _shown;
|
||||
|
||||
/// Added options not given a value since, the only ones a save keeps empty.
|
||||
late Set<ProxyField> _untouched;
|
||||
|
||||
/// Bumped when the whole mapping is replaced, so the fields drop the text
|
||||
/// they were initialized with.
|
||||
int _revision = 0;
|
||||
@@ -765,7 +781,14 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
final proxy = ref.read(customProxyProvider);
|
||||
_isNew = proxy.id == -1;
|
||||
_shown = _shownIn(proxy);
|
||||
_untouched = {
|
||||
for (final field in proxy.emptyOptions)
|
||||
if (proxy.valueOf(field) == null) field,
|
||||
};
|
||||
NestedFormSheet.bindSave(context, _handleSave);
|
||||
ref.listenManual(customProxyProvider, (_, proxy) {
|
||||
_untouched.removeWhere((field) => proxy.valueOf(field) != null);
|
||||
});
|
||||
ref.listenManual(
|
||||
customProxyProvider.select((state) => state.definition),
|
||||
(_, _) => _scheduleValidate(),
|
||||
@@ -800,14 +823,22 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
child: _ShareLinkDialog(
|
||||
initialLink: link,
|
||||
read: (text) async {
|
||||
final definitions = (await core.decodeShareLinks([text])).single;
|
||||
final results = await core.decodeShareLinks([
|
||||
text,
|
||||
if (link.isNotEmpty) link,
|
||||
]);
|
||||
final definitions = results.first;
|
||||
if (definitions.isEmpty) {
|
||||
throw MessageException(appLocalizations.shareLinksInvalid);
|
||||
}
|
||||
if (definitions.length > 1) {
|
||||
throw MessageException(appLocalizations.singleShareLinkOnly);
|
||||
}
|
||||
return definitions.single;
|
||||
return withUnlinkedFields(
|
||||
definitions.single,
|
||||
previous: ref.read(customProxyProvider).definition,
|
||||
linked: results.skip(1).firstOrNull?.singleOrNull,
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
@@ -829,7 +860,10 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
if (proxy.id != current.id) proxy,
|
||||
],
|
||||
).single.definition;
|
||||
final next = current.copyWith(definition: named);
|
||||
final next = _withShown(current.copyWith(definition: named), {
|
||||
for (final field in _shown)
|
||||
if (current.valueOf(field) == null) field,
|
||||
});
|
||||
ref.read(customProxyProvider.notifier).value = next;
|
||||
setState(() {
|
||||
_shown = _shownIn(next);
|
||||
@@ -839,23 +873,36 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
|
||||
static Set<ProxyField> _shownIn(CustomProxy proxy) => {
|
||||
for (final field in proxy.options)
|
||||
if (proxy.valueOf(field) != null) field,
|
||||
if (proxy.valueOf(field) != null || proxy.emptyOptions.contains(field))
|
||||
field,
|
||||
};
|
||||
|
||||
CustomProxy _withShown(CustomProxy proxy, Set<ProxyField> shown) =>
|
||||
proxy.copyWith(
|
||||
emptyOptions: {
|
||||
for (final field in shown)
|
||||
if (proxy.valueOf(field) == null && _untouched.contains(field))
|
||||
field,
|
||||
},
|
||||
);
|
||||
|
||||
/// Keeps a row for every option [proxy] offers that has a value, was shown,
|
||||
/// or was [unlocked] by the change that led to [proxy].
|
||||
void _syncShown(
|
||||
CustomProxy proxy, {
|
||||
Iterable<ProxyField> unlocked = const [],
|
||||
}) {
|
||||
final shown = {
|
||||
for (final field in proxy.options)
|
||||
if (_shown.contains(field) ||
|
||||
unlocked.contains(field) ||
|
||||
proxy.valueOf(field) != null)
|
||||
field,
|
||||
};
|
||||
_untouched.addAll(unlocked.where((field) => proxy.valueOf(field) == null));
|
||||
ref.read(customProxyProvider.notifier).value = _withShown(proxy, shown);
|
||||
setState(() {
|
||||
_shown = {
|
||||
for (final field in proxy.options)
|
||||
if (_shown.contains(field) ||
|
||||
unlocked.contains(field) ||
|
||||
proxy.valueOf(field) != null)
|
||||
field,
|
||||
};
|
||||
_shown = shown;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -899,7 +946,6 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
void _updateOption(ProxyField field, Object? value) {
|
||||
final proxy = ref.read(customProxyProvider);
|
||||
final next = proxy.withField(field, value);
|
||||
ref.read(customProxyProvider.notifier).value = next;
|
||||
final offered = proxy.options;
|
||||
_syncShown(
|
||||
next,
|
||||
@@ -1066,9 +1112,7 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
if (res == null) {
|
||||
return;
|
||||
}
|
||||
final next = ref.read(customProxyProvider).withType(res);
|
||||
ref.read(customProxyProvider.notifier).value = next;
|
||||
_syncShown(next);
|
||||
_syncShown(ref.read(customProxyProvider).withType(res));
|
||||
}
|
||||
|
||||
Future<void> _handleSelectChoice(ProxyField field) async {
|
||||
@@ -1205,8 +1249,12 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
ProxyFieldKind.choice => proxy.choicesOf(field).firstOrNull,
|
||||
_ => null,
|
||||
};
|
||||
final value = proxy.valueOf(field) ?? initial;
|
||||
_shown = {..._shown, field};
|
||||
_updateOption(field, proxy.valueOf(field) ?? initial);
|
||||
if (value == null) {
|
||||
_untouched.add(field);
|
||||
}
|
||||
_updateOption(field, value);
|
||||
}
|
||||
|
||||
void _handleRemoveOption(ProxyField field) {
|
||||
@@ -1315,6 +1363,9 @@ class _EditCustomProxyViewState extends ConsumerState<_EditCustomProxyView> {
|
||||
Future<void> _handleSave() async {
|
||||
if (_saving) return;
|
||||
_saving = true;
|
||||
ref
|
||||
.read(customProxyProvider.notifier)
|
||||
.update((state) => _withShown(state, _shown));
|
||||
try {
|
||||
String? coreError;
|
||||
final saved = await _handleSaveCustomProxy(
|
||||
|
||||
@@ -10,9 +10,9 @@ import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
/// A group names its proxies under `proxies`, its providers under `use` and
|
||||
/// its filters, which only reach the providers' proxies, under `filter`.
|
||||
/// `exclude-filter` drops whatever matches it, named members too.
|
||||
/// `proxies` names a group's proxies, `use` its providers, `use-position` where
|
||||
/// those sit among the proxies, and `filter` what of the providers' proxies it
|
||||
/// takes. `exclude-filter` drops whatever matches it, named members too.
|
||||
enum _MemberKind {
|
||||
proxy,
|
||||
provider,
|
||||
@@ -21,6 +21,8 @@ enum _MemberKind {
|
||||
|
||||
bool get isFilter => this == filter || this == excludeFilter;
|
||||
|
||||
_MemberKind get run => this == provider ? proxy : this;
|
||||
|
||||
(Color, Color) chipColorsOf(ColorScheme colorScheme) => switch (this) {
|
||||
proxy || provider => (
|
||||
colorScheme.secondaryContainer,
|
||||
@@ -74,16 +76,40 @@ abstract class _MemberHost<M> {
|
||||
|
||||
M withNames(M model, _MemberKind kind, List<String> names);
|
||||
|
||||
List<String>? layoutOf(M model);
|
||||
|
||||
M withLayout(M model, List<String>? layout);
|
||||
|
||||
/// What the picker no longer offers as a [kind].
|
||||
Set<String> takenIn(M model, _MemberKind kind) => {...namesOf(model, kind)};
|
||||
|
||||
/// The core always lists a group's proxies before its providers' proxies,
|
||||
/// so a move only reorders members of the same kind.
|
||||
List<_Member> membersOf(M model) => [
|
||||
for (final kind in kinds)
|
||||
for (final name in namesOf(model, kind)) (kind: kind, name: name),
|
||||
Iterable<_MemberKind> get _runs => {for (final kind in kinds) kind.run};
|
||||
|
||||
List<_Member> runOf(M model, _MemberKind run) => [
|
||||
for (final name in namesOf(model, run)) (kind: run, name: name),
|
||||
];
|
||||
|
||||
/// The core reads each run's own order only, so the rows take a run each
|
||||
/// as [layoutOf] names it, and the rest follow run by run.
|
||||
List<_Member> membersOf(M model) {
|
||||
final runs = {
|
||||
for (final run in _runs) run.name: runOf(model, run).iterator,
|
||||
};
|
||||
final members = <_Member>[];
|
||||
for (final name in layoutOf(model) ?? const <String>[]) {
|
||||
final run = runs[name];
|
||||
if (run != null && run.moveNext()) {
|
||||
members.add(run.current);
|
||||
}
|
||||
}
|
||||
for (final run in runs.values) {
|
||||
while (run.moveNext()) {
|
||||
members.add(run.current);
|
||||
}
|
||||
}
|
||||
return members;
|
||||
}
|
||||
|
||||
M withMembers(M model, Iterable<_Member> members) {
|
||||
var next = model;
|
||||
for (final kind in kinds) {
|
||||
@@ -95,7 +121,24 @@ abstract class _MemberHost<M> {
|
||||
next = withNames(next, kind, names);
|
||||
}
|
||||
}
|
||||
return next;
|
||||
final layout = _layoutOf(members);
|
||||
if (const ListEquality<String>().equals(layoutOf(model), layout)) {
|
||||
return next;
|
||||
}
|
||||
return withLayout(next, layout);
|
||||
}
|
||||
|
||||
/// Leaves out the trailing rows [membersOf] would put there anyway.
|
||||
List<String>? _layoutOf(Iterable<_Member> members) {
|
||||
final order = _runs.toList();
|
||||
final runs = [for (final member in members) order.indexOf(member.kind.run)];
|
||||
var end = runs.length;
|
||||
while (end > 0 && (end == runs.length || runs[end - 1] <= runs[end])) {
|
||||
end--;
|
||||
}
|
||||
return end == 0
|
||||
? null
|
||||
: [for (final run in runs.take(end)) order[run].name];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,11 +184,66 @@ class _GroupMembers extends _MemberHost<ProxyGroup> {
|
||||
),
|
||||
};
|
||||
|
||||
@override
|
||||
List<String>? layoutOf(ProxyGroup group) => group.memberLayout;
|
||||
|
||||
@override
|
||||
ProxyGroup withLayout(ProxyGroup group, List<String>? layout) =>
|
||||
group.copyWith(memberLayout: layout);
|
||||
|
||||
@override
|
||||
Set<String> takenIn(ProxyGroup group, _MemberKind kind) => {
|
||||
...super.takenIn(group, kind),
|
||||
if (kind == _MemberKind.proxy) group.name,
|
||||
};
|
||||
|
||||
@override
|
||||
List<_Member> runOf(ProxyGroup group, _MemberKind run) {
|
||||
if (run != _MemberKind.proxy) {
|
||||
return super.runOf(group, run);
|
||||
}
|
||||
final proxies = group.proxies ?? const <String>[];
|
||||
final positions = group.effectiveUsePosition ?? const <String, int>{};
|
||||
final before = [for (var i = 0; i <= proxies.length; i++) <_Member>[]];
|
||||
for (final name in group.use ?? const <String>[]) {
|
||||
before[positions[name] ?? proxies.length].add((
|
||||
kind: _MemberKind.provider,
|
||||
name: name,
|
||||
));
|
||||
}
|
||||
return [
|
||||
for (final (index, providers) in before.indexed) ...[
|
||||
...providers,
|
||||
if (index < proxies.length)
|
||||
(kind: _MemberKind.proxy, name: proxies[index]),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
ProxyGroup withMembers(ProxyGroup group, Iterable<_Member> members) {
|
||||
final next = super.withMembers(group, members);
|
||||
final count = members
|
||||
.where((member) => member.kind == _MemberKind.proxy)
|
||||
.length;
|
||||
var seen = 0;
|
||||
final positions = <String, int>{};
|
||||
for (final member in members) {
|
||||
if (member.kind == _MemberKind.proxy) {
|
||||
seen++;
|
||||
} else if (member.kind == _MemberKind.provider && seen < count) {
|
||||
positions[member.name] = seen;
|
||||
}
|
||||
}
|
||||
final usePosition = positions.isEmpty ? null : positions;
|
||||
if (const MapEquality<String, int>().equals(
|
||||
group.usePosition,
|
||||
usePosition,
|
||||
)) {
|
||||
return next;
|
||||
}
|
||||
return next.copyWith(usePosition: usePosition);
|
||||
}
|
||||
}
|
||||
|
||||
class _ProviderFilters extends _MemberHost<ProxyProviderOptions> {
|
||||
@@ -190,6 +288,15 @@ class _ProviderFilters extends _MemberHost<ProxyProviderOptions> {
|
||||
),
|
||||
_ => options.copyWith(filter: joinFilterParts(names)),
|
||||
};
|
||||
|
||||
@override
|
||||
List<String>? layoutOf(ProxyProviderOptions options) => options.memberLayout;
|
||||
|
||||
@override
|
||||
ProxyProviderOptions withLayout(
|
||||
ProxyProviderOptions options,
|
||||
List<String>? layout,
|
||||
) => options.copyWith(memberLayout: layout);
|
||||
}
|
||||
|
||||
Map<String, String> _providerTypes(
|
||||
@@ -302,20 +409,19 @@ class _MembersSliverState<M> extends ConsumerState<_MembersSliver<M>>
|
||||
}
|
||||
|
||||
/// [oldIndex] and [newIndex] count the rows still collapsing too.
|
||||
void _handleReorder(List<_Member> run, int oldIndex, int newIndex) {
|
||||
final rest = [...run];
|
||||
void _handleReorder(int oldIndex, int newIndex) {
|
||||
final rest = [..._entries];
|
||||
final moved = rest.removeAt(oldIndex);
|
||||
final target = rest
|
||||
.take(newIndex)
|
||||
.where((member) => !_transitions.isLeaving(member))
|
||||
.length;
|
||||
final names = [
|
||||
for (final member in rest)
|
||||
if (!_transitions.isLeaving(member)) member.name,
|
||||
]..insert(target, moved.name);
|
||||
_host
|
||||
.notifierOf(ref)
|
||||
.update((state) => _host.withNames(state, moved.kind, names));
|
||||
_update(
|
||||
(_) => [
|
||||
for (final member in rest)
|
||||
if (!_transitions.isLeaving(member)) member,
|
||||
]..insert(target, moved),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -345,10 +451,10 @@ class _MembersSliverState<M> extends ConsumerState<_MembersSliver<M>>
|
||||
filter.regex: filter.label,
|
||||
};
|
||||
final count = members.length + 1;
|
||||
_transitions.place(_entries, trailing: 1);
|
||||
final runs = groupBy(_entries, (_Member member) => member.kind);
|
||||
Widget itemAt(List<_Member> run, int index) {
|
||||
final member = run[index];
|
||||
final entries = _entries;
|
||||
_transitions.place(entries, trailing: 1);
|
||||
Widget itemAt(int index) {
|
||||
final member = entries[index];
|
||||
final name = member.name;
|
||||
return EntryTransition(
|
||||
key: ValueKey(member),
|
||||
@@ -382,17 +488,13 @@ class _MembersSliverState<M> extends ConsumerState<_MembersSliver<M>>
|
||||
SliverToBoxAdapter(
|
||||
child: ListHeader(title: _host.title(appLocalizations)),
|
||||
),
|
||||
for (final kind in _host.kinds)
|
||||
if (runs[kind] case final run?)
|
||||
SuperSliverReorderableList(
|
||||
key: ValueKey(kind),
|
||||
itemBuilder: (_, index) => itemAt(run, index),
|
||||
itemCount: run.length,
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
commonProxyDecorator(itemAt(run, index), index, animation),
|
||||
onReorderItem: (oldIndex, newIndex) =>
|
||||
_handleReorder(run, oldIndex, newIndex),
|
||||
),
|
||||
SuperSliverReorderableList(
|
||||
itemBuilder: (_, index) => itemAt(index),
|
||||
itemCount: entries.length,
|
||||
proxyDecorator: (child, index, animation) =>
|
||||
commonProxyDecorator(itemAt(index), index, animation),
|
||||
onReorderItem: _handleReorder,
|
||||
),
|
||||
SliverToBoxAdapter(
|
||||
child: ItemPositionProvider(
|
||||
position: ItemPosition.get(members.length, count),
|
||||
|
||||
@@ -89,6 +89,32 @@ class _StandardContentState extends ConsumerState<StandardContent> {
|
||||
BaseNavigator.push(context, _EditGlobalAddedRules(_profileId));
|
||||
}
|
||||
|
||||
Widget _buildRuleItem(List<Rule> rules, int index, Set<dynamic> selected) {
|
||||
final rule = rules[index];
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ObjectKey(rule),
|
||||
index: index,
|
||||
child: ItemPositionProvider(
|
||||
position: ItemPosition.get(index, rules.length),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: RuleItem(
|
||||
hasMatch: true,
|
||||
isEditing: selected.isNotEmpty,
|
||||
isSelected: selected.contains(rule.id),
|
||||
rule: rule,
|
||||
onSelected: () {
|
||||
_handleSelected(rule.id);
|
||||
},
|
||||
onEdit: (rule) {
|
||||
_handleAddOrUpdate(rule);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
@@ -171,32 +197,13 @@ class _StandardContentState extends ConsumerState<StandardContent> {
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 8)),
|
||||
SliverReorderableList(
|
||||
itemCount: addedRules.length,
|
||||
itemBuilder: (_, index) {
|
||||
final rule = addedRules[index];
|
||||
final position = ItemPosition.get(index, addedRules.length);
|
||||
return ReorderableDelayedDragStartListener(
|
||||
key: ObjectKey(rule),
|
||||
index: index,
|
||||
child: ItemPositionProvider(
|
||||
position: position,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: RuleItem(
|
||||
hasMatch: true,
|
||||
isEditing: selectedRules.isNotEmpty,
|
||||
isSelected: selectedRules.contains(rule.id),
|
||||
rule: rule,
|
||||
onSelected: () {
|
||||
_handleSelected(rule.id);
|
||||
},
|
||||
onEdit: (rule) {
|
||||
_handleAddOrUpdate(rule);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
itemBuilder: (_, index) =>
|
||||
_buildRuleItem(addedRules, index, selectedRules),
|
||||
proxyDecorator: (child, index, animation) => commonProxyDecorator(
|
||||
_buildRuleItem(addedRules, index, selectedRules),
|
||||
index,
|
||||
animation,
|
||||
),
|
||||
itemExtent: ruleItemHeight,
|
||||
onReorderItem: ref
|
||||
.read(profileRulesProvider(_profileId).notifier)
|
||||
|
||||
@@ -375,6 +375,7 @@ class ProfileItem extends ConsumerWidget {
|
||||
key: const ValueKey('lastUpdate'),
|
||||
lastUpdateDate: profile.lastUpdateDate,
|
||||
style: context.textTheme.bodySmall?.toLighter,
|
||||
compact: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -294,7 +294,9 @@ class _DelayTextState extends ConsumerState<_DelayText> {
|
||||
child: _withFocusRing(
|
||||
context,
|
||||
Text(
|
||||
delay > 0 ? '$delay ms' : 'Timeout',
|
||||
delay > 0
|
||||
? '$delay ms'
|
||||
: delayFailureText(delay, context.appLocalizations),
|
||||
maxLines: 1,
|
||||
style: context.textTheme.labelSmall?.copyWith(
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -31,12 +31,18 @@ class ProxiesListView extends ConsumerStatefulWidget {
|
||||
}
|
||||
|
||||
class _ProxiesListViewState extends ConsumerState<ProxiesListView> {
|
||||
final _controller = ScrollController();
|
||||
late final ScrollController _controller;
|
||||
GroupOffsets _groupOffsets = GroupOffsets.empty;
|
||||
double containerHeight = 0;
|
||||
String? _enterGroupName;
|
||||
Timer? _enterTimer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_controller = sheetScrollController(context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_stopEnterAnimated();
|
||||
@@ -380,7 +386,7 @@ class ListHeader extends ConsumerWidget {
|
||||
Flexible(
|
||||
child: Row(
|
||||
children: [
|
||||
_GroupIcon(src: group.icon),
|
||||
_GroupIcon(src: group.icon, name: groupName),
|
||||
Flexible(child: _GroupSummary(groupName: groupName)),
|
||||
],
|
||||
),
|
||||
@@ -412,9 +418,10 @@ class ListHeader extends ConsumerWidget {
|
||||
}
|
||||
|
||||
class _GroupIcon extends ConsumerWidget {
|
||||
const _GroupIcon({required this.src});
|
||||
const _GroupIcon({required this.src, required this.name});
|
||||
|
||||
final String src;
|
||||
final String name;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -439,8 +446,14 @@ class _GroupIcon extends ConsumerWidget {
|
||||
),
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: IconTheme.merge(
|
||||
data: IconThemeData(size: constraints.maxHeight - 12.ap),
|
||||
child: CommonTargetIcon(src: src),
|
||||
data: IconThemeData(
|
||||
size: constraints.maxHeight - 12.ap,
|
||||
color: context.colorScheme.onSecondaryContainer,
|
||||
),
|
||||
child: CommonTargetIcon(
|
||||
src: src,
|
||||
fallback: GroupMonogram(name),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -452,8 +465,11 @@ class _GroupIcon extends ConsumerWidget {
|
||||
child: LayoutBuilder(
|
||||
builder: (_, constraints) {
|
||||
return IconTheme.merge(
|
||||
data: IconThemeData(size: constraints.maxHeight - 16.ap),
|
||||
child: CommonTargetIcon(src: src),
|
||||
data: IconThemeData(
|
||||
size: constraints.maxHeight - 16.ap,
|
||||
color: context.colorScheme.primary,
|
||||
),
|
||||
child: CommonTargetIcon(src: src, fallback: GroupMonogram(name)),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:fl_clash/enum/enum.dart';
|
||||
import 'package:fl_clash/icons/icons.dart';
|
||||
import 'package:fl_clash/models/common.dart';
|
||||
import 'package:fl_clash/models/core.dart';
|
||||
import 'package:fl_clash/models/profile.dart';
|
||||
import 'package:fl_clash/pages/editor.dart';
|
||||
import 'package:fl_clash/providers/action.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
@@ -50,9 +51,7 @@ class _ProvidersViewState extends ConsumerState<ProvidersView> {
|
||||
});
|
||||
await Future.wait(updateProviders);
|
||||
proxiesAction.updateGroupsDebounce();
|
||||
if (messages.isNotEmpty) {
|
||||
unawaited(dialogs.showAllUpdatingMessagesDialog(messages));
|
||||
}
|
||||
dialogs.showFailures(messages);
|
||||
}
|
||||
|
||||
List<Widget> _buildSection({
|
||||
@@ -139,9 +138,12 @@ class ProviderItem extends ConsumerStatefulWidget {
|
||||
|
||||
class _ProviderItemState extends ConsumerState<ProviderItem> {
|
||||
_ProviderFile _file = _ProviderFile.binary;
|
||||
bool _isAppRuleSet = false;
|
||||
|
||||
ExternalProvider get provider => widget.provider;
|
||||
|
||||
bool get _isSyncable => provider.vehicleType == 'HTTP' || _isAppRuleSet;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
@@ -158,17 +160,23 @@ class _ProviderItemState extends ConsumerState<ProviderItem> {
|
||||
}
|
||||
|
||||
Future<void> _probeFile() async {
|
||||
final clashProvidersAction = ref.read(
|
||||
clashProvidersActionProvider.notifier,
|
||||
);
|
||||
final file = await _probeProviderFile(provider.path);
|
||||
if (!mounted || file == _file) {
|
||||
final isAppRuleSet =
|
||||
await clashProvidersAction.remoteProviderOf(provider) != null;
|
||||
if (!mounted || (file == _file && isAppRuleSet == _isAppRuleSet)) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_file = file;
|
||||
_isAppRuleSet = isAppRuleSet;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _handleUpdateProvider() async {
|
||||
if (provider.vehicleType != 'HTTP') return;
|
||||
if (!_isSyncable) return;
|
||||
final proxiesAction = ref.read(proxiesActionProvider.notifier);
|
||||
await globalState.safeRun(() async {
|
||||
final message = await proxiesAction.updateProvider(
|
||||
@@ -205,12 +213,15 @@ class _ProviderItemState extends ConsumerState<ProviderItem> {
|
||||
final path = provider.path;
|
||||
if (path == null || path.isEmpty) return;
|
||||
final core = ref.read(coreHandlerProvider);
|
||||
final isRuleSet = _file == _ProviderFile.ruleSet;
|
||||
unawaited(
|
||||
BaseNavigator.push<void>(
|
||||
context,
|
||||
EditorPage(
|
||||
title: provider.name,
|
||||
load: () => core.dumpRuleSet(path),
|
||||
load: isRuleSet
|
||||
? () => core.dumpRuleSet(path)
|
||||
: () => _readProviderText(path),
|
||||
schema: EditorSchema.provider,
|
||||
),
|
||||
),
|
||||
@@ -244,25 +255,6 @@ class _ProviderItemState extends ConsumerState<ProviderItem> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget? _buildProviderMetadata(BuildContext context) {
|
||||
final countLabel = switch (provider.type) {
|
||||
'Proxy' => context.appLocalizations.proxiesCount(provider.count),
|
||||
'Rule' => context.appLocalizations.rulesCount(provider.count),
|
||||
_ => null,
|
||||
};
|
||||
final chips = [
|
||||
if (provider.updateAt.microsecondsSinceEpoch > 0)
|
||||
MetaChip(label: provider.updateAt.getLastUpdateTimeDesc(context)),
|
||||
if (provider.count > 0 && countLabel != null) MetaChip(label: countLabel),
|
||||
];
|
||||
return chips.isEmpty
|
||||
? null
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 2),
|
||||
child: Row(spacing: 4, children: chips),
|
||||
);
|
||||
}
|
||||
|
||||
List<CommonPopupMenuItem> _menuItems(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final subscriptionInfo = provider.subscriptionInfo;
|
||||
@@ -290,7 +282,7 @@ class _ProviderItemState extends ConsumerState<ProviderItem> {
|
||||
_handleSideLoadProvider();
|
||||
},
|
||||
),
|
||||
if (provider.vehicleType == 'HTTP')
|
||||
if (_isSyncable)
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.sync,
|
||||
label: appLocalizations.sync,
|
||||
@@ -310,44 +302,80 @@ class _ProviderItemState extends ConsumerState<ProviderItem> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final isUpdating = ref.watch(isUpdatingProvider(provider.updatingKey));
|
||||
return DecorationListItem(
|
||||
minVerticalPadding: 8,
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 0),
|
||||
title: Text(provider.name, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: _buildProviderMetadata(context),
|
||||
trailing: SizedBox.square(
|
||||
dimension: kMinInteractiveDimension,
|
||||
child: FadeThroughBox(
|
||||
alignment: Alignment.center,
|
||||
child: isUpdating
|
||||
? const SizedBox.square(
|
||||
key: ValueKey('loading'),
|
||||
dimension: kMinInteractiveDimension,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: CommonCircleLoading(),
|
||||
),
|
||||
)
|
||||
: CommonPopupBox(
|
||||
key: const ValueKey('menu'),
|
||||
popupBuilder: (_) =>
|
||||
CommonPopupMenu(items: _menuItems(context)),
|
||||
targetBuilder: (open) {
|
||||
return IconButton(
|
||||
tooltip: context.appLocalizations.more,
|
||||
onPressed: () {
|
||||
open();
|
||||
},
|
||||
icon: const GlyphIcon(AppGlyphs.more),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
final previewable = _file != _ProviderFile.binary;
|
||||
return ContextMenuRegion(
|
||||
menuItems: isUpdating ? null : _menuItems(context),
|
||||
child: ExternalProviderItem(
|
||||
provider: provider,
|
||||
isUpdating: isUpdating,
|
||||
onPressed: isUpdating || !previewable
|
||||
? null
|
||||
: () => _handlePreviewProvider(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class ExternalProviderItem extends StatelessWidget {
|
||||
final ExternalProvider provider;
|
||||
final bool isUpdating;
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
const ExternalProviderItem({
|
||||
super.key,
|
||||
required this.provider,
|
||||
this.isUpdating = false,
|
||||
this.onPressed,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final count = provider.count;
|
||||
final updateAt = provider.updateAt;
|
||||
return DecorationListItem(
|
||||
onPressed: onPressed,
|
||||
title: ChipTitle(
|
||||
title: provider.name,
|
||||
expire: provider.subscriptionInfo?.expireDate,
|
||||
),
|
||||
subtitle: Text(switch (provider.type) {
|
||||
'Proxy' => appLocalizations.proxiesCount(count, count.compact),
|
||||
_ => appLocalizations.rulesCount(count, count.compact),
|
||||
}),
|
||||
trailing: AnimatedSwitcher(
|
||||
duration: context.motionDuration(commonDuration),
|
||||
layoutBuilder: (current, previous) => Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [...previous, ?current],
|
||||
),
|
||||
child: isUpdating
|
||||
? const SizedBox.square(
|
||||
key: ValueKey('loading'),
|
||||
dimension: 24,
|
||||
child: CommonCircleLoading(),
|
||||
)
|
||||
: LastUpdateTimeText(
|
||||
key: const ValueKey('time'),
|
||||
lastUpdateDate: updateAt.microsecondsSinceEpoch > 0
|
||||
? updateAt
|
||||
: null,
|
||||
compact: true,
|
||||
style: context.listCaptionStyle,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _readProviderText(String path) async {
|
||||
try {
|
||||
return await readTextFileTask(path) ?? '';
|
||||
} on FormatException {
|
||||
throw MessageException(currentAppLocalizations.nonTextProviderFile);
|
||||
}
|
||||
}
|
||||
|
||||
const _zstdMagic = [0x28, 0xB5, 0x2F, 0xFD];
|
||||
const _probeLength = 512;
|
||||
|
||||
@@ -413,13 +441,7 @@ class _EditProviderView extends ConsumerStatefulWidget {
|
||||
class _EditProviderViewState extends ConsumerState<_EditProviderView> {
|
||||
String? _raw;
|
||||
|
||||
Future<String> _load() async {
|
||||
try {
|
||||
return _raw = await readTextFileTask(widget.path) ?? '';
|
||||
} on FormatException {
|
||||
throw MessageException(currentAppLocalizations.nonTextProviderFile);
|
||||
}
|
||||
}
|
||||
Future<String> _load() async => _raw = await _readProviderText(widget.path);
|
||||
|
||||
Future<void> _handleSave(BuildContext context, String content) async {
|
||||
final proxiesAction = ref.read(proxiesActionProvider.notifier);
|
||||
|
||||
@@ -33,7 +33,6 @@ class _ProxiesViewState extends ConsumerState<ProxiesView> {
|
||||
onPressed: () {
|
||||
showSheet(
|
||||
context: context,
|
||||
props: const SheetProps(isScrollControlled: true),
|
||||
builder: (_) {
|
||||
return CommonScaffold(
|
||||
body: const ProxiesSetting(),
|
||||
|
||||
@@ -6,7 +6,7 @@ import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
class ProxiesSetting extends ConsumerWidget {
|
||||
class ProxiesSetting extends StatelessWidget {
|
||||
const ProxiesSetting({super.key});
|
||||
|
||||
Glyph _getIconWithProxiesType(ProxiesType type) {
|
||||
@@ -304,40 +304,37 @@ class ProxiesSetting extends ConsumerWidget {
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxHeight: ref.sheetHeight(context, 0.7)),
|
||||
child: SingleChildScrollView(
|
||||
padding: EdgeInsets.only(top: context.contentTopPadding, bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildStyleSetting(context),
|
||||
..._buildSortSetting(context),
|
||||
..._buildLayoutSetting(context),
|
||||
..._buildSizeSetting(context),
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final isList = ref.watch(
|
||||
proxiesStyleSettingProvider.select(
|
||||
(state) => state.type == ProxiesType.list,
|
||||
),
|
||||
);
|
||||
if (isList) {
|
||||
return child!;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [..._buildGroupStyleSetting(context)],
|
||||
),
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.only(top: context.contentTopPadding, bottom: 32),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
..._buildStyleSetting(context),
|
||||
..._buildSortSetting(context),
|
||||
..._buildLayoutSetting(context),
|
||||
..._buildSizeSetting(context),
|
||||
Consumer(
|
||||
builder: (_, ref, child) {
|
||||
final isList = ref.watch(
|
||||
proxiesStyleSettingProvider.select(
|
||||
(state) => state.type == ProxiesType.list,
|
||||
),
|
||||
);
|
||||
if (isList) {
|
||||
return child!;
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [..._buildGroupStyleSetting(context)],
|
||||
),
|
||||
..._buildFilterSetting(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
..._buildFilterSetting(context),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -153,7 +153,6 @@ class ProxiesTabViewState extends ConsumerState<ProxiesTabView>
|
||||
void _showMoreMenu() {
|
||||
showSheet(
|
||||
context: context,
|
||||
props: const SheetProps(isScrollControlled: false),
|
||||
builder: (_) {
|
||||
return CommonScaffold(
|
||||
body: Builder(
|
||||
@@ -351,6 +350,8 @@ class ProxyGroupView extends ConsumerWidget {
|
||||
final ScrollController controller;
|
||||
final int columns;
|
||||
final ProxyCardType cardType;
|
||||
final bool shrinkWrap;
|
||||
final double topPadding;
|
||||
|
||||
const ProxyGroupView({
|
||||
super.key,
|
||||
@@ -358,6 +359,8 @@ class ProxyGroupView extends ConsumerWidget {
|
||||
required this.controller,
|
||||
required this.columns,
|
||||
required this.cardType,
|
||||
this.shrinkWrap = false,
|
||||
this.topPadding = 16,
|
||||
});
|
||||
|
||||
PageStorageKey _getPageStorageKey(WidgetRef ref) {
|
||||
@@ -379,8 +382,9 @@ class ProxyGroupView extends ConsumerWidget {
|
||||
child: GridView.builder(
|
||||
key: _getPageStorageKey(ref),
|
||||
controller: controller,
|
||||
shrinkWrap: shrinkWrap,
|
||||
padding: EdgeInsets.only(
|
||||
top: 16,
|
||||
top: topPadding,
|
||||
left: 16,
|
||||
right: 16,
|
||||
bottom: 16 + BottomInsetScope.of(context),
|
||||
|
||||
@@ -8,41 +8,132 @@ import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/action.dart';
|
||||
import 'package:fl_clash/providers/app.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/providers/database.dart';
|
||||
import 'package:fl_clash/state.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:path/path.dart' hide context;
|
||||
|
||||
class ResourcesView extends ConsumerWidget {
|
||||
typedef _RemoteResources = ({
|
||||
List<IconSet> iconSets,
|
||||
List<ClashProvider> ruleProviders,
|
||||
List<Script> scripts,
|
||||
});
|
||||
|
||||
const _RemoteResources _noResources = (
|
||||
iconSets: <IconSet>[],
|
||||
ruleProviders: <ClashProvider>[],
|
||||
scripts: <Script>[],
|
||||
);
|
||||
|
||||
class ResourcesView extends ConsumerStatefulWidget {
|
||||
const ResourcesView({super.key});
|
||||
|
||||
Future<void> _updateInterval(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
int updateInterval,
|
||||
) async {
|
||||
@override
|
||||
ConsumerState<ResourcesView> createState() => _ResourcesViewState();
|
||||
}
|
||||
|
||||
class _ResourcesViewState extends ConsumerState<ResourcesView>
|
||||
with RouteMotionHoldMixin<ResourcesView>, RouteSettledMixin<ResourcesView> {
|
||||
_RemoteResources? _resources;
|
||||
var _fileInfos = const <String, FileInfo?>{};
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
ref.listenManual(iconSetsProvider, (_, _) => _refreshResources());
|
||||
ref.listenManual(clashProvidersProvider, (_, _) => _refreshResources());
|
||||
ref.listenManual(scriptsProvider, (_, _) => _refreshResources());
|
||||
}
|
||||
|
||||
@override
|
||||
void didSettleRoute() => unawaited(_load());
|
||||
|
||||
Future<void> _load() async {
|
||||
await Future.wait<Object>([
|
||||
ref.read(iconSetsProvider.future),
|
||||
ref.read(clashProvidersProvider.future),
|
||||
ref.read(scriptsProvider.future),
|
||||
]).catchError((Object _) => const <Object>[]);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
final resources = _readResources();
|
||||
final loads = <String, Future<FileInfo?>>{
|
||||
for (final type in GeoResource.values)
|
||||
type.updatingKey: _geoFileInfo(type),
|
||||
for (final provider in resources.ruleProviders)
|
||||
provider.updatingKey: provider.fileInfo,
|
||||
for (final script in resources.scripts)
|
||||
script.updatingKey: script.fileInfo,
|
||||
};
|
||||
final fileInfos = await Future.wait([
|
||||
for (final load in loads.values) load.catchError((Object _) => null),
|
||||
]);
|
||||
if (!mounted) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_resources = _readResources();
|
||||
_fileInfos = Map.fromIterables(loads.keys, fileInfos);
|
||||
});
|
||||
}
|
||||
|
||||
_RemoteResources _readResources() => (
|
||||
iconSets: [
|
||||
for (final iconSet in ref.read(iconSetsProvider).value ?? <IconSet>[])
|
||||
if (iconSet.isRemote) iconSet,
|
||||
],
|
||||
ruleProviders: [
|
||||
for (final provider
|
||||
in ref.read(clashProvidersProvider).value ?? <ClashProvider>[])
|
||||
if (provider.isRemote) provider,
|
||||
],
|
||||
scripts: [
|
||||
for (final script in ref.read(scriptsProvider).value ?? <Script>[])
|
||||
if (script.url != null) script,
|
||||
],
|
||||
);
|
||||
|
||||
void _refreshResources() {
|
||||
if (_resources == null) {
|
||||
return;
|
||||
}
|
||||
updateWhenRouteSettled(() => setState(() => _resources = _readResources()));
|
||||
}
|
||||
|
||||
Future<void> _updateAll() async {
|
||||
final failures = await ref
|
||||
.read(resourcesActionProvider.notifier)
|
||||
.updateAll();
|
||||
dialogs.showFailures(failures);
|
||||
}
|
||||
|
||||
Future<void> _updateInterval() async {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final updateInterval = ref.read(patchClashConfigProvider).geoUpdateInterval;
|
||||
final value = await dialogs.showCommonDialog<String>(
|
||||
child: InputDialog(
|
||||
title: appLocalizations.geoAutoUpdateInterval,
|
||||
title: appLocalizations.resourceUpdateInterval,
|
||||
value: updateInterval.toString(),
|
||||
suffixText: appLocalizations.hours,
|
||||
keyboardType: TextInputType.number,
|
||||
validator: (value) {
|
||||
if (value == null || value.isEmpty) {
|
||||
return appLocalizations.emptyTip(
|
||||
appLocalizations.geoAutoUpdateInterval,
|
||||
appLocalizations.resourceUpdateInterval,
|
||||
);
|
||||
}
|
||||
final interval = int.tryParse(value);
|
||||
if (interval == null) {
|
||||
return appLocalizations.numberTip(
|
||||
appLocalizations.geoAutoUpdateInterval,
|
||||
appLocalizations.resourceUpdateInterval,
|
||||
);
|
||||
}
|
||||
if (interval <= 0) {
|
||||
return appLocalizations.geoAutoUpdateIntervalTip;
|
||||
return appLocalizations.resourceUpdateIntervalTip;
|
||||
}
|
||||
return null;
|
||||
},
|
||||
@@ -57,126 +148,326 @@ class ResourcesView extends ConsumerWidget {
|
||||
.update((state) => state.copyWith(geoUpdateInterval: interval));
|
||||
}
|
||||
|
||||
void _updateAutoUpdate(bool value) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.update((state) => state.copyWith(geoAutoUpdate: value));
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
const geoResources = GeoResource.values;
|
||||
Widget build(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final geoSetting = ref.watch(
|
||||
patchClashConfigProvider.select(
|
||||
(state) => (
|
||||
autoUpdate: state.geoAutoUpdate,
|
||||
updateInterval: state.geoUpdateInterval,
|
||||
final autoUpdate = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.geoAutoUpdate),
|
||||
);
|
||||
final resources = _resources;
|
||||
final (:iconSets, :ruleProviders, :scripts) = resources ?? _noResources;
|
||||
final resourceKeys = [
|
||||
for (final geoResource in GeoResource.values) geoResource.updatingKey,
|
||||
for (final iconSet in iconSets) iconSet.updatingKey,
|
||||
for (final provider in ruleProviders) provider.updatingKey,
|
||||
for (final script in scripts) script.updatingKey,
|
||||
];
|
||||
final isUpdating = ref.watch(
|
||||
updatingKeysProvider.select((keys) => resourceKeys.any(keys.contains)),
|
||||
);
|
||||
|
||||
return CommonScaffold(
|
||||
title: appLocalizations.resources,
|
||||
iconActions: [
|
||||
IconButtonData(
|
||||
glyph: AppGlyphs.sync,
|
||||
tooltip: appLocalizations.update,
|
||||
isLoading: isUpdating,
|
||||
onPressed: _updateAll,
|
||||
),
|
||||
],
|
||||
menuItems: [
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.toggle,
|
||||
label: appLocalizations.autoUpdate,
|
||||
subItems: [
|
||||
CommonPopupMenuItem(
|
||||
label: appLocalizations.turnOn,
|
||||
checked: autoUpdate,
|
||||
onPressed: () => _updateAutoUpdate(true),
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
label: appLocalizations.turnOff,
|
||||
checked: !autoUpdate,
|
||||
onPressed: () => _updateAutoUpdate(false),
|
||||
),
|
||||
],
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.clock,
|
||||
label: appLocalizations.resourceUpdateInterval,
|
||||
onPressed: _updateInterval,
|
||||
),
|
||||
],
|
||||
body: NullStatusSwitcher(
|
||||
isEmpty: resources == null,
|
||||
holdsArrival: true,
|
||||
nullStatus: const NullStatus(illustration: NullStatusIllustration.data),
|
||||
child: ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
).copyWith(top: context.appBarInset, bottom: 16),
|
||||
children: [
|
||||
generateSectionV3(
|
||||
title: appLocalizations.geoResources,
|
||||
items: [
|
||||
for (final geoResource in GeoResource.values)
|
||||
_GeoResourceItem(geoResource, preloaded: _fileInfos),
|
||||
],
|
||||
),
|
||||
generateSectionV3(
|
||||
title: appLocalizations.iconSets,
|
||||
items: [
|
||||
for (final iconSet in iconSets)
|
||||
_IconSetItem(iconSet, key: ValueKey(iconSet.id)),
|
||||
],
|
||||
),
|
||||
generateSectionV3(
|
||||
title: appLocalizations.ruleProviders,
|
||||
items: [
|
||||
for (final provider in ruleProviders)
|
||||
_RuleProviderItem(
|
||||
provider,
|
||||
preloaded: _fileInfos,
|
||||
key: ValueKey(provider.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
generateSectionV3(
|
||||
title: appLocalizations.script,
|
||||
items: [
|
||||
for (final script in scripts)
|
||||
_ScriptItem(
|
||||
script,
|
||||
preloaded: _fileInfos,
|
||||
key: ValueKey(script.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void updateAutoUpdate(bool value) {
|
||||
ref
|
||||
.read(patchClashConfigProvider.notifier)
|
||||
.update((state) => state.copyWith(geoAutoUpdate: value));
|
||||
class _ResourceItem extends StatelessWidget {
|
||||
final String title;
|
||||
final String? url;
|
||||
final String? chip;
|
||||
final DateTime? lastUpdateTime;
|
||||
final bool pending;
|
||||
final bool isUpdating;
|
||||
final Future<void> Function() onSync;
|
||||
final VoidCallback? onEdit;
|
||||
|
||||
const _ResourceItem({
|
||||
required this.title,
|
||||
required this.url,
|
||||
required this.chip,
|
||||
required this.lastUpdateTime,
|
||||
this.pending = false,
|
||||
required this.isUpdating,
|
||||
required this.onSync,
|
||||
this.onEdit,
|
||||
});
|
||||
|
||||
Future<void> _copyLink(BuildContext context, String url) async {
|
||||
await Clipboard.setData(ClipboardData(text: url));
|
||||
if (context.mounted) {
|
||||
context.showNotifier(
|
||||
context.appLocalizations.copySuccess,
|
||||
level: MessageLevel.success,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return CommonScaffold(
|
||||
title: context.appLocalizations.resources,
|
||||
body: ListView(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
).copyWith(top: context.appBarInset, bottom: 16),
|
||||
children: [
|
||||
generateSectionV3(
|
||||
title: appLocalizations.geoOptions,
|
||||
items: [
|
||||
DecorationListItem(
|
||||
minVerticalPadding: 8,
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 8),
|
||||
title: Text(appLocalizations.geoAutoUpdate),
|
||||
onPressed: () {
|
||||
updateAutoUpdate(!geoSetting.autoUpdate);
|
||||
},
|
||||
trailing: Switch(
|
||||
value: geoSetting.autoUpdate,
|
||||
onChanged: updateAutoUpdate,
|
||||
),
|
||||
List<CommonPopupMenuItem> _menuItems(BuildContext context) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final url = this.url;
|
||||
return [
|
||||
if (!isUpdating)
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.sync,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: () {
|
||||
globalState.safeRun<void>(onSync, title: title, silence: false);
|
||||
},
|
||||
),
|
||||
if (onEdit != null)
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.edit,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: onEdit,
|
||||
),
|
||||
if (url != null)
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.copy,
|
||||
label: appLocalizations.copyLink,
|
||||
onPressed: () {
|
||||
_copyLink(context, url);
|
||||
},
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
Widget _buildTrailing(BuildContext context) {
|
||||
final chip = this.chip;
|
||||
final colorScheme = context.colorScheme;
|
||||
return AnimatedSwitcher(
|
||||
duration: context.motionDuration(commonDuration),
|
||||
layoutBuilder: (currentChild, previousChildren) => Stack(
|
||||
alignment: AlignmentDirectional.centerEnd,
|
||||
children: [...previousChildren, ?currentChild],
|
||||
),
|
||||
child: isUpdating
|
||||
? const SizedBox.square(
|
||||
key: ValueKey('updating'),
|
||||
dimension: 24,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(2),
|
||||
child: CommonCircleLoading(),
|
||||
),
|
||||
DecorationListItem(
|
||||
minVerticalPadding: 8,
|
||||
title: Text(appLocalizations.geoAutoUpdateInterval),
|
||||
onPressed: () {
|
||||
_updateInterval(context, ref, geoSetting.updateInterval);
|
||||
},
|
||||
trailing: Text(
|
||||
appLocalizations.hoursCount(geoSetting.updateInterval),
|
||||
style: context.textTheme.bodyMedium?.toSoftBold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
generateSectionV3(
|
||||
title: appLocalizations.geoResources,
|
||||
items: [
|
||||
for (final geoResource in geoResources)
|
||||
_GeoResourceListItem(geoResource),
|
||||
],
|
||||
),
|
||||
],
|
||||
)
|
||||
: chip == null
|
||||
? const SizedBox.shrink()
|
||||
: TonalChip(
|
||||
key: const ValueKey('chip'),
|
||||
label: chip,
|
||||
color: colorScheme.secondaryContainer,
|
||||
foregroundColor: colorScheme.onSecondaryContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final subtitleStyle = context.listCaptionStyle;
|
||||
return ContextMenuRegion(
|
||||
menuItems: _menuItems(context),
|
||||
child: DecorationListItem(
|
||||
onPressed: onEdit,
|
||||
title: Text(title, maxLines: 1, overflow: TextOverflow.ellipsis),
|
||||
subtitle: lastUpdateTime != null || pending
|
||||
? LastUpdateTimeText(
|
||||
lastUpdateDate: lastUpdateTime,
|
||||
style: subtitleStyle,
|
||||
)
|
||||
: Text(context.appLocalizations.unknown, style: subtitleStyle),
|
||||
trailing: _buildTrailing(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _GeoResourceListItem extends ConsumerStatefulWidget {
|
||||
final GeoResource type;
|
||||
/// An update can rewrite the file without changing [source], so the file is
|
||||
/// read again once [updatingKey] settles.
|
||||
class _FileInfoBuilder extends ConsumerStatefulWidget {
|
||||
final Object source;
|
||||
final String updatingKey;
|
||||
final Map<String, FileInfo?> preloaded;
|
||||
final Future<FileInfo?> Function() load;
|
||||
final Widget Function(BuildContext context, FileInfo? fileInfo, bool pending)
|
||||
builder;
|
||||
|
||||
const _GeoResourceListItem(this.type);
|
||||
const _FileInfoBuilder({
|
||||
required this.source,
|
||||
required this.updatingKey,
|
||||
required this.preloaded,
|
||||
required this.load,
|
||||
required this.builder,
|
||||
});
|
||||
|
||||
@override
|
||||
ConsumerState<_GeoResourceListItem> createState() =>
|
||||
_GeoResourceListItemState();
|
||||
ConsumerState<_FileInfoBuilder> createState() => _FileInfoBuilderState();
|
||||
}
|
||||
|
||||
class _GeoResourceListItemState extends ConsumerState<_GeoResourceListItem> {
|
||||
late Future<FileInfo?> _fileInfoFuture;
|
||||
|
||||
String get fileName {
|
||||
return switch (widget.type) {
|
||||
GeoResource.MMDB => MMDB,
|
||||
GeoResource.ASN => ASN,
|
||||
GeoResource.GEOIP => GEOIP,
|
||||
GeoResource.GEOSITE => GEOSITE,
|
||||
};
|
||||
}
|
||||
class _FileInfoBuilderState extends ConsumerState<_FileInfoBuilder> {
|
||||
Future<FileInfo?>? _fileInfoFuture;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fileInfoFuture = _getGeoFileInfo(fileName);
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant _GeoResourceListItem oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.type != widget.type) {
|
||||
_fileInfoFuture = _getGeoFileInfo(fileName);
|
||||
if (!widget.preloaded.containsKey(widget.updatingKey)) {
|
||||
_fileInfoFuture = widget.load();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _updateUrl(String url) async {
|
||||
final newUrl = await dialogs.showCommonDialog<String>(
|
||||
child: UpdateGeoUrlFormDialog(
|
||||
title: widget.type.name,
|
||||
url: url,
|
||||
defaultValue: defaultGeoXUrl[widget.type],
|
||||
@override
|
||||
void didUpdateWidget(covariant _FileInfoBuilder oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.source != widget.source) {
|
||||
_fileInfoFuture = widget.load();
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
ref.listen(isUpdatingProvider(widget.updatingKey), (previous, next) {
|
||||
if (previous == true && !next) {
|
||||
setState(() {
|
||||
_fileInfoFuture = widget.load();
|
||||
});
|
||||
}
|
||||
});
|
||||
return FutureBuilder<FileInfo?>(
|
||||
future: _fileInfoFuture,
|
||||
initialData: widget.preloaded[widget.updatingKey],
|
||||
builder: (context, snapshot) => widget.builder(
|
||||
context,
|
||||
snapshot.data,
|
||||
_fileInfoFuture != null &&
|
||||
snapshot.connectionState != ConnectionState.done &&
|
||||
!snapshot.hasData,
|
||||
),
|
||||
);
|
||||
if (newUrl != null && newUrl != url && mounted) {
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileInfo?> _geoFileInfo(GeoResource type) async {
|
||||
final fileName = switch (type) {
|
||||
GeoResource.MMDB => MMDB,
|
||||
GeoResource.ASN => ASN,
|
||||
GeoResource.GEOIP => GEOIP,
|
||||
GeoResource.GEOSITE => GEOSITE,
|
||||
};
|
||||
final homePath = await appPath.homeDirPath;
|
||||
return File(join(homePath, fileName)).getFileInfo();
|
||||
}
|
||||
|
||||
class _GeoResourceItem extends ConsumerWidget {
|
||||
final GeoResource type;
|
||||
final Map<String, FileInfo?> preloaded;
|
||||
|
||||
const _GeoResourceItem(this.type, {required this.preloaded});
|
||||
|
||||
Future<void> _updateUrl(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String url,
|
||||
) async {
|
||||
final newUrl = await dialogs.showCommonDialog<String>(
|
||||
child: UpdateGeoUrlFormDialog(
|
||||
title: type.name,
|
||||
url: url,
|
||||
defaultValue: defaultGeoXUrl[type],
|
||||
),
|
||||
);
|
||||
if (newUrl != null && newUrl != url && context.mounted) {
|
||||
try {
|
||||
ref
|
||||
.read(geoResourceActionProvider.notifier)
|
||||
.updateGeoResourceUrl(widget.type, newUrl);
|
||||
.updateGeoResourceUrl(type, newUrl);
|
||||
} catch (e) {
|
||||
unawaited(
|
||||
dialogs.showMessage(
|
||||
title: widget.type.name,
|
||||
title: type.name,
|
||||
message: TextSpan(text: e.toString()),
|
||||
),
|
||||
);
|
||||
@@ -184,114 +475,104 @@ class _GeoResourceListItemState extends ConsumerState<_GeoResourceListItem> {
|
||||
}
|
||||
}
|
||||
|
||||
Future<FileInfo?> _getGeoFileInfo(String fileName) async {
|
||||
final homePath = await appPath.homeDirPath;
|
||||
final file = File(join(homePath, fileName));
|
||||
return file.getFileInfo();
|
||||
}
|
||||
|
||||
Future<void> _handleUpdateGeoDataItem() {
|
||||
return globalState.safeRun<void>(() async {
|
||||
await ref
|
||||
.read(geoResourceActionProvider.notifier)
|
||||
.updateGeoResource(widget.type);
|
||||
}, silence: false);
|
||||
}
|
||||
|
||||
void _refreshFileInfo() {
|
||||
setState(() {
|
||||
_fileInfoFuture = _getGeoFileInfo(fileName);
|
||||
});
|
||||
}
|
||||
|
||||
List<CommonPopupMenuItem> _menuItems(BuildContext context, String url) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
return [
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.edit,
|
||||
label: appLocalizations.edit,
|
||||
onPressed: () {
|
||||
_updateUrl(url);
|
||||
},
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isUpdating = ref.watch(isUpdatingProvider(type.updatingKey));
|
||||
final url = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.geoXUrl[type]),
|
||||
);
|
||||
return _FileInfoBuilder(
|
||||
source: type,
|
||||
updatingKey: type.updatingKey,
|
||||
preloaded: preloaded,
|
||||
load: () => _geoFileInfo(type),
|
||||
builder: (context, fileInfo, pending) => _ResourceItem(
|
||||
title: type.name,
|
||||
url: url,
|
||||
chip: fileInfo?.size.traffic.show,
|
||||
lastUpdateTime: fileInfo?.lastModified,
|
||||
pending: pending,
|
||||
isUpdating: isUpdating,
|
||||
onSync: () => ref
|
||||
.read(geoResourceActionProvider.notifier)
|
||||
.updateGeoResource(type),
|
||||
onEdit: url == null ? null : () => _updateUrl(context, ref, url),
|
||||
),
|
||||
CommonPopupMenuItem(
|
||||
glyph: AppGlyphs.sync,
|
||||
label: appLocalizations.sync,
|
||||
onPressed: _handleUpdateGeoDataItem,
|
||||
),
|
||||
];
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _IconSetItem extends ConsumerWidget {
|
||||
final IconSet iconSet;
|
||||
|
||||
const _IconSetItem(this.iconSet, {super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final updatingKey = widget.type.updatingKey;
|
||||
ref.listen(isUpdatingProvider(updatingKey), (previous, next) {
|
||||
if (previous == true && !next) {
|
||||
_refreshFileInfo();
|
||||
}
|
||||
});
|
||||
final isUpdating = ref.watch(isUpdatingProvider(updatingKey));
|
||||
final url = ref.watch(
|
||||
patchClashConfigProvider.select((state) => state.geoXUrl[widget.type]),
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return _ResourceItem(
|
||||
title: iconSet.name,
|
||||
url: iconSet.url,
|
||||
chip: iconSet.icons.length.compact,
|
||||
lastUpdateTime: iconSet.lastUpdateTime,
|
||||
isUpdating: ref.watch(isUpdatingProvider(iconSet.updatingKey)),
|
||||
onSync: () =>
|
||||
ref.read(resourcesActionProvider.notifier).updateIconSet(iconSet),
|
||||
);
|
||||
return FutureBuilder<FileInfo?>(
|
||||
future: _fileInfoFuture,
|
||||
builder: (context, snapshot) {
|
||||
final fileInfo = snapshot.data;
|
||||
return DecorationListItem(
|
||||
minVerticalPadding: 8,
|
||||
contentPadding: const EdgeInsets.only(left: 16, right: 0),
|
||||
title: Text(widget.type.name),
|
||||
subtitle: fileInfo == null
|
||||
? null
|
||||
: Padding(
|
||||
padding: const EdgeInsets.only(top: 4, bottom: 2),
|
||||
child: Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
MetaChip(label: fileInfo.size.traffic.show),
|
||||
MetaChip(
|
||||
label:
|
||||
fileInfo.lastModified?.getLastUpdateTimeDesc(
|
||||
context,
|
||||
) ??
|
||||
context.appLocalizations.unknown,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing: url == null
|
||||
? null
|
||||
: SizedBox.square(
|
||||
dimension: kMinInteractiveDimension,
|
||||
child: FadeThroughBox(
|
||||
alignment: Alignment.center,
|
||||
child: isUpdating
|
||||
? const SizedBox.square(
|
||||
key: ValueKey('loading'),
|
||||
dimension: kMinInteractiveDimension,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(12),
|
||||
child: CommonCircleLoading(),
|
||||
),
|
||||
)
|
||||
: CommonPopupBox(
|
||||
key: const ValueKey('menu'),
|
||||
popupBuilder: (_) => CommonPopupMenu(
|
||||
items: _menuItems(context, url),
|
||||
),
|
||||
targetBuilder: (open) {
|
||||
return IconButton(
|
||||
tooltip: context.appLocalizations.more,
|
||||
onPressed: open,
|
||||
icon: const GlyphIcon(AppGlyphs.more),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
class _RuleProviderItem extends ConsumerWidget {
|
||||
final ClashProvider provider;
|
||||
final Map<String, FileInfo?> preloaded;
|
||||
|
||||
const _RuleProviderItem(this.provider, {required this.preloaded, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isUpdating = ref.watch(isUpdatingProvider(provider.updatingKey));
|
||||
return _FileInfoBuilder(
|
||||
source: provider,
|
||||
updatingKey: provider.updatingKey,
|
||||
preloaded: preloaded,
|
||||
load: () => provider.fileInfo,
|
||||
builder: (context, fileInfo, pending) => _ResourceItem(
|
||||
title: provider.label,
|
||||
url: provider.url,
|
||||
chip: fileInfo?.size.traffic.show,
|
||||
lastUpdateTime: fileInfo?.lastModified,
|
||||
pending: pending,
|
||||
isUpdating: isUpdating,
|
||||
onSync: () => ref
|
||||
.read(resourcesActionProvider.notifier)
|
||||
.updateRuleProvider(provider),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ScriptItem extends ConsumerWidget {
|
||||
final Script script;
|
||||
final Map<String, FileInfo?> preloaded;
|
||||
|
||||
const _ScriptItem(this.script, {required this.preloaded, super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final isUpdating = ref.watch(isUpdatingProvider(script.updatingKey));
|
||||
return _FileInfoBuilder(
|
||||
source: script,
|
||||
updatingKey: script.updatingKey,
|
||||
preloaded: preloaded,
|
||||
load: () => script.fileInfo,
|
||||
builder: (context, fileInfo, _) => _ResourceItem(
|
||||
title: script.label,
|
||||
url: script.url,
|
||||
chip: fileInfo?.size.traffic.show,
|
||||
lastUpdateTime: script.lastUpdateTime,
|
||||
isUpdating: isUpdating,
|
||||
onSync: () =>
|
||||
ref.read(resourcesActionProvider.notifier).updateScript(script),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,19 +4,13 @@ import 'package:fl_clash/icons/icons.dart';
|
||||
import 'package:fl_clash/models/models.dart';
|
||||
import 'package:fl_clash/providers/config.dart';
|
||||
import 'package:fl_clash/providers/state.dart';
|
||||
import 'package:fl_clash/views/font_family.dart';
|
||||
import 'package:fl_clash/views/theme_preview.dart';
|
||||
import 'package:fl_clash/widgets/widgets.dart';
|
||||
import 'package:material_ui/material_ui.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:material_color_utilities/hct/hct.dart';
|
||||
|
||||
class FontFamilyItem {
|
||||
final FontFamily fontFamily;
|
||||
final String label;
|
||||
|
||||
const FontFamilyItem({required this.fontFamily, required this.label});
|
||||
}
|
||||
|
||||
class ThemeView extends StatelessWidget {
|
||||
const ThemeView({super.key});
|
||||
|
||||
@@ -41,6 +35,8 @@ class ThemeView extends StatelessWidget {
|
||||
const _TabAnimationItem(),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
const _SidebarBlurItem(),
|
||||
const _FontFamilyItem(),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 16)),
|
||||
const _TextScaleFactorItem(),
|
||||
const SliverToBoxAdapter(child: SizedBox(height: 32)),
|
||||
],
|
||||
@@ -390,7 +386,7 @@ class _PrimaryColorGrid extends StatelessWidget {
|
||||
child: IconButton.filledTonal(
|
||||
tooltip: context.appLocalizations.add,
|
||||
onPressed: onAdd,
|
||||
icon: const GlyphIcon(AppGlyphs.add, fill: 1),
|
||||
icon: const GlyphIcon(AppGlyphs.addCircle, fill: 1),
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -422,6 +418,7 @@ class _PrimaryColorTile extends StatelessWidget {
|
||||
children: [
|
||||
EffectGestureDetector(
|
||||
onLongPress: onRequestRemove,
|
||||
onSecondaryTap: onRequestRemove,
|
||||
child: ColorSchemeBox(
|
||||
isSelected: isSelected,
|
||||
primaryColor: color != null ? Color(color!) : null,
|
||||
@@ -631,6 +628,84 @@ class _SidebarBlurItem extends ConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
class _FontFamilyItem extends ConsumerWidget {
|
||||
const _FontFamilyItem();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final appLocalizations = context.appLocalizations;
|
||||
final colorScheme = context.colorScheme;
|
||||
final textTheme = context.textTheme;
|
||||
final fontFamily = ref.watch(
|
||||
themeSettingProvider.select((state) => state.fontFamily),
|
||||
);
|
||||
final preview = Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 14, 12, 14),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Text(
|
||||
'Aa',
|
||||
style: textTheme.titleMedium?.copyWith(fontFamily: fontFamily),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
fontFamily ?? appLocalizations.defaultText,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontFamily: fontFamily,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
GlyphIcon(
|
||||
AppGlyphs.chevronForward,
|
||||
size: 20,
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
Widget card(VoidCallback onTap) => Material(
|
||||
color: colorScheme.surfaceContainerLow,
|
||||
shape: AppShape.lg,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(customBorder: AppShape.lg, onTap: onTap, child: preview),
|
||||
);
|
||||
return SliverToBoxAdapter(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
InfoHeader(
|
||||
info: Info(
|
||||
label: appLocalizations.fontFamily,
|
||||
glyph: AppGlyphs.font,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 0),
|
||||
child: context.isMobileView
|
||||
? OpenContainer<void>(
|
||||
tappable: false,
|
||||
closedShape: AppShape.lg,
|
||||
closedColor: colorScheme.surfaceContainerLow,
|
||||
closedBuilder: (_, open) => card(open),
|
||||
openBuilder: (_, _) => const FontFamilyView(),
|
||||
)
|
||||
: card(
|
||||
() => showSheet<void>(
|
||||
context: context,
|
||||
builder: (_) => const FontFamilyView(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _TextScaleFactorItem extends ConsumerStatefulWidget {
|
||||
const _TextScaleFactorItem();
|
||||
|
||||
@@ -658,9 +733,9 @@ class _TextScaleFactorItemState extends ConsumerState<_TextScaleFactorItem> {
|
||||
final textScale = ref.watch(
|
||||
themeSettingProvider.select((state) => state.textScale),
|
||||
);
|
||||
final systemScale = defaultTextScaleFactor
|
||||
.clamp(minTextScale, maxTextScale)
|
||||
.toDouble();
|
||||
final systemScale = systemTextScaleOf(
|
||||
context,
|
||||
).clamp(minTextScale, maxTextScale).toDouble();
|
||||
final scale = _draft ?? (textScale.enable ? textScale.scale : systemScale);
|
||||
final percent = '${(scale * 100).round()}%';
|
||||
final divisions =
|
||||
|
||||