Add cloudflare warp options
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String, dynamic> json) =>
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
199
lib/features/config_option/overview/warp_options_widgets.dart
Normal file
199
lib/features/config_option/overview/warp_options_widgets.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user