add warp config, update to flutter 1.22

This commit is contained in:
hiddify-com
2024-05-31 13:41:35 +02:00
parent 218c6d49bc
commit e2b67c317b
21 changed files with 423 additions and 385 deletions

View File

@@ -51,8 +51,7 @@ abstract class ConfigOptions {
validator: (value) => value.isNotBlank,
);
static final remoteDnsDomainStrategy =
PreferencesNotifier.create<DomainStrategy, String>(
static final remoteDnsDomainStrategy = PreferencesNotifier.create<DomainStrategy, String>(
"remote-dns-domain-strategy",
DomainStrategy.auto,
mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value),
@@ -65,8 +64,7 @@ abstract class ConfigOptions {
validator: (value) => value.isNotBlank,
);
static final directDnsDomainStrategy =
PreferencesNotifier.create<DomainStrategy, String>(
static final directDnsDomainStrategy = PreferencesNotifier.create<DomainStrategy, String>(
"direct-dns-domain-strategy",
DomainStrategy.auto,
mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value),
@@ -91,8 +89,7 @@ abstract class ConfigOptions {
validator: (value) => isPort(value.toString()),
);
static final tunImplementation =
PreferencesNotifier.create<TunImplementation, String>(
static final tunImplementation = PreferencesNotifier.create<TunImplementation, String>(
"tun-implementation",
TunImplementation.mixed,
mapFrom: TunImplementation.values.byName,
@@ -101,8 +98,7 @@ abstract class ConfigOptions {
static final mtu = PreferencesNotifier.create<int, int>("mtu", 9000);
static final strictRoute =
PreferencesNotifier.create<bool, bool>("strict-route", true);
static final strictRoute = PreferencesNotifier.create<bool, bool>("strict-route", true);
static final connectionTestUrl = PreferencesNotifier.create<String, String>(
"connection-test-url",
@@ -128,8 +124,7 @@ abstract class ConfigOptions {
validator: (value) => isPort(value.toString()),
);
static final bypassLan =
PreferencesNotifier.create<bool, bool>("bypass-lan", false);
static final bypassLan = PreferencesNotifier.create<bool, bool>("bypass-lan", false);
static final allowConnectionFromLan = PreferencesNotifier.create<bool, bool>(
"allow-connection-from-lan",
@@ -156,16 +151,14 @@ abstract class ConfigOptions {
false,
);
static final tlsFragmentSize =
PreferencesNotifier.create<OptionalRange, String>(
static final tlsFragmentSize = PreferencesNotifier.create<OptionalRange, String>(
"tls-fragment-size",
const OptionalRange(min: 1, max: 500),
mapFrom: OptionalRange.parse,
mapTo: const OptionalRangeJsonConverter().toJson,
);
static final tlsFragmentSleep =
PreferencesNotifier.create<OptionalRange, String>(
static final tlsFragmentSleep = PreferencesNotifier.create<OptionalRange, String>(
"tls-fragment-sleep",
const OptionalRange(min: 0, max: 500),
mapFrom: OptionalRange.parse,
@@ -182,8 +175,7 @@ abstract class ConfigOptions {
false,
);
static final tlsPaddingSize =
PreferencesNotifier.create<OptionalRange, String>(
static final tlsPaddingSize = PreferencesNotifier.create<OptionalRange, String>(
"tls-padding-size",
const OptionalRange(min: 1, max: 1500),
mapFrom: OptionalRange.parse,
@@ -218,8 +210,7 @@ abstract class ConfigOptions {
false,
);
static final warpDetourMode =
PreferencesNotifier.create<WarpDetourMode, String>(
static final warpDetourMode = PreferencesNotifier.create<WarpDetourMode, String>(
"warp-detour-mode",
WarpDetourMode.proxyOverWarp,
mapFrom: WarpDetourMode.values.byName,
@@ -230,16 +221,28 @@ abstract class ConfigOptions {
"warp-license-key",
"",
);
static final warp2LicenseKey = PreferencesNotifier.create<String, String>(
"warp2s-license-key",
"",
);
static final warpAccountId = PreferencesNotifier.create<String, String>(
"warp-account-id",
"",
);
static final warp2AccountId = PreferencesNotifier.create<String, String>(
"warp2-account-id",
"",
);
static final warpAccessToken = PreferencesNotifier.create<String, String>(
"warp-access-token",
"",
);
static final warp2AccessToken = PreferencesNotifier.create<String, String>(
"warp2-access-token",
"",
);
static final warpCleanIp = PreferencesNotifier.create<String, String>(
"warp-clean-ip",
@@ -259,8 +262,7 @@ abstract class ConfigOptions {
mapTo: const OptionalRangeJsonConverter().toJson,
);
static final warpNoiseDelay =
PreferencesNotifier.create<OptionalRange, String>(
static final warpNoiseDelay = PreferencesNotifier.create<OptionalRange, String>(
"warp-noise-delay",
const OptionalRange(min: 20, max: 200),
mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true),
@@ -271,6 +273,10 @@ abstract class ConfigOptions {
"warp-wireguard-config",
"",
);
static final warp2WireguardConfig = PreferencesNotifier.create<String, String>(
"warp2-wireguard-config",
"",
);
static final hasExperimentalFeatures = Provider.autoDispose<bool>(
(ref) {
@@ -278,13 +284,7 @@ abstract class ConfigOptions {
if (PlatformUtils.isDesktop && mode == ServiceMode.tun) {
return true;
}
if (ref.watch(enableTlsFragment) ||
ref.watch(enableTlsMixedSniCase) ||
ref.watch(enableTlsPadding) ||
ref.watch(enableMux) ||
ref.watch(enableWarp) ||
ref.watch(bypassLan) ||
ref.watch(allowConnectionFromLan)) {
if (ref.watch(enableTlsFragment) || ref.watch(enableTlsMixedSniCase) || ref.watch(enableTlsPadding) || ref.watch(enableMux) || ref.watch(enableWarp) || ref.watch(bypassLan) || ref.watch(allowConnectionFromLan)) {
return true;
}
@@ -298,10 +298,13 @@ abstract class ConfigOptions {
"warp.access-token",
"warp.account-id",
"warp.wireguard-config",
"warp2.license-key",
"warp2.access-token",
"warp2.account-id",
"warp2.wireguard-config",
};
static final Map<String, StateNotifierProvider<PreferencesNotifier, dynamic>>
preferences = {
static final Map<String, StateNotifierProvider<PreferencesNotifier, dynamic>> preferences = {
"service-mode": serviceMode,
"log-level": logLevel,
"resolve-destination": resolveDestination,
@@ -348,6 +351,10 @@ abstract class ConfigOptions {
"warp.noise": warpNoise,
"warp.noise-delay": warpNoiseDelay,
"warp.wireguard-config": warpWireguardConfig,
"warp2.license-key": warp2LicenseKey,
"warp2.account-id": warp2AccountId,
"warp2.access-token": warp2AccessToken,
"warp2.wireguard-config": warp2WireguardConfig,
};
static final singboxConfigOptions = FutureProvider<SingboxConfigOption>(
@@ -386,8 +393,7 @@ abstract class ConfigOptions {
};
final geoAssetsRepo = await ref.watch(geoAssetRepositoryProvider.future);
final geoAssets =
await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run();
final geoAssets = await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run();
final mode = ref.watch(serviceMode);
return SingboxConfigOption(
@@ -443,6 +449,18 @@ abstract class ConfigOptions {
noise: ref.watch(warpNoise),
noiseDelay: ref.watch(warpNoiseDelay),
),
warp2: SingboxWarpOption(
enable: ref.watch(enableWarp),
mode: ref.watch(warpDetourMode),
wireguardConfig: ref.watch(warp2WireguardConfig),
licenseKey: ref.watch(warp2LicenseKey),
accountId: ref.watch(warp2AccountId),
accessToken: ref.watch(warp2AccessToken),
cleanIp: ref.watch(warpCleanIp),
cleanPort: ref.watch(warpPort),
noise: ref.watch(warpNoise),
noiseDelay: ref.watch(warpNoiseDelay),
),
geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath(
geoAssets.geoip.providerName,
geoAssets.geoip.fileName,
@@ -470,8 +488,7 @@ class ConfigOptionRepository with ExceptionHandler, InfraLogger {
final GeoAssetRepository geoAssetRepository;
final GeoAssetPathResolver geoAssetPathResolver;
TaskEither<ConfigOptionFailure, SingboxConfigOption>
getFullSingboxConfigOption() {
TaskEither<ConfigOptionFailure, SingboxConfigOption> getFullSingboxConfigOption() {
return exceptionHandler(
() async {
return right(await getConfigOptions());

View File

@@ -26,20 +26,14 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
return WarpOptions(
consentGiven: consent,
configGeneration: hasWarpConfig
? const AsyncValue.data("")
: AsyncError(const MissingWarpConfigFailure(), StackTrace.current),
configGeneration: hasWarpConfig ? const AsyncValue.data("") : AsyncError(const MissingWarpConfigFailure(), StackTrace.current),
);
}
SharedPreferences get _prefs =>
ref.read(sharedPreferencesProvider).requireValue;
SharedPreferences get _prefs => ref.read(sharedPreferencesProvider).requireValue;
Future<void> agree() async {
await ref
.read(sharedPreferencesProvider)
.requireValue
.setBool(warpConsentGiven, true);
await ref.read(sharedPreferencesProvider).requireValue.setBool(warpConsentGiven, true);
state = state.copyWith(consentGiven: true);
await generateWarpConfig();
}
@@ -59,15 +53,33 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
.getOrElse((l) => throw l)
.run();
await ref
.read(ConfigOptions.warpAccountId.notifier)
.update(warp.accountId);
await ref
.read(ConfigOptions.warpAccessToken.notifier)
.update(warp.accessToken);
await ref
.read(ConfigOptions.warpWireguardConfig.notifier)
.update(warp.wireguardConfig);
await ref.read(ConfigOptions.warpAccountId.notifier).update(warp.accountId);
await ref.read(ConfigOptions.warpAccessToken.notifier).update(warp.accessToken);
await ref.read(ConfigOptions.warpWireguardConfig.notifier).update(warp.wireguardConfig);
return warp.log;
});
state = state.copyWith(configGeneration: result);
}
Future<void> generateWarp2Config() async {
if (state.configGeneration.isLoading) return;
state = state.copyWith(configGeneration: const AsyncLoading());
final result = await AsyncValue.guard(() async {
final warp = await ref
.read(singboxServiceProvider)
.generateWarpConfig(
licenseKey: ref.read(ConfigOptions.warpLicenseKey),
previousAccountId: ref.read(ConfigOptions.warp2AccountId),
previousAccessToken: ref.read(ConfigOptions.warp2AccessToken),
)
.getOrElse((l) => throw l)
.run();
await ref.read(ConfigOptions.warp2AccountId.notifier).update(warp.accountId);
await ref.read(ConfigOptions.warp2AccessToken.notifier).update(warp.accessToken);
await ref.read(ConfigOptions.warp2WireguardConfig.notifier).update(warp.wireguardConfig);
return warp.log;
});

View File

@@ -23,17 +23,20 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:humanizer/humanizer.dart';
enum ConfigOptionSection {
warp;
warp,
fragment;
static final _warpKey = GlobalKey(debugLabel: "warp-section-key");
static final _fragmentKey = GlobalKey(debugLabel: "fragment-section-key");
GlobalKey get key => switch (this) { _ => _warpKey };
GlobalKey get key => switch (this) {
ConfigOptionSection.warp => _warpKey,
ConfigOptionSection.fragment => _fragmentKey,
};
}
class ConfigOptionsPage extends HookConsumerWidget {
ConfigOptionsPage({super.key, String? section})
: section =
section != null ? ConfigOptionSection.values.byName(section) : null;
ConfigOptionsPage({super.key, String? section}) : section = section != null ? ConfigOptionSection.values.byName(section) : null;
final ConfigOptionSection? section;
@@ -47,14 +50,10 @@ class ConfigOptionsPage extends HookConsumerWidget {
if (section != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
final box =
section!.key.currentContext?.findRenderObject() as RenderBox?;
final box = section!.key.currentContext?.findRenderObject() as RenderBox?;
final offset = box?.localToGlobal(Offset.zero);
if (offset == null) return;
final height = scrollController.offset +
offset.dy -
MediaQueryData.fromView(View.of(context)).padding.top -
kToolbarHeight;
final height = scrollController.offset + offset.dy - MediaQueryData.fromView(View.of(context)).padding.top - kToolbarHeight;
scrollController.animateTo(
height,
duration: const Duration(milliseconds: 500),
@@ -83,36 +82,26 @@ class ConfigOptionsPage extends HookConsumerWidget {
itemBuilder: (context) {
return [
PopupMenuItem(
onTap: () async => ref
.read(configOptionNotifierProvider.notifier)
.exportJsonToClipboard()
.then((success) {
onTap: () async => ref.read(configOptionNotifierProvider.notifier).exportJsonToClipboard().then((success) {
if (success) {
ref
.read(inAppNotificationControllerProvider)
.showSuccessToast(
ref.read(inAppNotificationControllerProvider).showSuccessToast(
t.general.clipboardExportSuccessMsg,
);
}
}),
child: Text(t.settings.exportOptions),
),
if (ref.watch(debugModeNotifierProvider))
PopupMenuItem(
onTap: () async => ref
.read(configOptionNotifierProvider.notifier)
.exportJsonToClipboard(excludePrivate: false)
.then((success) {
if (success) {
ref
.read(inAppNotificationControllerProvider)
.showSuccessToast(
t.general.clipboardExportSuccessMsg,
);
}
}),
child: Text(t.settings.exportAllOptions),
),
// if (ref.watch(debugModeNotifierProvider))
PopupMenuItem(
onTap: () async => ref.read(configOptionNotifierProvider.notifier).exportJsonToClipboard(excludePrivate: false).then((success) {
if (success) {
ref.read(inAppNotificationControllerProvider).showSuccessToast(
t.general.clipboardExportSuccessMsg,
);
}
}),
child: Text(t.settings.exportAllOptions),
),
PopupMenuItem(
onTap: () async {
final shouldImport = await showConfirmationDialog(
@@ -121,9 +110,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
message: t.settings.importOptionsMsg,
);
if (shouldImport) {
await ref
.read(configOptionNotifierProvider.notifier)
.importFromClipboard();
await ref.read(configOptionNotifierProvider.notifier).importFromClipboard();
}
},
child: Text(t.settings.importOptions),
@@ -131,9 +118,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
PopupMenuItem(
child: Text(t.config.resetBtn),
onTap: () async {
await ref
.read(configOptionNotifierProvider.notifier)
.resetOption();
await ref.read(configOptionNotifierProvider.notifier).resetOption();
},
),
];
@@ -158,15 +143,12 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(experimental(t.config.bypassLan)),
value: ref.watch(ConfigOptions.bypassLan),
onChanged:
ref.watch(ConfigOptions.bypassLan.notifier).update,
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update,
),
SwitchListTile(
title: Text(t.config.resolveDestination),
value: ref.watch(ConfigOptions.resolveDestination),
onChanged: ref
.watch(ConfigOptions.resolveDestination.notifier)
.update,
onChanged: ref.watch(ConfigOptions.resolveDestination.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.ipv6Mode),
@@ -179,28 +161,24 @@ class ConfigOptionsPage extends HookConsumerWidget {
SettingsSection(t.config.section.dns),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.remoteDnsAddress),
preferences:
ref.watch(ConfigOptions.remoteDnsAddress.notifier),
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier),
title: t.config.remoteDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
preferences: ref
.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
preferences: ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.config.remoteDnsDomainStrategy,
presentChoice: (value) => value.displayName,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.directDnsAddress),
preferences:
ref.watch(ConfigOptions.directDnsAddress.notifier),
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier),
title: t.config.directDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
preferences: ref
.watch(ConfigOptions.directDnsDomainStrategy.notifier),
preferences: ref.watch(ConfigOptions.directDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.config.directDnsDomainStrategy,
presentChoice: (value) => value.displayName,
@@ -208,9 +186,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(t.config.enableDnsRouting),
value: ref.watch(ConfigOptions.enableDnsRouting),
onChanged: ref
.watch(ConfigOptions.enableDnsRouting.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableDnsRouting.notifier).update,
),
// const SettingsDivider(),
// SettingsSection(experimental(t.config.section.mux)),
@@ -247,13 +223,11 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(t.config.strictRoute),
value: ref.watch(ConfigOptions.strictRoute),
onChanged:
ref.watch(ConfigOptions.strictRoute.notifier).update,
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.tunImplementation),
preferences:
ref.watch(ConfigOptions.tunImplementation.notifier),
preferences: ref.watch(ConfigOptions.tunImplementation.notifier),
choices: TunImplementation.values,
title: t.config.tunImplementation,
presentChoice: (value) => value.name,
@@ -287,25 +261,21 @@ class ConfigOptionsPage extends HookConsumerWidget {
experimental(t.config.allowConnectionFromLan),
),
value: ref.watch(ConfigOptions.allowConnectionFromLan),
onChanged: ref
.read(ConfigOptions.allowConnectionFromLan.notifier)
.update,
onChanged: ref.read(ConfigOptions.allowConnectionFromLan.notifier).update,
),
const SettingsDivider(),
SettingsSection(
experimental(t.config.section.tlsTricks),
key: ConfigOptionSection._fragmentKey,
),
SwitchListTile(
title: Text(t.config.enableTlsFragment),
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged: ref
.watch(ConfigOptions.enableTlsFragment.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSize),
preferences:
ref.watch(ConfigOptions.tlsFragmentSize.notifier),
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier),
title: t.config.tlsFragmentSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
@@ -313,8 +283,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSleep),
preferences:
ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
title: t.config.tlsFragmentSleep,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
@@ -323,21 +292,16 @@ class ConfigOptionsPage extends HookConsumerWidget {
SwitchListTile(
title: Text(t.config.enableTlsMixedSniCase),
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
onChanged: ref
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableTlsMixedSniCase.notifier).update,
),
SwitchListTile(
title: Text(t.config.enableTlsPadding),
value: ref.watch(ConfigOptions.enableTlsPadding),
onChanged: ref
.watch(ConfigOptions.enableTlsPadding.notifier)
.update,
onChanged: ref.watch(ConfigOptions.enableTlsPadding.notifier).update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsPaddingSize),
preferences:
ref.watch(ConfigOptions.tlsPaddingSize.notifier),
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier),
title: t.config.tlsPaddingSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.format(),
@@ -350,38 +314,26 @@ class ConfigOptionsPage extends HookConsumerWidget {
SettingsSection(t.config.section.misc),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.connectionTestUrl),
preferences:
ref.watch(ConfigOptions.connectionTestUrl.notifier),
preferences: ref.watch(ConfigOptions.connectionTestUrl.notifier),
title: t.config.connectionTestUrl,
),
ListTile(
title: Text(t.config.urlTestInterval),
subtitle: Text(
ref
.watch(ConfigOptions.urlTestInterval)
.toApproximateTime(isRelativeToNow: false),
ref.watch(ConfigOptions.urlTestInterval).toApproximateTime(isRelativeToNow: false),
),
onTap: () async {
final urlTestInterval = await SettingsSliderDialog(
title: t.config.urlTestInterval,
initialValue: ref
.watch(ConfigOptions.urlTestInterval)
.inMinutes
.coerceIn(0, 60)
.toDouble(),
onReset: ref
.read(ConfigOptions.urlTestInterval.notifier)
.reset,
initialValue: ref.watch(ConfigOptions.urlTestInterval).inMinutes.coerceIn(0, 60).toDouble(),
onReset: ref.read(ConfigOptions.urlTestInterval.notifier).reset,
min: 1,
max: 60,
divisions: 60,
labelGen: (value) => Duration(minutes: value.toInt())
.toApproximateTime(isRelativeToNow: false),
labelGen: (value) => Duration(minutes: value.toInt()).toApproximateTime(isRelativeToNow: false),
).show(context);
if (urlTestInterval == null) return;
await ref
.read(ConfigOptions.urlTestInterval.notifier)
.update(Duration(minutes: urlTestInterval.toInt()));
await ref.read(ConfigOptions.urlTestInterval.notifier).update(Duration(minutes: urlTestInterval.toInt()));
},
),
ValuePreferenceWidget(

View File

@@ -63,17 +63,15 @@ class WarpOptionsTiles extends HookConsumerWidget {
AsyncLoading() => const LinearProgressIndicator(),
AsyncError() => Text(
t.config.missingWarpConfig,
style:
TextStyle(color: Theme.of(context).colorScheme.error),
style: TextStyle(color: Theme.of(context).colorScheme.error),
),
_ => null,
}
: null,
enabled: canChangeOptions,
onTap: () async {
await ref
.read(warpOptionNotifierProvider.notifier)
.generateWarpConfig();
await ref.read(warpOptionNotifierProvider.notifier).generateWarpConfig();
await ref.read(warpOptionNotifierProvider.notifier).generateWarp2Config();
},
),
ChoicePreferenceWidget(
@@ -111,8 +109,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
preferences: ref.watch(ConfigOptions.warpNoise.notifier),
enabled: canChangeOptions,
title: t.config.warpNoise,
inputToValue: (input) =>
OptionalRange.tryParse(input, allowEmpty: true),
inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true),
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
@@ -121,8 +118,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier),
enabled: canChangeOptions,
title: t.config.warpNoiseDelay,
inputToValue: (input) =>
OptionalRange.tryParse(input, allowEmpty: true),
inputToValue: (input) => OptionalRange.tryParse(input, allowEmpty: true),
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),

View File

@@ -16,8 +16,7 @@ class QuickSettingsModal extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final warpPrefaceCompleted =
ref.watch(warpOptionNotifierProvider).consentGiven;
final warpPrefaceCompleted = ref.watch(warpOptionNotifierProvider).consentGiven;
return SingleChildScrollView(
child: Column(
@@ -33,37 +32,41 @@ class QuickSettingsModal extends HookConsumerWidget {
e.presentShort(t),
overflow: TextOverflow.ellipsis,
),
tooltip:
e.isExperimental ? t.settings.experimental : null,
tooltip: e.isExperimental ? t.settings.experimental : null,
),
)
.toList(),
selected: {ref.watch(ConfigOptions.serviceMode)},
onSelectionChanged: (newSet) => ref
.read(ConfigOptions.serviceMode.notifier)
.update(newSet.first),
onSelectionChanged: (newSet) => ref.read(ConfigOptions.serviceMode.notifier).update(newSet.first),
),
),
const Gap(8),
if (warpPrefaceCompleted)
SwitchListTile(
value: ref.watch(ConfigOptions.enableWarp),
onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update,
title: Text(t.config.enableWarp),
GestureDetector(
onLongPress: () {
ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context);
},
child: SwitchListTile(
value: ref.watch(ConfigOptions.enableWarp),
onChanged: ref.watch(ConfigOptions.enableWarp.notifier).update,
title: Text(t.config.enableWarp),
),
)
else
ListTile(
title: Text(t.config.setupWarp),
trailing: const Icon(FluentIcons.chevron_right_24_regular),
onTap: () =>
ConfigOptionsRoute(section: ConfigOptionSection.warp.name)
.go(context),
onTap: () => ConfigOptionsRoute(section: ConfigOptionSection.warp.name).go(context),
),
GestureDetector(
onLongPress: () {
ConfigOptionsRoute(section: ConfigOptionSection.fragment.name).go(context);
},
child: SwitchListTile(
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged: ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
title: Text(t.config.enableTlsFragment),
),
SwitchListTile(
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged:
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
title: Text(t.config.enableTlsFragment),
),
// SwitchListTile(
// value: ref.watch(ConfigOptions.enableMux),

View File

@@ -5,11 +5,16 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/common/qr_code_scanner_screen.dart';
import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart';
import 'package:hiddify/features/config_option/overview/config_options_page.dart';
import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart';
import 'package:hiddify/features/profile/notifier/profile_notifier.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AddProfileModal extends HookConsumerWidget {
const AddProfileModal({
@@ -17,7 +22,7 @@ class AddProfileModal extends HookConsumerWidget {
this.url,
this.scrollController,
});
static const warpConsentGiven = "warp_consent_given";
final String? url;
final ScrollController? scrollController;
@@ -58,8 +63,7 @@ class AddProfileModal extends HookConsumerWidget {
child: LayoutBuilder(
builder: (context, constraints) {
// temporary solution, aspect ratio widget relies on height and in a row there no height!
final buttonWidth =
constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
final buttonWidth = constraints.maxWidth / 2 - (buttonsPadding + (buttonsGap / 2));
return AnimatedCrossFade(
firstChild: SizedBox(
@@ -93,8 +97,7 @@ class AddProfileModal extends HookConsumerWidget {
secondChild: Column(
children: [
Padding(
padding:
const EdgeInsets.symmetric(horizontal: buttonsPadding),
padding: const EdgeInsets.symmetric(horizontal: buttonsPadding),
child: Row(
children: [
_Button(
@@ -103,13 +106,9 @@ class AddProfileModal extends HookConsumerWidget {
icon: FluentIcons.clipboard_paste_24_regular,
size: buttonWidth,
onTap: () async {
final captureResult =
await Clipboard.getData(Clipboard.kTextPlain)
.then((value) => value?.text ?? '');
final captureResult = await Clipboard.getData(Clipboard.kTextPlain).then((value) => value?.text ?? '');
if (addProfileState.isLoading) return;
ref
.read(addProfileProvider.notifier)
.add(captureResult);
ref.read(addProfileProvider.notifier).add(captureResult);
},
),
const Gap(buttonsGap),
@@ -120,8 +119,7 @@ class AddProfileModal extends HookConsumerWidget {
icon: FluentIcons.qr_code_24_regular,
size: buttonWidth,
onTap: () async {
final cr =
await QRCodeScannerScreen().open(context);
final cr = await QRCodeScannerScreen().open(context);
if (cr == null) return;
if (addProfileState.isLoading) return;
@@ -142,56 +140,118 @@ class AddProfileModal extends HookConsumerWidget {
],
),
),
if (!PlatformUtils.isDesktop)
Padding(
padding: const EdgeInsets.symmetric(
horizontal: buttonsPadding,
vertical: 16,
),
child: Semantics(
button: true,
child: SizedBox(
height: 36,
child: Material(
key: const ValueKey("add_manually_button"),
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
context.pop();
await const NewProfileRoute().push(context);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
const Gap(8),
Text(
t.profile.add.manually,
style: theme.textTheme.labelLarge?.copyWith(
Padding(
padding: const EdgeInsets.symmetric(
horizontal: buttonsPadding,
vertical: 16,
),
child: Column(
children: [
Semantics(
button: true,
child: SizedBox(
height: 36,
child: Material(
key: const ValueKey("add_warp_button"),
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
Future.microtask(() async {
context.pop();
final _prefs = ref.read(sharedPreferencesProvider).requireValue;
final consent = _prefs.getBool(warpConsentGiven) ?? false;
if (!consent) {
final agreed = await showDialog<bool>(
context: context,
builder: (context) => const WarpLicenseAgreementModal(),
);
if (agreed ?? false) {
await ref.read(warpOptionNotifierProvider.notifier).agree();
}
}
final accountId = _prefs.getString("warp2-account-id");
final accessToken = _prefs.getString("warp2-access-token");
final hasWarp2Config = accountId != null && accessToken != null;
if (!hasWarp2Config) {
await ref.read(warpOptionNotifierProvider.notifier).generateWarp2Config();
}
await ref.read(addProfileProvider.notifier).add("#profile-title: Hiddify WARP\nwarp://p2@auto#Remote&&detour=warp://p1@auto#Local"); //
});
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
),
],
const SizedBox(width: 8),
Text(
t.profile.add.addWarp,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary,
),
),
],
),
),
),
),
),
),
if (!PlatformUtils.isDesktop) const SizedBox(height: 16), // Spacing between the buttons
if (!PlatformUtils.isDesktop)
Semantics(
button: true,
child: SizedBox(
height: 36,
child: Material(
key: const ValueKey("add_manually_button"),
elevation: 8,
color: theme.colorScheme.surface,
surfaceTintColor: theme.colorScheme.surfaceTint,
shadowColor: Colors.transparent,
borderRadius: BorderRadius.circular(8),
clipBehavior: Clip.antiAlias,
child: InkWell(
onTap: () async {
context.pop();
await const NewProfileRoute().push(context);
},
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
FluentIcons.add_24_regular,
color: theme.colorScheme.primary,
),
const SizedBox(width: 8),
Text(
t.profile.add.manually,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.primary,
),
),
],
),
),
),
),
),
],
),
),
const Gap(24),
],
),
crossFadeState: addProfileState.isLoading
? CrossFadeState.showFirst
: CrossFadeState.showSecond,
crossFadeState: addProfileState.isLoading ? CrossFadeState.showFirst : CrossFadeState.showSecond,
duration: const Duration(milliseconds: 250),
);
},

View File

@@ -47,8 +47,7 @@ class AddProfile extends _$AddProfile with AppLogger {
return const AsyncData(null);
}
ProfileRepository get _profilesRepo =>
ref.read(profileRepositoryProvider).requireValue;
ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue;
CancelToken? _cancelToken;
Future<void> add(String rawInput) async {
@@ -57,8 +56,7 @@ class AddProfile extends _$AddProfile with AppLogger {
state = await AsyncValue.guard(
() async {
final activeProfile = await ref.read(activeProfileProvider.future);
final markAsActive =
activeProfile == null || ref.read(Preferences.markNewProfileActive);
final markAsActive = activeProfile == null || ref.read(Preferences.markNewProfileActive);
final TaskEither<ProfileFailure, Unit> task;
if (LinkParser.parse(rawInput) case (final link)?) {
loggy.debug("adding profile, url: [${link.url}]");
@@ -70,7 +68,11 @@ class AddProfile extends _$AddProfile with AppLogger {
} else if (LinkParser.protocol(rawInput) case (final parsed)?) {
loggy.debug("adding profile, content");
var name = parsed.name;
var oldItem = await _profilesRepo.getByName(name);
if (name == "Hiddify WARP" && oldItem != null) {
_profilesRepo.setAsActive(oldItem.id).run();
return unit;
}
while (await _profilesRepo.getByName(name) != null) {
name += '${randomInt(0, 9).run()}';
}
@@ -122,8 +124,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger {
return const AsyncData(null);
}
ProfileRepository get _profilesRepo =>
ref.read(profileRepositoryProvider).requireValue;
ProfileRepository get _profilesRepo => ref.read(profileRepositoryProvider).requireValue;
Future<void> updateProfile(RemoteProfileEntity profile) async {
if (state.isLoading) return;
@@ -143,9 +144,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger {
await ref.read(activeProfileProvider.future).then((active) async {
if (active != null && active.id == profile.id) {
await ref
.read(connectionNotifierProvider.notifier)
.reconnect(profile);
await ref.read(connectionNotifierProvider.notifier).reconnect(profile);
}
});
return unit;

View File

@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:combine/combine.dart';
import 'package:dartx/dartx.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/haptic/haptic_service.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart';
@@ -85,46 +85,41 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger {
List<ProxyGroupEntity> proxies,
ProxiesSort sortBy,
) async {
return CombineWorker().execute(
() {
final groupWithSelected = {
for (final o in proxies) o.tag: o.selected,
};
final sortedProxies = <ProxyGroupEntity>[];
for (final group in proxies) {
final sortedItems = switch (sortBy) {
ProxiesSort.name => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
return a.tag.compareTo(b.tag);
}),
ProxiesSort.delay => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
final groupWithSelected = {
for (final o in proxies) o.tag: o.selected,
};
final sortedProxies = <ProxyGroupEntity>[];
for (final group in proxies) {
final sortedItems = switch (sortBy) {
ProxiesSort.name => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
return a.tag.compareTo(b.tag);
}),
ProxiesSort.delay => group.items.sortedWith((a, b) {
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
final ai = a.urlTestDelay;
final bi = b.urlTestDelay;
if (ai == 0 && bi == 0) return -1;
if (ai == 0 && bi > 0) return 1;
if (ai > 0 && bi == 0) return -1;
return ai.compareTo(bi);
}),
ProxiesSort.unsorted => group.items,
};
final items = <ProxyItemEntity>[];
for (final item in sortedItems) {
if (groupWithSelected.keys.contains(item.tag)) {
items
.add(item.copyWith(selectedTag: groupWithSelected[item.tag]));
} else {
items.add(item);
}
}
sortedProxies.add(group.copyWith(items: items));
final ai = a.urlTestDelay;
final bi = b.urlTestDelay;
if (ai == 0 && bi == 0) return -1;
if (ai == 0 && bi > 0) return 1;
if (ai > 0 && bi == 0) return -1;
return ai.compareTo(bi);
}),
ProxiesSort.unsorted => group.items,
};
final items = <ProxyItemEntity>[];
for (final item in sortedItems) {
if (groupWithSelected.keys.contains(item.tag)) {
items.add(item.copyWith(selectedTag: groupWithSelected[item.tag]));
} else {
items.add(item);
}
return sortedProxies;
},
);
}
sortedProxies.add(group.copyWith(items: items));
}
return sortedProxies;
}
Future<void> changeProxy(String groupTag, String outboundTag) async {