Add quick settings
This commit is contained in:
@@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart';
|
||||
import 'package:hiddify/core/router/app_router.dart';
|
||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||
import 'package:hiddify/features/config_option/overview/config_options_page.dart';
|
||||
import 'package:hiddify/features/config_option/widget/quick_settings_modal.dart';
|
||||
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
|
||||
import 'package:hiddify/features/home/widget/home_page.dart';
|
||||
import 'package:hiddify/features/intro/widget/intro_page.dart';
|
||||
@@ -47,6 +48,10 @@ GlobalKey<NavigatorState>? _dynamicRootKey =
|
||||
path: "config-options",
|
||||
name: ConfigOptionsRoute.name,
|
||||
),
|
||||
TypedGoRoute<QuickSettingsRoute>(
|
||||
path: "quick-settings",
|
||||
name: QuickSettingsRoute.name,
|
||||
),
|
||||
TypedGoRoute<SettingsRoute>(
|
||||
path: "settings",
|
||||
name: SettingsRoute.name,
|
||||
@@ -108,6 +113,10 @@ class MobileWrapperRoute extends ShellRouteData {
|
||||
path: "profiles/:id",
|
||||
name: ProfileDetailsRoute.name,
|
||||
),
|
||||
TypedGoRoute<QuickSettingsRoute>(
|
||||
path: "quick-settings",
|
||||
name: QuickSettingsRoute.name,
|
||||
),
|
||||
],
|
||||
),
|
||||
TypedGoRoute<ProxiesRoute>(
|
||||
@@ -277,6 +286,22 @@ class LogsOverviewRoute extends GoRouteData {
|
||||
}
|
||||
}
|
||||
|
||||
class QuickSettingsRoute extends GoRouteData {
|
||||
const QuickSettingsRoute();
|
||||
static const name = "Quick Settings";
|
||||
|
||||
static final GlobalKey<NavigatorState> $parentNavigatorKey = rootNavigatorKey;
|
||||
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
return BottomSheetPage(
|
||||
fixed: true,
|
||||
name: name,
|
||||
builder: (controller) => const QuickSettingsModal(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SettingsRoute extends GoRouteData {
|
||||
const SettingsRoute();
|
||||
static const name = "Settings";
|
||||
@@ -296,7 +321,8 @@ class SettingsRoute extends GoRouteData {
|
||||
}
|
||||
|
||||
class ConfigOptionsRoute extends GoRouteData {
|
||||
const ConfigOptionsRoute();
|
||||
const ConfigOptionsRoute({this.section});
|
||||
final String? section;
|
||||
static const name = "Config Options";
|
||||
|
||||
static final GlobalKey<NavigatorState>? $parentNavigatorKey = _dynamicRootKey;
|
||||
@@ -304,12 +330,15 @@ class ConfigOptionsRoute extends GoRouteData {
|
||||
@override
|
||||
Page<void> buildPage(BuildContext context, GoRouterState state) {
|
||||
if (useMobileRouter) {
|
||||
return const MaterialPage(
|
||||
return MaterialPage(
|
||||
name: name,
|
||||
child: ConfigOptionsPage(),
|
||||
child: ConfigOptionsPage(section: section),
|
||||
);
|
||||
}
|
||||
return const NoTransitionPage(name: name, child: ConfigOptionsPage());
|
||||
return NoTransitionPage(
|
||||
name: name,
|
||||
child: ConfigOptionsPage(section: section),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
84
lib/features/config_option/widget/quick_settings_modal.dart
Normal file
84
lib/features/config_option/widget/quick_settings_modal.dart
Normal 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),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -45,6 +45,11 @@ class HomePage extends HookConsumerWidget {
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
IconButton(
|
||||
onPressed: () => const QuickSettingsRoute().push(context),
|
||||
icon: const Icon(FluentIcons.options_24_filled),
|
||||
tooltip: t.settings.config.quickSettings,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () => const AddProfileRoute().push(context),
|
||||
icon: const Icon(FluentIcons.add_circle_24_filled),
|
||||
|
||||
@@ -29,13 +29,26 @@ enum ServiceMode {
|
||||
return [proxy, tun];
|
||||
}
|
||||
|
||||
bool get isExperimental => switch (this) {
|
||||
tun => PlatformUtils.isDesktop,
|
||||
tunService => PlatformUtils.isDesktop,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
String present(TranslationsEn t) => switch (this) {
|
||||
proxy => t.settings.config.serviceModes.proxy,
|
||||
systemProxy => t.settings.config.serviceModes.systemProxy,
|
||||
tun =>
|
||||
"${t.settings.config.serviceModes.tun}${PlatformUtils.isDesktop ? " (${t.settings.experimental})" : ""}",
|
||||
"${t.settings.config.serviceModes.tun}${isExperimental ? " (${t.settings.experimental})" : ""}",
|
||||
tunService =>
|
||||
"${t.settings.config.serviceModes.tunService}${PlatformUtils.isDesktop ? " (${t.settings.experimental})" : ""}",
|
||||
"${t.settings.config.serviceModes.tunService}${isExperimental ? " (${t.settings.experimental})" : ""}",
|
||||
};
|
||||
|
||||
String presentShort(TranslationsEn t) => switch (this) {
|
||||
proxy => t.settings.config.shortServiceModes.proxy,
|
||||
systemProxy => t.settings.config.shortServiceModes.systemProxy,
|
||||
tun => t.settings.config.shortServiceModes.tun,
|
||||
tunService => t.settings.config.shortServiceModes.tunService,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user