diff --git a/Makefile b/Makefile index ee84238c..c310634f 100644 --- a/Makefile +++ b/Makefile @@ -182,7 +182,6 @@ build-linux-libs: build-macos-libs: make -C libcore -f Makefile macos-universal - mv $(BINDIR)/$(SRV_NAME) $(DESKTOP_OUT)/ build-ios-libs: rf -rf $(IOS_OUT)/Libcore.xcframework diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 3aee9c91..7526e05b 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -228,12 +228,21 @@ "reconnectMsg": "Reconnect for changes to take effect", "reconnectBtn": "Reconnect", "serviceMode": "Service Mode", + "quickSettings": "Quick Settings", + "setupWarp": "Setup WARP", + "allOptions": "All Config Options", "serviceModes": { "proxy": "Proxy Service Only", "systemProxy": "Set System Proxy", "tun": "VPN", "tunService": "VPN Service" }, + "shortServiceModes": { + "proxy": "Proxy", + "systemProxy": "System Proxy", + "tun": "VPN", + "tunService": "VPN Service" + }, "section": { "route": "Route Options", "dns": "DNS Options", diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index f7a1f349..0cfce287 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -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? _dynamicRootKey = path: "config-options", name: ConfigOptionsRoute.name, ), + TypedGoRoute( + path: "quick-settings", + name: QuickSettingsRoute.name, + ), TypedGoRoute( path: "settings", name: SettingsRoute.name, @@ -108,6 +113,10 @@ class MobileWrapperRoute extends ShellRouteData { path: "profiles/:id", name: ProfileDetailsRoute.name, ), + TypedGoRoute( + path: "quick-settings", + name: QuickSettingsRoute.name, + ), ], ), TypedGoRoute( @@ -277,6 +286,22 @@ class LogsOverviewRoute extends GoRouteData { } } +class QuickSettingsRoute extends GoRouteData { + const QuickSettingsRoute(); + static const name = "Quick Settings"; + + static final GlobalKey $parentNavigatorKey = rootNavigatorKey; + + @override + Page 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? $parentNavigatorKey = _dynamicRootKey; @@ -304,12 +330,15 @@ class ConfigOptionsRoute extends GoRouteData { @override Page 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), + ); } } diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index a916cad1..bc9764ff 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -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), - ], + ), ), ], ), diff --git a/lib/features/config_option/widget/quick_settings_modal.dart b/lib/features/config_option/widget/quick_settings_modal.dart new file mode 100644 index 00000000..e546091c --- /dev/null +++ b/lib/features/config_option/widget/quick_settings_modal.dart @@ -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), + ], + ), + ); + } +} diff --git a/lib/features/home/widget/home_page.dart b/lib/features/home/widget/home_page.dart index 13b20d30..0bf6d4a5 100644 --- a/lib/features/home/widget/home_page.dart +++ b/lib/features/home/widget/home_page.dart @@ -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), diff --git a/lib/singbox/model/singbox_config_enum.dart b/lib/singbox/model/singbox_config_enum.dart index 49c157a7..82990e8a 100644 --- a/lib/singbox/model/singbox_config_enum.dart +++ b/lib/singbox/model/singbox_config_enum.dart @@ -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, }; }