diff --git a/lib/core/utils/preferences_utils.dart b/lib/core/utils/preferences_utils.dart index be995b245..5edfc1937 100644 --- a/lib/core/utils/preferences_utils.dart +++ b/lib/core/utils/preferences_utils.dart @@ -96,35 +96,30 @@ class PreferencesNotifier extends StateNotifier { required Ref ref, required this.entry, this.overrideValue, + this.possibleValues, }) : _ref = ref, super(overrideValue ?? entry.read()); final Ref _ref; final PreferencesEntry entry; final T? overrideValue; + final List? possibleValues; - static StateNotifierProvider, T> create( - String key, - T defaultValue, { - T Function(Ref ref)? defaultValueFunction, - T Function(P value)? mapFrom, - P Function(T value)? mapTo, - bool Function(T value)? validator, - T? overrideValue, - }) => + static StateNotifierProvider, T> create(String key, T defaultValue, + {T Function(Ref ref)? defaultValueFunction, T Function(P value)? mapFrom, P Function(T value)? mapTo, bool Function(T value)? validator, T? overrideValue, List? possibleValues}) => StateNotifierProvider( (ref) => PreferencesNotifier._( - ref: ref, - entry: PreferencesEntry( - preferences: ref.read(sharedPreferencesProvider).requireValue, - key: key, - defaultValue: defaultValueFunction?.call(ref) ?? defaultValue, - mapFrom: mapFrom, - mapTo: mapTo, - validator: validator, - ), - overrideValue: overrideValue, - ), + ref: ref, + entry: PreferencesEntry( + preferences: ref.read(sharedPreferencesProvider).requireValue, + key: key, + defaultValue: defaultValueFunction?.call(ref) ?? defaultValue, + mapFrom: mapFrom, + mapTo: mapTo, + validator: validator, + ), + overrideValue: overrideValue, + possibleValues: possibleValues), ); static AutoDisposeStateNotifierProvider, T> createAutoDispose( diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index ede6ea8b4..5c2243e6e 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -61,7 +61,17 @@ abstract class ConfigOptions { static final remoteDnsAddress = PreferencesNotifier.create( "remote-dns-address", "udp://1.1.1.1", - // "https://sky.rethinkdns.com/dns-query", + possibleValues: List.of([ + "local", + "udp://223.5.5.5", + "udp://1.1.1.1", + "udp://1.1.1.2", + "tcp://1.1.1.1", + "https://1.1.1.1/dns-query", + "https://sky.rethinkdns.com/dns-query", + "4.4.2.2", + "8.8.8.8", + ]), validator: (value) => value.isNotBlank, ); @@ -74,7 +84,18 @@ abstract class ConfigOptions { static final directDnsAddress = PreferencesNotifier.create( "direct-dns-address", - "1.1.1.1", + "udp://1.1.1.1", + possibleValues: List.of([ + "local", + "udp://223.5.5.5", + "udp://1.1.1.1", + "udp://1.1.1.2", + "tcp://1.1.1.1", + "https://1.1.1.1/dns-query", + "https://sky.rethinkdns.com/dns-query", + "4.4.2.2", + "8.8.8.8", + ]), defaultValueFunction: (ref) => ref.read(region) == Region.cn ? "223.5.5.5" : "1.1.1.1", validator: (value) => value.isNotBlank, ); @@ -117,7 +138,18 @@ abstract class ConfigOptions { static final connectionTestUrl = PreferencesNotifier.create( "connection-test-url", - "http://connectivitycheck.gstatic.com/generate_204", + "http://cp.cloudflare.com", + possibleValues: List.of([ + "http://connectivitycheck.gstatic.com/generate_204", + "http://www.gstatic.com/generate_204", + "https://www.gstatic.com/generate_204", + "http://cp.cloudflare.com", + "http://kernel.org", + "http://detectportal.firefox.com", + "http://captive.apple.com/hotspot-detect.html", + "https://1.1.1.1", + "http://1.1.1.1", + ]), validator: (value) => value.isNotBlank && isUrl(value), ); diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index 371547146..1040f2de7 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -358,7 +358,7 @@ class ConfigOptionsPage extends HookConsumerWidget { digitsOnly: true, inputToValue: int.tryParse, ), - + SwitchListTile( title: Text(experimental(t.config.useXrayCoreWhenPossible.Label)), subtitle: Text(t.config.useXrayCoreWhenPossible.Description), diff --git a/lib/features/config_option/widget/preference_tile.dart b/lib/features/config_option/widget/preference_tile.dart index 868f8db21..d041a9092 100644 --- a/lib/features/config_option/widget/preference_tile.dart +++ b/lib/features/config_option/widget/preference_tile.dart @@ -41,6 +41,7 @@ class ValuePreferenceWidget extends StatelessWidget { onReset: preferences.reset, digitsOnly: digitsOnly, mapTo: inputToValue, + possibleValues: preferences.possibleValues, ).show(context); if (inputValue == null) { return; diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart index 7d79597de..96c93499c 100644 --- a/lib/features/settings/widgets/settings_input_dialog.dart +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -1,29 +1,20 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:flutter_typeahead/flutter_typeahead.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class SettingsInputDialog extends HookConsumerWidget with PresLogger { - const SettingsInputDialog({ - super.key, - required this.title, - required this.initialValue, - this.mapTo, - this.validator, - this.valueFormatter, - this.onReset, - this.optionalAction, - this.icon, - this.digitsOnly = false, - }); + const SettingsInputDialog({super.key, required this.title, required this.initialValue, this.mapTo, this.validator, this.valueFormatter, this.onReset, this.optionalAction, this.icon, this.digitsOnly = false, this.possibleValues}); final String title; final T initialValue; final T? Function(String value)? mapTo; final bool Function(String value)? validator; final String Function(T value)? valueFormatter; + final List? possibleValues; final VoidCallback? onReset; final (String text, VoidCallback)? optionalAction; final IconData? icon; @@ -53,14 +44,22 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { icon: icon != null ? Icon(icon) : null, content: FocusTraversalOrder( order: const NumericFocusOrder(1), - child: CustomTextFormField( - controller: textController, - inputFormatters: [ - FilteringTextInputFormatter.singleLineFormatter, - if (digitsOnly) FilteringTextInputFormatter.digitsOnly, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (possibleValues != null) + AutocompleteField(initialValue: initialValue.toString(), options: possibleValues!.map((e) => e.toString()).toList()) + else + CustomTextFormField( + controller: textController, + inputFormatters: [ + FilteringTextInputFormatter.singleLineFormatter, + if (digitsOnly) FilteringTextInputFormatter.digitsOnly, + ], + autoCorrect: true, + hint: title, + ), ], - autoCorrect: true, - hint: title, ), ), actions: [ @@ -70,8 +69,7 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { child: TextButton( onPressed: () async { optionalAction!.$2(); - await Navigator.of(context) - .maybePop(T == String ? textController.value.text : null); + await Navigator.of(context).maybePop(T == String ? textController.value.text : null); }, child: Text(optionalAction!.$1.toUpperCase()), ), @@ -103,11 +101,9 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { if (validator?.call(textController.value.text) == false) { await Navigator.of(context).maybePop(null); } else if (mapTo != null) { - await Navigator.of(context) - .maybePop(mapTo!.call(textController.value.text)); + await Navigator.of(context).maybePop(mapTo!.call(textController.value.text)); } else { - await Navigator.of(context) - .maybePop(T == String ? textController.value.text : null); + await Navigator.of(context).maybePop(T == String ? textController.value.text : null); } }, child: Text(localizations.okButtonLabel.toUpperCase()), @@ -119,6 +115,32 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { } } +class AutocompleteField extends StatelessWidget { + const AutocompleteField({super.key, required this.initialValue, required this.options}); + final List options; + final String initialValue; + + @override + Widget build(BuildContext context) { + return Autocomplete( + initialValue: TextEditingValue( + text: this.initialValue, selection: TextSelection(baseOffset: 0, extentOffset: this.initialValue.length), // Selects the entire text + ), + optionsBuilder: (TextEditingValue textEditingValue) { + // if (textEditingValue.text == '') { + // return const Iterable.empty(); + // } + return options.where((String option) { + return option.contains(textEditingValue.text.toLowerCase()); + }); + }, + onSelected: (String selection) { + //debugPrint('You just selected $selection'); + }, + ); + } +} + class SettingsPickerDialog extends HookConsumerWidget with PresLogger { const SettingsPickerDialog({ super.key,