diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index b9dbee2f..6207e66a 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -50,29 +50,30 @@ GoRouter router(RouterRef ref) { ); } +final tabLocations = [ + const HomeRoute().location, + const ProxiesRoute().location, + const ConfigOptionsRoute().location, + const SettingsRoute().location, + const LogsOverviewRoute().location, + const AboutRoute().location, +]; + int getCurrentIndex(BuildContext context) { final String location = GoRouterState.of(context).uri.path; if (location == const HomeRoute().location) return 0; - if (location.startsWith(const ProxiesRoute().location)) return 1; - if (location.startsWith(const LogsOverviewRoute().location)) return 2; - if (location.startsWith(const SettingsRoute().location)) return 3; - if (location.startsWith(const AboutRoute().location)) return 4; + var index = 0; + for (final tab in tabLocations.sublist(1)) { + index++; + if (location.startsWith(tab)) return index; + } return 0; } void switchTab(int index, BuildContext context) { - switch (index) { - case 0: - const HomeRoute().go(context); - case 1: - const ProxiesRoute().go(context); - case 2: - const LogsOverviewRoute().go(context); - case 3: - const SettingsRoute().go(context); - case 4: - const AboutRoute().go(context); - } + assert(index >= 0 && index < tabLocations.length); + final location = tabLocations[index]; + return context.go(location); } @riverpod diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index ced9f8fc..5e2423c4 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -43,18 +43,14 @@ GlobalKey? _dynamicRootKey = path: "profiles/:id", name: ProfileDetailsRoute.name, ), - TypedGoRoute( - path: "logs", - name: LogsOverviewRoute.name, + TypedGoRoute( + path: "config-options", + name: ConfigOptionsRoute.name, ), TypedGoRoute( path: "settings", name: SettingsRoute.name, routes: [ - TypedGoRoute( - path: "config-options", - name: ConfigOptionsRoute.name, - ), TypedGoRoute( path: "per-app-proxy", name: PerAppProxyRoute.name, @@ -65,6 +61,10 @@ GlobalKey? _dynamicRootKey = ), ], ), + TypedGoRoute( + path: "logs", + name: LogsOverviewRoute.name, + ), TypedGoRoute( path: "about", name: AboutRoute.name, @@ -114,24 +114,24 @@ class MobileWrapperRoute extends ShellRouteData { path: "/proxies", name: ProxiesRoute.name, ), - TypedGoRoute( - path: "/logs", - name: LogsOverviewRoute.name, + TypedGoRoute( + path: "/config-options", + name: ConfigOptionsRoute.name, ), TypedGoRoute( path: "/settings", name: SettingsRoute.name, routes: [ - TypedGoRoute( - path: "config-options", - name: ConfigOptionsRoute.name, - ), TypedGoRoute( path: "routing-assets", name: GeoAssetsRoute.name, ), ], ), + TypedGoRoute( + path: "/logs", + name: LogsOverviewRoute.name, + ), TypedGoRoute( path: "/about", name: AboutRoute.name, @@ -309,11 +309,7 @@ class ConfigOptionsRoute extends GoRouteData { child: ConfigOptionsPage(), ); } - return const MaterialPage( - fullscreenDialog: true, - name: name, - child: ConfigOptionsPage(), - ); + return const NoTransitionPage(name: name, child: ConfigOptionsPage()); } } diff --git a/lib/features/common/adaptive_root_scaffold.dart b/lib/features/common/adaptive_root_scaffold.dart index 99c1d2bb..6f235c65 100644 --- a/lib/features/common/adaptive_root_scaffold.dart +++ b/lib/features/common/adaptive_root_scaffold.dart @@ -1,3 +1,4 @@ +import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; import 'package:hiddify/core/localization/translations.dart'; @@ -25,23 +26,27 @@ class AdaptiveRootScaffold extends HookConsumerWidget { final destinations = [ NavigationDestination( - icon: const Icon(Icons.power_settings_new), + icon: const Icon(FluentIcons.power_20_filled), label: t.home.pageTitle, ), NavigationDestination( - icon: const Icon(Icons.filter_list), + icon: const Icon(FluentIcons.filter_20_filled), label: t.proxies.pageTitle, ), NavigationDestination( - icon: const Icon(Icons.article), - label: t.logs.pageTitle, + icon: const Icon(FluentIcons.box_edit_20_filled), + label: t.settings.config.pageTitle, ), NavigationDestination( - icon: const Icon(Icons.settings), + icon: const Icon(FluentIcons.settings_20_filled), label: t.settings.pageTitle, ), NavigationDestination( - icon: const Icon(Icons.info), + icon: const Icon(FluentIcons.document_text_20_filled), + label: t.logs.pageTitle, + ), + NavigationDestination( + icon: const Icon(FluentIcons.info_20_filled), label: t.about.pageTitle, ), ]; diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index c2df00cd..2ed1f8f2 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -6,6 +6,7 @@ import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/model/range.dart'; import 'package:hiddify/core/widget/tip_card.dart'; +import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/config_option/model/config_option_entity.dart'; import 'package:hiddify/features/config_option/model/config_option_patch.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; @@ -37,447 +38,470 @@ class ConfigOptionsPage extends HookConsumerWidget { } return Scaffold( - appBar: AppBar( - title: Text(t.settings.config.pageTitle), - actions: [ - if (asyncOptions case AsyncData(value: final options)) - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(t.general.addToClipboard), - onTap: () { - Clipboard.setData( - ClipboardData(text: options.format()), + body: CustomScrollView( + slivers: [ + NestedAppBar( + title: Text(t.settings.config.pageTitle), + actions: [ + if (asyncOptions case AsyncData(value: final options)) + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(t.general.addToClipboard), + onTap: () { + Clipboard.setData( + ClipboardData(text: options.format()), + ); + }, + ), + PopupMenuItem( + child: Text(t.settings.config.resetBtn), + onTap: () async { + await ref + .read(configOptionNotifierProvider.notifier) + .resetOption(); + }, + ), + ]; + }, + ), + ], + ), + switch (asyncOptions) { + AsyncData(value: final options) => SliverList.list( + children: [ + TipCard(message: t.settings.experimentalMsg), + ListTile( + title: Text(t.settings.config.logLevel), + subtitle: Text(options.logLevel.name.toUpperCase()), + onTap: () async { + final logLevel = await SettingsPickerDialog( + title: t.settings.config.logLevel, + selected: options.logLevel, + options: LogLevel.choices, + getTitle: (e) => e.name.toUpperCase(), + resetValue: defaultOptions.logLevel, + ).show(context); + if (logLevel == null) return; + await changeOption(ConfigOptionPatch(logLevel: logLevel)); + }, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.route), + SwitchListTile( + title: Text(experimental(t.settings.config.bypassLan)), + value: options.bypassLan, + onChanged: (value) async => + changeOption(ConfigOptionPatch(bypassLan: value)), + ), + SwitchListTile( + title: Text(t.settings.config.resolveDestination), + value: options.resolveDestination, + onChanged: (value) async => changeOption( + ConfigOptionPatch(resolveDestination: value), + ), + ), + ListTile( + title: Text(t.settings.config.ipv6Mode), + subtitle: Text(options.ipv6Mode.present(t)), + onTap: () async { + final ipv6Mode = await SettingsPickerDialog( + title: t.settings.config.ipv6Mode, + selected: options.ipv6Mode, + options: IPv6Mode.values, + getTitle: (e) => e.present(t), + resetValue: defaultOptions.ipv6Mode, + ).show(context); + if (ipv6Mode == null) return; + await changeOption(ConfigOptionPatch(ipv6Mode: ipv6Mode)); + }, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.dns), + ListTile( + title: Text(t.settings.config.remoteDnsAddress), + subtitle: Text(options.remoteDnsAddress), + onTap: () async { + final url = await SettingsInputDialog( + title: t.settings.config.remoteDnsAddress, + initialValue: options.remoteDnsAddress, + resetValue: defaultOptions.remoteDnsAddress, + ).show(context); + if (url == null || url.isEmpty) return; + await changeOption( + ConfigOptionPatch(remoteDnsAddress: url), ); }, ), - PopupMenuItem( - child: Text(t.settings.config.resetBtn), + ListTile( + title: Text(t.settings.config.remoteDnsDomainStrategy), + subtitle: Text(options.remoteDnsDomainStrategy.displayName), onTap: () async { - await ref - .read(configOptionNotifierProvider.notifier) - .resetOption(); + final domainStrategy = await SettingsPickerDialog( + title: t.settings.config.remoteDnsDomainStrategy, + selected: options.remoteDnsDomainStrategy, + options: DomainStrategy.values, + getTitle: (e) => e.displayName, + resetValue: defaultOptions.remoteDnsDomainStrategy, + ).show(context); + if (domainStrategy == null) return; + await changeOption( + ConfigOptionPatch( + remoteDnsDomainStrategy: domainStrategy, + ), + ); }, ), - ]; - }, - ), - ], - ), - body: switch (asyncOptions) { - AsyncData(value: final options) => ListView( - children: [ - TipCard(message: t.settings.experimentalMsg), - ListTile( - title: Text(t.settings.config.logLevel), - subtitle: Text(options.logLevel.name.toUpperCase()), - onTap: () async { - final logLevel = await SettingsPickerDialog( - title: t.settings.config.logLevel, - selected: options.logLevel, - options: LogLevel.choices, - getTitle: (e) => e.name.toUpperCase(), - resetValue: defaultOptions.logLevel, - ).show(context); - if (logLevel == null) return; - await changeOption(ConfigOptionPatch(logLevel: logLevel)); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.route), - SwitchListTile( - title: Text(experimental(t.settings.config.bypassLan)), - value: options.bypassLan, - onChanged: (value) async => - changeOption(ConfigOptionPatch(bypassLan: value)), - ), - SwitchListTile( - title: Text(t.settings.config.resolveDestination), - value: options.resolveDestination, - onChanged: (value) async => - changeOption(ConfigOptionPatch(resolveDestination: value)), - ), - ListTile( - title: Text(t.settings.config.ipv6Mode), - subtitle: Text(options.ipv6Mode.present(t)), - onTap: () async { - final ipv6Mode = await SettingsPickerDialog( - title: t.settings.config.ipv6Mode, - selected: options.ipv6Mode, - options: IPv6Mode.values, - getTitle: (e) => e.present(t), - resetValue: defaultOptions.ipv6Mode, - ).show(context); - if (ipv6Mode == null) return; - await changeOption(ConfigOptionPatch(ipv6Mode: ipv6Mode)); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.dns), - ListTile( - title: Text(t.settings.config.remoteDnsAddress), - subtitle: Text(options.remoteDnsAddress), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.remoteDnsAddress, - initialValue: options.remoteDnsAddress, - resetValue: defaultOptions.remoteDnsAddress, - ).show(context); - if (url == null || url.isEmpty) return; - await changeOption(ConfigOptionPatch(remoteDnsAddress: url)); - }, - ), - ListTile( - title: Text(t.settings.config.remoteDnsDomainStrategy), - subtitle: Text(options.remoteDnsDomainStrategy.displayName), - onTap: () async { - final domainStrategy = await SettingsPickerDialog( - title: t.settings.config.remoteDnsDomainStrategy, - selected: options.remoteDnsDomainStrategy, - options: DomainStrategy.values, - getTitle: (e) => e.displayName, - resetValue: defaultOptions.remoteDnsDomainStrategy, - ).show(context); - if (domainStrategy == null) return; - await changeOption( - ConfigOptionPatch(remoteDnsDomainStrategy: domainStrategy), - ); - }, - ), - ListTile( - title: Text(t.settings.config.directDnsAddress), - subtitle: Text(options.directDnsAddress), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.directDnsAddress, - initialValue: options.directDnsAddress, - resetValue: defaultOptions.directDnsAddress, - ).show(context); - if (url == null || url.isEmpty) return; - await changeOption(ConfigOptionPatch(directDnsAddress: url)); - }, - ), - ListTile( - title: Text(t.settings.config.directDnsDomainStrategy), - subtitle: Text(options.directDnsDomainStrategy.displayName), - onTap: () async { - final domainStrategy = await SettingsPickerDialog( - title: t.settings.config.directDnsDomainStrategy, - selected: options.directDnsDomainStrategy, - options: DomainStrategy.values, - getTitle: (e) => e.displayName, - resetValue: defaultOptions.directDnsDomainStrategy, - ).show(context); - if (domainStrategy == null) return; - await changeOption( - ConfigOptionPatch(directDnsDomainStrategy: domainStrategy), - ); - }, - ), - SwitchListTile( - title: Text(t.settings.config.enableDnsRouting), - value: options.enableDnsRouting, - onChanged: (value) => changeOption( - ConfigOptionPatch(enableDnsRouting: value), - ), - ), - const SettingsDivider(), - SettingsSection(experimental(t.settings.config.section.mux)), - SwitchListTile( - title: Text(t.settings.config.enableMux), - value: options.enableMux, - onChanged: (value) => changeOption( - ConfigOptionPatch(enableMux: value), - ), - ), - ListTile( - title: Text(t.settings.config.muxProtocol), - subtitle: Text(options.muxProtocol.name), - onTap: () async { - final pickedProtocol = await SettingsPickerDialog( - title: t.settings.config.muxProtocol, - selected: options.muxProtocol, - options: MuxProtocol.values, - getTitle: (e) => e.name, - resetValue: defaultOptions.muxProtocol, - ).show(context); - if (pickedProtocol == null) return; - await changeOption( - ConfigOptionPatch(muxProtocol: pickedProtocol), - ); - }, - ), - ListTile( - title: Text(t.settings.config.muxMaxStreams), - subtitle: Text(options.muxMaxStreams.toString()), - onTap: () async { - final maxStreams = await SettingsInputDialog( - title: t.settings.config.muxMaxStreams, - initialValue: options.muxMaxStreams, - resetValue: defaultOptions.muxMaxStreams, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (maxStreams == null || maxStreams < 1) return; - await changeOption( - ConfigOptionPatch(muxMaxStreams: maxStreams), - ); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.inbound), - ListTile( - title: Text(t.settings.config.serviceMode), - subtitle: Text(options.serviceMode.present(t)), - onTap: () async { - final pickedMode = await SettingsPickerDialog( - title: t.settings.config.serviceMode, - selected: options.serviceMode, - options: ServiceMode.choices, - getTitle: (e) => e.present(t), - resetValue: ServiceMode.defaultMode, - ).show(context); - if (pickedMode == null) return; - await changeOption( - ConfigOptionPatch(serviceMode: pickedMode), - ); - }, - ), - SwitchListTile( - title: Text(t.settings.config.strictRoute), - value: options.strictRoute, - onChanged: (value) async => - changeOption(ConfigOptionPatch(strictRoute: value)), - ), - ListTile( - title: Text(t.settings.config.tunImplementation), - subtitle: Text(options.tunImplementation.name), - onTap: () async { - final tunImplementation = await SettingsPickerDialog( - title: t.settings.config.tunImplementation, - selected: options.tunImplementation, - options: TunImplementation.values, - getTitle: (e) => e.name, - resetValue: defaultOptions.tunImplementation, - ).show(context); - if (tunImplementation == null) return; - await changeOption( - ConfigOptionPatch(tunImplementation: tunImplementation), - ); - }, - ), - ListTile( - title: Text(t.settings.config.mixedPort), - subtitle: Text(options.mixedPort.toString()), - onTap: () async { - final mixedPort = await SettingsInputDialog( - title: t.settings.config.mixedPort, - initialValue: options.mixedPort, - resetValue: defaultOptions.mixedPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (mixedPort == null) return; - await changeOption(ConfigOptionPatch(mixedPort: mixedPort)); - }, - ), - ListTile( - title: Text(t.settings.config.localDnsPort), - subtitle: Text(options.localDnsPort.toString()), - onTap: () async { - final localDnsPort = await SettingsInputDialog( - title: t.settings.config.localDnsPort, - initialValue: options.localDnsPort, - resetValue: defaultOptions.localDnsPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (localDnsPort == null) return; - await changeOption( - ConfigOptionPatch(localDnsPort: localDnsPort), - ); - }, - ), - SwitchListTile( - title: Text( - experimental(t.settings.config.allowConnectionFromLan), - ), - value: options.allowConnectionFromLan, - onChanged: (value) => changeOption( - ConfigOptionPatch(allowConnectionFromLan: value), - ), - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.tlsTricks), - SwitchListTile( - title: Text(experimental(t.settings.config.enableTlsFragment)), - value: options.enableTlsFragment, - onChanged: (value) async => - changeOption(ConfigOptionPatch(enableTlsFragment: value)), - ), - ListTile( - title: Text(t.settings.config.tlsFragmentSize), - subtitle: Text(options.tlsFragmentSize.present(t)), - onTap: () async { - final range = await SettingsInputDialog( - title: t.settings.config.tlsFragmentSize, - initialValue: options.tlsFragmentSize.format(), - resetValue: defaultOptions.tlsFragmentSize.format(), - ).show(context); - if (range == null) return; - await changeOption( - ConfigOptionPatch( - tlsFragmentSize: RangeWithOptionalCeil.tryParse(range), - ), - ); - }, - ), - ListTile( - title: Text(t.settings.config.tlsFragmentSleep), - subtitle: Text(options.tlsFragmentSleep.present(t)), - onTap: () async { - final range = await SettingsInputDialog( - title: t.settings.config.tlsFragmentSleep, - initialValue: options.tlsFragmentSleep.format(), - resetValue: defaultOptions.tlsFragmentSleep.format(), - ).show(context); - if (range == null) return; - await changeOption( - ConfigOptionPatch( - tlsFragmentSleep: RangeWithOptionalCeil.tryParse(range), - ), - ); - }, - ), - SwitchListTile( - title: - Text(experimental(t.settings.config.enableTlsMixedSniCase)), - value: options.enableTlsMixedSniCase, - onChanged: (value) async => changeOption( - ConfigOptionPatch(enableTlsMixedSniCase: value), - ), - ), - SwitchListTile( - title: Text(experimental(t.settings.config.enableTlsPadding)), - value: options.enableTlsPadding, - onChanged: (value) async => changeOption( - ConfigOptionPatch(enableTlsPadding: value), - ), - ), - ListTile( - title: Text(t.settings.config.tlsPaddingSize), - subtitle: Text(options.tlsPaddingSize.present(t)), - onTap: () async { - final range = await SettingsInputDialog( - title: t.settings.config.tlsPaddingSize, - initialValue: options.tlsPaddingSize.format(), - resetValue: defaultOptions.tlsPaddingSize.format(), - ).show(context); - if (range == null) return; - await changeOption( - ConfigOptionPatch( - tlsPaddingSize: RangeWithOptionalCeil.tryParse(range), - ), - ); - }, - ), - const SettingsDivider(), - SettingsSection(experimental(t.settings.config.section.warp)), - WarpOptionsTiles( - options: options, - defaultOptions: defaultOptions, - onChange: changeOption, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.misc), - ListTile( - title: Text(t.settings.config.connectionTestUrl), - subtitle: Text(options.connectionTestUrl), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.connectionTestUrl, - initialValue: options.connectionTestUrl, - resetValue: defaultOptions.connectionTestUrl, - ).show(context); - if (url == null || url.isEmpty || !isUrl(url)) return; - await changeOption(ConfigOptionPatch(connectionTestUrl: url)); - }, - ), - ListTile( - title: Text(t.settings.config.urlTestInterval), - subtitle: Text( - options.urlTestInterval - .toApproximateTime(isRelativeToNow: false), - ), - onTap: () async { - final urlTestInterval = await SettingsSliderDialog( - title: t.settings.config.urlTestInterval, - initialValue: options.urlTestInterval.inMinutes - .coerceIn(0, 60) - .toDouble(), - resetValue: - defaultOptions.urlTestInterval.inMinutes.toDouble(), - min: 1, - max: 60, - divisions: 60, - labelGen: (value) => Duration(minutes: value.toInt()) - .toApproximateTime(isRelativeToNow: false), - ).show(context); - if (urlTestInterval == null) return; - await changeOption( - ConfigOptionPatch( - urlTestInterval: - Duration(minutes: urlTestInterval.toInt()), - ), - ); - }, - ), - ListTile( - title: Text(t.settings.config.clashApiPort), - subtitle: Text(options.clashApiPort.toString()), - onTap: () async { - final clashApiPort = await SettingsInputDialog( - title: t.settings.config.clashApiPort, - initialValue: options.clashApiPort, - resetValue: defaultOptions.clashApiPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (clashApiPort == null) return; - await changeOption( - ConfigOptionPatch(clashApiPort: clashApiPort), - ); - }, - ), - const Gap(24), - ], - ), - AsyncError(:final error) => Center( - child: SingleChildScrollView( - child: Column( - children: [ - const Icon(Icons.error), - const Gap(2), - Text(t.presentShortError(error)), - const Gap(2), - TextButton( - onPressed: () async { - await ref - .read(configOptionNotifierProvider.notifier) - .resetOption(); + ListTile( + title: Text(t.settings.config.directDnsAddress), + subtitle: Text(options.directDnsAddress), + onTap: () async { + final url = await SettingsInputDialog( + title: t.settings.config.directDnsAddress, + initialValue: options.directDnsAddress, + resetValue: defaultOptions.directDnsAddress, + ).show(context); + if (url == null || url.isEmpty) return; + await changeOption( + ConfigOptionPatch(directDnsAddress: url), + ); }, - child: Text(t.settings.config.resetBtn), ), + ListTile( + title: Text(t.settings.config.directDnsDomainStrategy), + subtitle: Text(options.directDnsDomainStrategy.displayName), + onTap: () async { + final domainStrategy = await SettingsPickerDialog( + title: t.settings.config.directDnsDomainStrategy, + selected: options.directDnsDomainStrategy, + options: DomainStrategy.values, + getTitle: (e) => e.displayName, + resetValue: defaultOptions.directDnsDomainStrategy, + ).show(context); + if (domainStrategy == null) return; + await changeOption( + ConfigOptionPatch( + directDnsDomainStrategy: domainStrategy, + ), + ); + }, + ), + SwitchListTile( + title: Text(t.settings.config.enableDnsRouting), + value: options.enableDnsRouting, + onChanged: (value) => changeOption( + ConfigOptionPatch(enableDnsRouting: value), + ), + ), + const SettingsDivider(), + SettingsSection(experimental(t.settings.config.section.mux)), + SwitchListTile( + title: Text(t.settings.config.enableMux), + value: options.enableMux, + onChanged: (value) => changeOption( + ConfigOptionPatch(enableMux: value), + ), + ), + ListTile( + title: Text(t.settings.config.muxProtocol), + subtitle: Text(options.muxProtocol.name), + onTap: () async { + final pickedProtocol = await SettingsPickerDialog( + title: t.settings.config.muxProtocol, + selected: options.muxProtocol, + options: MuxProtocol.values, + getTitle: (e) => e.name, + resetValue: defaultOptions.muxProtocol, + ).show(context); + if (pickedProtocol == null) return; + await changeOption( + ConfigOptionPatch(muxProtocol: pickedProtocol), + ); + }, + ), + ListTile( + title: Text(t.settings.config.muxMaxStreams), + subtitle: Text(options.muxMaxStreams.toString()), + onTap: () async { + final maxStreams = await SettingsInputDialog( + title: t.settings.config.muxMaxStreams, + initialValue: options.muxMaxStreams, + resetValue: defaultOptions.muxMaxStreams, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (maxStreams == null || maxStreams < 1) return; + await changeOption( + ConfigOptionPatch(muxMaxStreams: maxStreams), + ); + }, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.inbound), + ListTile( + title: Text(t.settings.config.serviceMode), + subtitle: Text(options.serviceMode.present(t)), + onTap: () async { + final pickedMode = await SettingsPickerDialog( + title: t.settings.config.serviceMode, + selected: options.serviceMode, + options: ServiceMode.choices, + getTitle: (e) => e.present(t), + resetValue: ServiceMode.defaultMode, + ).show(context); + if (pickedMode == null) return; + await changeOption( + ConfigOptionPatch(serviceMode: pickedMode), + ); + }, + ), + SwitchListTile( + title: Text(t.settings.config.strictRoute), + value: options.strictRoute, + onChanged: (value) async => + changeOption(ConfigOptionPatch(strictRoute: value)), + ), + ListTile( + title: Text(t.settings.config.tunImplementation), + subtitle: Text(options.tunImplementation.name), + onTap: () async { + final tunImplementation = await SettingsPickerDialog( + title: t.settings.config.tunImplementation, + selected: options.tunImplementation, + options: TunImplementation.values, + getTitle: (e) => e.name, + resetValue: defaultOptions.tunImplementation, + ).show(context); + if (tunImplementation == null) return; + await changeOption( + ConfigOptionPatch(tunImplementation: tunImplementation), + ); + }, + ), + ListTile( + title: Text(t.settings.config.mixedPort), + subtitle: Text(options.mixedPort.toString()), + onTap: () async { + final mixedPort = await SettingsInputDialog( + title: t.settings.config.mixedPort, + initialValue: options.mixedPort, + resetValue: defaultOptions.mixedPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (mixedPort == null) return; + await changeOption( + ConfigOptionPatch(mixedPort: mixedPort), + ); + }, + ), + ListTile( + title: Text(t.settings.config.localDnsPort), + subtitle: Text(options.localDnsPort.toString()), + onTap: () async { + final localDnsPort = await SettingsInputDialog( + title: t.settings.config.localDnsPort, + initialValue: options.localDnsPort, + resetValue: defaultOptions.localDnsPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (localDnsPort == null) return; + await changeOption( + ConfigOptionPatch(localDnsPort: localDnsPort), + ); + }, + ), + SwitchListTile( + title: Text( + experimental(t.settings.config.allowConnectionFromLan), + ), + value: options.allowConnectionFromLan, + onChanged: (value) => changeOption( + ConfigOptionPatch(allowConnectionFromLan: value), + ), + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.tlsTricks), + SwitchListTile( + title: + Text(experimental(t.settings.config.enableTlsFragment)), + value: options.enableTlsFragment, + onChanged: (value) async => changeOption( + ConfigOptionPatch(enableTlsFragment: value), + ), + ), + ListTile( + title: Text(t.settings.config.tlsFragmentSize), + subtitle: Text(options.tlsFragmentSize.present(t)), + onTap: () async { + final range = await SettingsInputDialog( + title: t.settings.config.tlsFragmentSize, + initialValue: options.tlsFragmentSize.format(), + resetValue: defaultOptions.tlsFragmentSize.format(), + ).show(context); + if (range == null) return; + await changeOption( + ConfigOptionPatch( + tlsFragmentSize: + RangeWithOptionalCeil.tryParse(range), + ), + ); + }, + ), + ListTile( + title: Text(t.settings.config.tlsFragmentSleep), + subtitle: Text(options.tlsFragmentSleep.present(t)), + onTap: () async { + final range = await SettingsInputDialog( + title: t.settings.config.tlsFragmentSleep, + initialValue: options.tlsFragmentSleep.format(), + resetValue: defaultOptions.tlsFragmentSleep.format(), + ).show(context); + if (range == null) return; + await changeOption( + ConfigOptionPatch( + tlsFragmentSleep: + RangeWithOptionalCeil.tryParse(range), + ), + ); + }, + ), + SwitchListTile( + title: Text( + experimental(t.settings.config.enableTlsMixedSniCase), + ), + value: options.enableTlsMixedSniCase, + onChanged: (value) async => changeOption( + ConfigOptionPatch(enableTlsMixedSniCase: value), + ), + ), + SwitchListTile( + title: + Text(experimental(t.settings.config.enableTlsPadding)), + value: options.enableTlsPadding, + onChanged: (value) async => changeOption( + ConfigOptionPatch(enableTlsPadding: value), + ), + ), + ListTile( + title: Text(t.settings.config.tlsPaddingSize), + subtitle: Text(options.tlsPaddingSize.present(t)), + onTap: () async { + final range = await SettingsInputDialog( + title: t.settings.config.tlsPaddingSize, + initialValue: options.tlsPaddingSize.format(), + resetValue: defaultOptions.tlsPaddingSize.format(), + ).show(context); + if (range == null) return; + await changeOption( + ConfigOptionPatch( + tlsPaddingSize: RangeWithOptionalCeil.tryParse(range), + ), + ); + }, + ), + const SettingsDivider(), + SettingsSection(experimental(t.settings.config.section.warp)), + WarpOptionsTiles( + options: options, + defaultOptions: defaultOptions, + onChange: changeOption, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.misc), + ListTile( + title: Text(t.settings.config.connectionTestUrl), + subtitle: Text(options.connectionTestUrl), + onTap: () async { + final url = await SettingsInputDialog( + title: t.settings.config.connectionTestUrl, + initialValue: options.connectionTestUrl, + resetValue: defaultOptions.connectionTestUrl, + ).show(context); + if (url == null || url.isEmpty || !isUrl(url)) return; + await changeOption( + ConfigOptionPatch(connectionTestUrl: url), + ); + }, + ), + ListTile( + title: Text(t.settings.config.urlTestInterval), + subtitle: Text( + options.urlTestInterval + .toApproximateTime(isRelativeToNow: false), + ), + onTap: () async { + final urlTestInterval = await SettingsSliderDialog( + title: t.settings.config.urlTestInterval, + initialValue: options.urlTestInterval.inMinutes + .coerceIn(0, 60) + .toDouble(), + resetValue: + defaultOptions.urlTestInterval.inMinutes.toDouble(), + min: 1, + max: 60, + divisions: 60, + labelGen: (value) => Duration(minutes: value.toInt()) + .toApproximateTime(isRelativeToNow: false), + ).show(context); + if (urlTestInterval == null) return; + await changeOption( + ConfigOptionPatch( + urlTestInterval: + Duration(minutes: urlTestInterval.toInt()), + ), + ); + }, + ), + ListTile( + title: Text(t.settings.config.clashApiPort), + subtitle: Text(options.clashApiPort.toString()), + onTap: () async { + final clashApiPort = await SettingsInputDialog( + title: t.settings.config.clashApiPort, + initialValue: options.clashApiPort, + resetValue: defaultOptions.clashApiPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (clashApiPort == null) return; + await changeOption( + ConfigOptionPatch(clashApiPort: clashApiPort), + ); + }, + ), + const Gap(24), ], ), - ), - ), - _ => const SizedBox(), - }, + AsyncError(:final error) => SliverFillRemaining( + hasScrollBody: false, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon(Icons.error), + const Gap(2), + Text(t.presentShortError(error)), + const Gap(2), + TextButton( + onPressed: () async { + await ref + .read(configOptionNotifierProvider.notifier) + .resetOption(); + }, + child: Text(t.settings.config.resetBtn), + ), + ], + ), + ), + _ => const SliverToBoxAdapter(), + }, + ], + ), ); } } diff --git a/lib/features/home/widget/connection_button.dart b/lib/features/home/widget/connection_button.dart index 3fcbad18..485bccae 100644 --- a/lib/features/home/widget/connection_button.dart +++ b/lib/features/home/widget/connection_button.dart @@ -151,7 +151,7 @@ class _ConnectionButton extends StatelessWidget { const Gap(16), Text( label, - style: Theme.of(context).textTheme.bodyLarge, + style: Theme.of(context).textTheme.titleMedium, ), ], ); diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index 097aabcd..8a4de050 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -23,13 +23,6 @@ class AdvancedSettingTiles extends HookConsumerWidget { return Column( children: [ const RegionPrefTile(), - ListTile( - title: Text(t.settings.config.pageTitle), - leading: const Icon(Icons.edit_document), - onTap: () async { - await const ConfigOptionsRoute().push(context); - }, - ), ListTile( title: Text(t.settings.geoAssets.pageTitle), leading: const Icon(Icons.folder),