import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/core/widget/tip_card.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart'; import 'package:hiddify/features/config_option/widget/preference_tile.dart'; import 'package:hiddify/features/log/model/log_level.dart'; import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; class ConfigOptionsPage extends HookConsumerWidget { const ConfigOptionsPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); String experimental(String txt) { return "$txt (${t.settings.experimental})"; } return Scaffold( body: CustomScrollView( slivers: [ NestedAppBar( title: Text(t.settings.config.pageTitle), actions: [ PopupMenuButton( icon: Icon(AdaptiveIcon(context).more), itemBuilder: (context) { return [ PopupMenuItem( onTap: () async => ref .read(configOptionNotifierProvider.notifier) .exportJsonToClipboard() .then((success) { if (success) { ref .read(inAppNotificationControllerProvider) .showSuccessToast( t.general.clipboardExportSuccessMsg, ); } }), child: Text(t.settings.exportOptions), ), if (ref.watch(debugModeNotifierProvider)) PopupMenuItem( onTap: () async => ref .read(configOptionNotifierProvider.notifier) .exportJsonToClipboard(excludePrivate: false) .then((success) { if (success) { ref .read(inAppNotificationControllerProvider) .showSuccessToast( t.general.clipboardExportSuccessMsg, ); } }), child: Text(t.settings.exportAllOptions), ), PopupMenuItem( onTap: () async { final shouldImport = await showConfirmationDialog( context, title: t.settings.importOptions, message: t.settings.importOptionsMsg, ); if (shouldImport) { await ref .read(configOptionNotifierProvider.notifier) .importFromClipboard(); } }, child: Text(t.settings.importOptions), ), PopupMenuItem( child: Text(t.settings.config.resetBtn), onTap: () async { await ref .read(configOptionNotifierProvider.notifier) .resetOption(); }, ), ]; }, ), ], ), 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(), ), 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), ], ), ], ), ); } }