diff --git a/assets/translations/strings.i18n.json b/assets/translations/strings.i18n.json index 545216c9..306ec733 100644 --- a/assets/translations/strings.i18n.json +++ b/assets/translations/strings.i18n.json @@ -134,6 +134,7 @@ "directDnsDomainStrategy": "Direct DNS Domain Strategy", "mixedPort": "Mixed Port", "localDnsPort": "Local DNS Port", + "tunImplementation": "TUN Implementation", "mtu": "MTU", "connectionTestUrl": "Connection Test URL", "urlTestInterval": "URL Test Interval", diff --git a/assets/translations/strings_fa.i18n.json b/assets/translations/strings_fa.i18n.json index 27669b26..bcd050a2 100644 --- a/assets/translations/strings_fa.i18n.json +++ b/assets/translations/strings_fa.i18n.json @@ -134,6 +134,7 @@ "directDnsDomainStrategy": "Direct DNS Domain Strategy", "mixedPort": "Mixed Port", "localDnsPort": "Local DNS Port", + "tunImplementation": "TUN Implementation", "mtu": "MTU", "connectionTestUrl": "Connection Test URL", "urlTestInterval": "URL Test Interval", diff --git a/lib/features/settings/view/config_options_page.dart b/lib/features/settings/view/config_options_page.dart index 76a4e505..00d4da77 100644 --- a/lib/features/settings/view/config_options_page.dart +++ b/lib/features/settings/view/config_options_page.dart @@ -7,6 +7,7 @@ import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/settings/widgets/widgets.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}); @@ -145,6 +146,23 @@ class ConfigOptionsPage extends HookConsumerWidget { onChanged: ref.read(setSystemProxyStore.notifier).update, ), ], + 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: _default.tunImplementation, + ).show(context); + if (tunImplementation == null) return; + await ref + .read(tunImplementationStore.notifier) + .update(tunImplementation); + }, + ), ListTile( title: Text(t.settings.config.mixedPort), subtitle: Text(options.mixedPort.toString()), @@ -192,6 +210,28 @@ class ConfigOptionsPage extends HookConsumerWidget { await ref.read(connectionTestUrlStore.notifier).update(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.toDouble(), + resetValue: _default.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 ref + .read(urlTestIntervalStore.notifier) + .update(Duration(minutes: urlTestInterval.toInt())); + }, + ), ListTile( title: Text(t.settings.config.clashApiPort), subtitle: Text(options.clashApiPort.toString()), diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart index 0fb528ac..80140036 100644 --- a/lib/features/settings/widgets/settings_input_dialog.dart +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -149,3 +149,75 @@ class SettingsPickerDialog extends HookConsumerWidget with PresLogger { ); } } + +class SettingsSliderDialog extends HookConsumerWidget with PresLogger { + const SettingsSliderDialog({ + super.key, + required this.title, + required this.initialValue, + this.resetValue, + this.min = 0, + this.max = 1, + this.divisions, + this.labelGen, + }); + + final String title; + final double initialValue; + final double? resetValue; + final double min; + final double max; + final int? divisions; + final String Function(double value)? labelGen; + + Future show(BuildContext context) async { + return showDialog( + context: context, + useRootNavigator: true, + builder: (context) => this, + ); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + final localizations = MaterialLocalizations.of(context); + + final sliderValue = useState(initialValue); + + return AlertDialog( + title: Text(title), + content: IntrinsicHeight( + child: Slider( + value: sliderValue.value, + min: min, + max: max, + divisions: divisions, + onChanged: (value) => sliderValue.value = value, + label: labelGen?.call(sliderValue.value), + ), + ), + actions: [ + if (resetValue != null) + TextButton( + onPressed: () async { + await Navigator.of(context).maybePop(resetValue); + }, + child: Text(t.general.reset.toUpperCase()), + ), + TextButton( + onPressed: () async { + await Navigator.of(context).maybePop(); + }, + child: Text(localizations.cancelButtonLabel.toUpperCase()), + ), + TextButton( + onPressed: () async { + await Navigator.of(context).maybePop(sliderValue.value); + }, + child: Text(localizations.okButtonLabel.toUpperCase()), + ), + ], + ); + } +}