From 11defeb0102ef74d9be59660ad114f372fe09edf Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 3 Feb 2024 12:36:27 +0330 Subject: [PATCH] Add cloudflare warp options --- assets/translations/strings_en.i18n.json | 22 +- assets/translations/strings_es.i18n.json | 22 +- assets/translations/strings_fa.i18n.json | 22 +- assets/translations/strings_ru.i18n.json | 22 +- assets/translations/strings_tr.i18n.json | 22 +- assets/translations/strings_zh-CN.i18n.json | 22 +- lib/core/model/constants.dart | 4 + lib/core/model/range.dart | 24 ++- .../data/config_option_repository.dart | 6 + .../model/config_option_entity.dart | 20 +- .../model/config_option_patch.dart | 6 + .../notifier/warp_option_notifier.dart | 26 +++ .../overview/config_options_page.dart | 14 +- .../overview/warp_options_widgets.dart | 199 ++++++++++++++++++ lib/singbox/model/singbox_config_enum.dart | 10 + lib/singbox/model/singbox_config_option.dart | 7 + 16 files changed, 426 insertions(+), 22 deletions(-) create mode 100644 lib/features/config_option/notifier/warp_option_notifier.dart create mode 100644 lib/features/config_option/overview/warp_options_widgets.dart diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index ae723f96..669d4e97 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -11,7 +11,10 @@ }, "sort": "Sort", "sortBy": "Sort by", - "addToClipboard": "Add to clipboard" + "addToClipboard": "Add to clipboard", + "notSet": "Not Set", + "agree": "Agree", + "decline": "Decline" }, "intro": { "termsAndPolicyCaution(rich)": "by continuing you agree with ${tap(@:about.termsAndConditions)}", @@ -204,8 +207,13 @@ "mux": "Multiplexer", "outbound": "Outbound Options", "tlsTricks": "TLS Tricks", + "warp": "WARP Options", "misc": "Misc Options" }, + "warpConsent": { + "title": "Cloudflare WARP Consent", + "description(rich)": "Cloudflare WARP is a free WireGuard VPN provider. By enabling this option you are agreeing to the Cloudflare WARP's ${tos(Terms of Service)} and ${privacy(Privacy Policy)}." + }, "pageTitle": "Config Options", "logLevel": "Log Level", "resolveDestination": "Resolve Destination", @@ -243,7 +251,17 @@ "tlsPaddingSize": "TLS Padding", "enableMux": "Enable Mux", "muxProtocol": "Mux Protocol", - "muxMaxStreams": "Max Concurrent Streams" + "muxMaxStreams": "Max Concurrent Streams", + "enableWarp": "Enable WARP", + "warpDetourMode": "Detour Mode", + "warpDetourModes": { + "inbound": "Detour WARP through proxies", + "outbound": "Detour proxies through WARP" + }, + "warpLicenseKey": "License Key", + "warpCleanIp": "Clean IP", + "warpPort": "Port", + "warpNoise": "Noise" }, "geoAssets": { "pageTitle": "Routing Assets", diff --git a/assets/translations/strings_es.i18n.json b/assets/translations/strings_es.i18n.json index 0fb923cb..b15358cf 100644 --- a/assets/translations/strings_es.i18n.json +++ b/assets/translations/strings_es.i18n.json @@ -11,7 +11,10 @@ }, "sort": "Clasificar", "sortBy": "Ordenar por", - "addToClipboard": "Añadir al portapapeles" + "addToClipboard": "Añadir al portapapeles", + "notSet": "No establecido", + "agree": "Aceptar", + "decline": "Rechazar" }, "home": { "emptyProfilesMsg": "Comience agregando un perfil de suscripción", @@ -211,6 +214,7 @@ "mux": "Multiplexer", "outbound": "Opciones de salida", "tlsTricks": "Trucos TLS", + "warp": "WARP Options", "misc": "Opciones varias" }, "pageTitle": "Opciones de configuración", @@ -237,7 +241,21 @@ "tlsPaddingSize": "Relleno TLS", "enableMux": "Enable Mux", "muxProtocol": "Mux Protocol", - "muxMaxStreams": "Max Concurrent Streams" + "muxMaxStreams": "Max Concurrent Streams", + "enableWarp": "Enable WARP", + "warpDetourMode": "Detour Mode", + "warpDetourModes": { + "inbound": "Detour WARP through proxies", + "outbound": "Detour proxies through WARP" + }, + "warpLicenseKey": "License Key", + "warpCleanIp": "Clean IP", + "warpPort": "Port", + "warpNoise": "Noise", + "warpConsent": { + "title": "Consentimiento WARP de Cloudflare", + "description(rich)": "Cloudflare WARP es un proveedor de VPN WireGuard gratuito. Al habilitar esta opción, acepta los ${tos(Términos de servicio)} y ${privacy(Política de privacidad)} de Cloudflare WARP." + } }, "geoAssets": { "successMsg": "Activo actualizado correctamente", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index dd81bf08..024da3bc 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -11,7 +11,10 @@ }, "sort": "مرتب‌سازی", "sortBy": "مرتب‌سازی براساس", - "addToClipboard": "به کلیپ بورد اضافه کنید" + "addToClipboard": "به کلیپ بورد اضافه کنید", + "notSet": "تنظیم نشده", + "agree": "موافق", + "decline": "کاهش می یابد" }, "intro": { "termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت میکنید", @@ -204,6 +207,7 @@ "mux": "Multiplexer", "outbound": "Outbound Options", "tlsTricks": "TLS Tricks", + "warp": "WARP Options", "misc": "تنظیمات متفرقه" }, "pageTitle": "تنظیمات کانفیگ", @@ -243,7 +247,21 @@ "tlsPaddingSize": "TLS Padding", "enableMux": "Enable Mux", "muxProtocol": "Mux Protocol", - "muxMaxStreams": "Max Concurrent Streams" + "muxMaxStreams": "Max Concurrent Streams", + "enableWarp": "Enable WARP", + "warpDetourMode": "Detour Mode", + "warpDetourModes": { + "inbound": "Detour WARP through proxies", + "outbound": "Detour proxies through WARP" + }, + "warpLicenseKey": "License Key", + "warpCleanIp": "Clean IP", + "warpPort": "Port", + "warpNoise": "Noise", + "warpConsent": { + "title": "رضایت Cloudflare WARP", + "description(rich)": "Cloudflare WARP یک ارائه دهنده رایگان WireGuard VPN است. با فعال کردن این گزینه، با ${tos(شرایط خدمات)} و ${privacy(خط‌مشی رازداری)} Cloudflare WARP موافقت می‌کنید." + } }, "geoAssets": { "pageTitle": "فایل‌های مسیریابی", diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index 12a4b278..ce4f1eab 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -11,7 +11,10 @@ }, "sort": "Сортировка", "sortBy": "Сортировка", - "addToClipboard": "Копировать в буфер обмена" + "addToClipboard": "Копировать в буфер обмена", + "notSet": "Не задано", + "agree": "Соглашаться", + "decline": "Отклонить" }, "intro": { "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", @@ -204,6 +207,7 @@ "mux": "Multiplexer", "outbound": "Outbound Options", "tlsTricks": "TLS Tricks", + "warp": "WARP Options", "misc": "Разные параметры" }, "pageTitle": "Параметры конфигурации", @@ -243,7 +247,21 @@ "tlsPaddingSize": "TLS Padding", "enableMux": "Enable Mux", "muxProtocol": "Mux Protocol", - "muxMaxStreams": "Max Concurrent Streams" + "muxMaxStreams": "Max Concurrent Streams", + "enableWarp": "Enable WARP", + "warpDetourMode": "Detour Mode", + "warpDetourModes": { + "inbound": "Detour WARP through proxies", + "outbound": "Detour proxies through WARP" + }, + "warpLicenseKey": "License Key", + "warpCleanIp": "Clean IP", + "warpPort": "Port", + "warpNoise": "Noise", + "warpConsent": { + "title": "Согласие Cloudflare WARP", + "description(rich)": "Cloudflare WARP — бесплатный провайдер WireGuard VPN. Включая эту опцию, вы соглашаетесь с ${tos(Условиями обслуживания)} и ${privacy(Политикой конфиденциальности)} Cloudflare WARP." + } }, "geoAssets": { "pageTitle": "Активы маршрутизации", diff --git a/assets/translations/strings_tr.i18n.json b/assets/translations/strings_tr.i18n.json index e1aa5e69..5f4148b9 100644 --- a/assets/translations/strings_tr.i18n.json +++ b/assets/translations/strings_tr.i18n.json @@ -11,7 +11,10 @@ }, "sort": "Sırala", "sortBy": "Sırala", - "addToClipboard": "Panoya ekle" + "addToClipboard": "Panoya ekle", + "notSet": "Ayarlanmadı", + "agree": "Kabul etmek", + "decline": "Reddetmek" }, "intro": { "termsAndPolicyCaution(rich)": "devam ederek ${tap(@:about.termsAndConditions)} kabul etmiş olursunuz", @@ -204,6 +207,7 @@ "mux": "Multiplexer", "outbound": "Outbound Options", "tlsTricks": "TLS Tricks", + "warp": "WARP Options", "misc": "Çeşitli Seçenekler" }, "pageTitle": "Yapılandırma Seçenekleri", @@ -243,7 +247,21 @@ "tlsPaddingSize": "TLS Padding", "enableMux": "Enable Mux", "muxProtocol": "Mux Protocol", - "muxMaxStreams": "Max Concurrent Streams" + "muxMaxStreams": "Max Concurrent Streams", + "enableWarp": "Enable WARP", + "warpDetourMode": "Detour Mode", + "warpDetourModes": { + "inbound": "Detour WARP through proxies", + "outbound": "Detour proxies through WARP" + }, + "warpLicenseKey": "License Key", + "warpCleanIp": "Clean IP", + "warpPort": "Port", + "warpNoise": "Noise", + "warpConsent": { + "title": "Cloudflare WARP Onayı", + "description(rich)": "Cloudflare WARP ücretsiz bir WireGuard VPN sağlayıcısıdır. Bu seçeneği etkinleştirerek Cloudflare WARP'ın ${tos(Hizmet Şartları)} ve ${privacy(Gizlilik Politikası)}'nı kabul etmiş olursunuz." + } }, "geoAssets": { "pageTitle": "Varlıkları Yönlendirme", diff --git a/assets/translations/strings_zh-CN.i18n.json b/assets/translations/strings_zh-CN.i18n.json index fc7a4f77..3f4e3b25 100644 --- a/assets/translations/strings_zh-CN.i18n.json +++ b/assets/translations/strings_zh-CN.i18n.json @@ -11,7 +11,10 @@ }, "sort": "排序", "sortBy": "排序方式", - "addToClipboard": "添加到剪贴板" + "addToClipboard": "添加到剪贴板", + "notSet": "没有设置", + "agree": "同意", + "decline": "衰退" }, "intro": { "termsAndPolicyCaution(rich)": "继续即表示您同意 ${tap(@:about.termsAndConditions)}", @@ -204,6 +207,7 @@ "mux": "Multiplexer", "outbound": "出站选项", "tlsTricks": "TLS Tricks", + "warp": "WARP Options", "misc": "其它选项" }, "pageTitle": "配置选项", @@ -243,7 +247,21 @@ "tlsPaddingSize": "TLS 填充", "enableMux": "Enable Mux", "muxProtocol": "Mux Protocol", - "muxMaxStreams": "Max Concurrent Streams" + "muxMaxStreams": "Max Concurrent Streams", + "enableWarp": "Enable WARP", + "warpDetourMode": "Detour Mode", + "warpDetourModes": { + "inbound": "Detour WARP through proxies", + "outbound": "Detour proxies through WARP" + }, + "warpLicenseKey": "License Key", + "warpCleanIp": "Clean IP", + "warpPort": "Port", + "warpNoise": "Noise", + "warpConsent": { + "title": "Cloudflare WARP 同意", + "description(rich)": "Cloudflare WARP 是免费的 WireGuard VPN 提供商。启用此选项即表示您同意 Cloudflare WARP 的 ${tos(服务条款)} 和 ${privacy(隐私政策)}" + } }, "geoAssets": { "pageTitle": "路由资源文件", diff --git a/lib/core/model/constants.dart b/lib/core/model/constants.dart index 8882ab83..fcb7bddf 100644 --- a/lib/core/model/constants.dart +++ b/lib/core/model/constants.dart @@ -10,4 +10,8 @@ abstract class Constants { static const telegramChannelUrl = "https://t.me/hiddify"; static const privacyPolicyUrl = "https://hiddify.com/privacy-policy/"; static const termsAndConditionsUrl = "https://hiddify.com/terms/"; + static const cfWarpPrivacyPolicy = + "https://www.cloudflare.com/application/privacypolicy/"; + static const cfWarpTermsOfService = + "https://www.cloudflare.com/application/terms/"; } diff --git a/lib/core/model/range.dart b/lib/core/model/range.dart index fba247b3..d644da6f 100644 --- a/lib/core/model/range.dart +++ b/lib/core/model/range.dart @@ -1,4 +1,6 @@ +import 'package:dartx/dartx.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; part 'range.freezed.dart'; @@ -7,14 +9,21 @@ class RangeWithOptionalCeil with _$RangeWithOptionalCeil { const RangeWithOptionalCeil._(); const factory RangeWithOptionalCeil({ - required int min, + int? min, int? max, }) = _RangeWithOptionalCeil; - String format() => "$min${max != null ? "-$max" : ""}"; + String format() => [min, max].whereNotNull().join("-"); + String present(TranslationsEn t) => + format().isEmpty ? t.general.notSet : format(); - factory RangeWithOptionalCeil.fromString(String input) => + factory RangeWithOptionalCeil._fromString( + String input, { + bool allowEmpty = true, + }) => switch (input.split("-")) { + [final String val] when val.isEmpty && allowEmpty => + const RangeWithOptionalCeil(), [final String min] => RangeWithOptionalCeil(min: int.parse(min)), [final String min, final String max] => RangeWithOptionalCeil( min: int.parse(min), @@ -23,9 +32,12 @@ class RangeWithOptionalCeil with _$RangeWithOptionalCeil { _ => throw Exception("Invalid range: $input"), }; - static RangeWithOptionalCeil? tryParse(String input) { + static RangeWithOptionalCeil? tryParse( + String input, { + bool allowEmpty = false, + }) { try { - return RangeWithOptionalCeil.fromString(input); + return RangeWithOptionalCeil._fromString(input); } catch (_) { return null; } @@ -38,7 +50,7 @@ class RangeWithOptionalCeilJsonConverter @override RangeWithOptionalCeil fromJson(String json) => - RangeWithOptionalCeil.fromString(json); + RangeWithOptionalCeil._fromString(json); @override String toJson(RangeWithOptionalCeil object) => object.format(); diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index 7d8f985d..82faa16f 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -117,6 +117,12 @@ class ConfigOptionRepositoryImpl muxPadding: persisted.muxPadding, muxMaxStreams: persisted.muxMaxStreams, muxProtocol: persisted.muxProtocol, + enableWarp: persisted.enableWarp, + warpDetourMode: persisted.warpDetourMode, + warpLicenseKey: persisted.warpLicenseKey, + warpCleanIp: persisted.warpCleanIp, + warpPort: persisted.warpPort, + warpNoise: persisted.warpNoise, geoipPath: geoAssetPathResolver.relativePath( geoAssets.geoip.providerName, geoAssets.geoip.fileName, diff --git a/lib/features/config_option/model/config_option_entity.dart b/lib/features/config_option/model/config_option_entity.dart index 4412f07e..676f575a 100644 --- a/lib/features/config_option/model/config_option_entity.dart +++ b/lib/features/config_option/model/config_option_entity.dart @@ -57,6 +57,14 @@ class ConfigOptionEntity with _$ConfigOptionEntity { @Default(false) bool muxPadding, @Default(8) int muxMaxStreams, @Default(MuxProtocol.h2mux) MuxProtocol muxProtocol, + @Default(false) bool enableWarp, + @Default(WarpDetourMode.outbound) WarpDetourMode warpDetourMode, + @Default("") String warpLicenseKey, + @Default("auto") String warpCleanIp, + @Default(0) int warpPort, + @RangeWithOptionalCeilJsonConverter() + @Default(RangeWithOptionalCeil()) + RangeWithOptionalCeil warpNoise, }) = _ConfigOptionEntity; static ConfigOptionEntity initial = ConfigOptionEntity( @@ -67,7 +75,11 @@ class ConfigOptionEntity with _$ConfigOptionEntity { if (PlatformUtils.isDesktop && serviceMode == ServiceMode.tun) { return true; } - if (enableTlsFragment || enableTlsMixedSniCase || enableTlsPadding||enableMux) { + if (enableTlsFragment || + enableTlsMixedSniCase || + enableTlsPadding || + enableMux || + enableWarp) { return true; } @@ -117,6 +129,12 @@ class ConfigOptionEntity with _$ConfigOptionEntity { muxPadding: patch.muxPadding ?? muxPadding, muxMaxStreams: patch.muxMaxStreams ?? muxMaxStreams, muxProtocol: patch.muxProtocol ?? muxProtocol, + enableWarp: patch.enableWarp ?? enableWarp, + warpDetourMode: patch.warpDetourMode ?? warpDetourMode, + warpLicenseKey: patch.warpLicenseKey ?? warpLicenseKey, + warpCleanIp: patch.warpCleanIp ?? warpCleanIp, + warpPort: patch.warpPort ?? warpPort, + warpNoise: patch.warpNoise ?? warpNoise, ); } diff --git a/lib/features/config_option/model/config_option_patch.dart b/lib/features/config_option/model/config_option_patch.dart index 501117a4..b9558a3f 100644 --- a/lib/features/config_option/model/config_option_patch.dart +++ b/lib/features/config_option/model/config_option_patch.dart @@ -47,6 +47,12 @@ class ConfigOptionPatch with _$ConfigOptionPatch { bool? muxPadding, int? muxMaxStreams, MuxProtocol? muxProtocol, + bool? enableWarp, + WarpDetourMode? warpDetourMode, + String? warpLicenseKey, + String? warpCleanIp, + int? warpPort, + @RangeWithOptionalCeilJsonConverter() RangeWithOptionalCeil? warpNoise, }) = _ConfigOptionPatch; factory ConfigOptionPatch.fromJson(Map json) => diff --git a/lib/features/config_option/notifier/warp_option_notifier.dart b/lib/features/config_option/notifier/warp_option_notifier.dart new file mode 100644 index 00000000..44e21953 --- /dev/null +++ b/lib/features/config_option/notifier/warp_option_notifier.dart @@ -0,0 +1,26 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'warp_option_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class WarpOptionNotifier extends _$WarpOptionNotifier { + @override + bool build() { + return ref + .read(sharedPreferencesProvider) + .requireValue + .getBool(warpConsentGiven) ?? + false; + } + + Future agree() async { + await ref + .read(sharedPreferencesProvider) + .requireValue + .setBool(warpConsentGiven, true); + state = true; + } + + static const warpConsentGiven = "warp_consent_given"; +} diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index ad8bb52a..c2df00cd 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -9,6 +9,7 @@ import 'package:hiddify/core/widget/tip_card.dart'; import 'package:hiddify/features/config_option/model/config_option_entity.dart'; import 'package:hiddify/features/config_option/model/config_option_patch.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart'; import 'package:hiddify/features/log/model/log_level.dart'; import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; @@ -319,7 +320,7 @@ class ConfigOptionsPage extends HookConsumerWidget { ), ListTile( title: Text(t.settings.config.tlsFragmentSize), - subtitle: Text(options.tlsFragmentSize.format()), + subtitle: Text(options.tlsFragmentSize.present(t)), onTap: () async { final range = await SettingsInputDialog( title: t.settings.config.tlsFragmentSize, @@ -336,7 +337,7 @@ class ConfigOptionsPage extends HookConsumerWidget { ), ListTile( title: Text(t.settings.config.tlsFragmentSleep), - subtitle: Text(options.tlsFragmentSleep.format()), + subtitle: Text(options.tlsFragmentSleep.present(t)), onTap: () async { final range = await SettingsInputDialog( title: t.settings.config.tlsFragmentSleep, @@ -368,7 +369,7 @@ class ConfigOptionsPage extends HookConsumerWidget { ), ListTile( title: Text(t.settings.config.tlsPaddingSize), - subtitle: Text(options.tlsPaddingSize.format()), + subtitle: Text(options.tlsPaddingSize.present(t)), onTap: () async { final range = await SettingsInputDialog( title: t.settings.config.tlsPaddingSize, @@ -384,6 +385,13 @@ class ConfigOptionsPage extends HookConsumerWidget { }, ), const SettingsDivider(), + SettingsSection(experimental(t.settings.config.section.warp)), + WarpOptionsTiles( + options: options, + defaultOptions: defaultOptions, + onChange: changeOption, + ), + const SettingsDivider(), SettingsSection(t.settings.config.section.misc), ListTile( title: Text(t.settings.config.connectionTestUrl), diff --git a/lib/features/config_option/overview/warp_options_widgets.dart b/lib/features/config_option/overview/warp_options_widgets.dart new file mode 100644 index 00000000..a5b798a4 --- /dev/null +++ b/lib/features/config_option/overview/warp_options_widgets.dart @@ -0,0 +1,199 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/model/range.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart'; +import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; +import 'package:hiddify/utils/uri_utils.dart'; +import 'package:hiddify/utils/validators.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class WarpOptionsTiles extends HookConsumerWidget { + const WarpOptionsTiles({ + required this.options, + required this.defaultOptions, + required this.onChange, + super.key, + }); + + final ConfigOptionEntity options; + final ConfigOptionEntity defaultOptions; + final Future Function(ConfigOptionPatch patch) onChange; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final warpPrefaceCompleted = ref.watch(warpOptionNotifierProvider); + final canChangeOptions = warpPrefaceCompleted && options.enableWarp; + + return Column( + children: [ + SwitchListTile.adaptive( + title: Text(t.settings.config.enableWarp), + value: options.enableWarp, + onChanged: (value) async { + if (!warpPrefaceCompleted) { + final agreed = await showAdaptiveDialog( + context: context, + builder: (context) => const WarpLicenseAgreementModal(), + ); + if (agreed ?? false) { + await ref.read(warpOptionNotifierProvider.notifier).agree(); + await onChange(ConfigOptionPatch(enableWarp: value)); + } + } else { + await onChange(ConfigOptionPatch(enableWarp: value)); + } + }, + ), + ListTile( + title: Text(t.settings.config.warpDetourMode), + subtitle: Text(options.warpDetourMode.present(t)), + enabled: canChangeOptions, + onTap: () async { + final warpDetourMode = await SettingsPickerDialog( + title: t.settings.config.warpDetourMode, + selected: options.warpDetourMode, + options: WarpDetourMode.values, + getTitle: (e) => e.present(t), + resetValue: defaultOptions.warpDetourMode, + ).show(context); + if (warpDetourMode == null) return; + await onChange( + ConfigOptionPatch(warpDetourMode: warpDetourMode), + ); + }, + ), + ListTile( + title: Text(t.settings.config.warpLicenseKey), + subtitle: Text( + options.warpLicenseKey.isEmpty + ? t.general.notSet + : options.warpLicenseKey, + ), + enabled: canChangeOptions, + onTap: () async { + final licenseKey = await SettingsInputDialog( + title: t.settings.config.warpLicenseKey, + initialValue: options.warpLicenseKey, + resetValue: defaultOptions.warpLicenseKey, + ).show(context); + if (licenseKey == null) return; + await onChange(ConfigOptionPatch(warpLicenseKey: licenseKey)); + }, + ), + ListTile( + title: Text(t.settings.config.warpCleanIp), + subtitle: Text(options.warpCleanIp), + enabled: canChangeOptions, + onTap: () async { + final warpCleanIp = await SettingsInputDialog( + title: t.settings.config.warpCleanIp, + initialValue: options.warpCleanIp, + resetValue: defaultOptions.warpCleanIp, + ).show(context); + if (warpCleanIp == null || warpCleanIp.isBlank) return; + await onChange(ConfigOptionPatch(warpCleanIp: warpCleanIp)); + }, + ), + ListTile( + title: Text(t.settings.config.warpPort), + subtitle: Text(options.warpPort.toString()), + enabled: canChangeOptions, + onTap: () async { + final warpPort = await SettingsInputDialog( + title: t.settings.config.warpPort, + initialValue: options.warpPort, + resetValue: defaultOptions.warpPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (warpPort == null) return; + await onChange( + ConfigOptionPatch(warpPort: warpPort), + ); + }, + ), + ListTile( + title: Text(t.settings.config.warpNoise), + subtitle: Text(options.warpNoise.present(t)), + enabled: canChangeOptions, + onTap: () async { + final warpNoise = await SettingsInputDialog( + title: t.settings.config.warpNoise, + initialValue: options.warpNoise.format(), + resetValue: defaultOptions.warpNoise.format(), + ).show(context); + if (warpNoise == null) return; + await onChange( + ConfigOptionPatch( + warpNoise: RangeWithOptionalCeil.tryParse( + warpNoise, + allowEmpty: true, + ), + ), + ); + }, + ), + ], + ); + } +} + +class WarpLicenseAgreementModal extends HookConsumerWidget { + const WarpLicenseAgreementModal({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + return AlertDialog.adaptive( + title: Text(t.settings.config.warpConsent.title), + content: Text.rich( + t.settings.config.warpConsent.description( + tos: (text) => TextSpan( + text: text, + style: const TextStyle(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await UriUtils.tryLaunch( + Uri.parse(Constants.cfWarpTermsOfService), + ); + }, + ), + privacy: (text) => TextSpan( + text: text, + style: const TextStyle(color: Colors.blue), + recognizer: TapGestureRecognizer() + ..onTap = () async { + await UriUtils.tryLaunch( + Uri.parse(Constants.cfWarpPrivacyPolicy), + ); + }, + ), + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(false); + }, + child: Text(t.general.decline), + ), + TextButton( + onPressed: () { + Navigator.of(context).pop(true); + }, + child: Text(t.general.agree), + ), + ], + ); + } +} diff --git a/lib/singbox/model/singbox_config_enum.dart b/lib/singbox/model/singbox_config_enum.dart index 5c1a966e..30aaac78 100644 --- a/lib/singbox/model/singbox_config_enum.dart +++ b/lib/singbox/model/singbox_config_enum.dart @@ -78,3 +78,13 @@ enum MuxProtocol { smux, yamux; } + +enum WarpDetourMode { + outbound, + inbound; + + String present(TranslationsEn t) => switch (this) { + outbound => t.settings.config.warpDetourModes.outbound, + inbound => t.settings.config.warpDetourModes.inbound, + }; +} diff --git a/lib/singbox/model/singbox_config_option.dart b/lib/singbox/model/singbox_config_option.dart index 3a0c66fc..5cbf4614 100644 --- a/lib/singbox/model/singbox_config_option.dart +++ b/lib/singbox/model/singbox_config_option.dart @@ -52,6 +52,13 @@ class SingboxConfigOption with _$SingboxConfigOption { required bool muxPadding, required int muxMaxStreams, required MuxProtocol muxProtocol, + required bool enableWarp, + required WarpDetourMode warpDetourMode, + required String warpLicenseKey, + required String warpCleanIp, + required int warpPort, + @RangeWithOptionalCeilJsonConverter() + required RangeWithOptionalCeil warpNoise, required String geoipPath, required String geositePath, required List rules,