Refactor preferences
This commit is contained in:
@@ -99,7 +99,7 @@ Future<void> 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) {
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<void> 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, String>(
|
||||
"region",
|
||||
Region.other,
|
||||
mapFrom: Region.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
@override
|
||||
Region build() => _pref.getValue();
|
||||
|
||||
Future<void> 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<bool, bool>(
|
||||
"silent_start",
|
||||
false,
|
||||
);
|
||||
|
||||
@override
|
||||
bool build() => _pref.getValue();
|
||||
|
||||
Future<void> 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<bool, bool>(
|
||||
"disable_memory_limit",
|
||||
// disable memory limit on desktop by default
|
||||
PlatformUtils.isDesktop,
|
||||
);
|
||||
|
||||
@override
|
||||
bool build() => _pref.getValue();
|
||||
|
||||
Future<void> 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<void> 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<PerAppProxyMode, String>(
|
||||
"per_app_proxy_mode",
|
||||
PerAppProxyMode.off,
|
||||
mapFrom: PerAppProxyMode.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
@override
|
||||
PerAppProxyMode build() => _pref.getValue();
|
||||
static final markNewProfileActive = PreferencesNotifier.create<bool, bool>(
|
||||
"mark_new_profile_active",
|
||||
true,
|
||||
);
|
||||
|
||||
Future<void> update(PerAppProxyMode value) {
|
||||
static final dynamicNotification = PreferencesNotifier.create<bool, bool>(
|
||||
"dynamic_notification",
|
||||
true,
|
||||
);
|
||||
|
||||
static final autoCheckIp = PreferencesNotifier.create<bool, bool>(
|
||||
"auto_check_ip",
|
||||
true,
|
||||
);
|
||||
|
||||
static final startedByUser = PreferencesNotifier.create<bool, bool>(
|
||||
"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<void> 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",
|
||||
<String>[],
|
||||
late final _include = PreferencesEntry(
|
||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||
key: "per_app_proxy_include_list",
|
||||
defaultValue: <String>[],
|
||||
);
|
||||
|
||||
late final _exclude = Pref(
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
"per_app_proxy_exclude_list",
|
||||
<String>[],
|
||||
late final _exclude = PreferencesEntry(
|
||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||
key: "per_app_proxy_exclude_list",
|
||||
defaultValue: <String>[],
|
||||
);
|
||||
|
||||
@override
|
||||
List<String> build() =>
|
||||
ref.watch(perAppProxyModeNotifierProvider) == PerAppProxyMode.include
|
||||
? _include.getValue()
|
||||
: _exclude.getValue();
|
||||
ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include
|
||||
? _include.read()
|
||||
: _exclude.read();
|
||||
|
||||
Future<void> update(List<String> 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<void> 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<void> 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<void> update(bool value) {
|
||||
state = value;
|
||||
return _pref.update(value);
|
||||
return _exclude.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> update(bool value) {
|
||||
state = value;
|
||||
return _pref.update(value);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ class RouterListenable extends _$RouterListenable
|
||||
|
||||
@override
|
||||
Future<void> build() async {
|
||||
_introCompleted = ref.watch(introCompletedProvider);
|
||||
_introCompleted = ref.watch(Preferences.introCompleted);
|
||||
|
||||
ref.listenSelf((_, __) {
|
||||
if (state.isLoading) return;
|
||||
|
||||
156
lib/core/utils/preferences_utils.dart
Normal file
156
lib/core/utils/preferences_utils.dart
Normal file
@@ -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<T, P> 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<String>) {
|
||||
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<bool> 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<String> 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<void> remove() async {
|
||||
try {
|
||||
await preferences.remove(key);
|
||||
} catch (e, stackTrace) {
|
||||
loggy.warning("error removing preference[$key]: $e", e, stackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class PreferencesNotifier<T, P> extends StateNotifier<T> {
|
||||
PreferencesNotifier._({
|
||||
required Ref ref,
|
||||
required this.entry,
|
||||
this.overrideValue,
|
||||
}) : _ref = ref,
|
||||
super(overrideValue ?? entry.read());
|
||||
|
||||
final Ref _ref;
|
||||
final PreferencesEntry<T, P> entry;
|
||||
final T? overrideValue;
|
||||
|
||||
static StateNotifierProvider<PreferencesNotifier<T, P>, T> create<T, P>(
|
||||
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<T, P>(
|
||||
preferences: ref.read(sharedPreferencesProvider).requireValue,
|
||||
key: key,
|
||||
defaultValue: defaultValue,
|
||||
mapFrom: mapFrom,
|
||||
mapTo: mapTo,
|
||||
validator: validator,
|
||||
),
|
||||
overrideValue: overrideValue,
|
||||
),
|
||||
);
|
||||
|
||||
static AutoDisposeStateNotifierProvider<PreferencesNotifier<T, P>, T>
|
||||
createAutoDispose<T, P>(
|
||||
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<T, P>(
|
||||
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<void> update(T value) async {
|
||||
if (await entry.write(value)) state = value;
|
||||
}
|
||||
|
||||
Future<void> reset() async {
|
||||
await entry.remove();
|
||||
_ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
@@ -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<String?, dynamic> get _ignoreReleasePref => Pref(
|
||||
ref.read(sharedPreferencesProvider).requireValue,
|
||||
'ignored_release_version',
|
||||
null,
|
||||
PreferencesEntry<String?, dynamic> get _ignoreReleasePref => PreferencesEntry(
|
||||
preferences: ref.read(sharedPreferencesProvider).requireValue,
|
||||
key: 'ignored_release_version',
|
||||
defaultValue: null,
|
||||
);
|
||||
|
||||
Future<AppUpdateState> 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<void> ignoreRelease(RemoteVersionEntity version) async {
|
||||
loggy.debug("ignoring release [${version.version}]");
|
||||
await _ignoreReleasePref.update(version.version);
|
||||
await _ignoreReleasePref.write(version.version);
|
||||
state = AppUpdateStateIgnored(version);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
@@ -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),
|
||||
);
|
||||
|
||||
@@ -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<ConfigOptionFailure, ConfigOptionEntity> getConfigOption();
|
||||
TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
|
||||
ConfigOptionPatch patch,
|
||||
abstract class ConfigOptions {
|
||||
static final serviceMode = PreferencesNotifier.create<ServiceMode, String>(
|
||||
"service-mode",
|
||||
ServiceMode.defaultMode,
|
||||
mapFrom: (value) => ServiceMode.choices.firstWhere((e) => e.key == value),
|
||||
mapTo: (value) => value.key,
|
||||
);
|
||||
TaskEither<ConfigOptionFailure, Unit> resetConfigOption();
|
||||
TaskEither<ConfigOptionFailure, String> generateWarpConfig();
|
||||
}
|
||||
|
||||
abstract interface class SingBoxConfigOptionRepository {
|
||||
TaskEither<ConfigOptionFailure, SingboxConfigOption>
|
||||
getFullSingboxConfigOption();
|
||||
}
|
||||
|
||||
class ConfigOptionRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements ConfigOptionRepository {
|
||||
ConfigOptionRepositoryImpl({
|
||||
required this.preferences,
|
||||
required this.singbox,
|
||||
});
|
||||
|
||||
final SharedPreferences preferences;
|
||||
final SingboxService singbox;
|
||||
|
||||
@override
|
||||
Either<ConfigOptionFailure, ConfigOptionEntity> 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})",
|
||||
static final logLevel = PreferencesNotifier.create<LogLevel, String>(
|
||||
"log-level",
|
||||
LogLevel.warn,
|
||||
mapFrom: LogLevel.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
map[key] = persisted;
|
||||
}
|
||||
}
|
||||
final options = ConfigOptionEntity.fromJson(map);
|
||||
return right(options);
|
||||
} catch (error, stackTrace) {
|
||||
return left(ConfigOptionUnexpectedFailure(error, stackTrace));
|
||||
|
||||
static final resolveDestination = PreferencesNotifier.create<bool, bool>(
|
||||
"resolve-destination",
|
||||
false,
|
||||
);
|
||||
|
||||
static final ipv6Mode = PreferencesNotifier.create<IPv6Mode, String>(
|
||||
"ipv6-mode",
|
||||
IPv6Mode.disable,
|
||||
mapFrom: (value) => IPv6Mode.values.firstWhere((e) => e.key == value),
|
||||
mapTo: (value) => value.key,
|
||||
);
|
||||
|
||||
static final remoteDnsAddress = PreferencesNotifier.create<String, String>(
|
||||
"remote-dns-address",
|
||||
"udp://1.1.1.1",
|
||||
validator: (value) => value.isNotBlank,
|
||||
);
|
||||
|
||||
static final remoteDnsDomainStrategy =
|
||||
PreferencesNotifier.create<DomainStrategy, String>(
|
||||
"remote-dns-domain-strategy",
|
||||
DomainStrategy.auto,
|
||||
mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value),
|
||||
mapTo: (value) => value.key,
|
||||
);
|
||||
|
||||
static final directDnsAddress = PreferencesNotifier.create<String, String>(
|
||||
"direct-dns-address",
|
||||
"1.1.1.1",
|
||||
validator: (value) => value.isNotBlank,
|
||||
);
|
||||
|
||||
static final directDnsDomainStrategy =
|
||||
PreferencesNotifier.create<DomainStrategy, String>(
|
||||
"direct-dns-domain-strategy",
|
||||
DomainStrategy.auto,
|
||||
mapFrom: (value) => DomainStrategy.values.firstWhere((e) => e.key == value),
|
||||
mapTo: (value) => value.key,
|
||||
);
|
||||
|
||||
static final mixedPort = PreferencesNotifier.create<int, int>(
|
||||
"mixed-port",
|
||||
2334,
|
||||
validator: (value) => isPort(value.toString()),
|
||||
);
|
||||
|
||||
static final localDnsPort = PreferencesNotifier.create<int, int>(
|
||||
"local-dns-port",
|
||||
6450,
|
||||
validator: (value) => isPort(value.toString()),
|
||||
);
|
||||
|
||||
static final tunImplementation =
|
||||
PreferencesNotifier.create<TunImplementation, String>(
|
||||
"tun-implementation",
|
||||
TunImplementation.mixed,
|
||||
mapFrom: TunImplementation.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
static final mtu = PreferencesNotifier.create<int, int>("mtu", 9000);
|
||||
|
||||
static final strictRoute =
|
||||
PreferencesNotifier.create<bool, bool>("strict-route", true);
|
||||
|
||||
static final connectionTestUrl = PreferencesNotifier.create<String, String>(
|
||||
"connection-test-url",
|
||||
"http://cp.cloudflare.com/",
|
||||
validator: (value) => value.isNotBlank && isUrl(value),
|
||||
);
|
||||
|
||||
static final urlTestInterval = PreferencesNotifier.create<Duration, int>(
|
||||
"url-test-interval",
|
||||
const Duration(minutes: 10),
|
||||
mapFrom: const IntervalInSecondsConverter().fromJson,
|
||||
mapTo: const IntervalInSecondsConverter().toJson,
|
||||
);
|
||||
|
||||
static final enableClashApi = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-clash-api",
|
||||
true,
|
||||
);
|
||||
|
||||
static final clashApiPort = PreferencesNotifier.create<int, int>(
|
||||
"clash-api-port",
|
||||
6756,
|
||||
validator: (value) => isPort(value.toString()),
|
||||
);
|
||||
|
||||
static final bypassLan =
|
||||
PreferencesNotifier.create<bool, bool>("bypass-lan", false);
|
||||
|
||||
static final allowConnectionFromLan = PreferencesNotifier.create<bool, bool>(
|
||||
"allow-connection-from-lan",
|
||||
false,
|
||||
);
|
||||
|
||||
static final enableFakeDns = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-fake-dns",
|
||||
false,
|
||||
);
|
||||
|
||||
static final enableDnsRouting = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-dns-routing",
|
||||
true,
|
||||
);
|
||||
|
||||
static final independentDnsCache = PreferencesNotifier.create<bool, bool>(
|
||||
"independent-dns-cache",
|
||||
true,
|
||||
);
|
||||
|
||||
static final enableTlsFragment = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-tls-fragment",
|
||||
false,
|
||||
);
|
||||
|
||||
static final tlsFragmentSize =
|
||||
PreferencesNotifier.create<OptionalRange, String>(
|
||||
"tls-fragment-size",
|
||||
const OptionalRange(min: 1, max: 500),
|
||||
mapFrom: OptionalRange.parse,
|
||||
mapTo: const OptionalRangeJsonConverter().toJson,
|
||||
);
|
||||
|
||||
static final tlsFragmentSleep =
|
||||
PreferencesNotifier.create<OptionalRange, String>(
|
||||
"tls-fragment-sleep",
|
||||
const OptionalRange(min: 0, max: 500),
|
||||
mapFrom: OptionalRange.parse,
|
||||
mapTo: const OptionalRangeJsonConverter().toJson,
|
||||
);
|
||||
|
||||
static final enableTlsMixedSniCase = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-tls-mixed-sni-case",
|
||||
false,
|
||||
);
|
||||
|
||||
static final enableTlsPadding = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-tls-padding",
|
||||
false,
|
||||
);
|
||||
|
||||
static final tlsPaddingSize =
|
||||
PreferencesNotifier.create<OptionalRange, String>(
|
||||
"tls-padding-size",
|
||||
const OptionalRange(min: 1, max: 1500),
|
||||
mapFrom: OptionalRange.parse,
|
||||
mapTo: const OptionalRangeJsonConverter().toJson,
|
||||
);
|
||||
|
||||
static final enableMux = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-mux",
|
||||
false,
|
||||
);
|
||||
|
||||
static final muxPadding = PreferencesNotifier.create<bool, bool>(
|
||||
"mux-padding",
|
||||
false,
|
||||
);
|
||||
|
||||
static final muxMaxStreams = PreferencesNotifier.create<int, int>(
|
||||
"mux-max-streams",
|
||||
8,
|
||||
validator: (value) => value > 0,
|
||||
);
|
||||
|
||||
static final muxProtocol = PreferencesNotifier.create<MuxProtocol, String>(
|
||||
"mux-protocol",
|
||||
MuxProtocol.h2mux,
|
||||
mapFrom: MuxProtocol.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
static final enableWarp = PreferencesNotifier.create<bool, bool>(
|
||||
"enable-warp",
|
||||
false,
|
||||
);
|
||||
|
||||
static final warpDetourMode =
|
||||
PreferencesNotifier.create<WarpDetourMode, String>(
|
||||
"warp-detour-mode",
|
||||
WarpDetourMode.outbound,
|
||||
mapFrom: WarpDetourMode.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
static final warpLicenseKey = PreferencesNotifier.create<String, String>(
|
||||
"warp-license-key",
|
||||
"",
|
||||
);
|
||||
|
||||
static final warpAccountId = PreferencesNotifier.create<String, String>(
|
||||
"warp-account-id",
|
||||
"",
|
||||
);
|
||||
|
||||
static final warpAccessToken = PreferencesNotifier.create<String, String>(
|
||||
"warp-access-token",
|
||||
"",
|
||||
);
|
||||
|
||||
static final warpCleanIp = PreferencesNotifier.create<String, String>(
|
||||
"warp-clean-ip",
|
||||
"auto",
|
||||
);
|
||||
|
||||
static final warpPort = PreferencesNotifier.create<int, int>(
|
||||
"warp-port",
|
||||
0,
|
||||
validator: (value) => isPort(value.toString()),
|
||||
);
|
||||
|
||||
static final warpNoise = PreferencesNotifier.create<OptionalRange, String>(
|
||||
"warp-noise",
|
||||
const OptionalRange(min: 5, max: 10),
|
||||
mapFrom: (value) => OptionalRange.parse(value, allowEmpty: true),
|
||||
mapTo: const OptionalRangeJsonConverter().toJson,
|
||||
);
|
||||
|
||||
static final warpNoiseDelay =
|
||||
PreferencesNotifier.create<OptionalRange, String>(
|
||||
"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<String, String>(
|
||||
"warp-wireguard-config",
|
||||
"",
|
||||
);
|
||||
|
||||
static final hasExperimentalFeatures = Provider.autoDispose<bool>(
|
||||
(ref) {
|
||||
final mode = ref.watch(serviceMode);
|
||||
if (PlatformUtils.isDesktop && mode == ServiceMode.tun) {
|
||||
return true;
|
||||
}
|
||||
if (ref.watch(enableTlsFragment) ||
|
||||
ref.watch(enableTlsMixedSniCase) ||
|
||||
ref.watch(enableTlsPadding) ||
|
||||
ref.watch(enableMux) ||
|
||||
ref.watch(enableWarp)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
|
||||
ConfigOptionPatch patch,
|
||||
) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final map = patch.toJson();
|
||||
await updateByJson(map);
|
||||
return right(unit);
|
||||
return false;
|
||||
},
|
||||
ConfigOptionUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, Unit> resetConfigOption() {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final map = ConfigOptionEntity.initial().toJson();
|
||||
await updateByJson(map);
|
||||
return right(unit);
|
||||
},
|
||||
ConfigOptionUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
/// 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,
|
||||
];
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> updateByJson(
|
||||
Map<String, dynamic> 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, String> 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,
|
||||
/// 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),
|
||||
),
|
||||
).map((_) => warp.log),
|
||||
)
|
||||
.run();
|
||||
},
|
||||
(error, stackTrace) {
|
||||
loggy.error(error);
|
||||
return ConfigOptionUnexpectedFailure(error, stackTrace);
|
||||
},
|
||||
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<ConfigOptionFailure, SingboxConfigOption>
|
||||
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,
|
||||
|
||||
@@ -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<SingboxRule> 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<String, dynamic> 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<String, dynamic> json) =>
|
||||
_$ConfigOptionPatchFromJson(json);
|
||||
}
|
||||
@@ -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<ConfigOptionEntity> build() async {
|
||||
return ref
|
||||
.watch(configOptionRepositoryProvider)
|
||||
.getConfigOption()
|
||||
.getOrElse((l) {
|
||||
loggy.error("error getting persisted options $l", l);
|
||||
throw l;
|
||||
});
|
||||
}
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<void> 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<void> 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<void> resetOption() async {
|
||||
await ref.read(configOptionRepositoryProvider).resetConfigOption().run();
|
||||
for (final option in ConfigOptions.preferences) {
|
||||
await ref.read(option.notifier).reset();
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<void> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<void> changeOption(ConfigOptionPatch patch) async {
|
||||
await ref.read(configOptionNotifierProvider.notifier).updateOption(patch);
|
||||
}
|
||||
|
||||
String experimental(String txt) {
|
||||
return "$txt (${t.settings.experimental})";
|
||||
}
|
||||
@@ -44,18 +35,15 @@ 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(
|
||||
onTap: ref
|
||||
.read(configOptionNotifierProvider.notifier)
|
||||
.exportJsonToClipboard,
|
||||
child: Text(t.general.addToClipboard),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: options.format()),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.config.resetBtn),
|
||||
@@ -70,378 +58,213 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
switch (asyncOptions) {
|
||||
AsyncData(value: final options) => SliverList.list(
|
||||
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(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.logLevel),
|
||||
preferences: ref.watch(ConfigOptions.logLevel.notifier),
|
||||
choices: LogLevel.choices,
|
||||
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));
|
||||
},
|
||||
presentChoice: (value) => value.name.toUpperCase(),
|
||||
),
|
||||
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)),
|
||||
value: ref.watch(ConfigOptions.bypassLan),
|
||||
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.resolveDestination),
|
||||
value: options.resolveDestination,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(resolveDestination: value),
|
||||
value: ref.watch(ConfigOptions.resolveDestination),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.resolveDestination.notifier).update,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.ipv6Mode),
|
||||
subtitle: Text(options.ipv6Mode.present(t)),
|
||||
onTap: () async {
|
||||
final ipv6Mode = await SettingsPickerDialog(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.ipv6Mode),
|
||||
preferences: ref.watch(ConfigOptions.ipv6Mode.notifier),
|
||||
choices: IPv6Mode.values,
|
||||
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));
|
||||
},
|
||||
presentChoice: (value) => value.present(t),
|
||||
),
|
||||
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(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.remoteDnsAddress),
|
||||
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier),
|
||||
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(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
|
||||
choices: DomainStrategy.values,
|
||||
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,
|
||||
presentChoice: (value) => value.displayName,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.directDnsAddress),
|
||||
subtitle: Text(options.directDnsAddress),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.directDnsAddress),
|
||||
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier),
|
||||
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(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.directDnsDomainStrategy.notifier),
|
||||
choices: DomainStrategy.values,
|
||||
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,
|
||||
),
|
||||
);
|
||||
},
|
||||
presentChoice: (value) => value.displayName,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.enableDnsRouting),
|
||||
value: options.enableDnsRouting,
|
||||
onChanged: (value) => changeOption(
|
||||
ConfigOptionPatch(enableDnsRouting: value),
|
||||
),
|
||||
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: options.enableMux,
|
||||
onChanged: (value) => changeOption(
|
||||
ConfigOptionPatch(enableMux: value),
|
||||
value: ref.watch(ConfigOptions.enableMux),
|
||||
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.muxProtocol),
|
||||
subtitle: Text(options.muxProtocol.name),
|
||||
onTap: () async {
|
||||
final pickedProtocol = await SettingsPickerDialog(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.muxProtocol),
|
||||
preferences: ref.watch(ConfigOptions.muxProtocol.notifier),
|
||||
choices: MuxProtocol.values,
|
||||
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),
|
||||
);
|
||||
},
|
||||
presentChoice: (value) => value.name,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.muxMaxStreams),
|
||||
subtitle: Text(options.muxMaxStreams.toString()),
|
||||
onTap: () async {
|
||||
final maxStreams = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.muxMaxStreams),
|
||||
preferences: ref.watch(ConfigOptions.muxMaxStreams.notifier),
|
||||
title: t.settings.config.muxMaxStreams,
|
||||
initialValue: options.muxMaxStreams,
|
||||
resetValue: defaultOptions.muxMaxStreams,
|
||||
mapTo: int.tryParse,
|
||||
inputToValue: 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(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.serviceMode),
|
||||
preferences: ref.watch(ConfigOptions.serviceMode.notifier),
|
||||
choices: ServiceMode.choices,
|
||||
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),
|
||||
);
|
||||
},
|
||||
presentChoice: (value) => value.present(t),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.strictRoute),
|
||||
value: options.strictRoute,
|
||||
onChanged: (value) async =>
|
||||
changeOption(ConfigOptionPatch(strictRoute: value)),
|
||||
value: ref.watch(ConfigOptions.strictRoute),
|
||||
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tunImplementation),
|
||||
subtitle: Text(options.tunImplementation.name),
|
||||
onTap: () async {
|
||||
final tunImplementation = await SettingsPickerDialog(
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.tunImplementation),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.tunImplementation.notifier),
|
||||
choices: TunImplementation.values,
|
||||
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),
|
||||
);
|
||||
},
|
||||
presentChoice: (value) => value.name,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.mixedPort),
|
||||
subtitle: Text(options.mixedPort.toString()),
|
||||
onTap: () async {
|
||||
final mixedPort = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.mixedPort),
|
||||
preferences: ref.watch(ConfigOptions.mixedPort.notifier),
|
||||
title: t.settings.config.mixedPort,
|
||||
initialValue: options.mixedPort,
|
||||
resetValue: defaultOptions.mixedPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
inputToValue: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (mixedPort == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(mixedPort: mixedPort),
|
||||
);
|
||||
},
|
||||
validateInput: isPort,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.localDnsPort),
|
||||
subtitle: Text(options.localDnsPort.toString()),
|
||||
onTap: () async {
|
||||
final localDnsPort = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.localDnsPort),
|
||||
preferences: ref.watch(ConfigOptions.localDnsPort.notifier),
|
||||
title: t.settings.config.localDnsPort,
|
||||
initialValue: options.localDnsPort,
|
||||
resetValue: defaultOptions.localDnsPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
inputToValue: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (localDnsPort == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(localDnsPort: localDnsPort),
|
||||
);
|
||||
},
|
||||
validateInput: isPort,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
experimental(t.settings.config.allowConnectionFromLan),
|
||||
),
|
||||
value: options.allowConnectionFromLan,
|
||||
onChanged: (value) => changeOption(
|
||||
ConfigOptionPatch(allowConnectionFromLan: value),
|
||||
),
|
||||
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: options.enableTlsFragment,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(enableTlsFragment: value),
|
||||
title: Text(experimental(t.settings.config.enableTlsFragment)),
|
||||
value: ref.watch(ConfigOptions.enableTlsFragment),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tlsFragmentSize),
|
||||
subtitle: Text(options.tlsFragmentSize.present(t)),
|
||||
onTap: () async {
|
||||
final range = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.tlsFragmentSize),
|
||||
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier),
|
||||
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),
|
||||
inputToValue: OptionalRange.tryParse,
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tlsFragmentSleep),
|
||||
subtitle: Text(options.tlsFragmentSleep.present(t)),
|
||||
onTap: () async {
|
||||
final range = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.tlsFragmentSleep),
|
||||
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
inputToValue: OptionalRange.tryParse,
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
experimental(t.settings.config.enableTlsMixedSniCase),
|
||||
),
|
||||
value: options.enableTlsMixedSniCase,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(enableTlsMixedSniCase: value),
|
||||
),
|
||||
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
|
||||
onChanged: ref
|
||||
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
|
||||
.update,
|
||||
),
|
||||
SwitchListTile(
|
||||
title:
|
||||
Text(experimental(t.settings.config.enableTlsPadding)),
|
||||
value: options.enableTlsPadding,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(enableTlsPadding: value),
|
||||
title: Text(experimental(t.settings.config.enableTlsPadding)),
|
||||
value: ref.watch(ConfigOptions.enableTlsPadding),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.enableTlsPadding.notifier).update,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tlsPaddingSize),
|
||||
subtitle: Text(options.tlsPaddingSize.present(t)),
|
||||
onTap: () async {
|
||||
final range = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.tlsPaddingSize),
|
||||
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier),
|
||||
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),
|
||||
),
|
||||
);
|
||||
},
|
||||
inputToValue: OptionalRange.tryParse,
|
||||
presentValue: (value) => value.format(),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(experimental(t.settings.config.section.warp)),
|
||||
WarpOptionsTiles(
|
||||
options: options,
|
||||
defaultOptions: defaultOptions,
|
||||
onChange: changeOption,
|
||||
),
|
||||
const WarpOptionsTiles(),
|
||||
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(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.connectionTestUrl),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.connectionTestUrl.notifier),
|
||||
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
|
||||
ref
|
||||
.watch(ConfigOptions.urlTestInterval)
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
),
|
||||
onTap: () async {
|
||||
final urlTestInterval = await SettingsSliderDialog(
|
||||
title: t.settings.config.urlTestInterval,
|
||||
initialValue: options.urlTestInterval.inMinutes
|
||||
initialValue: ref
|
||||
.watch(ConfigOptions.urlTestInterval)
|
||||
.inMinutes
|
||||
.coerceIn(0, 60)
|
||||
.toDouble(),
|
||||
resetValue:
|
||||
defaultOptions.urlTestInterval.inMinutes.toDouble(),
|
||||
onReset:
|
||||
ref.read(ConfigOptions.urlTestInterval.notifier).reset,
|
||||
min: 1,
|
||||
max: 60,
|
||||
divisions: 60,
|
||||
@@ -449,57 +272,22 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
).show(context);
|
||||
if (urlTestInterval == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
urlTestInterval:
|
||||
Duration(minutes: urlTestInterval.toInt()),
|
||||
),
|
||||
);
|
||||
await ref
|
||||
.read(ConfigOptions.urlTestInterval.notifier)
|
||||
.update(Duration(minutes: urlTestInterval.toInt()));
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.clashApiPort),
|
||||
subtitle: Text(options.clashApiPort.toString()),
|
||||
onTap: () async {
|
||||
final clashApiPort = await SettingsInputDialog(
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.clashApiPort),
|
||||
preferences: ref.watch(ConfigOptions.clashApiPort.notifier),
|
||||
title: t.settings.config.clashApiPort,
|
||||
initialValue: options.clashApiPort,
|
||||
resetValue: defaultOptions.clashApiPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
validateInput: isPort,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (clashApiPort == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(clashApiPort: clashApiPort),
|
||||
);
|
||||
},
|
||||
inputToValue: int.tryParse,
|
||||
),
|
||||
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 {
|
||||
await ref
|
||||
.read(configOptionNotifierProvider.notifier)
|
||||
.resetOption();
|
||||
},
|
||||
child: Text(t.settings.config.resetBtn),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -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<void> 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<bool>(
|
||||
@@ -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(
|
||||
choices: WarpDetourMode.values,
|
||||
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),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpLicenseKey),
|
||||
subtitle: Text(
|
||||
options.warpLicenseKey.isEmpty
|
||||
? t.general.notSet
|
||||
: options.warpLicenseKey,
|
||||
presentChoice: (value) => value.present(t),
|
||||
),
|
||||
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));
|
||||
},
|
||||
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));
|
||||
},
|
||||
),
|
||||
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,
|
||||
inputToValue: int.tryParse,
|
||||
validateInput: isPort,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (warpPort == null) return;
|
||||
await onChange(
|
||||
ConfigOptionPatch(warpPort: warpPort),
|
||||
);
|
||||
},
|
||||
),
|
||||
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),
|
||||
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),
|
||||
inputToValue: (input) =>
|
||||
OptionalRange.tryParse(input, allowEmpty: true),
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
95
lib/features/config_option/widget/preference_tile.dart
Normal file
95
lib/features/config_option/widget/preference_tile.dart
Normal file
@@ -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<T> 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<T, dynamic> 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<T> 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<T, dynamic> preferences;
|
||||
final bool enabled;
|
||||
final List<T> 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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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<ConnectionFailure, SingboxConfigOption>.Do(
|
||||
($) async {
|
||||
final options = await $(
|
||||
singBoxConfigOptionRepository
|
||||
configOptionRepository
|
||||
.getFullSingboxConfigOption()
|
||||
.mapLeft((l) => const InvalidConfigOption()),
|
||||
);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
@@ -147,7 +144,8 @@ class IntroPage extends HookConsumerWidget with PresLogger {
|
||||
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);
|
||||
debug: true,
|
||||
);
|
||||
final response =
|
||||
await client.get<Map<String, dynamic>>('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);
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<ProfileFailure, Unit> task;
|
||||
if (LinkParser.parse(rawInput) case (final link)?) {
|
||||
loggy.debug("adding profile, url: [${link.url}]");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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]",
|
||||
|
||||
@@ -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<void> update(ProxiesSort value) {
|
||||
state = value;
|
||||
return _pref.update(value);
|
||||
return _pref.write(value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
@@ -12,7 +12,8 @@ class SettingsInputDialog<T> 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<T> 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<T> 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<T> 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<T> 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<T> options;
|
||||
final String Function(T e) getTitle;
|
||||
final T? resetValue;
|
||||
final VoidCallback? onReset;
|
||||
|
||||
Future<T?> show(BuildContext context) async {
|
||||
return showDialog(
|
||||
@@ -162,10 +165,11 @@ class SettingsPickerDialog<T> 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()),
|
||||
),
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<T, P> 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<void> 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<String> _:
|
||||
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<String>) {
|
||||
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<T, P> extends AutoDisposeNotifier<T> 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<T, P> _pref = Pref(
|
||||
ref.watch(sharedPreferencesProvider).requireValue,
|
||||
_key,
|
||||
_defaultValue,
|
||||
mapFrom: _mapFrom,
|
||||
mapTo: _mapTo,
|
||||
);
|
||||
|
||||
static AutoDisposeNotifierProvider<PrefNotifier<T, P>, T> provider<T, P>(
|
||||
String key,
|
||||
T defaultValue, {
|
||||
T Function(P value)? mapFrom,
|
||||
P Function(T value)? mapTo,
|
||||
}) =>
|
||||
AutoDisposeNotifierProvider(
|
||||
() => PrefNotifier(key, defaultValue, mapFrom, mapTo),
|
||||
);
|
||||
|
||||
Future<void> update(T value) async {
|
||||
_pref.update(value);
|
||||
super.state = value;
|
||||
}
|
||||
|
||||
@override
|
||||
T build() => _pref.getValue();
|
||||
}
|
||||
Reference in New Issue
Block a user