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'; import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.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'; enum ConfigOptionSection { warp, fragment; static final _warpKey = GlobalKey(debugLabel: "warp-section-key"); static final _fragmentKey = GlobalKey(debugLabel: "fragment-section-key"); GlobalKey get key => switch (this) { ConfigOptionSection.warp => _warpKey, ConfigOptionSection.fragment => _fragmentKey, }; } class ConfigOptionsPage extends HookConsumerWidget { 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})"; } return Scaffold( body: CustomScrollView( controller: scrollController, shrinkWrap: true, slivers: [ NestedAppBar( title: Text(t.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.config.resetBtn), onTap: () async { await ref.read(configOptionNotifierProvider.notifier).resetOption(); }, ), ]; }, ), ], ), 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.config.logLevel, presentChoice: (value) => value.name.toUpperCase(), ), const SettingsDivider(), SettingsSection(t.config.section.route), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.region), preferences: ref.watch(ConfigOptions.region.notifier), choices: Region.values, title: t.settings.general.region, presentChoice: (value) => value.present(t), onChanged: (val) => ref.watch(ConfigOptions.directDnsAddress.notifier).reset(), ), SwitchListTile( title: Text(experimental(t.config.blockAds)), value: ref.watch(ConfigOptions.blockAds), onChanged: ref.watch(ConfigOptions.blockAds.notifier).update, ), SwitchListTile( title: Text(experimental(t.config.bypassLan)), value: ref.watch(ConfigOptions.bypassLan), onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update, ), SwitchListTile( title: Text(t.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.config.ipv6Mode, presentChoice: (value) => value.present(t), ), const SettingsDivider(), SettingsSection(t.config.section.dns), ValuePreferenceWidget( value: ref.watch(ConfigOptions.remoteDnsAddress), preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier), title: t.config.remoteDnsAddress, ), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy), preferences: ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier), choices: DomainStrategy.values, title: t.config.remoteDnsDomainStrategy, presentChoice: (value) => value.displayName, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.directDnsAddress), preferences: ref.watch(ConfigOptions.directDnsAddress.notifier), title: t.config.directDnsAddress, ), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.directDnsDomainStrategy), preferences: ref.watch(ConfigOptions.directDnsDomainStrategy.notifier), choices: DomainStrategy.values, title: t.config.directDnsDomainStrategy, presentChoice: (value) => value.displayName, ), SwitchListTile( title: Text(t.config.enableDnsRouting), value: ref.watch(ConfigOptions.enableDnsRouting), onChanged: ref.watch(ConfigOptions.enableDnsRouting.notifier).update, ), // const SettingsDivider(), // SettingsSection(experimental(t.config.section.mux)), // SwitchListTile( // title: Text(t.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.config.muxProtocol, // presentChoice: (value) => value.name, // ), // ValuePreferenceWidget( // value: ref.watch(ConfigOptions.muxMaxStreams), // preferences: // ref.watch(ConfigOptions.muxMaxStreams.notifier), // title: t.config.muxMaxStreams, // inputToValue: int.tryParse, // digitsOnly: true, // ), const SettingsDivider(), SettingsSection(t.config.section.inbound), ChoicePreferenceWidget( selected: ref.watch(ConfigOptions.serviceMode), preferences: ref.watch(ConfigOptions.serviceMode.notifier), choices: ServiceMode.choices, title: t.config.serviceMode, presentChoice: (value) => value.present(t), ), SwitchListTile( title: Text(t.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.config.tunImplementation, presentChoice: (value) => value.name, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.mixedPort), preferences: ref.watch(ConfigOptions.mixedPort.notifier), title: t.config.mixedPort, inputToValue: int.tryParse, digitsOnly: true, validateInput: isPort, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.tproxyPort), preferences: ref.watch(ConfigOptions.tproxyPort.notifier), title: t.config.tproxyPort, inputToValue: int.tryParse, digitsOnly: true, validateInput: isPort, ), ValuePreferenceWidget( value: ref.watch(ConfigOptions.localDnsPort), preferences: ref.watch(ConfigOptions.localDnsPort.notifier), title: t.config.localDnsPort, inputToValue: int.tryParse, digitsOnly: true, validateInput: isPort, ), SwitchListTile( title: Text( experimental(t.config.allowConnectionFromLan), ), value: ref.watch(ConfigOptions.allowConnectionFromLan), onChanged: ref.read(ConfigOptions.allowConnectionFromLan.notifier).update, ), const SettingsDivider(), SettingsSection( experimental(t.config.section.tlsTricks), key: ConfigOptionSection._fragmentKey, ), SwitchListTile( title: Text(t.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.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.config.tlsFragmentSleep, inputToValue: OptionalRange.tryParse, presentValue: (value) => value.present(t), formatInputValue: (value) => value.format(), ), SwitchListTile( title: Text(t.config.enableTlsMixedSniCase), value: ref.watch(ConfigOptions.enableTlsMixedSniCase), onChanged: ref.watch(ConfigOptions.enableTlsMixedSniCase.notifier).update, ), SwitchListTile( title: Text(t.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.config.tlsPaddingSize, inputToValue: OptionalRange.tryParse, presentValue: (value) => value.format(), formatInputValue: (value) => value.format(), ), const SettingsDivider(), SettingsSection(experimental(t.config.section.warp)), WarpOptionsTiles(key: ConfigOptionSection._warpKey), const SettingsDivider(), SettingsSection(t.config.section.misc), ValuePreferenceWidget( value: ref.watch(ConfigOptions.connectionTestUrl), preferences: ref.watch(ConfigOptions.connectionTestUrl.notifier), title: t.config.connectionTestUrl, ), ListTile( title: Text(t.config.urlTestInterval), subtitle: Text( ref.watch(ConfigOptions.urlTestInterval).toApproximateTime(isRelativeToNow: false), ), onTap: () async { final urlTestInterval = await SettingsSliderDialog( title: t.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.config.clashApiPort, validateInput: isPort, digitsOnly: true, inputToValue: int.tryParse, ), const Gap(24), SwitchListTile( title: Text(experimental(t.config.useXrayCoreWhenPossible.Label)), subtitle: Text(t.config.useXrayCoreWhenPossible.Description), value: ref.watch(ConfigOptions.useXrayCoreWhenPossible), onChanged: ref.watch(ConfigOptions.useXrayCoreWhenPossible.notifier).update, ), ], ), ), ), ], ), ); } }