Add cloudflare warp options

This commit is contained in:
problematicconsumer
2024-02-03 12:36:27 +03:30
parent 18f1f389e0
commit 11defeb010
16 changed files with 426 additions and 22 deletions

View File

@@ -11,7 +11,10 @@
}, },
"sort": "Sort", "sort": "Sort",
"sortBy": "Sort by", "sortBy": "Sort by",
"addToClipboard": "Add to clipboard" "addToClipboard": "Add to clipboard",
"notSet": "Not Set",
"agree": "Agree",
"decline": "Decline"
}, },
"intro": { "intro": {
"termsAndPolicyCaution(rich)": "by continuing you agree with ${tap(@:about.termsAndConditions)}", "termsAndPolicyCaution(rich)": "by continuing you agree with ${tap(@:about.termsAndConditions)}",
@@ -204,8 +207,13 @@
"mux": "Multiplexer", "mux": "Multiplexer",
"outbound": "Outbound Options", "outbound": "Outbound Options",
"tlsTricks": "TLS Tricks", "tlsTricks": "TLS Tricks",
"warp": "WARP Options",
"misc": "Misc 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", "pageTitle": "Config Options",
"logLevel": "Log Level", "logLevel": "Log Level",
"resolveDestination": "Resolve Destination", "resolveDestination": "Resolve Destination",
@@ -243,7 +251,17 @@
"tlsPaddingSize": "TLS Padding", "tlsPaddingSize": "TLS Padding",
"enableMux": "Enable Mux", "enableMux": "Enable Mux",
"muxProtocol": "Mux Protocol", "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": { "geoAssets": {
"pageTitle": "Routing Assets", "pageTitle": "Routing Assets",

View File

@@ -11,7 +11,10 @@
}, },
"sort": "Clasificar", "sort": "Clasificar",
"sortBy": "Ordenar por", "sortBy": "Ordenar por",
"addToClipboard": "Añadir al portapapeles" "addToClipboard": "Añadir al portapapeles",
"notSet": "No establecido",
"agree": "Aceptar",
"decline": "Rechazar"
}, },
"home": { "home": {
"emptyProfilesMsg": "Comience agregando un perfil de suscripción", "emptyProfilesMsg": "Comience agregando un perfil de suscripción",
@@ -211,6 +214,7 @@
"mux": "Multiplexer", "mux": "Multiplexer",
"outbound": "Opciones de salida", "outbound": "Opciones de salida",
"tlsTricks": "Trucos TLS", "tlsTricks": "Trucos TLS",
"warp": "WARP Options",
"misc": "Opciones varias" "misc": "Opciones varias"
}, },
"pageTitle": "Opciones de configuración", "pageTitle": "Opciones de configuración",
@@ -237,7 +241,21 @@
"tlsPaddingSize": "Relleno TLS", "tlsPaddingSize": "Relleno TLS",
"enableMux": "Enable Mux", "enableMux": "Enable Mux",
"muxProtocol": "Mux Protocol", "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": { "geoAssets": {
"successMsg": "Activo actualizado correctamente", "successMsg": "Activo actualizado correctamente",

View File

@@ -11,7 +11,10 @@
}, },
"sort": "مرتب‌سازی", "sort": "مرتب‌سازی",
"sortBy": "مرتب‌سازی براساس", "sortBy": "مرتب‌سازی براساس",
"addToClipboard": "به کلیپ بورد اضافه کنید" "addToClipboard": "به کلیپ بورد اضافه کنید",
"notSet": "تنظیم نشده",
"agree": "موافق",
"decline": "کاهش می یابد"
}, },
"intro": { "intro": {
"termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت میکنید", "termsAndPolicyCaution(rich)": "در صورت ادامه با ${tap(@:about.termsAndConditions)} موافقت میکنید",
@@ -204,6 +207,7 @@
"mux": "Multiplexer", "mux": "Multiplexer",
"outbound": "Outbound Options", "outbound": "Outbound Options",
"tlsTricks": "TLS Tricks", "tlsTricks": "TLS Tricks",
"warp": "WARP Options",
"misc": "تنظیمات متفرقه" "misc": "تنظیمات متفرقه"
}, },
"pageTitle": "تنظیمات کانفیگ", "pageTitle": "تنظیمات کانفیگ",
@@ -243,7 +247,21 @@
"tlsPaddingSize": "TLS Padding", "tlsPaddingSize": "TLS Padding",
"enableMux": "Enable Mux", "enableMux": "Enable Mux",
"muxProtocol": "Mux Protocol", "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": { "geoAssets": {
"pageTitle": "فایل‌های مسیریابی", "pageTitle": "فایل‌های مسیریابی",

View File

@@ -11,7 +11,10 @@
}, },
"sort": "Сортировка", "sort": "Сортировка",
"sortBy": "Сортировка", "sortBy": "Сортировка",
"addToClipboard": "Копировать в буфер обмена" "addToClipboard": "Копировать в буфер обмена",
"notSet": "Не задано",
"agree": "Соглашаться",
"decline": "Отклонить"
}, },
"intro": { "intro": {
"termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}", "termsAndPolicyCaution(rich)": "Продолжая, Вы соглашаетесь с ${tap(@:about.termsAndConditions)}",
@@ -204,6 +207,7 @@
"mux": "Multiplexer", "mux": "Multiplexer",
"outbound": "Outbound Options", "outbound": "Outbound Options",
"tlsTricks": "TLS Tricks", "tlsTricks": "TLS Tricks",
"warp": "WARP Options",
"misc": "Разные параметры" "misc": "Разные параметры"
}, },
"pageTitle": "Параметры конфигурации", "pageTitle": "Параметры конфигурации",
@@ -243,7 +247,21 @@
"tlsPaddingSize": "TLS Padding", "tlsPaddingSize": "TLS Padding",
"enableMux": "Enable Mux", "enableMux": "Enable Mux",
"muxProtocol": "Mux Protocol", "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": { "geoAssets": {
"pageTitle": "Активы маршрутизации", "pageTitle": "Активы маршрутизации",

View File

@@ -11,7 +11,10 @@
}, },
"sort": "Sırala", "sort": "Sırala",
"sortBy": "Sırala", "sortBy": "Sırala",
"addToClipboard": "Panoya ekle" "addToClipboard": "Panoya ekle",
"notSet": "Ayarlanmadı",
"agree": "Kabul etmek",
"decline": "Reddetmek"
}, },
"intro": { "intro": {
"termsAndPolicyCaution(rich)": "devam ederek ${tap(@:about.termsAndConditions)} kabul etmiş olursunuz", "termsAndPolicyCaution(rich)": "devam ederek ${tap(@:about.termsAndConditions)} kabul etmiş olursunuz",
@@ -204,6 +207,7 @@
"mux": "Multiplexer", "mux": "Multiplexer",
"outbound": "Outbound Options", "outbound": "Outbound Options",
"tlsTricks": "TLS Tricks", "tlsTricks": "TLS Tricks",
"warp": "WARP Options",
"misc": "Çeşitli Seçenekler" "misc": "Çeşitli Seçenekler"
}, },
"pageTitle": "Yapılandırma Seçenekleri", "pageTitle": "Yapılandırma Seçenekleri",
@@ -243,7 +247,21 @@
"tlsPaddingSize": "TLS Padding", "tlsPaddingSize": "TLS Padding",
"enableMux": "Enable Mux", "enableMux": "Enable Mux",
"muxProtocol": "Mux Protocol", "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": { "geoAssets": {
"pageTitle": "Varlıkları Yönlendirme", "pageTitle": "Varlıkları Yönlendirme",

View File

@@ -11,7 +11,10 @@
}, },
"sort": "排序", "sort": "排序",
"sortBy": "排序方式", "sortBy": "排序方式",
"addToClipboard": "添加到剪贴板" "addToClipboard": "添加到剪贴板",
"notSet": "没有设置",
"agree": "同意",
"decline": "衰退"
}, },
"intro": { "intro": {
"termsAndPolicyCaution(rich)": "继续即表示您同意 ${tap(@:about.termsAndConditions)}", "termsAndPolicyCaution(rich)": "继续即表示您同意 ${tap(@:about.termsAndConditions)}",
@@ -204,6 +207,7 @@
"mux": "Multiplexer", "mux": "Multiplexer",
"outbound": "出站选项", "outbound": "出站选项",
"tlsTricks": "TLS Tricks", "tlsTricks": "TLS Tricks",
"warp": "WARP Options",
"misc": "其它选项" "misc": "其它选项"
}, },
"pageTitle": "配置选项", "pageTitle": "配置选项",
@@ -243,7 +247,21 @@
"tlsPaddingSize": "TLS 填充", "tlsPaddingSize": "TLS 填充",
"enableMux": "Enable Mux", "enableMux": "Enable Mux",
"muxProtocol": "Mux Protocol", "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": { "geoAssets": {
"pageTitle": "路由资源文件", "pageTitle": "路由资源文件",

View File

@@ -10,4 +10,8 @@ abstract class Constants {
static const telegramChannelUrl = "https://t.me/hiddify"; static const telegramChannelUrl = "https://t.me/hiddify";
static const privacyPolicyUrl = "https://hiddify.com/privacy-policy/"; static const privacyPolicyUrl = "https://hiddify.com/privacy-policy/";
static const termsAndConditionsUrl = "https://hiddify.com/terms/"; 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/";
} }

View File

@@ -1,4 +1,6 @@
import 'package:dartx/dartx.dart';
import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/localization/translations.dart';
part 'range.freezed.dart'; part 'range.freezed.dart';
@@ -7,14 +9,21 @@ class RangeWithOptionalCeil with _$RangeWithOptionalCeil {
const RangeWithOptionalCeil._(); const RangeWithOptionalCeil._();
const factory RangeWithOptionalCeil({ const factory RangeWithOptionalCeil({
required int min, int? min,
int? max, int? max,
}) = _RangeWithOptionalCeil; }) = _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("-")) { switch (input.split("-")) {
[final String val] when val.isEmpty && allowEmpty =>
const RangeWithOptionalCeil(),
[final String min] => RangeWithOptionalCeil(min: int.parse(min)), [final String min] => RangeWithOptionalCeil(min: int.parse(min)),
[final String min, final String max] => RangeWithOptionalCeil( [final String min, final String max] => RangeWithOptionalCeil(
min: int.parse(min), min: int.parse(min),
@@ -23,9 +32,12 @@ class RangeWithOptionalCeil with _$RangeWithOptionalCeil {
_ => throw Exception("Invalid range: $input"), _ => throw Exception("Invalid range: $input"),
}; };
static RangeWithOptionalCeil? tryParse(String input) { static RangeWithOptionalCeil? tryParse(
String input, {
bool allowEmpty = false,
}) {
try { try {
return RangeWithOptionalCeil.fromString(input); return RangeWithOptionalCeil._fromString(input);
} catch (_) { } catch (_) {
return null; return null;
} }
@@ -38,7 +50,7 @@ class RangeWithOptionalCeilJsonConverter
@override @override
RangeWithOptionalCeil fromJson(String json) => RangeWithOptionalCeil fromJson(String json) =>
RangeWithOptionalCeil.fromString(json); RangeWithOptionalCeil._fromString(json);
@override @override
String toJson(RangeWithOptionalCeil object) => object.format(); String toJson(RangeWithOptionalCeil object) => object.format();

View File

@@ -117,6 +117,12 @@ class ConfigOptionRepositoryImpl
muxPadding: persisted.muxPadding, muxPadding: persisted.muxPadding,
muxMaxStreams: persisted.muxMaxStreams, muxMaxStreams: persisted.muxMaxStreams,
muxProtocol: persisted.muxProtocol, muxProtocol: persisted.muxProtocol,
enableWarp: persisted.enableWarp,
warpDetourMode: persisted.warpDetourMode,
warpLicenseKey: persisted.warpLicenseKey,
warpCleanIp: persisted.warpCleanIp,
warpPort: persisted.warpPort,
warpNoise: persisted.warpNoise,
geoipPath: geoAssetPathResolver.relativePath( geoipPath: geoAssetPathResolver.relativePath(
geoAssets.geoip.providerName, geoAssets.geoip.providerName,
geoAssets.geoip.fileName, geoAssets.geoip.fileName,

View File

@@ -57,6 +57,14 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
@Default(false) bool muxPadding, @Default(false) bool muxPadding,
@Default(8) int muxMaxStreams, @Default(8) int muxMaxStreams,
@Default(MuxProtocol.h2mux) MuxProtocol muxProtocol, @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; }) = _ConfigOptionEntity;
static ConfigOptionEntity initial = ConfigOptionEntity( static ConfigOptionEntity initial = ConfigOptionEntity(
@@ -67,7 +75,11 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
if (PlatformUtils.isDesktop && serviceMode == ServiceMode.tun) { if (PlatformUtils.isDesktop && serviceMode == ServiceMode.tun) {
return true; return true;
} }
if (enableTlsFragment || enableTlsMixedSniCase || enableTlsPadding||enableMux) { if (enableTlsFragment ||
enableTlsMixedSniCase ||
enableTlsPadding ||
enableMux ||
enableWarp) {
return true; return true;
} }
@@ -117,6 +129,12 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
muxPadding: patch.muxPadding ?? muxPadding, muxPadding: patch.muxPadding ?? muxPadding,
muxMaxStreams: patch.muxMaxStreams ?? muxMaxStreams, muxMaxStreams: patch.muxMaxStreams ?? muxMaxStreams,
muxProtocol: patch.muxProtocol ?? muxProtocol, 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,
); );
} }

View File

@@ -47,6 +47,12 @@ class ConfigOptionPatch with _$ConfigOptionPatch {
bool? muxPadding, bool? muxPadding,
int? muxMaxStreams, int? muxMaxStreams,
MuxProtocol? muxProtocol, MuxProtocol? muxProtocol,
bool? enableWarp,
WarpDetourMode? warpDetourMode,
String? warpLicenseKey,
String? warpCleanIp,
int? warpPort,
@RangeWithOptionalCeilJsonConverter() RangeWithOptionalCeil? warpNoise,
}) = _ConfigOptionPatch; }) = _ConfigOptionPatch;
factory ConfigOptionPatch.fromJson(Map<String, dynamic> json) => factory ConfigOptionPatch.fromJson(Map<String, dynamic> json) =>

View File

@@ -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<void> agree() async {
await ref
.read(sharedPreferencesProvider)
.requireValue
.setBool(warpConsentGiven, true);
state = true;
}
static const warpConsentGiven = "warp_consent_given";
}

View File

@@ -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_entity.dart';
import 'package:hiddify/features/config_option/model/config_option_patch.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/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/log/model/log_level.dart';
import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; import 'package:hiddify/features/settings/widgets/sections_widgets.dart';
import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart';
@@ -319,7 +320,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
ListTile( ListTile(
title: Text(t.settings.config.tlsFragmentSize), title: Text(t.settings.config.tlsFragmentSize),
subtitle: Text(options.tlsFragmentSize.format()), subtitle: Text(options.tlsFragmentSize.present(t)),
onTap: () async { onTap: () async {
final range = await SettingsInputDialog( final range = await SettingsInputDialog(
title: t.settings.config.tlsFragmentSize, title: t.settings.config.tlsFragmentSize,
@@ -336,7 +337,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
ListTile( ListTile(
title: Text(t.settings.config.tlsFragmentSleep), title: Text(t.settings.config.tlsFragmentSleep),
subtitle: Text(options.tlsFragmentSleep.format()), subtitle: Text(options.tlsFragmentSleep.present(t)),
onTap: () async { onTap: () async {
final range = await SettingsInputDialog( final range = await SettingsInputDialog(
title: t.settings.config.tlsFragmentSleep, title: t.settings.config.tlsFragmentSleep,
@@ -368,7 +369,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
ListTile( ListTile(
title: Text(t.settings.config.tlsPaddingSize), title: Text(t.settings.config.tlsPaddingSize),
subtitle: Text(options.tlsPaddingSize.format()), subtitle: Text(options.tlsPaddingSize.present(t)),
onTap: () async { onTap: () async {
final range = await SettingsInputDialog( final range = await SettingsInputDialog(
title: t.settings.config.tlsPaddingSize, title: t.settings.config.tlsPaddingSize,
@@ -384,6 +385,13 @@ class ConfigOptionsPage extends HookConsumerWidget {
}, },
), ),
const SettingsDivider(), const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.warp)),
WarpOptionsTiles(
options: options,
defaultOptions: defaultOptions,
onChange: changeOption,
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.misc), SettingsSection(t.settings.config.section.misc),
ListTile( ListTile(
title: Text(t.settings.config.connectionTestUrl), title: Text(t.settings.config.connectionTestUrl),

View File

@@ -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<void> 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<bool>(
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),
),
],
);
}
}

View File

@@ -78,3 +78,13 @@ enum MuxProtocol {
smux, smux,
yamux; yamux;
} }
enum WarpDetourMode {
outbound,
inbound;
String present(TranslationsEn t) => switch (this) {
outbound => t.settings.config.warpDetourModes.outbound,
inbound => t.settings.config.warpDetourModes.inbound,
};
}

View File

@@ -52,6 +52,13 @@ class SingboxConfigOption with _$SingboxConfigOption {
required bool muxPadding, required bool muxPadding,
required int muxMaxStreams, required int muxMaxStreams,
required MuxProtocol muxProtocol, 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 geoipPath,
required String geositePath, required String geositePath,
required List<SingboxRule> rules, required List<SingboxRule> rules,