import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:umbrix/core/localization/translations.dart'; import 'package:umbrix/core/notification/in_app_notification_controller.dart'; import 'package:umbrix/core/widget/adaptive_icon.dart'; import 'package:umbrix/core/widget/tip_card.dart'; import 'package:umbrix/features/common/confirmation_dialogs.dart'; import 'package:umbrix/features/common/nested_app_bar.dart'; import 'package:umbrix/features/config_option/data/config_option_repository.dart'; import 'package:umbrix/features/config_option/notifier/config_option_notifier.dart'; import 'package:umbrix/features/config_option/widget/preference_tile.dart'; import 'package:umbrix/features/settings/widgets/sections_widgets.dart'; import 'package:umbrix/features/settings/widgets/settings_input_dialog.dart'; import 'package:umbrix/singbox/model/singbox_config_enum.dart'; import 'package:umbrix/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); return Scaffold( body: CustomScrollView( 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), const SettingsDivider(), // Варианты маршрутизации - раскрывающаяся секция Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( leading: const Icon(Icons.route), title: Text(t.config.section.route), initiallyExpanded: false, children: [ 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(), // Параметры DNS - раскрывающаяся секция Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( leading: const Icon(Icons.dns), title: Text(t.config.section.dns), initiallyExpanded: false, children: [ 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(), // Входящие параметры - раскрывающаяся секция Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( leading: const Icon(Icons.input), title: Text(t.config.section.inbound), initiallyExpanded: false, children: [ 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, ), ], ), ), const SettingsDivider(), // Разное - раскрывающаяся секция Theme( data: Theme.of(context).copyWith(dividerColor: Colors.transparent), child: ExpansionTile( leading: const Icon(Icons.more_horiz), title: Text(t.config.section.misc), initiallyExpanded: false, children: [ 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), ], ), ), ), ], ), ); } }