Add quick settings

This commit is contained in:
problematicconsumer
2024-03-07 17:04:32 +03:30
parent 596e085e6e
commit 70123d80a8
7 changed files with 435 additions and 236 deletions

View File

@@ -1,5 +1,6 @@
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/optional_range.dart';
@@ -21,12 +22,49 @@ import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:humanizer/humanizer.dart';
enum ConfigOptionSection {
warp;
static final _warpKey = GlobalKey(debugLabel: "warp-section-key");
GlobalKey get key => switch (this) { _ => _warpKey };
}
class ConfigOptionsPage extends HookConsumerWidget {
const ConfigOptionsPage({super.key});
ConfigOptionsPage({super.key, String? section})
: section =
section != null ? ConfigOptionSection.values.byName(section) : null;
final ConfigOptionSection? section;
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final scrollController = useScrollController();
useMemoized(
() {
if (section != null) {
WidgetsBinding.instance.addPostFrameCallback(
(_) {
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;
scrollController.animateTo(
height,
duration: const Duration(milliseconds: 500),
curve: Curves.decelerate,
);
},
);
}
},
);
String experimental(String txt) {
return "$txt (${t.settings.experimental})";
@@ -34,6 +72,8 @@ class ConfigOptionsPage extends HookConsumerWidget {
return Scaffold(
body: CustomScrollView(
controller: scrollController,
shrinkWrap: true,
slivers: [
NestedAppBar(
title: Text(t.settings.config.pageTitle),
@@ -101,235 +141,255 @@ class ConfigOptionsPage extends HookConsumerWidget {
),
],
),
SliverList.list(
children: [
TipCard(message: t.settings.experimentalMsg),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.logLevel),
preferences: ref.watch(ConfigOptions.logLevel.notifier),
choices: LogLevel.choices,
title: t.settings.config.logLevel,
presentChoice: (value) => value.name.toUpperCase(),
SliverToBoxAdapter(
child: SingleChildScrollView(
child: Column(
children: [
TipCard(message: t.settings.experimentalMsg),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.logLevel),
preferences: ref.watch(ConfigOptions.logLevel.notifier),
choices: LogLevel.choices,
title: t.settings.config.logLevel,
presentChoice: (value) => value.name.toUpperCase(),
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.route),
SwitchListTile(
title: Text(experimental(t.settings.config.bypassLan)),
value: ref.watch(ConfigOptions.bypassLan),
onChanged:
ref.watch(ConfigOptions.bypassLan.notifier).update,
),
SwitchListTile(
title: Text(t.settings.config.resolveDestination),
value: ref.watch(ConfigOptions.resolveDestination),
onChanged: ref
.watch(ConfigOptions.resolveDestination.notifier)
.update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.ipv6Mode),
preferences: ref.watch(ConfigOptions.ipv6Mode.notifier),
choices: IPv6Mode.values,
title: t.settings.config.ipv6Mode,
presentChoice: (value) => value.present(t),
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.dns),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.remoteDnsAddress),
preferences:
ref.watch(ConfigOptions.remoteDnsAddress.notifier),
title: t.settings.config.remoteDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
preferences: ref
.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.settings.config.remoteDnsDomainStrategy,
presentChoice: (value) => value.displayName,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.directDnsAddress),
preferences:
ref.watch(ConfigOptions.directDnsAddress.notifier),
title: t.settings.config.directDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
preferences: ref
.watch(ConfigOptions.directDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.settings.config.directDnsDomainStrategy,
presentChoice: (value) => value.displayName,
),
SwitchListTile(
title: Text(t.settings.config.enableDnsRouting),
value: ref.watch(ConfigOptions.enableDnsRouting),
onChanged: ref
.watch(ConfigOptions.enableDnsRouting.notifier)
.update,
),
const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.mux)),
SwitchListTile(
title: Text(t.settings.config.enableMux),
value: ref.watch(ConfigOptions.enableMux),
onChanged:
ref.watch(ConfigOptions.enableMux.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.muxProtocol),
preferences: ref.watch(ConfigOptions.muxProtocol.notifier),
choices: MuxProtocol.values,
title: t.settings.config.muxProtocol,
presentChoice: (value) => value.name,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.muxMaxStreams),
preferences:
ref.watch(ConfigOptions.muxMaxStreams.notifier),
title: t.settings.config.muxMaxStreams,
inputToValue: int.tryParse,
digitsOnly: true,
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.inbound),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.serviceMode),
preferences: ref.watch(ConfigOptions.serviceMode.notifier),
choices: ServiceMode.choices,
title: t.settings.config.serviceMode,
presentChoice: (value) => value.present(t),
),
SwitchListTile(
title: Text(t.settings.config.strictRoute),
value: ref.watch(ConfigOptions.strictRoute),
onChanged:
ref.watch(ConfigOptions.strictRoute.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.tunImplementation),
preferences:
ref.watch(ConfigOptions.tunImplementation.notifier),
choices: TunImplementation.values,
title: t.settings.config.tunImplementation,
presentChoice: (value) => value.name,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.mixedPort),
preferences: ref.watch(ConfigOptions.mixedPort.notifier),
title: t.settings.config.mixedPort,
inputToValue: int.tryParse,
digitsOnly: true,
validateInput: isPort,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.localDnsPort),
preferences: ref.watch(ConfigOptions.localDnsPort.notifier),
title: t.settings.config.localDnsPort,
inputToValue: int.tryParse,
digitsOnly: true,
validateInput: isPort,
),
SwitchListTile(
title: Text(
experimental(t.settings.config.allowConnectionFromLan),
),
value: ref.watch(ConfigOptions.allowConnectionFromLan),
onChanged: ref
.read(ConfigOptions.allowConnectionFromLan.notifier)
.update,
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.tlsTricks),
SwitchListTile(
title:
Text(experimental(t.settings.config.enableTlsFragment)),
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged: ref
.watch(ConfigOptions.enableTlsFragment.notifier)
.update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSize),
preferences:
ref.watch(ConfigOptions.tlsFragmentSize.notifier),
title: t.settings.config.tlsFragmentSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSleep),
preferences:
ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
title: t.settings.config.tlsFragmentSleep,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
SwitchListTile(
title: Text(
experimental(t.settings.config.enableTlsMixedSniCase),
),
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
onChanged: ref
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
.update,
),
SwitchListTile(
title:
Text(experimental(t.settings.config.enableTlsPadding)),
value: ref.watch(ConfigOptions.enableTlsPadding),
onChanged: ref
.watch(ConfigOptions.enableTlsPadding.notifier)
.update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsPaddingSize),
preferences:
ref.watch(ConfigOptions.tlsPaddingSize.notifier),
title: t.settings.config.tlsPaddingSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.format(),
formatInputValue: (value) => value.format(),
),
const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.warp)),
WarpOptionsTiles(key: ConfigOptionSection._warpKey),
const SettingsDivider(),
SettingsSection(t.settings.config.section.misc),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.connectionTestUrl),
preferences:
ref.watch(ConfigOptions.connectionTestUrl.notifier),
title: t.settings.config.connectionTestUrl,
),
ListTile(
title: Text(t.settings.config.urlTestInterval),
subtitle: Text(
ref
.watch(ConfigOptions.urlTestInterval)
.toApproximateTime(isRelativeToNow: false),
),
onTap: () async {
final urlTestInterval = await SettingsSliderDialog(
title: t.settings.config.urlTestInterval,
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),
).show(context);
if (urlTestInterval == null) return;
await ref
.read(ConfigOptions.urlTestInterval.notifier)
.update(Duration(minutes: urlTestInterval.toInt()));
},
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.clashApiPort),
preferences: ref.watch(ConfigOptions.clashApiPort.notifier),
title: t.settings.config.clashApiPort,
validateInput: isPort,
digitsOnly: true,
inputToValue: int.tryParse,
),
const Gap(24),
],
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.route),
SwitchListTile(
title: Text(experimental(t.settings.config.bypassLan)),
value: ref.watch(ConfigOptions.bypassLan),
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update,
),
SwitchListTile(
title: Text(t.settings.config.resolveDestination),
value: ref.watch(ConfigOptions.resolveDestination),
onChanged:
ref.watch(ConfigOptions.resolveDestination.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.ipv6Mode),
preferences: ref.watch(ConfigOptions.ipv6Mode.notifier),
choices: IPv6Mode.values,
title: t.settings.config.ipv6Mode,
presentChoice: (value) => value.present(t),
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.dns),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.remoteDnsAddress),
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier),
title: t.settings.config.remoteDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
preferences:
ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.settings.config.remoteDnsDomainStrategy,
presentChoice: (value) => value.displayName,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.directDnsAddress),
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier),
title: t.settings.config.directDnsAddress,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
preferences:
ref.watch(ConfigOptions.directDnsDomainStrategy.notifier),
choices: DomainStrategy.values,
title: t.settings.config.directDnsDomainStrategy,
presentChoice: (value) => value.displayName,
),
SwitchListTile(
title: Text(t.settings.config.enableDnsRouting),
value: ref.watch(ConfigOptions.enableDnsRouting),
onChanged:
ref.watch(ConfigOptions.enableDnsRouting.notifier).update,
),
const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.mux)),
SwitchListTile(
title: Text(t.settings.config.enableMux),
value: ref.watch(ConfigOptions.enableMux),
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.muxProtocol),
preferences: ref.watch(ConfigOptions.muxProtocol.notifier),
choices: MuxProtocol.values,
title: t.settings.config.muxProtocol,
presentChoice: (value) => value.name,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.muxMaxStreams),
preferences: ref.watch(ConfigOptions.muxMaxStreams.notifier),
title: t.settings.config.muxMaxStreams,
inputToValue: int.tryParse,
digitsOnly: true,
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.inbound),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.serviceMode),
preferences: ref.watch(ConfigOptions.serviceMode.notifier),
choices: ServiceMode.choices,
title: t.settings.config.serviceMode,
presentChoice: (value) => value.present(t),
),
SwitchListTile(
title: Text(t.settings.config.strictRoute),
value: ref.watch(ConfigOptions.strictRoute),
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update,
),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.tunImplementation),
preferences:
ref.watch(ConfigOptions.tunImplementation.notifier),
choices: TunImplementation.values,
title: t.settings.config.tunImplementation,
presentChoice: (value) => value.name,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.mixedPort),
preferences: ref.watch(ConfigOptions.mixedPort.notifier),
title: t.settings.config.mixedPort,
inputToValue: int.tryParse,
digitsOnly: true,
validateInput: isPort,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.localDnsPort),
preferences: ref.watch(ConfigOptions.localDnsPort.notifier),
title: t.settings.config.localDnsPort,
inputToValue: int.tryParse,
digitsOnly: true,
validateInput: isPort,
),
SwitchListTile(
title: Text(
experimental(t.settings.config.allowConnectionFromLan),
),
value: ref.watch(ConfigOptions.allowConnectionFromLan),
onChanged: ref
.read(ConfigOptions.allowConnectionFromLan.notifier)
.update,
),
const SettingsDivider(),
SettingsSection(t.settings.config.section.tlsTricks),
SwitchListTile(
title: Text(experimental(t.settings.config.enableTlsFragment)),
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged:
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSize),
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier),
title: t.settings.config.tlsFragmentSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsFragmentSleep),
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
title: t.settings.config.tlsFragmentSleep,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.present(t),
formatInputValue: (value) => value.format(),
),
SwitchListTile(
title: Text(
experimental(t.settings.config.enableTlsMixedSniCase),
),
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
onChanged: ref
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
.update,
),
SwitchListTile(
title: Text(experimental(t.settings.config.enableTlsPadding)),
value: ref.watch(ConfigOptions.enableTlsPadding),
onChanged:
ref.watch(ConfigOptions.enableTlsPadding.notifier).update,
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.tlsPaddingSize),
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier),
title: t.settings.config.tlsPaddingSize,
inputToValue: OptionalRange.tryParse,
presentValue: (value) => value.format(),
formatInputValue: (value) => value.format(),
),
const SettingsDivider(),
SettingsSection(experimental(t.settings.config.section.warp)),
const WarpOptionsTiles(),
const SettingsDivider(),
SettingsSection(t.settings.config.section.misc),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.connectionTestUrl),
preferences:
ref.watch(ConfigOptions.connectionTestUrl.notifier),
title: t.settings.config.connectionTestUrl,
),
ListTile(
title: Text(t.settings.config.urlTestInterval),
subtitle: Text(
ref
.watch(ConfigOptions.urlTestInterval)
.toApproximateTime(isRelativeToNow: false),
),
onTap: () async {
final urlTestInterval = await SettingsSliderDialog(
title: t.settings.config.urlTestInterval,
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),
).show(context);
if (urlTestInterval == null) return;
await ref
.read(ConfigOptions.urlTestInterval.notifier)
.update(Duration(minutes: urlTestInterval.toInt()));
},
),
ValuePreferenceWidget(
value: ref.watch(ConfigOptions.clashApiPort),
preferences: ref.watch(ConfigOptions.clashApiPort.notifier),
title: t.settings.config.clashApiPort,
validateInput: isPort,
digitsOnly: true,
inputToValue: int.tryParse,
),
const Gap(24),
],
),
),
],
),

View File

@@ -0,0 +1,84 @@
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.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/singbox/model/singbox_config_enum.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class QuickSettingsModal extends HookConsumerWidget {
const QuickSettingsModal({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final warpPrefaceCompleted =
ref.watch(warpOptionNotifierProvider).consentGiven;
return SingleChildScrollView(
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16),
child: SegmentedButton(
segments: ServiceMode.choices
.map(
(e) => ButtonSegment(
value: e,
label: Text(
e.presentShort(t),
overflow: TextOverflow.ellipsis,
),
tooltip:
e.isExperimental ? t.settings.experimental : null,
),
)
.toList(),
selected: {ref.watch(ConfigOptions.serviceMode)},
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.settings.config.enableWarp),
)
else
ListTile(
title: Text(t.settings.config.setupWarp),
trailing: const Icon(FluentIcons.chevron_right_24_regular),
onTap: () =>
ConfigOptionsRoute(section: ConfigOptionSection.warp.name)
.go(context),
),
SwitchListTile(
value: ref.watch(ConfigOptions.enableTlsFragment),
onChanged:
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
title: Text(t.settings.config.enableTlsFragment),
),
SwitchListTile(
value: ref.watch(ConfigOptions.enableMux),
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update,
title: Text(t.settings.config.enableMux),
),
ListTile(
title: Text(t.settings.config.allOptions),
trailing: const Icon(FluentIcons.chevron_right_24_regular),
dense: true,
onTap: () => const ConfigOptionsRoute().go(context),
),
const Gap(16),
],
),
);
}
}