From 2a994dc34887c41ecdb56c4e5515075b04744f90 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sat, 2 Mar 2024 22:53:14 +0330 Subject: [PATCH] Refactor preferences --- lib/bootstrap.dart | 2 +- .../http_client/http_client_provider.dart | 6 +- lib/core/model/optional_range.dart | 9 +- lib/core/preferences/general_preferences.dart | 204 ++--- lib/core/preferences/service_preferences.dart | 23 - lib/core/router/app_router.dart | 2 +- lib/core/utils/preferences_utils.dart | 156 ++++ .../notifier/app_update_notifier.dart | 14 +- lib/features/common/general_pref_tiles.dart | 6 +- .../data/config_option_data_providers.dart | 15 +- .../data/config_option_repository.dart | 502 +++++++++---- .../model/config_option_entity.dart | 267 ------- .../notifier/config_option_notifier.dart | 36 +- .../notifier/warp_option_notifier.dart | 34 +- .../overview/config_options_page.dart | 708 ++++++------------ .../overview/warp_options_widgets.dart | 159 ++-- .../config_option/widget/preference_tile.dart | 95 +++ .../data/connection_data_providers.dart | 3 +- .../data/connection_repository.dart | 6 +- .../notifier/connection_notifier.dart | 15 +- .../home/widget/connection_button.dart | 7 +- lib/features/intro/widget/intro_page.dart | 30 +- .../overview/per_app_proxy_page.dart | 4 +- .../profile/notifier/profile_notifier.dart | 2 +- .../notifier/profiles_update_notifier.dart | 2 +- .../proxy/active/active_proxy_notifier.dart | 2 +- .../overview/proxies_overview_notifier.dart | 14 +- .../widgets/advanced_setting_tiles.dart | 12 +- .../widgets/general_setting_tiles.dart | 14 +- .../widgets/settings_input_dialog.dart | 31 +- .../notifier/system_tray_notifier.dart | 11 +- lib/utils/pref_notifier.dart | 102 --- 32 files changed, 1104 insertions(+), 1389 deletions(-) delete mode 100644 lib/core/preferences/service_preferences.dart create mode 100644 lib/core/utils/preferences_utils.dart delete mode 100644 lib/features/config_option/model/config_option_entity.dart create mode 100644 lib/features/config_option/widget/preference_tile.dart delete mode 100644 lib/utils/pref_notifier.dart diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index c580e0a4..f3d806a0 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -99,7 +99,7 @@ Future lazyBootstrap( () => container.read(windowNotifierProvider.future), ); - final silentStart = container.read(silentStartNotifierProvider); + final silentStart = container.read(Preferences.silentStart); Logger.bootstrap .debug("silent start [${silentStart ? "Enabled" : "Disabled"}]"); if (!silentStart) { diff --git a/lib/core/http_client/http_client_provider.dart b/lib/core/http_client/http_client_provider.dart index 8b73ca7c..0acda287 100644 --- a/lib/core/http_client/http_client_provider.dart +++ b/lib/core/http_client/http_client_provider.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/http_client/dio_http_client.dart'; -import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'http_client_provider.g.dart'; @@ -15,9 +15,9 @@ DioHttpClient httpClient(HttpClientRef ref) { ); ref.listen( - configOptionNotifierProvider.selectAsync((data) => data.mixedPort), + ConfigOptions.mixedPort, (_, next) async { - client.setProxyPort(await next); + client.setProxyPort(next); }, fireImmediately: true, ); diff --git a/lib/core/model/optional_range.dart b/lib/core/model/optional_range.dart index 98f25674..b4cd9365 100644 --- a/lib/core/model/optional_range.dart +++ b/lib/core/model/optional_range.dart @@ -16,9 +16,9 @@ class OptionalRange with OptionalRangeMappable { String present(TranslationsEn t) => format().isEmpty ? t.general.notSet : format(); - factory OptionalRange._fromString( + factory OptionalRange.parse( String input, { - bool allowEmpty = true, + bool allowEmpty = false, }) => switch (input.split("-")) { [final String val] when val.isEmpty && allowEmpty => @@ -36,7 +36,7 @@ class OptionalRange with OptionalRangeMappable { bool allowEmpty = false, }) { try { - return OptionalRange._fromString(input); + return OptionalRange.parse(input, allowEmpty: allowEmpty); } catch (_) { return null; } @@ -48,7 +48,8 @@ class OptionalRangeJsonConverter const OptionalRangeJsonConverter(); @override - OptionalRange fromJson(String json) => OptionalRange._fromString(json); + OptionalRange fromJson(String json) => + OptionalRange.parse(json, allowEmpty: true); @override String toJson(OptionalRange object) => object.format(); diff --git a/lib/core/preferences/general_preferences.dart b/lib/core/preferences/general_preferences.dart index b76b5df1..aefae330 100644 --- a/lib/core/preferences/general_preferences.dart +++ b/lib/core/preferences/general_preferences.dart @@ -3,203 +3,111 @@ import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/model/environment.dart'; import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; import 'package:hiddify/utils/platform_utils.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'general_preferences.g.dart'; -// TODO refactor - bool _debugIntroPage = false; -@Riverpod(keepAlive: true) -class IntroCompleted extends _$IntroCompleted { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, +abstract class Preferences { + static final introCompleted = PreferencesNotifier.create( "intro_completed", false, + overrideValue: _debugIntroPage && kDebugMode ? false : null, ); - @override - bool build() { - if (_debugIntroPage && kDebugMode) return false; - return _pref.getValue(); - } - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} - -@Riverpod(keepAlive: true) -class RegionNotifier extends _$RegionNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, + static final region = PreferencesNotifier.create( "region", Region.other, mapFrom: Region.values.byName, mapTo: (value) => value.name, ); - @override - Region build() => _pref.getValue(); - - Future update(Region value) { - state = value; - return _pref.update(value); - } -} - -@Riverpod(keepAlive: true) -class SilentStartNotifier extends _$SilentStartNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, + static final silentStart = PreferencesNotifier.create( "silent_start", false, ); - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} - -@Riverpod(keepAlive: true) -class DisableMemoryLimit extends _$DisableMemoryLimit { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, + static final disableMemoryLimit = PreferencesNotifier.create( "disable_memory_limit", // disable memory limit on desktop by default PlatformUtils.isDesktop, ); - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} - -@Riverpod(keepAlive: true) -class DebugModeNotifier extends _$DebugModeNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "debug_mode", - ref.read(environmentProvider) == Environment.dev, - ); - - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} - -@Riverpod(keepAlive: true) -class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, + static final perAppProxyMode = + PreferencesNotifier.create( "per_app_proxy_mode", PerAppProxyMode.off, mapFrom: PerAppProxyMode.values.byName, mapTo: (value) => value.name, ); - @override - PerAppProxyMode build() => _pref.getValue(); + static final markNewProfileActive = PreferencesNotifier.create( + "mark_new_profile_active", + true, + ); - Future update(PerAppProxyMode value) { + static final dynamicNotification = PreferencesNotifier.create( + "dynamic_notification", + true, + ); + + static final autoCheckIp = PreferencesNotifier.create( + "auto_check_ip", + true, + ); + + static final startedByUser = PreferencesNotifier.create( + "started_by_user", + false, + ); +} + +@Riverpod(keepAlive: true) +class DebugModeNotifier extends _$DebugModeNotifier { + late final _pref = PreferencesEntry( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + key: "debug_mode", + defaultValue: ref.read(environmentProvider) == Environment.dev, + ); + + @override + bool build() => _pref.read(); + + Future update(bool value) { state = value; - return _pref.update(value); + return _pref.write(value); } } @Riverpod(keepAlive: true) class PerAppProxyList extends _$PerAppProxyList { - late final _include = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "per_app_proxy_include_list", - [], + late final _include = PreferencesEntry( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + key: "per_app_proxy_include_list", + defaultValue: [], ); - late final _exclude = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "per_app_proxy_exclude_list", - [], + late final _exclude = PreferencesEntry( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + key: "per_app_proxy_exclude_list", + defaultValue: [], ); @override List build() => - ref.watch(perAppProxyModeNotifierProvider) == PerAppProxyMode.include - ? _include.getValue() - : _exclude.getValue(); + ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include + ? _include.read() + : _exclude.read(); Future update(List value) { state = value; - if (ref.read(perAppProxyModeNotifierProvider) == PerAppProxyMode.include) { - return _include.update(value); + if (ref.read(Preferences.perAppProxyMode) == PerAppProxyMode.include) { + return _include.write(value); } - return _exclude.update(value); - } -} - -@riverpod -class MarkNewProfileActive extends _$MarkNewProfileActive { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "mark_new_profile_active", - true, - ); - - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} - -@riverpod -class DynamicNotification extends _$DynamicNotification { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "dynamic_notification", - true, - ); - - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} - -@riverpod -class AutoCheckIp extends _$AutoCheckIp { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "auto_check_ip", - true, - ); - - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); + return _exclude.write(value); } } diff --git a/lib/core/preferences/service_preferences.dart b/lib/core/preferences/service_preferences.dart deleted file mode 100644 index 847c614f..00000000 --- a/lib/core/preferences/service_preferences.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hiddify/core/preferences/preferences_provider.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'service_preferences.g.dart'; - -@Riverpod(keepAlive: true) -class StartedByUser extends _$StartedByUser with AppLogger { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "started_by_user", - false, - ); - - @override - bool build() => _pref.getValue(); - - Future update(bool value) { - state = value; - return _pref.update(value); - } -} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 6207e66a..c21703ed 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -85,7 +85,7 @@ class RouterListenable extends _$RouterListenable @override Future build() async { - _introCompleted = ref.watch(introCompletedProvider); + _introCompleted = ref.watch(Preferences.introCompleted); ref.listenSelf((_, __) { if (state.isLoading) return; diff --git a/lib/core/utils/preferences_utils.dart b/lib/core/utils/preferences_utils.dart new file mode 100644 index 00000000..b79b5d8d --- /dev/null +++ b/lib/core/utils/preferences_utils.dart @@ -0,0 +1,156 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class PreferencesEntry with InfraLogger { + PreferencesEntry({ + required this.preferences, + required this.key, + required this.defaultValue, + this.mapFrom, + this.mapTo, + this.validator, + }); + + final SharedPreferences preferences; + final String key; + final T defaultValue; + final T Function(P value)? mapFrom; + final P Function(T value)? mapTo; + final bool Function(T value)? validator; + + T read() { + try { + loggy.debug("getting persisted preference [$key]($T)"); + final T value; + if (mapFrom != null) { + final persisted = preferences.get(key) as P?; + if (persisted == null) { + value = defaultValue; + } else { + value = mapFrom!(persisted); + } + } else if (T == List) { + value = preferences.getStringList(key) as T? ?? defaultValue; + } else { + value = preferences.get(key) as T? ?? defaultValue; + } + + if (validator?.call(value) ?? true) return value; + return defaultValue; + } catch (e, stackTrace) { + loggy.warning("error getting preference[$key]: $e", e, stackTrace); + return defaultValue; + } + } + + Future write(T value) async { + Object? mapped = value; + if (mapTo != null) { + mapped = mapTo!(value); + } + loggy.debug("updating preference [$key]($T) to [$mapped]"); + try { + if (!(validator?.call(value) ?? true)) { + loggy.warning("invalid value [$value] for preference [$key]($T)"); + return false; + } + + return switch (mapped) { + final String value => await preferences.setString(key, value), + final bool value => await preferences.setBool(key, value), + final int value => await preferences.setInt(key, value), + final double value => await preferences.setDouble(key, value), + final List value => await preferences.setStringList(key, value), + _ => throw const FormatException("Invalid Type"), + }; + } catch (e, stackTrace) { + loggy.warning("error updating preference[$key]: $e", e, stackTrace); + return false; + } + } + + Future remove() async { + try { + await preferences.remove(key); + } catch (e, stackTrace) { + loggy.warning("error removing preference[$key]: $e", e, stackTrace); + } + } +} + +class PreferencesNotifier extends StateNotifier { + PreferencesNotifier._({ + required Ref ref, + required this.entry, + this.overrideValue, + }) : _ref = ref, + super(overrideValue ?? entry.read()); + + final Ref _ref; + final PreferencesEntry entry; + final T? overrideValue; + + static StateNotifierProvider, T> create( + String key, + T defaultValue, { + T Function(P value)? mapFrom, + P Function(T value)? mapTo, + bool Function(T value)? validator, + T? overrideValue, + }) => + StateNotifierProvider( + (ref) => PreferencesNotifier._( + ref: ref, + entry: PreferencesEntry( + preferences: ref.read(sharedPreferencesProvider).requireValue, + key: key, + defaultValue: defaultValue, + mapFrom: mapFrom, + mapTo: mapTo, + validator: validator, + ), + overrideValue: overrideValue, + ), + ); + + static AutoDisposeStateNotifierProvider, T> + createAutoDispose( + String key, + T defaultValue, { + T Function(P value)? mapFrom, + P Function(T value)? mapTo, + bool Function(T value)? validator, + T? overrideValue, + }) => + StateNotifierProvider.autoDispose( + (ref) => PreferencesNotifier._( + ref: ref, + entry: PreferencesEntry( + preferences: ref.read(sharedPreferencesProvider).requireValue, + key: key, + defaultValue: defaultValue, + mapFrom: mapFrom, + mapTo: mapTo, + validator: validator, + ), + overrideValue: overrideValue, + ), + ); + + P raw() { + final value = overrideValue ?? state; + if (entry.mapTo != null) return entry.mapTo!(value); + return value as P; + } + + Future update(T value) async { + if (await entry.write(value)) state = value; + } + + Future reset() async { + await entry.remove(); + _ref.invalidateSelf(); + } +} diff --git a/lib/features/app_update/notifier/app_update_notifier.dart b/lib/features/app_update/notifier/app_update_notifier.dart index 357b95bb..d59330d5 100644 --- a/lib/features/app_update/notifier/app_update_notifier.dart +++ b/lib/features/app_update/notifier/app_update_notifier.dart @@ -3,11 +3,11 @@ import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/features/app_update/data/app_update_data_providers.dart'; import 'package:hiddify/features/app_update/model/app_update_failure.dart'; import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; import 'package:hiddify/features/app_update/notifier/app_update_state.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:upgrader/upgrader.dart'; @@ -32,10 +32,10 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { @override AppUpdateState build() => const AppUpdateState.initial(); - Pref get _ignoreReleasePref => Pref( - ref.read(sharedPreferencesProvider).requireValue, - 'ignored_release_version', - null, + PreferencesEntry get _ignoreReleasePref => PreferencesEntry( + preferences: ref.read(sharedPreferencesProvider).requireValue, + key: 'ignored_release_version', + defaultValue: null, ); Future check() async { @@ -58,7 +58,7 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { final latestVersion = Version.parse(remote.version); final currentVersion = Version.parse(appInfo.version); if (latestVersion > currentVersion) { - if (remote.version == _ignoreReleasePref.getValue()) { + if (remote.version == _ignoreReleasePref.read()) { loggy.debug("ignored release [${remote.version}]"); return state = AppUpdateStateIgnored(remote); } @@ -81,7 +81,7 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { Future ignoreRelease(RemoteVersionEntity version) async { loggy.debug("ignoring release [${version.version}]"); - await _ignoreReleasePref.update(version.version); + await _ignoreReleasePref.write(version.version); state = AppUpdateStateIgnored(version); } } diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index 69f18d57..60884ba3 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -57,7 +57,7 @@ class RegionPrefTile extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final region = ref.watch(regionNotifierProvider); + final region = ref.watch(Preferences.region); return ListTile( title: Text(t.settings.general.region), @@ -83,9 +83,7 @@ class RegionPrefTile extends HookConsumerWidget { }, ); if (selectedRegion != null) { - await ref - .read(regionNotifierProvider.notifier) - .update(selectedRegion); + await ref.read(Preferences.region.notifier).update(selectedRegion); } }, ); diff --git a/lib/features/config_option/data/config_option_data_providers.dart b/lib/features/config_option/data/config_option_data_providers.dart index 8176d8d5..987a4c31 100644 --- a/lib/features/config_option/data/config_option_data_providers.dart +++ b/lib/features/config_option/data/config_option_data_providers.dart @@ -1,7 +1,6 @@ import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; -import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'config_option_data_providers.g.dart'; @@ -10,19 +9,9 @@ part 'config_option_data_providers.g.dart'; ConfigOptionRepository configOptionRepository( ConfigOptionRepositoryRef ref, ) { - return ConfigOptionRepositoryImpl( + return ConfigOptionRepository( preferences: ref.watch(sharedPreferencesProvider).requireValue, - singbox: ref.watch(singboxServiceProvider), - ); -} - -@Riverpod(keepAlive: true) -SingBoxConfigOptionRepository singBoxConfigOptionRepository( - SingBoxConfigOptionRepositoryRef ref, -) { - return SingBoxConfigOptionRepositoryImpl( - preferences: ref.watch(sharedPreferencesProvider).requireValue, - optionsRepository: ref.watch(configOptionRepositoryProvider), + getConfigOptions: () => ConfigOptions.singboxOptions(ref), geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue, geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), ); diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index 8a890562..bb851969 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -1,166 +1,402 @@ +import 'package:dartx/dartx.dart'; import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/utils/exception_handler.dart'; -import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/core/utils/json_converters.dart'; +import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/features/config_option/model/config_option_failure.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/singbox/model/singbox_config_option.dart'; import 'package:hiddify/singbox/model/singbox_rule.dart'; -import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; -import 'package:meta/meta.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; -abstract interface class ConfigOptionRepository { - Either getConfigOption(); - TaskEither updateConfigOption( - ConfigOptionPatch patch, +abstract class ConfigOptions { + static final serviceMode = PreferencesNotifier.create( + "service-mode", + ServiceMode.defaultMode, + mapFrom: (value) => ServiceMode.choices.firstWhere((e) => e.key == value), + mapTo: (value) => value.key, ); - TaskEither resetConfigOption(); - TaskEither generateWarpConfig(); -} -abstract interface class SingBoxConfigOptionRepository { - TaskEither - getFullSingboxConfigOption(); -} + static final logLevel = PreferencesNotifier.create( + "log-level", + LogLevel.warn, + mapFrom: LogLevel.values.byName, + mapTo: (value) => value.name, + ); -class ConfigOptionRepositoryImpl - with ExceptionHandler, InfraLogger - implements ConfigOptionRepository { - ConfigOptionRepositoryImpl({ - required this.preferences, - required this.singbox, - }); + static final resolveDestination = PreferencesNotifier.create( + "resolve-destination", + false, + ); - final SharedPreferences preferences; - final SingboxService singbox; + static final ipv6Mode = PreferencesNotifier.create( + "ipv6-mode", + IPv6Mode.disable, + mapFrom: (value) => IPv6Mode.values.firstWhere((e) => e.key == value), + mapTo: (value) => value.key, + ); - @override - Either getConfigOption() { - try { - final map = ConfigOptionEntity.initial().toJson(); - for (final key in map.keys) { - final persisted = preferences.get(key); - if (persisted != null) { - final defaultValue = map[key]; - if (defaultValue != null && - persisted.runtimeType != defaultValue.runtimeType) { - loggy.warning( - "error getting preference[$key], expected type: [${defaultValue.runtimeType}] - received value: [$persisted](${persisted.runtimeType})", - ); - continue; - } - map[key] = persisted; - } + static final remoteDnsAddress = PreferencesNotifier.create( + "remote-dns-address", + "udp://1.1.1.1", + validator: (value) => value.isNotBlank, + ); + + static final remoteDnsDomainStrategy = + PreferencesNotifier.create( + "remote-dns-domain-strategy", + DomainStrategy.auto, + mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value), + mapTo: (value) => value.key, + ); + + static final directDnsAddress = PreferencesNotifier.create( + "direct-dns-address", + "1.1.1.1", + validator: (value) => value.isNotBlank, + ); + + static final directDnsDomainStrategy = + PreferencesNotifier.create( + "direct-dns-domain-strategy", + DomainStrategy.auto, + mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value), + mapTo: (value) => value.key, + ); + + static final mixedPort = PreferencesNotifier.create( + "mixed-port", + 2334, + validator: (value) => isPort(value.toString()), + ); + + static final localDnsPort = PreferencesNotifier.create( + "local-dns-port", + 6450, + validator: (value) => isPort(value.toString()), + ); + + static final tunImplementation = + PreferencesNotifier.create( + "tun-implementation", + TunImplementation.mixed, + mapFrom: TunImplementation.values.byName, + mapTo: (value) => value.name, + ); + + static final mtu = PreferencesNotifier.create("mtu", 9000); + + static final strictRoute = + PreferencesNotifier.create("strict-route", true); + + static final connectionTestUrl = PreferencesNotifier.create( + "connection-test-url", + "http://cp.cloudflare.com/", + validator: (value) => value.isNotBlank && isUrl(value), + ); + + static final urlTestInterval = PreferencesNotifier.create( + "url-test-interval", + const Duration(minutes: 10), + mapFrom: const IntervalInSecondsConverter().fromJson, + mapTo: const IntervalInSecondsConverter().toJson, + ); + + static final enableClashApi = PreferencesNotifier.create( + "enable-clash-api", + true, + ); + + static final clashApiPort = PreferencesNotifier.create( + "clash-api-port", + 6756, + validator: (value) => isPort(value.toString()), + ); + + static final bypassLan = + PreferencesNotifier.create("bypass-lan", false); + + static final allowConnectionFromLan = PreferencesNotifier.create( + "allow-connection-from-lan", + false, + ); + + static final enableFakeDns = PreferencesNotifier.create( + "enable-fake-dns", + false, + ); + + static final enableDnsRouting = PreferencesNotifier.create( + "enable-dns-routing", + true, + ); + + static final independentDnsCache = PreferencesNotifier.create( + "independent-dns-cache", + true, + ); + + static final enableTlsFragment = PreferencesNotifier.create( + "enable-tls-fragment", + false, + ); + + static final tlsFragmentSize = + PreferencesNotifier.create( + "tls-fragment-size", + const OptionalRange(min: 1, max: 500), + mapFrom: OptionalRange.parse, + mapTo: const OptionalRangeJsonConverter().toJson, + ); + + static final tlsFragmentSleep = + PreferencesNotifier.create( + "tls-fragment-sleep", + const OptionalRange(min: 0, max: 500), + mapFrom: OptionalRange.parse, + mapTo: const OptionalRangeJsonConverter().toJson, + ); + + static final enableTlsMixedSniCase = PreferencesNotifier.create( + "enable-tls-mixed-sni-case", + false, + ); + + static final enableTlsPadding = PreferencesNotifier.create( + "enable-tls-padding", + false, + ); + + static final tlsPaddingSize = + PreferencesNotifier.create( + "tls-padding-size", + const OptionalRange(min: 1, max: 1500), + mapFrom: OptionalRange.parse, + mapTo: const OptionalRangeJsonConverter().toJson, + ); + + static final enableMux = PreferencesNotifier.create( + "enable-mux", + false, + ); + + static final muxPadding = PreferencesNotifier.create( + "mux-padding", + false, + ); + + static final muxMaxStreams = PreferencesNotifier.create( + "mux-max-streams", + 8, + validator: (value) => value > 0, + ); + + static final muxProtocol = PreferencesNotifier.create( + "mux-protocol", + MuxProtocol.h2mux, + mapFrom: MuxProtocol.values.byName, + mapTo: (value) => value.name, + ); + + static final enableWarp = PreferencesNotifier.create( + "enable-warp", + false, + ); + + static final warpDetourMode = + PreferencesNotifier.create( + "warp-detour-mode", + WarpDetourMode.outbound, + mapFrom: WarpDetourMode.values.byName, + mapTo: (value) => value.name, + ); + + static final warpLicenseKey = PreferencesNotifier.create( + "warp-license-key", + "", + ); + + static final warpAccountId = PreferencesNotifier.create( + "warp-account-id", + "", + ); + + static final warpAccessToken = PreferencesNotifier.create( + "warp-access-token", + "", + ); + + static final warpCleanIp = PreferencesNotifier.create( + "warp-clean-ip", + "auto", + ); + + static final warpPort = PreferencesNotifier.create( + "warp-port", + 0, + validator: (value) => isPort(value.toString()), + ); + + static final warpNoise = PreferencesNotifier.create( + "warp-noise", + const OptionalRange(min: 5, max: 10), + mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true), + mapTo: const OptionalRangeJsonConverter().toJson, + ); + + static final warpNoiseDelay = + PreferencesNotifier.create( + "warp-noise-delay", + const OptionalRange(min: 20, max: 200), + mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true), + mapTo: const OptionalRangeJsonConverter().toJson, + ); + + static final warpWireguardConfig = PreferencesNotifier.create( + "warp-wireguard-config", + "", + ); + + static final hasExperimentalFeatures = Provider.autoDispose( + (ref) { + final mode = ref.watch(serviceMode); + if (PlatformUtils.isDesktop && mode == ServiceMode.tun) { + return true; } - final options = ConfigOptionEntity.fromJson(map); - return right(options); - } catch (error, stackTrace) { - return left(ConfigOptionUnexpectedFailure(error, stackTrace)); - } - } - - @override - TaskEither updateConfigOption( - ConfigOptionPatch patch, - ) { - return exceptionHandler( - () async { - final map = patch.toJson(); - await updateByJson(map); - return right(unit); - }, - ConfigOptionUnexpectedFailure.new, - ); - } - - @override - TaskEither resetConfigOption() { - return exceptionHandler( - () async { - final map = ConfigOptionEntity.initial().toJson(); - await updateByJson(map); - return right(unit); - }, - ConfigOptionUnexpectedFailure.new, - ); - } - - @visibleForTesting - Future updateByJson( - Map options, - ) async { - final map = ConfigOptionEntity.initial().toJson(); - for (final key in map.keys) { - final value = options[key]; - if (value != null) { - loggy.debug("updating [$key] to [$value]"); - - switch (value) { - case bool _: - await preferences.setBool(key, value); - case String _: - await preferences.setString(key, value); - case int _: - await preferences.setInt(key, value); - case double _: - await preferences.setDouble(key, value); - default: - loggy.warning("unexpected type"); - } + if (ref.watch(enableTlsFragment) || + ref.watch(enableTlsMixedSniCase) || + ref.watch(enableTlsPadding) || + ref.watch(enableMux) || + ref.watch(enableWarp)) { + return true; } - } - } - @override - TaskEither generateWarpConfig() { - return exceptionHandler( - () async { - final options = getConfigOption().getOrElse((l) => throw l); - return await singbox - .generateWarpConfig( - licenseKey: options.warpLicenseKey, - previousAccountId: options.warpAccountId, - previousAccessToken: options.warpAccessToken, - ) - .mapLeft((l) => ConfigOptionFailure.unexpected(l)) - .flatMap( - (warp) => updateConfigOption( - ConfigOptionPatch( - warpAccountId: warp.accountId, - warpAccessToken: warp.accessToken, - warpWireguardConfig: warp.wireguardConfig, - ), - ).map((_) => warp.log), - ) - .run(); - }, - (error, stackTrace) { - loggy.error(error); - return ConfigOptionUnexpectedFailure(error, stackTrace); - }, + return false; + }, + ); + + /// list of all config option preferences + static final preferences = [ + serviceMode, + logLevel, + resolveDestination, + ipv6Mode, + remoteDnsAddress, + remoteDnsDomainStrategy, + directDnsAddress, + directDnsDomainStrategy, + mixedPort, + localDnsPort, + tunImplementation, + mtu, + strictRoute, + connectionTestUrl, + urlTestInterval, + clashApiPort, + bypassLan, + allowConnectionFromLan, + enableDnsRouting, + enableTlsFragment, + tlsFragmentSize, + tlsFragmentSleep, + enableTlsMixedSniCase, + enableTlsPadding, + tlsPaddingSize, + enableMux, + muxPadding, + muxMaxStreams, + muxProtocol, + enableWarp, + warpDetourMode, + warpLicenseKey, + warpAccountId, + warpAccessToken, + warpCleanIp, + warpPort, + warpNoise, + warpWireguardConfig, + ]; + + /// singbox options + /// + /// **this is partial, don't use it directly** + static SingboxConfigOption singboxOptions(ProviderRef ref) { + final mode = ref.read(serviceMode); + return SingboxConfigOption( + executeConfigAsIs: false, + logLevel: ref.read(logLevel), + resolveDestination: ref.read(resolveDestination), + ipv6Mode: ref.read(ipv6Mode), + remoteDnsAddress: ref.read(remoteDnsAddress), + remoteDnsDomainStrategy: ref.read(remoteDnsDomainStrategy), + directDnsAddress: ref.read(directDnsAddress), + directDnsDomainStrategy: ref.read(directDnsDomainStrategy), + mixedPort: ref.read(mixedPort), + localDnsPort: ref.read(localDnsPort), + tunImplementation: ref.read(tunImplementation), + mtu: ref.read(mtu), + strictRoute: ref.read(strictRoute), + connectionTestUrl: ref.read(connectionTestUrl), + urlTestInterval: ref.read(urlTestInterval), + enableClashApi: ref.read(enableClashApi), + clashApiPort: ref.read(clashApiPort), + enableTun: mode == ServiceMode.tun, + enableTunService: mode == ServiceMode.tunService, + setSystemProxy: mode == ServiceMode.systemProxy, + bypassLan: ref.read(bypassLan), + allowConnectionFromLan: ref.read(allowConnectionFromLan), + enableFakeDns: ref.read(enableFakeDns), + enableDnsRouting: ref.read(enableDnsRouting), + independentDnsCache: ref.read(independentDnsCache), + enableTlsFragment: ref.read(enableTlsFragment), + tlsFragmentSize: ref.read(tlsFragmentSize), + tlsFragmentSleep: ref.read(tlsFragmentSleep), + enableTlsMixedSniCase: ref.read(enableTlsMixedSniCase), + enableTlsPadding: ref.read(enableTlsPadding), + tlsPaddingSize: ref.read(tlsPaddingSize), + enableMux: ref.read(enableMux), + muxPadding: ref.read(muxPadding), + muxMaxStreams: ref.read(muxMaxStreams), + muxProtocol: ref.read(muxProtocol), + warp: SingboxWarpOption( + enable: ref.read(enableWarp), + mode: ref.read(warpDetourMode), + wireguardConfig: ref.read(warpWireguardConfig), + licenseKey: ref.read(warpLicenseKey), + accountId: ref.read(warpAccountId), + accessToken: ref.read(warpAccessToken), + cleanIp: ref.read(warpCleanIp), + cleanPort: ref.read(warpPort), + warpNoise: ref.read(warpNoise), + warpNoiseDelay: ref.read(warpNoiseDelay), + ), + geoipPath: "", + geositePath: "", + rules: [], ); } } -class SingBoxConfigOptionRepositoryImpl - with ExceptionHandler, InfraLogger - implements SingBoxConfigOptionRepository { - SingBoxConfigOptionRepositoryImpl({ +class ConfigOptionRepository with ExceptionHandler, InfraLogger { + ConfigOptionRepository({ required this.preferences, - required this.optionsRepository, + required this.getConfigOptions, required this.geoAssetRepository, required this.geoAssetPathResolver, }); final SharedPreferences preferences; - final ConfigOptionRepository optionsRepository; + final SingboxConfigOption Function() getConfigOptions; final GeoAssetRepository geoAssetRepository; final GeoAssetPathResolver geoAssetPathResolver; - @override TaskEither getFullSingboxConfigOption() { return exceptionHandler( @@ -204,9 +440,7 @@ class SingBoxConfigOptionRepositoryImpl .getOrElse((l) => throw l) .run(); - final persisted = - optionsRepository.getConfigOption().getOrElse((l) => throw l); - final singboxConfigOption = persisted.toSingbox( + final singboxConfigOption = getConfigOptions().copyWith( geoipPath: geoAssetPathResolver.relativePath( geoAssets.geoip.providerName, geoAssets.geoip.fileName, diff --git a/lib/features/config_option/model/config_option_entity.dart b/lib/features/config_option/model/config_option_entity.dart deleted file mode 100644 index 2df83309..00000000 --- a/lib/features/config_option/model/config_option_entity.dart +++ /dev/null @@ -1,267 +0,0 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/model/optional_range.dart'; -import 'package:hiddify/core/utils/json_converters.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; -import 'package:hiddify/singbox/model/singbox_config_enum.dart'; -import 'package:hiddify/singbox/model/singbox_config_option.dart'; -import 'package:hiddify/singbox/model/singbox_rule.dart'; -import 'package:hiddify/utils/platform_utils.dart'; - -part 'config_option_entity.freezed.dart'; -part 'config_option_entity.g.dart'; - -@freezed -class ConfigOptionEntity with _$ConfigOptionEntity { - const ConfigOptionEntity._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory ConfigOptionEntity({ - required ServiceMode serviceMode, - @Default(LogLevel.warn) LogLevel logLevel, - @Default(false) bool resolveDestination, - @Default(IPv6Mode.disable) IPv6Mode ipv6Mode, - @Default("udp://1.1.1.1") String remoteDnsAddress, - @Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy, - @Default("1.1.1.1") String directDnsAddress, - @Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy, - @Default(2334) int mixedPort, - @Default(6450) int localDnsPort, - @Default(TunImplementation.mixed) TunImplementation tunImplementation, - @Default(9000) int mtu, - @Default(true) bool strictRoute, - @Default("http://cp.cloudflare.com/") String connectionTestUrl, - @IntervalInSecondsConverter() - @Default(Duration(minutes: 10)) - Duration urlTestInterval, - @Default(true) bool enableClashApi, - @Default(6756) int clashApiPort, - @Default(false) bool bypassLan, - @Default(false) bool allowConnectionFromLan, - @Default(false) bool enableFakeDns, - @Default(true) bool enableDnsRouting, - @Default(true) bool independentDnsCache, - @Default(false) bool enableTlsFragment, - @OptionalRangeJsonConverter() - @Default(OptionalRange(min: 1, max: 500)) - OptionalRange tlsFragmentSize, - @OptionalRangeJsonConverter() - @Default(OptionalRange(min: 0, max: 500)) - OptionalRange tlsFragmentSleep, - @Default(false) bool enableTlsMixedSniCase, - @Default(false) bool enableTlsPadding, - @OptionalRangeJsonConverter() - @Default(OptionalRange(min: 1, max: 1500)) - OptionalRange tlsPaddingSize, - @Default(false) bool enableMux, - @Default(false) bool muxPadding, - @Default(8) int muxMaxStreams, - @Default(MuxProtocol.h2mux) MuxProtocol muxProtocol, - @Default(false) bool enableWarp, - @Default(WarpDetourMode.outbound) WarpDetourMode warpDetourMode, - @Default("") String warpLicenseKey, - @Default("") String warpAccountId, - @Default("") String warpAccessToken, - @Default("auto") String warpCleanIp, - @Default(0) int warpPort, - @OptionalRangeJsonConverter() - @Default(OptionalRange(min: 5, max: 10)) - OptionalRange warpNoise, - @OptionalRangeJsonConverter() - @Default(OptionalRange(min: 20, max: 200)) - OptionalRange warpNoiseDelay, - @Default("") String warpWireguardConfig, - }) = _ConfigOptionEntity; - - factory ConfigOptionEntity.initial() => ConfigOptionEntity( - serviceMode: ServiceMode.defaultMode, - ); - - bool hasExperimentalOptions() { - if (PlatformUtils.isDesktop && serviceMode == ServiceMode.tun) { - return true; - } - if (enableTlsFragment || - enableTlsMixedSniCase || - enableTlsPadding || - enableMux || - enableWarp) { - return true; - } - - return false; - } - - String format() { - const encoder = JsonEncoder.withIndent(' '); - return encoder.convert(toJson()); - } - - ConfigOptionEntity patch(ConfigOptionPatch patch) { - return copyWith( - serviceMode: patch.serviceMode ?? serviceMode, - logLevel: patch.logLevel ?? logLevel, - resolveDestination: patch.resolveDestination ?? resolveDestination, - ipv6Mode: patch.ipv6Mode ?? ipv6Mode, - remoteDnsAddress: patch.remoteDnsAddress ?? remoteDnsAddress, - remoteDnsDomainStrategy: - patch.remoteDnsDomainStrategy ?? remoteDnsDomainStrategy, - directDnsAddress: patch.directDnsAddress ?? directDnsAddress, - directDnsDomainStrategy: - patch.directDnsDomainStrategy ?? directDnsDomainStrategy, - mixedPort: patch.mixedPort ?? mixedPort, - localDnsPort: patch.localDnsPort ?? localDnsPort, - tunImplementation: patch.tunImplementation ?? tunImplementation, - mtu: patch.mtu ?? mtu, - strictRoute: patch.strictRoute ?? strictRoute, - connectionTestUrl: patch.connectionTestUrl ?? connectionTestUrl, - urlTestInterval: patch.urlTestInterval ?? urlTestInterval, - enableClashApi: patch.enableClashApi ?? enableClashApi, - clashApiPort: patch.clashApiPort ?? clashApiPort, - bypassLan: patch.bypassLan ?? bypassLan, - allowConnectionFromLan: - patch.allowConnectionFromLan ?? allowConnectionFromLan, - enableFakeDns: patch.enableFakeDns ?? enableFakeDns, - enableDnsRouting: patch.enableDnsRouting ?? enableDnsRouting, - independentDnsCache: patch.independentDnsCache ?? independentDnsCache, - enableTlsFragment: patch.enableTlsFragment ?? enableTlsFragment, - tlsFragmentSize: patch.tlsFragmentSize ?? tlsFragmentSize, - tlsFragmentSleep: patch.tlsFragmentSleep ?? tlsFragmentSleep, - enableTlsMixedSniCase: - patch.enableTlsMixedSniCase ?? enableTlsMixedSniCase, - enableTlsPadding: patch.enableTlsPadding ?? enableTlsPadding, - tlsPaddingSize: patch.tlsPaddingSize ?? tlsPaddingSize, - enableMux: patch.enableMux ?? enableMux, - muxPadding: patch.muxPadding ?? muxPadding, - muxMaxStreams: patch.muxMaxStreams ?? muxMaxStreams, - muxProtocol: patch.muxProtocol ?? muxProtocol, - enableWarp: patch.enableWarp ?? enableWarp, - warpDetourMode: patch.warpDetourMode ?? warpDetourMode, - warpLicenseKey: patch.warpLicenseKey ?? warpLicenseKey, - warpAccountId: patch.warpAccountId ?? warpAccountId, - warpAccessToken: patch.warpAccessToken ?? warpAccessToken, - warpCleanIp: patch.warpCleanIp ?? warpCleanIp, - warpPort: patch.warpPort ?? warpPort, - warpNoise: patch.warpNoise ?? warpNoise, - warpNoiseDelay: patch.warpNoiseDelay ?? warpNoiseDelay, - warpWireguardConfig: patch.warpWireguardConfig ?? warpWireguardConfig, - ); - } - - SingboxConfigOption toSingbox({ - required String geoipPath, - required String geositePath, - required List rules, - }) { - return SingboxConfigOption( - executeConfigAsIs: false, - logLevel: logLevel, - resolveDestination: resolveDestination, - ipv6Mode: ipv6Mode, - remoteDnsAddress: remoteDnsAddress, - remoteDnsDomainStrategy: remoteDnsDomainStrategy, - directDnsAddress: directDnsAddress, - directDnsDomainStrategy: directDnsDomainStrategy, - mixedPort: mixedPort, - localDnsPort: localDnsPort, - tunImplementation: tunImplementation, - mtu: mtu, - strictRoute: strictRoute, - connectionTestUrl: connectionTestUrl, - urlTestInterval: urlTestInterval, - enableClashApi: enableClashApi, - clashApiPort: clashApiPort, - enableTun: serviceMode == ServiceMode.tun, - enableTunService: serviceMode == ServiceMode.tunService, - setSystemProxy: serviceMode == ServiceMode.systemProxy, - bypassLan: bypassLan, - allowConnectionFromLan: allowConnectionFromLan, - enableFakeDns: enableFakeDns, - enableDnsRouting: enableDnsRouting, - independentDnsCache: independentDnsCache, - enableTlsFragment: enableTlsFragment, - tlsFragmentSize: tlsFragmentSize, - tlsFragmentSleep: tlsFragmentSleep, - enableTlsMixedSniCase: enableTlsMixedSniCase, - enableTlsPadding: enableTlsPadding, - tlsPaddingSize: tlsPaddingSize, - enableMux: enableMux, - muxPadding: muxPadding, - muxMaxStreams: muxMaxStreams, - muxProtocol: muxProtocol, - geoipPath: geoipPath, - geositePath: geositePath, - rules: rules, - warp: SingboxWarpOption( - enable: enableWarp, - mode: warpDetourMode, - licenseKey: warpLicenseKey, - accountId: warpAccountId, - accessToken: warpAccessToken, - cleanIp: warpCleanIp, - cleanPort: warpPort, - warpNoise: warpNoise, - warpNoiseDelay: warpNoiseDelay, - wireguardConfig: warpWireguardConfig, - ), - ); - } - - factory ConfigOptionEntity.fromJson(Map json) => - _$ConfigOptionEntityFromJson(json); -} - -@freezed -class ConfigOptionPatch with _$ConfigOptionPatch { - const ConfigOptionPatch._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory ConfigOptionPatch({ - ServiceMode? serviceMode, - LogLevel? logLevel, - bool? resolveDestination, - IPv6Mode? ipv6Mode, - String? remoteDnsAddress, - DomainStrategy? remoteDnsDomainStrategy, - String? directDnsAddress, - DomainStrategy? directDnsDomainStrategy, - int? mixedPort, - int? localDnsPort, - TunImplementation? tunImplementation, - int? mtu, - bool? strictRoute, - String? connectionTestUrl, - @IntervalInSecondsConverter() Duration? urlTestInterval, - bool? enableClashApi, - int? clashApiPort, - bool? bypassLan, - bool? allowConnectionFromLan, - bool? enableFakeDns, - bool? enableDnsRouting, - bool? independentDnsCache, - bool? enableTlsFragment, - @OptionalRangeJsonConverter() OptionalRange? tlsFragmentSize, - @OptionalRangeJsonConverter() OptionalRange? tlsFragmentSleep, - bool? enableTlsMixedSniCase, - bool? enableTlsPadding, - @OptionalRangeJsonConverter() OptionalRange? tlsPaddingSize, - bool? enableMux, - bool? muxPadding, - int? muxMaxStreams, - MuxProtocol? muxProtocol, - bool? enableWarp, - WarpDetourMode? warpDetourMode, - String? warpLicenseKey, - String? warpAccountId, - String? warpAccessToken, - String? warpCleanIp, - int? warpPort, - @OptionalRangeJsonConverter() OptionalRange? warpNoise, - @OptionalRangeJsonConverter() OptionalRange? warpNoiseDelay, - String? warpWireguardConfig, - }) = _ConfigOptionPatch; - - factory ConfigOptionPatch.fromJson(Map json) => - _$ConfigOptionPatchFromJson(json); -} diff --git a/lib/features/config_option/notifier/config_option_notifier.dart b/lib/features/config_option/notifier/config_option_notifier.dart index d826b5f0..3efda727 100644 --- a/lib/features/config_option/notifier/config_option_notifier.dart +++ b/lib/features/config_option/notifier/config_option_notifier.dart @@ -1,5 +1,7 @@ -import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; -import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -8,28 +10,22 @@ part 'config_option_notifier.g.dart'; @Riverpod(keepAlive: true) class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { @override - Future build() async { - return ref - .watch(configOptionRepositoryProvider) - .getConfigOption() - .getOrElse((l) { - loggy.error("error getting persisted options $l", l); - throw l; - }); - } + Future build() async {} - Future updateOption(ConfigOptionPatch patch) async { - if (state case AsyncData(value: final options)) { - await ref - .read(configOptionRepositoryProvider) - .updateConfigOption(patch) - .map((_) => state = AsyncData(options.patch(patch))) - .run(); - } + Future exportJsonToClipboard() async { + final map = { + for (final option in ConfigOptions.preferences) + ref.read(option.notifier).entry.key: ref.read(option.notifier).raw(), + }; + const encoder = JsonEncoder.withIndent(' '); + final json = encoder.convert(map); + await Clipboard.setData(ClipboardData(text: json)); } Future resetOption() async { - await ref.read(configOptionRepositoryProvider).resetConfigOption().run(); + for (final option in ConfigOptions.preferences) { + await ref.read(option.notifier).reset(); + } ref.invalidateSelf(); } } diff --git a/lib/features/config_option/notifier/warp_option_notifier.dart b/lib/features/config_option/notifier/warp_option_notifier.dart index fcfdc551..31cb58a9 100644 --- a/lib/features/config_option/notifier/warp_option_notifier.dart +++ b/lib/features/config_option/notifier/warp_option_notifier.dart @@ -1,7 +1,8 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; -import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/model/config_option_failure.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -46,15 +47,28 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger { Future generateWarpConfig() async { if (state.configGeneration.isLoading) return; state = state.copyWith(configGeneration: const AsyncLoading()); - final result = await AsyncValue.guard( - () async => await ref - .read(configOptionRepositoryProvider) - .generateWarpConfig() - .getOrElse((l) { - loggy.warning("error generating warp config: $l", l); - throw l; - }).run(), - ); + + final result = await AsyncValue.guard(() async { + final warp = await ref + .read(singboxServiceProvider) + .generateWarpConfig( + licenseKey: ref.read(ConfigOptions.warpLicenseKey), + previousAccountId: ref.read(ConfigOptions.warpAccountId), + previousAccessToken: ref.read(ConfigOptions.warpAccessToken), + ) + .getOrElse((l) => throw l) + .run(); + + await ref + .read(ConfigOptions.warpAccountId.notifier) + .update(warp.accountId); + await ref + .read(ConfigOptions.warpAccessToken.notifier) + .update(warp.accessToken); + + return warp.log; + }); + state = state.copyWith(configGeneration: result); } diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index 29fee700..571ae985 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -1,17 +1,15 @@ import 'package:dartx/dartx.dart'; -import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; -import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/core/widget/tip_card.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/config_option/model/config_option_entity.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'; @@ -27,13 +25,6 @@ class ConfigOptionsPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final defaultOptions = ConfigOptionEntity.initial(); - final asyncOptions = ref.watch(configOptionNotifierProvider); - - Future changeOption(ConfigOptionPatch patch) async { - await ref.read(configOptionNotifierProvider.notifier).updateOption(patch); - } - String experimental(String txt) { return "$txt (${t.settings.experimental})"; } @@ -44,462 +35,259 @@ class ConfigOptionsPage extends HookConsumerWidget { NestedAppBar( title: Text(t.settings.config.pageTitle), actions: [ - if (asyncOptions case AsyncData(value: final options)) - PopupMenuButton( - icon: Icon(AdaptiveIcon(context).more), - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(t.general.addToClipboard), - onTap: () { - Clipboard.setData( - ClipboardData(text: options.format()), - ); - }, - ), - PopupMenuItem( - child: Text(t.settings.config.resetBtn), - onTap: () async { - await ref - .read(configOptionNotifierProvider.notifier) - .resetOption(); - }, - ), - ]; - }, - ), - ], - ), - switch (asyncOptions) { - AsyncData(value: final options) => SliverList.list( - children: [ - TipCard(message: t.settings.experimentalMsg), - ListTile( - title: Text(t.settings.config.logLevel), - subtitle: Text(options.logLevel.name.toUpperCase()), - onTap: () async { - final logLevel = await SettingsPickerDialog( - title: t.settings.config.logLevel, - selected: options.logLevel, - options: LogLevel.choices, - getTitle: (e) => e.name.toUpperCase(), - resetValue: defaultOptions.logLevel, - ).show(context); - if (logLevel == null) return; - await changeOption(ConfigOptionPatch(logLevel: logLevel)); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.route), - SwitchListTile( - title: Text(experimental(t.settings.config.bypassLan)), - value: options.bypassLan, - onChanged: (value) async => - changeOption(ConfigOptionPatch(bypassLan: value)), - ), - SwitchListTile( - title: Text(t.settings.config.resolveDestination), - value: options.resolveDestination, - onChanged: (value) async => changeOption( - ConfigOptionPatch(resolveDestination: value), + PopupMenuButton( + icon: Icon(AdaptiveIcon(context).more), + itemBuilder: (context) { + return [ + PopupMenuItem( + onTap: ref + .read(configOptionNotifierProvider.notifier) + .exportJsonToClipboard, + child: Text(t.general.addToClipboard), ), - ), - ListTile( - title: Text(t.settings.config.ipv6Mode), - subtitle: Text(options.ipv6Mode.present(t)), - onTap: () async { - final ipv6Mode = await SettingsPickerDialog( - title: t.settings.config.ipv6Mode, - selected: options.ipv6Mode, - options: IPv6Mode.values, - getTitle: (e) => e.present(t), - resetValue: defaultOptions.ipv6Mode, - ).show(context); - if (ipv6Mode == null) return; - await changeOption(ConfigOptionPatch(ipv6Mode: ipv6Mode)); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.dns), - ListTile( - title: Text(t.settings.config.remoteDnsAddress), - subtitle: Text(options.remoteDnsAddress), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.remoteDnsAddress, - initialValue: options.remoteDnsAddress, - resetValue: defaultOptions.remoteDnsAddress, - ).show(context); - if (url == null || url.isEmpty) return; - await changeOption( - ConfigOptionPatch(remoteDnsAddress: url), - ); - }, - ), - ListTile( - title: Text(t.settings.config.remoteDnsDomainStrategy), - subtitle: Text(options.remoteDnsDomainStrategy.displayName), - onTap: () async { - final domainStrategy = await SettingsPickerDialog( - title: t.settings.config.remoteDnsDomainStrategy, - selected: options.remoteDnsDomainStrategy, - options: DomainStrategy.values, - getTitle: (e) => e.displayName, - resetValue: defaultOptions.remoteDnsDomainStrategy, - ).show(context); - if (domainStrategy == null) return; - await changeOption( - ConfigOptionPatch( - remoteDnsDomainStrategy: domainStrategy, - ), - ); - }, - ), - ListTile( - title: Text(t.settings.config.directDnsAddress), - subtitle: Text(options.directDnsAddress), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.directDnsAddress, - initialValue: options.directDnsAddress, - resetValue: defaultOptions.directDnsAddress, - ).show(context); - if (url == null || url.isEmpty) return; - await changeOption( - ConfigOptionPatch(directDnsAddress: url), - ); - }, - ), - ListTile( - title: Text(t.settings.config.directDnsDomainStrategy), - subtitle: Text(options.directDnsDomainStrategy.displayName), - onTap: () async { - final domainStrategy = await SettingsPickerDialog( - title: t.settings.config.directDnsDomainStrategy, - selected: options.directDnsDomainStrategy, - options: DomainStrategy.values, - getTitle: (e) => e.displayName, - resetValue: defaultOptions.directDnsDomainStrategy, - ).show(context); - if (domainStrategy == null) return; - await changeOption( - ConfigOptionPatch( - directDnsDomainStrategy: domainStrategy, - ), - ); - }, - ), - SwitchListTile( - title: Text(t.settings.config.enableDnsRouting), - value: options.enableDnsRouting, - onChanged: (value) => changeOption( - ConfigOptionPatch(enableDnsRouting: value), - ), - ), - const SettingsDivider(), - SettingsSection(experimental(t.settings.config.section.mux)), - SwitchListTile( - title: Text(t.settings.config.enableMux), - value: options.enableMux, - onChanged: (value) => changeOption( - ConfigOptionPatch(enableMux: value), - ), - ), - ListTile( - title: Text(t.settings.config.muxProtocol), - subtitle: Text(options.muxProtocol.name), - onTap: () async { - final pickedProtocol = await SettingsPickerDialog( - title: t.settings.config.muxProtocol, - selected: options.muxProtocol, - options: MuxProtocol.values, - getTitle: (e) => e.name, - resetValue: defaultOptions.muxProtocol, - ).show(context); - if (pickedProtocol == null) return; - await changeOption( - ConfigOptionPatch(muxProtocol: pickedProtocol), - ); - }, - ), - ListTile( - title: Text(t.settings.config.muxMaxStreams), - subtitle: Text(options.muxMaxStreams.toString()), - onTap: () async { - final maxStreams = await SettingsInputDialog( - title: t.settings.config.muxMaxStreams, - initialValue: options.muxMaxStreams, - resetValue: defaultOptions.muxMaxStreams, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (maxStreams == null || maxStreams < 1) return; - await changeOption( - ConfigOptionPatch(muxMaxStreams: maxStreams), - ); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.inbound), - ListTile( - title: Text(t.settings.config.serviceMode), - subtitle: Text(options.serviceMode.present(t)), - onTap: () async { - final pickedMode = await SettingsPickerDialog( - title: t.settings.config.serviceMode, - selected: options.serviceMode, - options: ServiceMode.choices, - getTitle: (e) => e.present(t), - resetValue: ServiceMode.defaultMode, - ).show(context); - if (pickedMode == null) return; - await changeOption( - ConfigOptionPatch(serviceMode: pickedMode), - ); - }, - ), - SwitchListTile( - title: Text(t.settings.config.strictRoute), - value: options.strictRoute, - onChanged: (value) async => - changeOption(ConfigOptionPatch(strictRoute: value)), - ), - 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: defaultOptions.tunImplementation, - ).show(context); - if (tunImplementation == null) return; - await changeOption( - ConfigOptionPatch(tunImplementation: tunImplementation), - ); - }, - ), - ListTile( - title: Text(t.settings.config.mixedPort), - subtitle: Text(options.mixedPort.toString()), - onTap: () async { - final mixedPort = await SettingsInputDialog( - title: t.settings.config.mixedPort, - initialValue: options.mixedPort, - resetValue: defaultOptions.mixedPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (mixedPort == null) return; - await changeOption( - ConfigOptionPatch(mixedPort: mixedPort), - ); - }, - ), - ListTile( - title: Text(t.settings.config.localDnsPort), - subtitle: Text(options.localDnsPort.toString()), - onTap: () async { - final localDnsPort = await SettingsInputDialog( - title: t.settings.config.localDnsPort, - initialValue: options.localDnsPort, - resetValue: defaultOptions.localDnsPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (localDnsPort == null) return; - await changeOption( - ConfigOptionPatch(localDnsPort: localDnsPort), - ); - }, - ), - SwitchListTile( - title: Text( - experimental(t.settings.config.allowConnectionFromLan), - ), - value: options.allowConnectionFromLan, - onChanged: (value) => changeOption( - ConfigOptionPatch(allowConnectionFromLan: value), - ), - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.tlsTricks), - SwitchListTile( - title: - Text(experimental(t.settings.config.enableTlsFragment)), - value: options.enableTlsFragment, - onChanged: (value) async => changeOption( - ConfigOptionPatch(enableTlsFragment: value), - ), - ), - ListTile( - title: Text(t.settings.config.tlsFragmentSize), - subtitle: Text(options.tlsFragmentSize.present(t)), - onTap: () async { - final range = await SettingsInputDialog( - title: t.settings.config.tlsFragmentSize, - initialValue: options.tlsFragmentSize.format(), - resetValue: defaultOptions.tlsFragmentSize.format(), - ).show(context); - if (range == null) return; - await changeOption( - ConfigOptionPatch( - tlsFragmentSize: OptionalRange.tryParse(range), - ), - ); - }, - ), - ListTile( - title: Text(t.settings.config.tlsFragmentSleep), - subtitle: Text(options.tlsFragmentSleep.present(t)), - onTap: () async { - final range = await SettingsInputDialog( - title: t.settings.config.tlsFragmentSleep, - initialValue: options.tlsFragmentSleep.format(), - resetValue: defaultOptions.tlsFragmentSleep.format(), - ).show(context); - if (range == null) return; - await changeOption( - ConfigOptionPatch( - tlsFragmentSleep: OptionalRange.tryParse(range), - ), - ); - }, - ), - SwitchListTile( - title: Text( - experimental(t.settings.config.enableTlsMixedSniCase), - ), - value: options.enableTlsMixedSniCase, - onChanged: (value) async => changeOption( - ConfigOptionPatch(enableTlsMixedSniCase: value), - ), - ), - SwitchListTile( - title: - Text(experimental(t.settings.config.enableTlsPadding)), - value: options.enableTlsPadding, - onChanged: (value) async => changeOption( - ConfigOptionPatch(enableTlsPadding: value), - ), - ), - ListTile( - title: Text(t.settings.config.tlsPaddingSize), - subtitle: Text(options.tlsPaddingSize.present(t)), - onTap: () async { - final range = await SettingsInputDialog( - title: t.settings.config.tlsPaddingSize, - initialValue: options.tlsPaddingSize.format(), - resetValue: defaultOptions.tlsPaddingSize.format(), - ).show(context); - if (range == null) return; - await changeOption( - ConfigOptionPatch( - tlsPaddingSize: OptionalRange.tryParse(range), - ), - ); - }, - ), - const SettingsDivider(), - SettingsSection(experimental(t.settings.config.section.warp)), - WarpOptionsTiles( - options: options, - defaultOptions: defaultOptions, - onChange: changeOption, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.misc), - ListTile( - title: Text(t.settings.config.connectionTestUrl), - subtitle: Text(options.connectionTestUrl), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.connectionTestUrl, - initialValue: options.connectionTestUrl, - resetValue: defaultOptions.connectionTestUrl, - ).show(context); - if (url == null || url.isEmpty || !isUrl(url)) return; - await changeOption( - ConfigOptionPatch(connectionTestUrl: 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 - .coerceIn(0, 60) - .toDouble(), - resetValue: - defaultOptions.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 changeOption( - ConfigOptionPatch( - urlTestInterval: - Duration(minutes: urlTestInterval.toInt()), - ), - ); - }, - ), - ListTile( - title: Text(t.settings.config.clashApiPort), - subtitle: Text(options.clashApiPort.toString()), - onTap: () async { - final clashApiPort = await SettingsInputDialog( - title: t.settings.config.clashApiPort, - initialValue: options.clashApiPort, - resetValue: defaultOptions.clashApiPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (clashApiPort == null) return; - await changeOption( - ConfigOptionPatch(clashApiPort: clashApiPort), - ); - }, - ), - const Gap(24), - ], - ), - AsyncError(:final error) => SliverFillRemaining( - hasScrollBody: false, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - const Icon(FluentIcons.error_circle_24_regular), - const Gap(2), - Text(t.presentShortError(error)), - const Gap(2), - TextButton( - onPressed: () async { + PopupMenuItem( + child: Text(t.settings.config.resetBtn), + onTap: () async { await ref .read(configOptionNotifierProvider.notifier) .resetOption(); }, - child: Text(t.settings.config.resetBtn), ), - ], - ), + ]; + }, ), - _ => const SliverToBoxAdapter(), - }, + ], + ), + 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), + ], + ), ], ), ); diff --git a/lib/features/config_option/overview/warp_options_widgets.dart b/lib/features/config_option/overview/warp_options_widgets.dart index 22c33157..2e857441 100644 --- a/lib/features/config_option/overview/warp_options_widgets.dart +++ b/lib/features/config_option/overview/warp_options_widgets.dart @@ -1,29 +1,19 @@ -import 'package:dartx/dartx.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/widget/custom_alert_dialog.dart'; -import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart'; -import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; +import 'package:hiddify/features/config_option/widget/preference_tile.dart'; import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/utils/uri_utils.dart'; import 'package:hiddify/utils/validators.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class WarpOptionsTiles extends HookConsumerWidget { - const WarpOptionsTiles({ - required this.options, - required this.defaultOptions, - required this.onChange, - super.key, - }); - - final ConfigOptionEntity options; - final ConfigOptionEntity defaultOptions; - final Future Function(ConfigOptionPatch patch) onChange; + const WarpOptionsTiles({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -31,7 +21,8 @@ class WarpOptionsTiles extends HookConsumerWidget { final warpOptions = ref.watch(warpOptionNotifierProvider); final warpPrefaceCompleted = warpOptions.consentGiven; - final canChangeOptions = warpPrefaceCompleted && options.enableWarp; + final enableWarp = ref.watch(ConfigOptions.enableWarp); + final canChangeOptions = warpPrefaceCompleted && enableWarp; ref.listen( warpOptionNotifierProvider.select((value) => value.configGeneration), @@ -49,7 +40,7 @@ class WarpOptionsTiles extends HookConsumerWidget { children: [ SwitchListTile( title: Text(t.settings.config.enableWarp), - value: options.enableWarp, + value: enableWarp, onChanged: (value) async { if (!warpPrefaceCompleted) { final agreed = await showDialog( @@ -58,10 +49,10 @@ class WarpOptionsTiles extends HookConsumerWidget { ); if (agreed ?? false) { await ref.read(warpOptionNotifierProvider.notifier).agree(); - await onChange(ConfigOptionPatch(enableWarp: value)); + await ref.read(ConfigOptions.enableWarp.notifier).update(value); } } else { - await onChange(ConfigOptionPatch(enableWarp: value)); + await ref.read(ConfigOptions.enableWarp.notifier).update(value); } }, ), @@ -85,112 +76,56 @@ class WarpOptionsTiles extends HookConsumerWidget { .generateWarpConfig(); }, ), - ListTile( - title: Text(t.settings.config.warpDetourMode), - subtitle: Text(options.warpDetourMode.present(t)), + ChoicePreferenceWidget( + selected: ref.watch(ConfigOptions.warpDetourMode), + preferences: ref.watch(ConfigOptions.warpDetourMode.notifier), enabled: canChangeOptions, - onTap: () async { - final warpDetourMode = await SettingsPickerDialog( - title: t.settings.config.warpDetourMode, - selected: options.warpDetourMode, - options: WarpDetourMode.values, - getTitle: (e) => e.present(t), - resetValue: defaultOptions.warpDetourMode, - ).show(context); - if (warpDetourMode == null) return; - await onChange( - ConfigOptionPatch(warpDetourMode: warpDetourMode), - ); - }, + choices: WarpDetourMode.values, + title: t.settings.config.warpDetourMode, + presentChoice: (value) => value.present(t), ), - ListTile( - title: Text(t.settings.config.warpLicenseKey), - subtitle: Text( - options.warpLicenseKey.isEmpty - ? t.general.notSet - : options.warpLicenseKey, - ), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.warpLicenseKey), + preferences: ref.watch(ConfigOptions.warpLicenseKey.notifier), enabled: canChangeOptions, - onTap: () async { - final licenseKey = await SettingsInputDialog( - title: t.settings.config.warpLicenseKey, - initialValue: options.warpLicenseKey, - resetValue: defaultOptions.warpLicenseKey, - ).show(context); - if (licenseKey == null) return; - await onChange(ConfigOptionPatch(warpLicenseKey: licenseKey)); - }, + title: t.settings.config.warpLicenseKey, + presentValue: (value) => value.isEmpty ? t.general.notSet : value, ), - ListTile( - title: Text(t.settings.config.warpCleanIp), - subtitle: Text(options.warpCleanIp), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.warpCleanIp), + preferences: ref.watch(ConfigOptions.warpCleanIp.notifier), enabled: canChangeOptions, - onTap: () async { - final warpCleanIp = await SettingsInputDialog( - title: t.settings.config.warpCleanIp, - initialValue: options.warpCleanIp, - resetValue: defaultOptions.warpCleanIp, - ).show(context); - if (warpCleanIp == null || warpCleanIp.isBlank) return; - await onChange(ConfigOptionPatch(warpCleanIp: warpCleanIp)); - }, + title: t.settings.config.warpCleanIp, ), - ListTile( - title: Text(t.settings.config.warpPort), - subtitle: Text(options.warpPort.toString()), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.warpPort), + preferences: ref.watch(ConfigOptions.warpPort.notifier), enabled: canChangeOptions, - onTap: () async { - final warpPort = await SettingsInputDialog( - title: t.settings.config.warpPort, - initialValue: options.warpPort, - resetValue: defaultOptions.warpPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (warpPort == null) return; - await onChange( - ConfigOptionPatch(warpPort: warpPort), - ); - }, + title: t.settings.config.warpPort, + inputToValue: int.tryParse, + validateInput: isPort, + digitsOnly: true, ), - ListTile( - title: Text(t.settings.config.warpNoise), - subtitle: Text(options.warpNoise.present(t)), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.warpNoise), + preferences: ref.watch(ConfigOptions.warpNoise.notifier), enabled: canChangeOptions, - onTap: () async { - final warpNoise = await SettingsInputDialog( - title: t.settings.config.warpNoise, - initialValue: options.warpNoise.format(), - resetValue: defaultOptions.warpNoise.format(), - ).show(context); - if (warpNoise == null) return; - await onChange( - ConfigOptionPatch( - warpNoise: OptionalRange.tryParse(warpNoise, allowEmpty: true), - ), - ); - }, + title: t.settings.config.warpNoise, + inputToValue: (input) => + OptionalRange.tryParse(input, allowEmpty: true), + presentValue: (value) => value.present(t), + formatInputValue: (value) => value.format(), ), - ListTile( - title: Text(t.settings.config.warpNoiseDelay), - subtitle: Text(options.warpNoiseDelay.present(t)), + ValuePreferenceWidget( + value: ref.watch(ConfigOptions.warpNoiseDelay), + preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier), enabled: canChangeOptions, - onTap: () async { - final warpNoiseDelay = await SettingsInputDialog( - title: t.settings.config.warpNoiseDelay, - initialValue: options.warpNoiseDelay.format(), - resetValue: defaultOptions.warpNoiseDelay.format(), - ).show(context); - if (warpNoiseDelay == null) return; - await onChange( - ConfigOptionPatch( - warpNoiseDelay: - OptionalRange.tryParse(warpNoiseDelay, allowEmpty: true), - ), - ); - }, - ) + title: t.settings.config.warpNoiseDelay, + inputToValue: (input) => + OptionalRange.tryParse(input, allowEmpty: true), + presentValue: (value) => value.present(t), + formatInputValue: (value) => value.format(), + ), ], ); } diff --git a/lib/features/config_option/widget/preference_tile.dart b/lib/features/config_option/widget/preference_tile.dart new file mode 100644 index 00000000..ddcf072e --- /dev/null +++ b/lib/features/config_option/widget/preference_tile.dart @@ -0,0 +1,95 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/utils/preferences_utils.dart'; +import 'package:hiddify/features/settings/widgets/widgets.dart'; + +class ValuePreferenceWidget extends StatelessWidget { + const ValuePreferenceWidget({ + super.key, + required this.value, + required this.preferences, + this.enabled = true, + required this.title, + this.presentValue, + this.formatInputValue, + this.validateInput, + this.inputToValue, + this.digitsOnly = false, + }); + + final T value; + final PreferencesNotifier preferences; + final bool enabled; + final String title; + final String Function(T value)? presentValue; + final String Function(T value)? formatInputValue; + final bool Function(String value)? validateInput; + final T? Function(String input)? inputToValue; + final bool digitsOnly; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + subtitle: Text(presentValue?.call(value) ?? value.toString()), + enabled: enabled, + onTap: () async { + final inputValue = await SettingsInputDialog( + title: title, + initialValue: value, + validator: validateInput, + valueFormatter: formatInputValue, + onReset: preferences.reset, + digitsOnly: digitsOnly, + mapTo: inputToValue, + ).show(context); + if (inputValue == null) { + return; + } + await preferences.update(inputValue); + }, + ); + } +} + +class ChoicePreferenceWidget extends StatelessWidget { + const ChoicePreferenceWidget({ + super.key, + required this.selected, + required this.preferences, + this.enabled = true, + required this.choices, + required this.title, + required this.presentChoice, + this.validateInput, + }); + + final T selected; + final PreferencesNotifier preferences; + final bool enabled; + final List choices; + final String title; + final String Function(T value) presentChoice; + final bool Function(String value)? validateInput; + + @override + Widget build(BuildContext context) { + return ListTile( + title: Text(title), + subtitle: Text(presentChoice(selected)), + enabled: enabled, + onTap: () async { + final selection = await SettingsPickerDialog( + title: title, + selected: selected, + options: choices, + getTitle: (e) => presentChoice(e), + onReset: preferences.reset, + ).show(context); + if (selection == null) { + return; + } + await preferences.update(selection); + }, + ); + } +} diff --git a/lib/features/connection/data/connection_data_providers.dart b/lib/features/connection/data/connection_data_providers.dart index 6287964b..96de56d9 100644 --- a/lib/features/connection/data/connection_data_providers.dart +++ b/lib/features/connection/data/connection_data_providers.dart @@ -15,8 +15,7 @@ ConnectionRepository connectionRepository( ) { return ConnectionRepositoryImpl( directories: ref.watch(appDirectoriesProvider).requireValue, - singBoxConfigOptionRepository: - ref.watch(singBoxConfigOptionRepositoryProvider), + configOptionRepository: ref.watch(configOptionRepositoryProvider), singbox: ref.watch(singboxServiceProvider), platformSource: ConnectionPlatformSourceImpl(), profilePathResolver: ref.watch(profilePathResolverProvider), diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart index 9d3aeb5b..67917541 100644 --- a/lib/features/connection/data/connection_repository.dart +++ b/lib/features/connection/data/connection_repository.dart @@ -38,7 +38,7 @@ class ConnectionRepositoryImpl required this.directories, required this.singbox, required this.platformSource, - required this.singBoxConfigOptionRepository, + required this.configOptionRepository, required this.profilePathResolver, required this.geoAssetPathResolver, }); @@ -46,7 +46,7 @@ class ConnectionRepositoryImpl final Directories directories; final SingboxService singbox; final ConnectionPlatformSource platformSource; - final SingBoxConfigOptionRepository singBoxConfigOptionRepository; + final ConfigOptionRepository configOptionRepository; final ProfilePathResolver profilePathResolver; final GeoAssetPathResolver geoAssetPathResolver; @@ -83,7 +83,7 @@ class ConnectionRepositoryImpl return TaskEither.Do( ($) async { final options = await $( - singBoxConfigOptionRepository + configOptionRepository .getFullSingboxConfigOption() .mapLeft((l) => const InvalidConfigOption()), ); diff --git a/lib/features/connection/notifier/connection_notifier.dart b/lib/features/connection/notifier/connection_notifier.dart index ddcccd91..8e3b07f7 100644 --- a/lib/features/connection/notifier/connection_notifier.dart +++ b/lib/features/connection/notifier/connection_notifier.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:hiddify/core/haptic/haptic_service.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; -import 'package:hiddify/core/preferences/service_preferences.dart'; import 'package:hiddify/features/connection/data/connection_data_providers.dart'; import 'package:hiddify/features/connection/data/connection_repository.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; @@ -49,7 +48,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { yield* _connectionRepo.watchConnectionStatus().doOnData((event) { if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) { - ref.read(startedByUserProvider.notifier).update(false); + ref.read(Preferences.startedByUser.notifier).update(false); } loggy.info("connection status: ${event.format()}"); }); @@ -73,11 +72,11 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { switch (value) { case Disconnected(): await haptic.lightImpact(); - await ref.read(startedByUserProvider.notifier).update(true); + await ref.read(Preferences.startedByUser.notifier).update(true); await _connect(); case Connected(): await haptic.mediumImpact(); - await ref.read(startedByUserProvider.notifier).update(false); + await ref.read(Preferences.startedByUser.notifier).update(false); await _disconnect(); default: loggy.warning("switching status, debounce"); @@ -92,12 +91,12 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { return _disconnect(); } loggy.info("active profile changed, reconnecting"); - await ref.read(startedByUserProvider.notifier).update(true); + await ref.read(Preferences.startedByUser.notifier).update(true); await _connectionRepo .reconnect( profile.id, profile.name, - ref.read(disableMemoryLimitProvider), + ref.read(Preferences.disableMemoryLimit), ) .mapLeft((err) { loggy.warning("error reconnecting", err); @@ -127,7 +126,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { .connect( activeProfile.id, activeProfile.name, - ref.read(disableMemoryLimitProvider), + ref.read(Preferences.disableMemoryLimit), ) .mapLeft((err) async { loggy.warning("error connecting", err); @@ -136,7 +135,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { if (err.toString().contains("panic")) { await Sentry.captureException(Exception(err.toString())); } - await ref.read(startedByUserProvider.notifier).update(false); + await ref.read(Preferences.startedByUser.notifier).update(false); state = AsyncError(err, StackTrace.current); }).run(); } diff --git a/lib/features/home/widget/connection_button.dart b/lib/features/home/widget/connection_button.dart index eb1c3efe..7ea7d0bf 100644 --- a/lib/features/home/widget/connection_button.dart +++ b/lib/features/home/widget/connection_button.dart @@ -4,7 +4,7 @@ import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/theme/theme_extensions.dart'; -import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/connection/widget/experimental_feature_notice.dart'; @@ -48,10 +48,7 @@ class ConnectionButton extends HookConsumerWidget { var canConnect = true; if (status case Disconnected()) { final hasExperimental = - await ref.read(configOptionNotifierProvider.future).then( - (value) => value.hasExperimentalOptions(), - onError: (_) => false, - ); + ref.read(ConfigOptions.hasExperimentalFeatures); final canShowNotice = !ref.read(disableExperimentalFeatureNoticeProvider); diff --git a/lib/features/intro/widget/intro_page.dart b/lib/features/intro/widget/intro_page.dart index 78446b5e..2c880276 100644 --- a/lib/features/intro/widget/intro_page.dart +++ b/lib/features/intro/widget/intro_page.dart @@ -1,12 +1,9 @@ -import 'dart:convert'; import 'package:flutter/gestures.dart'; -import 'package:hiddify/core/http_client/dio_http_client.dart'; -import 'package:timezone_to_country/timezone_to_country.dart'; - import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/analytics/analytics_controller.dart'; +import 'package:hiddify/core/http_client/dio_http_client.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; @@ -16,12 +13,14 @@ import 'package:hiddify/features/common/general_pref_tiles.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:sliver_tools/sliver_tools.dart'; +import 'package:timezone_to_country/timezone_to_country.dart'; class IntroPage extends HookConsumerWidget with PresLogger { - bool locationInfoLoaded = false; IntroPage({super.key}); + + bool locationInfoLoaded = false; + @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); @@ -101,7 +100,7 @@ class IntroPage extends HookConsumerWidget with PresLogger { } } await ref - .read(introCompletedProvider.notifier) + .read(Preferences.introCompleted.notifier) .update(true); }, child: isStarting.value @@ -128,9 +127,7 @@ class IntroPage extends HookConsumerWidget with PresLogger { loggy.debug( 'Timezone Region: ${regionLocale.region} Locale: ${regionLocale.locale}', ); - await ref - .read(regionNotifierProvider.notifier) - .update(regionLocale.region); + await ref.read(Preferences.region.notifier).update(regionLocale.region); await ref .read(localePreferencesProvider.notifier) .changeLocale(regionLocale.locale); @@ -144,10 +141,11 @@ class IntroPage extends HookConsumerWidget with PresLogger { try { final DioHttpClient client = DioHttpClient( - timeout: const Duration(seconds: 2), - userAgent: - "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", - debug: true); + timeout: const Duration(seconds: 2), + userAgent: + "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0", + debug: true, + ); final response = await client.get>('https://api.ip.sb/geoip/'); @@ -159,9 +157,7 @@ class IntroPage extends HookConsumerWidget with PresLogger { loggy.debug( 'Region: ${regionLocale.region} Locale: ${regionLocale.locale}', ); - await ref - .read(regionNotifierProvider.notifier) - .update(regionLocale.region); + await ref.read(Preferences.region.notifier).update(regionLocale.region); await ref .read(localePreferencesProvider.notifier) .changeLocale(regionLocale.locale); diff --git a/lib/features/per_app_proxy/overview/per_app_proxy_page.dart b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart index c500e15f..f8474932 100644 --- a/lib/features/per_app_proxy/overview/per_app_proxy_page.dart +++ b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart @@ -22,7 +22,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { final localizations = MaterialLocalizations.of(context); final asyncPackages = ref.watch(installedPackagesInfoProvider); - final perAppProxyMode = ref.watch(perAppProxyModeNotifierProvider); + final perAppProxyMode = ref.watch(Preferences.perAppProxyMode); final perAppProxyList = ref.watch(perAppProxyListProvider); final showSystemApps = useState(true); @@ -130,7 +130,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger { groupValue: perAppProxyMode, onChanged: (value) async { await ref - .read(perAppProxyModeNotifierProvider.notifier) + .read(Preferences.perAppProxyMode.notifier) .update(e); if (e == PerAppProxyMode.off && context.mounted) { context.pop(); diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart index a51b7c99..98c1368e 100644 --- a/lib/features/profile/notifier/profile_notifier.dart +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -58,7 +58,7 @@ class AddProfile extends _$AddProfile with AppLogger { () async { final activeProfile = await ref.read(activeProfileProvider.future); final markAsActive = - activeProfile == null || ref.read(markNewProfileActiveProvider); + activeProfile == null || ref.read(Preferences.markNewProfileActive); final TaskEither task; if (LinkParser.parse(rawInput) case (final link)?) { loggy.debug("adding profile, url: [${link.url}]"); diff --git a/lib/features/profile/notifier/profiles_update_notifier.dart b/lib/features/profile/notifier/profiles_update_notifier.dart index ff5388ce..1b347427 100644 --- a/lib/features/profile/notifier/profiles_update_notifier.dart +++ b/lib/features/profile/notifier/profiles_update_notifier.dart @@ -36,7 +36,7 @@ class ForegroundProfilesUpdateNotifier _scheduler = null; }); - if (ref.watch(introCompletedProvider)) { + if (ref.watch(Preferences.introCompleted)) { loggy.debug("intro done, starting"); _scheduler?.start(); } else { diff --git a/lib/features/proxy/active/active_proxy_notifier.dart b/lib/features/proxy/active/active_proxy_notifier.dart index 4628ec74..0fd5fc57 100644 --- a/lib/features/proxy/active/active_proxy_notifier.dart +++ b/lib/features/proxy/active/active_proxy_notifier.dart @@ -33,7 +33,7 @@ class IpInfoNotifier extends _$IpInfoNotifier with AppLogger { (_, next) => _idle = false, ); - final autoCheck = ref.watch(autoCheckIpProvider); + final autoCheck = ref.watch(Preferences.autoCheckIp); final serviceRunning = await ref.watch(serviceRunningProvider.future); // loggy.debug( // "idle? [$_idle], forced? [$_forceCheck], connected? [$serviceRunning]", diff --git a/lib/features/proxy/overview/proxies_overview_notifier.dart b/lib/features/proxy/overview/proxies_overview_notifier.dart index 4f967e54..deb27f38 100644 --- a/lib/features/proxy/overview/proxies_overview_notifier.dart +++ b/lib/features/proxy/overview/proxies_overview_notifier.dart @@ -5,11 +5,11 @@ import 'package:dartx/dartx.dart'; import 'package:hiddify/core/haptic/haptic_service.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/proxy/data/proxy_data_providers.dart'; import 'package:hiddify/features/proxy/model/proxy_entity.dart'; import 'package:hiddify/features/proxy/model/proxy_failure.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -31,24 +31,24 @@ enum ProxiesSort { @Riverpod(keepAlive: true) class ProxiesSortNotifier extends _$ProxiesSortNotifier with AppLogger { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - "proxies_sort_mode", - ProxiesSort.delay, + late final _pref = PreferencesEntry( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + key: "proxies_sort_mode", + defaultValue: ProxiesSort.delay, mapFrom: ProxiesSort.values.byName, mapTo: (value) => value.name, ); @override ProxiesSort build() { - final sortBy = _pref.getValue(); + final sortBy = _pref.read(); loggy.info("sort proxies by: [${sortBy.name}]"); return sortBy; } Future update(ProxiesSort value) { state = value; - return _pref.update(value); + return _pref.write(value); } } diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index 26b980e2..6558158b 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -18,8 +18,8 @@ class AdvancedSettingTiles extends HookConsumerWidget { final t = ref.watch(translationsProvider); final debug = ref.watch(debugModeNotifierProvider); - final perAppProxy = ref.watch(perAppProxyModeNotifierProvider).enabled; - final disableMemoryLimit = ref.watch(disableMemoryLimitProvider); + final perAppProxy = ref.watch(Preferences.perAppProxyMode).enabled; + final disableMemoryLimit = ref.watch(Preferences.disableMemoryLimit); return Column( children: [ @@ -43,7 +43,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { final newMode = perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude; await ref - .read(perAppProxyModeNotifierProvider.notifier) + .read(Preferences.perAppProxyMode.notifier) .update(newMode); if (!perAppProxy && context.mounted) { await const PerAppProxyRoute().push(context); @@ -53,7 +53,7 @@ class AdvancedSettingTiles extends HookConsumerWidget { onTap: () async { if (!perAppProxy) { await ref - .read(perAppProxyModeNotifierProvider.notifier) + .read(Preferences.perAppProxyMode.notifier) .update(PerAppProxyMode.exclude); } if (context.mounted) await const PerAppProxyRoute().push(context); @@ -66,7 +66,9 @@ class AdvancedSettingTiles extends HookConsumerWidget { value: !disableMemoryLimit, secondary: const Icon(FluentIcons.developer_board_24_regular), onChanged: (value) async { - await ref.read(disableMemoryLimitProvider.notifier).update(!value); + await ref + .read(Preferences.disableMemoryLimit.notifier) + .update(!value); }, ), if (Platform.isIOS) diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index bdac67bb..0ac84a68 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -58,17 +58,17 @@ class GeneralSettingTiles extends HookConsumerWidget { SwitchListTile( title: Text(t.settings.general.autoIpCheck), secondary: const Icon(FluentIcons.globe_search_24_regular), - value: ref.watch(autoCheckIpProvider), - onChanged: ref.read(autoCheckIpProvider.notifier).update, + value: ref.watch(Preferences.autoCheckIp), + onChanged: ref.read(Preferences.autoCheckIp.notifier).update, ), if (Platform.isAndroid) ...[ SwitchListTile( title: Text(t.settings.general.dynamicNotification), secondary: const Icon(FluentIcons.top_speed_24_regular), - value: ref.watch(dynamicNotificationProvider), + value: ref.watch(Preferences.dynamicNotification), onChanged: (value) async { await ref - .read(dynamicNotificationProvider.notifier) + .read(Preferences.dynamicNotification.notifier) .update(value); }, ), @@ -94,11 +94,9 @@ class GeneralSettingTiles extends HookConsumerWidget { ), SwitchListTile( title: Text(t.settings.general.silentStart), - value: ref.watch(silentStartNotifierProvider), + value: ref.watch(Preferences.silentStart), onChanged: (value) async { - await ref - .read(silentStartNotifierProvider.notifier) - .update(value); + await ref.read(Preferences.silentStart.notifier).update(value); }, ), ], diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart index 81dcf276..7d79597d 100644 --- a/lib/features/settings/widgets/settings_input_dialog.dart +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -12,7 +12,8 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { required this.initialValue, this.mapTo, this.validator, - this.resetValue, + this.valueFormatter, + this.onReset, this.optionalAction, this.icon, this.digitsOnly = false, @@ -22,7 +23,8 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { final T initialValue; final T? Function(String value)? mapTo; final bool Function(String value)? validator; - final T? resetValue; + final String Function(T value)? valueFormatter; + final VoidCallback? onReset; final (String text, VoidCallback)? optionalAction; final IconData? icon; final bool digitsOnly; @@ -41,7 +43,7 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { final localizations = MaterialLocalizations.of(context); final textController = useTextEditingController( - text: initialValue?.toString(), + text: valueFormatter?.call(initialValue) ?? initialValue.toString(), ); return FocusTraversalGroup( @@ -74,12 +76,13 @@ class SettingsInputDialog extends HookConsumerWidget with PresLogger { child: Text(optionalAction!.$1.toUpperCase()), ), ), - if (resetValue != null) + if (onReset != null) FocusTraversalOrder( order: const NumericFocusOrder(4), child: TextButton( onPressed: () async { - await Navigator.of(context).maybePop(resetValue); + onReset!(); + await Navigator.of(context).maybePop(null); }, child: Text(t.general.reset.toUpperCase()), ), @@ -123,14 +126,14 @@ class SettingsPickerDialog extends HookConsumerWidget with PresLogger { required this.selected, required this.options, required this.getTitle, - this.resetValue, + this.onReset, }); final String title; final T selected; final List options; final String Function(T e) getTitle; - final T? resetValue; + final VoidCallback? onReset; Future show(BuildContext context) async { return showDialog( @@ -162,10 +165,11 @@ class SettingsPickerDialog extends HookConsumerWidget with PresLogger { .toList(), ), actions: [ - if (resetValue != null) + if (onReset != null) TextButton( onPressed: () async { - await Navigator.of(context).maybePop(resetValue); + onReset!(); + await Navigator.of(context).maybePop(null); }, child: Text(t.general.reset.toUpperCase()), ), @@ -186,7 +190,7 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger { super.key, required this.title, required this.initialValue, - this.resetValue, + this.onReset, this.min = 0, this.max = 1, this.divisions, @@ -195,7 +199,7 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger { final String title; final double initialValue; - final double? resetValue; + final VoidCallback? onReset; final double min; final double max; final int? divisions; @@ -229,10 +233,11 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger { ), ), actions: [ - if (resetValue != null) + if (onReset != null) TextButton( onPressed: () async { - await Navigator.of(context).maybePop(resetValue); + onReset!(); + await Navigator.of(context).maybePop(null); }, child: Text(t.general.reset.toUpperCase()), ), diff --git a/lib/features/system_tray/notifier/system_tray_notifier.dart b/lib/features/system_tray/notifier/system_tray_notifier.dart index 8ed0f646..8251dd4f 100644 --- a/lib/features/system_tray/notifier/system_tray_notifier.dart +++ b/lib/features/system_tray/notifier/system_tray_notifier.dart @@ -3,8 +3,7 @@ import 'dart:io'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/config_option/model/config_option_entity.dart'; -import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/window/notifier/window_notifier.dart'; @@ -36,9 +35,7 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger { connection = const ConnectionStatus.disconnected(); } - final serviceMode = await ref - .watch(configOptionNotifierProvider.future) - .then((value) => value.serviceMode); + final serviceMode = ref.watch(ConfigOptions.serviceMode); final t = ref.watch(translationsProvider); final destinations = <(String label, String location)>[ @@ -88,8 +85,8 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger { final newMode = ServiceMode.values.byName(menuItem.key!); loggy.debug("switching service mode: [$newMode]"); await ref - .read(configOptionNotifierProvider.notifier) - .updateOption(ConfigOptionPatch(serviceMode: newMode)); + .read(ConfigOptions.serviceMode.notifier) + .update(newMode); }, ), ), diff --git a/lib/utils/pref_notifier.dart b/lib/utils/pref_notifier.dart deleted file mode 100644 index 6d05cc2d..00000000 --- a/lib/utils/pref_notifier.dart +++ /dev/null @@ -1,102 +0,0 @@ -import 'package:hiddify/core/preferences/preferences_provider.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:shared_preferences/shared_preferences.dart'; - -class Pref with InfraLogger { - const Pref( - this.prefs, - this.key, - this.defaultValue, { - this.mapFrom, - this.mapTo, - }); - - final SharedPreferences prefs; - final String key; - final T defaultValue; - final T Function(P value)? mapFrom; - final P Function(T value)? mapTo; - - /// Updates the value asynchronously. - Future update(T value) async { - loggy.debug("updating preference [$key]($T) to [$value]"); - Object? mapped = value; - if (mapTo != null) { - mapped = mapTo!(value); - } - try { - switch (mapped) { - case String _: - await prefs.setString(key, mapped); - case bool _: - await prefs.setBool(key, mapped); - case int _: - await prefs.setInt(key, mapped); - case double _: - await prefs.setDouble(key, mapped); - case List _: - await prefs.setStringList(key, mapped); - } - } catch (e) { - loggy.warning("error updating preference[$key]: $e"); - } - } - - T getValue() { - try { - loggy.debug("getting persisted preference [$key]($T)"); - if (mapFrom != null) { - final persisted = prefs.get(key) as P?; - if (persisted == null) return defaultValue; - return mapFrom!(persisted); - } else if (T == List) { - return prefs.getStringList(key) as T? ?? defaultValue; - } - return prefs.get(key) as T? ?? defaultValue; - } catch (e) { - loggy.warning("error getting preference[$key]: $e"); - return defaultValue; - } - } -} - -class PrefNotifier extends AutoDisposeNotifier with InfraLogger { - PrefNotifier( - this._key, - this._defaultValue, - this._mapFrom, - this._mapTo, - ); - - final String _key; - final T _defaultValue; - final T Function(P value)? _mapFrom; - final P Function(T)? _mapTo; - - late final Pref _pref = Pref( - ref.watch(sharedPreferencesProvider).requireValue, - _key, - _defaultValue, - mapFrom: _mapFrom, - mapTo: _mapTo, - ); - - static AutoDisposeNotifierProvider, T> provider( - String key, - T defaultValue, { - T Function(P value)? mapFrom, - P Function(T value)? mapTo, - }) => - AutoDisposeNotifierProvider( - () => PrefNotifier(key, defaultValue, mapFrom, mapTo), - ); - - Future update(T value) async { - _pref.update(value); - super.state = value; - } - - @override - T build() => _pref.getValue(); -}