Refactor preferences
This commit is contained in:
@@ -99,7 +99,7 @@ Future<void> lazyBootstrap(
|
|||||||
() => container.read(windowNotifierProvider.future),
|
() => container.read(windowNotifierProvider.future),
|
||||||
);
|
);
|
||||||
|
|
||||||
final silentStart = container.read(silentStartNotifierProvider);
|
final silentStart = container.read(Preferences.silentStart);
|
||||||
Logger.bootstrap
|
Logger.bootstrap
|
||||||
.debug("silent start [${silentStart ? "Enabled" : "Disabled"}]");
|
.debug("silent start [${silentStart ? "Enabled" : "Disabled"}]");
|
||||||
if (!silentStart) {
|
if (!silentStart) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
import 'package:hiddify/core/app_info/app_info_provider.dart';
|
||||||
import 'package:hiddify/core/http_client/dio_http_client.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';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'http_client_provider.g.dart';
|
part 'http_client_provider.g.dart';
|
||||||
@@ -15,9 +15,9 @@ DioHttpClient httpClient(HttpClientRef ref) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
configOptionNotifierProvider.selectAsync((data) => data.mixedPort),
|
ConfigOptions.mixedPort,
|
||||||
(_, next) async {
|
(_, next) async {
|
||||||
client.setProxyPort(await next);
|
client.setProxyPort(next);
|
||||||
},
|
},
|
||||||
fireImmediately: true,
|
fireImmediately: true,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ class OptionalRange with OptionalRangeMappable {
|
|||||||
String present(TranslationsEn t) =>
|
String present(TranslationsEn t) =>
|
||||||
format().isEmpty ? t.general.notSet : format();
|
format().isEmpty ? t.general.notSet : format();
|
||||||
|
|
||||||
factory OptionalRange._fromString(
|
factory OptionalRange.parse(
|
||||||
String input, {
|
String input, {
|
||||||
bool allowEmpty = true,
|
bool allowEmpty = false,
|
||||||
}) =>
|
}) =>
|
||||||
switch (input.split("-")) {
|
switch (input.split("-")) {
|
||||||
[final String val] when val.isEmpty && allowEmpty =>
|
[final String val] when val.isEmpty && allowEmpty =>
|
||||||
@@ -36,7 +36,7 @@ class OptionalRange with OptionalRangeMappable {
|
|||||||
bool allowEmpty = false,
|
bool allowEmpty = false,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
return OptionalRange._fromString(input);
|
return OptionalRange.parse(input, allowEmpty: allowEmpty);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -48,7 +48,8 @@ class OptionalRangeJsonConverter
|
|||||||
const OptionalRangeJsonConverter();
|
const OptionalRangeJsonConverter();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
OptionalRange fromJson(String json) => OptionalRange._fromString(json);
|
OptionalRange fromJson(String json) =>
|
||||||
|
OptionalRange.parse(json, allowEmpty: true);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toJson(OptionalRange object) => object.format();
|
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/environment.dart';
|
||||||
import 'package:hiddify/core/model/region.dart';
|
import 'package:hiddify/core/model/region.dart';
|
||||||
import 'package:hiddify/core/preferences/preferences_provider.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/features/per_app_proxy/model/per_app_proxy_mode.dart';
|
||||||
import 'package:hiddify/utils/platform_utils.dart';
|
import 'package:hiddify/utils/platform_utils.dart';
|
||||||
import 'package:hiddify/utils/pref_notifier.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'general_preferences.g.dart';
|
part 'general_preferences.g.dart';
|
||||||
|
|
||||||
// TODO refactor
|
|
||||||
|
|
||||||
bool _debugIntroPage = false;
|
bool _debugIntroPage = false;
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
abstract class Preferences {
|
||||||
class IntroCompleted extends _$IntroCompleted {
|
static final introCompleted = PreferencesNotifier.create(
|
||||||
late final _pref = Pref(
|
|
||||||
ref.watch(sharedPreferencesProvider).requireValue,
|
|
||||||
"intro_completed",
|
"intro_completed",
|
||||||
false,
|
false,
|
||||||
|
overrideValue: _debugIntroPage && kDebugMode ? false : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
static final region = PreferencesNotifier.create<Region, String>(
|
||||||
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,
|
|
||||||
"region",
|
"region",
|
||||||
Region.other,
|
Region.other,
|
||||||
mapFrom: Region.values.byName,
|
mapFrom: Region.values.byName,
|
||||||
mapTo: (value) => value.name,
|
mapTo: (value) => value.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
static final silentStart = PreferencesNotifier.create<bool, bool>(
|
||||||
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,
|
|
||||||
"silent_start",
|
"silent_start",
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
static final disableMemoryLimit = PreferencesNotifier.create<bool, bool>(
|
||||||
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,
|
|
||||||
"disable_memory_limit",
|
"disable_memory_limit",
|
||||||
// disable memory limit on desktop by default
|
// disable memory limit on desktop by default
|
||||||
PlatformUtils.isDesktop,
|
PlatformUtils.isDesktop,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
static final perAppProxyMode =
|
||||||
bool build() => _pref.getValue();
|
PreferencesNotifier.create<PerAppProxyMode, String>(
|
||||||
|
|
||||||
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,
|
|
||||||
"per_app_proxy_mode",
|
"per_app_proxy_mode",
|
||||||
PerAppProxyMode.off,
|
PerAppProxyMode.off,
|
||||||
mapFrom: PerAppProxyMode.values.byName,
|
mapFrom: PerAppProxyMode.values.byName,
|
||||||
mapTo: (value) => value.name,
|
mapTo: (value) => value.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
static final markNewProfileActive = PreferencesNotifier.create<bool, bool>(
|
||||||
PerAppProxyMode build() => _pref.getValue();
|
"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;
|
state = value;
|
||||||
return _pref.update(value);
|
return _pref.write(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class PerAppProxyList extends _$PerAppProxyList {
|
class PerAppProxyList extends _$PerAppProxyList {
|
||||||
late final _include = Pref(
|
late final _include = PreferencesEntry(
|
||||||
ref.watch(sharedPreferencesProvider).requireValue,
|
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||||
"per_app_proxy_include_list",
|
key: "per_app_proxy_include_list",
|
||||||
<String>[],
|
defaultValue: <String>[],
|
||||||
);
|
);
|
||||||
|
|
||||||
late final _exclude = Pref(
|
late final _exclude = PreferencesEntry(
|
||||||
ref.watch(sharedPreferencesProvider).requireValue,
|
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||||
"per_app_proxy_exclude_list",
|
key: "per_app_proxy_exclude_list",
|
||||||
<String>[],
|
defaultValue: <String>[],
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<String> build() =>
|
List<String> build() =>
|
||||||
ref.watch(perAppProxyModeNotifierProvider) == PerAppProxyMode.include
|
ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include
|
||||||
? _include.getValue()
|
? _include.read()
|
||||||
: _exclude.getValue();
|
: _exclude.read();
|
||||||
|
|
||||||
Future<void> update(List<String> value) {
|
Future<void> update(List<String> value) {
|
||||||
state = value;
|
state = value;
|
||||||
if (ref.read(perAppProxyModeNotifierProvider) == PerAppProxyMode.include) {
|
if (ref.read(Preferences.perAppProxyMode) == PerAppProxyMode.include) {
|
||||||
return _include.update(value);
|
return _include.write(value);
|
||||||
}
|
}
|
||||||
return _exclude.update(value);
|
return _exclude.write(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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
@override
|
||||||
Future<void> build() async {
|
Future<void> build() async {
|
||||||
_introCompleted = ref.watch(introCompletedProvider);
|
_introCompleted = ref.watch(Preferences.introCompleted);
|
||||||
|
|
||||||
ref.listenSelf((_, __) {
|
ref.listenSelf((_, __) {
|
||||||
if (state.isLoading) return;
|
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/localization/locale_preferences.dart';
|
||||||
import 'package:hiddify/core/model/constants.dart';
|
import 'package:hiddify/core/model/constants.dart';
|
||||||
import 'package:hiddify/core/preferences/preferences_provider.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/data/app_update_data_providers.dart';
|
||||||
import 'package:hiddify/features/app_update/model/app_update_failure.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/model/remote_version_entity.dart';
|
||||||
import 'package:hiddify/features/app_update/notifier/app_update_state.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:hiddify/utils/utils.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:upgrader/upgrader.dart';
|
import 'package:upgrader/upgrader.dart';
|
||||||
@@ -32,10 +32,10 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
|
|||||||
@override
|
@override
|
||||||
AppUpdateState build() => const AppUpdateState.initial();
|
AppUpdateState build() => const AppUpdateState.initial();
|
||||||
|
|
||||||
Pref<String?, dynamic> get _ignoreReleasePref => Pref(
|
PreferencesEntry<String?, dynamic> get _ignoreReleasePref => PreferencesEntry(
|
||||||
ref.read(sharedPreferencesProvider).requireValue,
|
preferences: ref.read(sharedPreferencesProvider).requireValue,
|
||||||
'ignored_release_version',
|
key: 'ignored_release_version',
|
||||||
null,
|
defaultValue: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
Future<AppUpdateState> check() async {
|
Future<AppUpdateState> check() async {
|
||||||
@@ -58,7 +58,7 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
|
|||||||
final latestVersion = Version.parse(remote.version);
|
final latestVersion = Version.parse(remote.version);
|
||||||
final currentVersion = Version.parse(appInfo.version);
|
final currentVersion = Version.parse(appInfo.version);
|
||||||
if (latestVersion > currentVersion) {
|
if (latestVersion > currentVersion) {
|
||||||
if (remote.version == _ignoreReleasePref.getValue()) {
|
if (remote.version == _ignoreReleasePref.read()) {
|
||||||
loggy.debug("ignored release [${remote.version}]");
|
loggy.debug("ignored release [${remote.version}]");
|
||||||
return state = AppUpdateStateIgnored(remote);
|
return state = AppUpdateStateIgnored(remote);
|
||||||
}
|
}
|
||||||
@@ -81,7 +81,7 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger {
|
|||||||
|
|
||||||
Future<void> ignoreRelease(RemoteVersionEntity version) async {
|
Future<void> ignoreRelease(RemoteVersionEntity version) async {
|
||||||
loggy.debug("ignoring release [${version.version}]");
|
loggy.debug("ignoring release [${version.version}]");
|
||||||
await _ignoreReleasePref.update(version.version);
|
await _ignoreReleasePref.write(version.version);
|
||||||
state = AppUpdateStateIgnored(version);
|
state = AppUpdateStateIgnored(version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ class RegionPrefTile extends HookConsumerWidget {
|
|||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
|
|
||||||
final region = ref.watch(regionNotifierProvider);
|
final region = ref.watch(Preferences.region);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(t.settings.general.region),
|
title: Text(t.settings.general.region),
|
||||||
@@ -83,9 +83,7 @@ class RegionPrefTile extends HookConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
if (selectedRegion != null) {
|
if (selectedRegion != null) {
|
||||||
await ref
|
await ref.read(Preferences.region.notifier).update(selectedRegion);
|
||||||
.read(regionNotifierProvider.notifier)
|
|
||||||
.update(selectedRegion);
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||||
import 'package:hiddify/features/config_option/data/config_option_repository.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/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';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
part 'config_option_data_providers.g.dart';
|
part 'config_option_data_providers.g.dart';
|
||||||
@@ -10,19 +9,9 @@ part 'config_option_data_providers.g.dart';
|
|||||||
ConfigOptionRepository configOptionRepository(
|
ConfigOptionRepository configOptionRepository(
|
||||||
ConfigOptionRepositoryRef ref,
|
ConfigOptionRepositoryRef ref,
|
||||||
) {
|
) {
|
||||||
return ConfigOptionRepositoryImpl(
|
return ConfigOptionRepository(
|
||||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||||
singbox: ref.watch(singboxServiceProvider),
|
getConfigOptions: () => ConfigOptions.singboxOptions(ref),
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
SingBoxConfigOptionRepository singBoxConfigOptionRepository(
|
|
||||||
SingBoxConfigOptionRepositoryRef ref,
|
|
||||||
) {
|
|
||||||
return SingBoxConfigOptionRepositoryImpl(
|
|
||||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
|
||||||
optionsRepository: ref.watch(configOptionRepositoryProvider),
|
|
||||||
geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue,
|
geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue,
|
||||||
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
|
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,166 +1,402 @@
|
|||||||
|
import 'package:dartx/dartx.dart';
|
||||||
import 'package:fpdart/fpdart.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/model/region.dart';
|
||||||
import 'package:hiddify/core/utils/exception_handler.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/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_path_resolver.dart';
|
||||||
import 'package:hiddify/features/geo_asset/data/geo_asset_repository.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_config_option.dart';
|
||||||
import 'package:hiddify/singbox/model/singbox_rule.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:hiddify/utils/utils.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
|
||||||
abstract interface class ConfigOptionRepository {
|
abstract class ConfigOptions {
|
||||||
Either<ConfigOptionFailure, ConfigOptionEntity> getConfigOption();
|
static final serviceMode = PreferencesNotifier.create<ServiceMode, String>(
|
||||||
TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
|
"service-mode",
|
||||||
ConfigOptionPatch patch,
|
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 {
|
static final logLevel = PreferencesNotifier.create<LogLevel, String>(
|
||||||
TaskEither<ConfigOptionFailure, SingboxConfigOption>
|
"log-level",
|
||||||
getFullSingboxConfigOption();
|
LogLevel.warn,
|
||||||
}
|
mapFrom: LogLevel.values.byName,
|
||||||
|
mapTo: (value) => value.name,
|
||||||
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})",
|
|
||||||
);
|
);
|
||||||
continue;
|
|
||||||
}
|
static final resolveDestination = PreferencesNotifier.create<bool, bool>(
|
||||||
map[key] = persisted;
|
"resolve-destination",
|
||||||
}
|
false,
|
||||||
}
|
);
|
||||||
final options = ConfigOptionEntity.fromJson(map);
|
|
||||||
return right(options);
|
static final ipv6Mode = PreferencesNotifier.create<IPv6Mode, String>(
|
||||||
} catch (error, stackTrace) {
|
"ipv6-mode",
|
||||||
return left(ConfigOptionUnexpectedFailure(error, stackTrace));
|
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
|
return false;
|
||||||
TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
|
|
||||||
ConfigOptionPatch patch,
|
|
||||||
) {
|
|
||||||
return exceptionHandler(
|
|
||||||
() async {
|
|
||||||
final map = patch.toJson();
|
|
||||||
await updateByJson(map);
|
|
||||||
return right(unit);
|
|
||||||
},
|
},
|
||||||
ConfigOptionUnexpectedFailure.new,
|
|
||||||
);
|
);
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
/// list of all config option preferences
|
||||||
TaskEither<ConfigOptionFailure, Unit> resetConfigOption() {
|
static final preferences = [
|
||||||
return exceptionHandler(
|
serviceMode,
|
||||||
() async {
|
logLevel,
|
||||||
final map = ConfigOptionEntity.initial().toJson();
|
resolveDestination,
|
||||||
await updateByJson(map);
|
ipv6Mode,
|
||||||
return right(unit);
|
remoteDnsAddress,
|
||||||
},
|
remoteDnsDomainStrategy,
|
||||||
ConfigOptionUnexpectedFailure.new,
|
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
|
/// singbox options
|
||||||
Future<void> updateByJson(
|
///
|
||||||
Map<String, dynamic> options,
|
/// **this is partial, don't use it directly**
|
||||||
) async {
|
static SingboxConfigOption singboxOptions(ProviderRef ref) {
|
||||||
final map = ConfigOptionEntity.initial().toJson();
|
final mode = ref.read(serviceMode);
|
||||||
for (final key in map.keys) {
|
return SingboxConfigOption(
|
||||||
final value = options[key];
|
executeConfigAsIs: false,
|
||||||
if (value != null) {
|
logLevel: ref.read(logLevel),
|
||||||
loggy.debug("updating [$key] to [$value]");
|
resolveDestination: ref.read(resolveDestination),
|
||||||
|
ipv6Mode: ref.read(ipv6Mode),
|
||||||
switch (value) {
|
remoteDnsAddress: ref.read(remoteDnsAddress),
|
||||||
case bool _:
|
remoteDnsDomainStrategy: ref.read(remoteDnsDomainStrategy),
|
||||||
await preferences.setBool(key, value);
|
directDnsAddress: ref.read(directDnsAddress),
|
||||||
case String _:
|
directDnsDomainStrategy: ref.read(directDnsDomainStrategy),
|
||||||
await preferences.setString(key, value);
|
mixedPort: ref.read(mixedPort),
|
||||||
case int _:
|
localDnsPort: ref.read(localDnsPort),
|
||||||
await preferences.setInt(key, value);
|
tunImplementation: ref.read(tunImplementation),
|
||||||
case double _:
|
mtu: ref.read(mtu),
|
||||||
await preferences.setDouble(key, value);
|
strictRoute: ref.read(strictRoute),
|
||||||
default:
|
connectionTestUrl: ref.read(connectionTestUrl),
|
||||||
loggy.warning("unexpected type");
|
urlTestInterval: ref.read(urlTestInterval),
|
||||||
}
|
enableClashApi: ref.read(enableClashApi),
|
||||||
}
|
clashApiPort: ref.read(clashApiPort),
|
||||||
}
|
enableTun: mode == ServiceMode.tun,
|
||||||
}
|
enableTunService: mode == ServiceMode.tunService,
|
||||||
|
setSystemProxy: mode == ServiceMode.systemProxy,
|
||||||
@override
|
bypassLan: ref.read(bypassLan),
|
||||||
TaskEither<ConfigOptionFailure, String> generateWarpConfig() {
|
allowConnectionFromLan: ref.read(allowConnectionFromLan),
|
||||||
return exceptionHandler(
|
enableFakeDns: ref.read(enableFakeDns),
|
||||||
() async {
|
enableDnsRouting: ref.read(enableDnsRouting),
|
||||||
final options = getConfigOption().getOrElse((l) => throw l);
|
independentDnsCache: ref.read(independentDnsCache),
|
||||||
return await singbox
|
enableTlsFragment: ref.read(enableTlsFragment),
|
||||||
.generateWarpConfig(
|
tlsFragmentSize: ref.read(tlsFragmentSize),
|
||||||
licenseKey: options.warpLicenseKey,
|
tlsFragmentSleep: ref.read(tlsFragmentSleep),
|
||||||
previousAccountId: options.warpAccountId,
|
enableTlsMixedSniCase: ref.read(enableTlsMixedSniCase),
|
||||||
previousAccessToken: options.warpAccessToken,
|
enableTlsPadding: ref.read(enableTlsPadding),
|
||||||
)
|
tlsPaddingSize: ref.read(tlsPaddingSize),
|
||||||
.mapLeft((l) => ConfigOptionFailure.unexpected(l))
|
enableMux: ref.read(enableMux),
|
||||||
.flatMap(
|
muxPadding: ref.read(muxPadding),
|
||||||
(warp) => updateConfigOption(
|
muxMaxStreams: ref.read(muxMaxStreams),
|
||||||
ConfigOptionPatch(
|
muxProtocol: ref.read(muxProtocol),
|
||||||
warpAccountId: warp.accountId,
|
warp: SingboxWarpOption(
|
||||||
warpAccessToken: warp.accessToken,
|
enable: ref.read(enableWarp),
|
||||||
warpWireguardConfig: warp.wireguardConfig,
|
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),
|
geoipPath: "",
|
||||||
)
|
geositePath: "",
|
||||||
.run();
|
rules: [],
|
||||||
},
|
|
||||||
(error, stackTrace) {
|
|
||||||
loggy.error(error);
|
|
||||||
return ConfigOptionUnexpectedFailure(error, stackTrace);
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SingBoxConfigOptionRepositoryImpl
|
class ConfigOptionRepository with ExceptionHandler, InfraLogger {
|
||||||
with ExceptionHandler, InfraLogger
|
ConfigOptionRepository({
|
||||||
implements SingBoxConfigOptionRepository {
|
|
||||||
SingBoxConfigOptionRepositoryImpl({
|
|
||||||
required this.preferences,
|
required this.preferences,
|
||||||
required this.optionsRepository,
|
required this.getConfigOptions,
|
||||||
required this.geoAssetRepository,
|
required this.geoAssetRepository,
|
||||||
required this.geoAssetPathResolver,
|
required this.geoAssetPathResolver,
|
||||||
});
|
});
|
||||||
|
|
||||||
final SharedPreferences preferences;
|
final SharedPreferences preferences;
|
||||||
final ConfigOptionRepository optionsRepository;
|
final SingboxConfigOption Function() getConfigOptions;
|
||||||
final GeoAssetRepository geoAssetRepository;
|
final GeoAssetRepository geoAssetRepository;
|
||||||
final GeoAssetPathResolver geoAssetPathResolver;
|
final GeoAssetPathResolver geoAssetPathResolver;
|
||||||
|
|
||||||
@override
|
|
||||||
TaskEither<ConfigOptionFailure, SingboxConfigOption>
|
TaskEither<ConfigOptionFailure, SingboxConfigOption>
|
||||||
getFullSingboxConfigOption() {
|
getFullSingboxConfigOption() {
|
||||||
return exceptionHandler(
|
return exceptionHandler(
|
||||||
@@ -204,9 +440,7 @@ class SingBoxConfigOptionRepositoryImpl
|
|||||||
.getOrElse((l) => throw l)
|
.getOrElse((l) => throw l)
|
||||||
.run();
|
.run();
|
||||||
|
|
||||||
final persisted =
|
final singboxConfigOption = getConfigOptions().copyWith(
|
||||||
optionsRepository.getConfigOption().getOrElse((l) => throw l);
|
|
||||||
final singboxConfigOption = persisted.toSingbox(
|
|
||||||
geoipPath: geoAssetPathResolver.relativePath(
|
geoipPath: geoAssetPathResolver.relativePath(
|
||||||
geoAssets.geoip.providerName,
|
geoAssets.geoip.providerName,
|
||||||
geoAssets.geoip.fileName,
|
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 'dart:convert';
|
||||||
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
|
|
||||||
|
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:hiddify/utils/custom_loggers.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -8,28 +10,22 @@ part 'config_option_notifier.g.dart';
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
|
class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
|
||||||
@override
|
@override
|
||||||
Future<ConfigOptionEntity> build() async {
|
Future<void> build() async {}
|
||||||
return ref
|
|
||||||
.watch(configOptionRepositoryProvider)
|
|
||||||
.getConfigOption()
|
|
||||||
.getOrElse((l) {
|
|
||||||
loggy.error("error getting persisted options $l", l);
|
|
||||||
throw l;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateOption(ConfigOptionPatch patch) async {
|
Future<void> exportJsonToClipboard() async {
|
||||||
if (state case AsyncData(value: final options)) {
|
final map = {
|
||||||
await ref
|
for (final option in ConfigOptions.preferences)
|
||||||
.read(configOptionRepositoryProvider)
|
ref.read(option.notifier).entry.key: ref.read(option.notifier).raw(),
|
||||||
.updateConfigOption(patch)
|
};
|
||||||
.map((_) => state = AsyncData(options.patch(patch)))
|
const encoder = JsonEncoder.withIndent(' ');
|
||||||
.run();
|
final json = encoder.convert(map);
|
||||||
}
|
await Clipboard.setData(ClipboardData(text: json));
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> resetOption() async {
|
Future<void> resetOption() async {
|
||||||
await ref.read(configOptionRepositoryProvider).resetConfigOption().run();
|
for (final option in ConfigOptions.preferences) {
|
||||||
|
await ref.read(option.notifier).reset();
|
||||||
|
}
|
||||||
ref.invalidateSelf();
|
ref.invalidateSelf();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
import 'package:hiddify/core/preferences/preferences_provider.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/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:hiddify/utils/utils.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
@@ -46,15 +47,28 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
|
|||||||
Future<void> generateWarpConfig() async {
|
Future<void> generateWarpConfig() async {
|
||||||
if (state.configGeneration.isLoading) return;
|
if (state.configGeneration.isLoading) return;
|
||||||
state = state.copyWith(configGeneration: const AsyncLoading());
|
state = state.copyWith(configGeneration: const AsyncLoading());
|
||||||
final result = await AsyncValue.guard(
|
|
||||||
() async => await ref
|
final result = await AsyncValue.guard(() async {
|
||||||
.read(configOptionRepositoryProvider)
|
final warp = await ref
|
||||||
.generateWarpConfig()
|
.read(singboxServiceProvider)
|
||||||
.getOrElse((l) {
|
.generateWarpConfig(
|
||||||
loggy.warning("error generating warp config: $l", l);
|
licenseKey: ref.read(ConfigOptions.warpLicenseKey),
|
||||||
throw l;
|
previousAccountId: ref.read(ConfigOptions.warpAccountId),
|
||||||
}).run(),
|
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);
|
state = state.copyWith(configGeneration: result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import 'package:dartx/dartx.dart';
|
import 'package:dartx/dartx.dart';
|
||||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hiddify/core/localization/translations.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/model/optional_range.dart';
|
||||||
import 'package:hiddify/core/widget/adaptive_icon.dart';
|
import 'package:hiddify/core/widget/adaptive_icon.dart';
|
||||||
import 'package:hiddify/core/widget/tip_card.dart';
|
import 'package:hiddify/core/widget/tip_card.dart';
|
||||||
import 'package:hiddify/features/common/nested_app_bar.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/notifier/config_option_notifier.dart';
|
||||||
import 'package:hiddify/features/config_option/overview/warp_options_widgets.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/log/model/log_level.dart';
|
||||||
import 'package:hiddify/features/settings/widgets/sections_widgets.dart';
|
import 'package:hiddify/features/settings/widgets/sections_widgets.dart';
|
||||||
import 'package:hiddify/features/settings/widgets/settings_input_dialog.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) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final t = ref.watch(translationsProvider);
|
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) {
|
String experimental(String txt) {
|
||||||
return "$txt (${t.settings.experimental})";
|
return "$txt (${t.settings.experimental})";
|
||||||
}
|
}
|
||||||
@@ -44,18 +35,15 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
|||||||
NestedAppBar(
|
NestedAppBar(
|
||||||
title: Text(t.settings.config.pageTitle),
|
title: Text(t.settings.config.pageTitle),
|
||||||
actions: [
|
actions: [
|
||||||
if (asyncOptions case AsyncData(value: final options))
|
|
||||||
PopupMenuButton(
|
PopupMenuButton(
|
||||||
icon: Icon(AdaptiveIcon(context).more),
|
icon: Icon(AdaptiveIcon(context).more),
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
|
onTap: ref
|
||||||
|
.read(configOptionNotifierProvider.notifier)
|
||||||
|
.exportJsonToClipboard,
|
||||||
child: Text(t.general.addToClipboard),
|
child: Text(t.general.addToClipboard),
|
||||||
onTap: () {
|
|
||||||
Clipboard.setData(
|
|
||||||
ClipboardData(text: options.format()),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
child: Text(t.settings.config.resetBtn),
|
child: Text(t.settings.config.resetBtn),
|
||||||
@@ -70,378 +58,213 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
switch (asyncOptions) {
|
SliverList.list(
|
||||||
AsyncData(value: final options) => SliverList.list(
|
|
||||||
children: [
|
children: [
|
||||||
TipCard(message: t.settings.experimentalMsg),
|
TipCard(message: t.settings.experimentalMsg),
|
||||||
ListTile(
|
ChoicePreferenceWidget(
|
||||||
title: Text(t.settings.config.logLevel),
|
selected: ref.watch(ConfigOptions.logLevel),
|
||||||
subtitle: Text(options.logLevel.name.toUpperCase()),
|
preferences: ref.watch(ConfigOptions.logLevel.notifier),
|
||||||
onTap: () async {
|
choices: LogLevel.choices,
|
||||||
final logLevel = await SettingsPickerDialog(
|
|
||||||
title: t.settings.config.logLevel,
|
title: t.settings.config.logLevel,
|
||||||
selected: options.logLevel,
|
presentChoice: (value) => value.name.toUpperCase(),
|
||||||
options: LogLevel.choices,
|
|
||||||
getTitle: (e) => e.name.toUpperCase(),
|
|
||||||
resetValue: defaultOptions.logLevel,
|
|
||||||
).show(context);
|
|
||||||
if (logLevel == null) return;
|
|
||||||
await changeOption(ConfigOptionPatch(logLevel: logLevel));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(t.settings.config.section.route),
|
SettingsSection(t.settings.config.section.route),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(experimental(t.settings.config.bypassLan)),
|
title: Text(experimental(t.settings.config.bypassLan)),
|
||||||
value: options.bypassLan,
|
value: ref.watch(ConfigOptions.bypassLan),
|
||||||
onChanged: (value) async =>
|
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update,
|
||||||
changeOption(ConfigOptionPatch(bypassLan: value)),
|
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.config.resolveDestination),
|
title: Text(t.settings.config.resolveDestination),
|
||||||
value: options.resolveDestination,
|
value: ref.watch(ConfigOptions.resolveDestination),
|
||||||
onChanged: (value) async => changeOption(
|
onChanged:
|
||||||
ConfigOptionPatch(resolveDestination: value),
|
ref.watch(ConfigOptions.resolveDestination.notifier).update,
|
||||||
),
|
),
|
||||||
),
|
ChoicePreferenceWidget(
|
||||||
ListTile(
|
selected: ref.watch(ConfigOptions.ipv6Mode),
|
||||||
title: Text(t.settings.config.ipv6Mode),
|
preferences: ref.watch(ConfigOptions.ipv6Mode.notifier),
|
||||||
subtitle: Text(options.ipv6Mode.present(t)),
|
choices: IPv6Mode.values,
|
||||||
onTap: () async {
|
|
||||||
final ipv6Mode = await SettingsPickerDialog(
|
|
||||||
title: t.settings.config.ipv6Mode,
|
title: t.settings.config.ipv6Mode,
|
||||||
selected: options.ipv6Mode,
|
presentChoice: (value) => value.present(t),
|
||||||
options: IPv6Mode.values,
|
|
||||||
getTitle: (e) => e.present(t),
|
|
||||||
resetValue: defaultOptions.ipv6Mode,
|
|
||||||
).show(context);
|
|
||||||
if (ipv6Mode == null) return;
|
|
||||||
await changeOption(ConfigOptionPatch(ipv6Mode: ipv6Mode));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(t.settings.config.section.dns),
|
SettingsSection(t.settings.config.section.dns),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.remoteDnsAddress),
|
value: ref.watch(ConfigOptions.remoteDnsAddress),
|
||||||
subtitle: Text(options.remoteDnsAddress),
|
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier),
|
||||||
onTap: () async {
|
|
||||||
final url = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.remoteDnsAddress,
|
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(
|
ChoicePreferenceWidget(
|
||||||
title: Text(t.settings.config.remoteDnsDomainStrategy),
|
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
|
||||||
subtitle: Text(options.remoteDnsDomainStrategy.displayName),
|
preferences:
|
||||||
onTap: () async {
|
ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
|
||||||
final domainStrategy = await SettingsPickerDialog(
|
choices: DomainStrategy.values,
|
||||||
title: t.settings.config.remoteDnsDomainStrategy,
|
title: t.settings.config.remoteDnsDomainStrategy,
|
||||||
selected: options.remoteDnsDomainStrategy,
|
presentChoice: (value) => value.displayName,
|
||||||
options: DomainStrategy.values,
|
|
||||||
getTitle: (e) => e.displayName,
|
|
||||||
resetValue: defaultOptions.remoteDnsDomainStrategy,
|
|
||||||
).show(context);
|
|
||||||
if (domainStrategy == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
remoteDnsDomainStrategy: domainStrategy,
|
|
||||||
),
|
),
|
||||||
);
|
ValuePreferenceWidget(
|
||||||
},
|
value: ref.watch(ConfigOptions.directDnsAddress),
|
||||||
),
|
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier),
|
||||||
ListTile(
|
|
||||||
title: Text(t.settings.config.directDnsAddress),
|
|
||||||
subtitle: Text(options.directDnsAddress),
|
|
||||||
onTap: () async {
|
|
||||||
final url = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.directDnsAddress,
|
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(
|
ChoicePreferenceWidget(
|
||||||
title: Text(t.settings.config.directDnsDomainStrategy),
|
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
|
||||||
subtitle: Text(options.directDnsDomainStrategy.displayName),
|
preferences:
|
||||||
onTap: () async {
|
ref.watch(ConfigOptions.directDnsDomainStrategy.notifier),
|
||||||
final domainStrategy = await SettingsPickerDialog(
|
choices: DomainStrategy.values,
|
||||||
title: t.settings.config.directDnsDomainStrategy,
|
title: t.settings.config.directDnsDomainStrategy,
|
||||||
selected: options.directDnsDomainStrategy,
|
presentChoice: (value) => value.displayName,
|
||||||
options: DomainStrategy.values,
|
|
||||||
getTitle: (e) => e.displayName,
|
|
||||||
resetValue: defaultOptions.directDnsDomainStrategy,
|
|
||||||
).show(context);
|
|
||||||
if (domainStrategy == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
directDnsDomainStrategy: domainStrategy,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.config.enableDnsRouting),
|
title: Text(t.settings.config.enableDnsRouting),
|
||||||
value: options.enableDnsRouting,
|
value: ref.watch(ConfigOptions.enableDnsRouting),
|
||||||
onChanged: (value) => changeOption(
|
onChanged:
|
||||||
ConfigOptionPatch(enableDnsRouting: value),
|
ref.watch(ConfigOptions.enableDnsRouting.notifier).update,
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(experimental(t.settings.config.section.mux)),
|
SettingsSection(experimental(t.settings.config.section.mux)),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.config.enableMux),
|
title: Text(t.settings.config.enableMux),
|
||||||
value: options.enableMux,
|
value: ref.watch(ConfigOptions.enableMux),
|
||||||
onChanged: (value) => changeOption(
|
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update,
|
||||||
ConfigOptionPatch(enableMux: value),
|
|
||||||
),
|
),
|
||||||
),
|
ChoicePreferenceWidget(
|
||||||
ListTile(
|
selected: ref.watch(ConfigOptions.muxProtocol),
|
||||||
title: Text(t.settings.config.muxProtocol),
|
preferences: ref.watch(ConfigOptions.muxProtocol.notifier),
|
||||||
subtitle: Text(options.muxProtocol.name),
|
choices: MuxProtocol.values,
|
||||||
onTap: () async {
|
|
||||||
final pickedProtocol = await SettingsPickerDialog(
|
|
||||||
title: t.settings.config.muxProtocol,
|
title: t.settings.config.muxProtocol,
|
||||||
selected: options.muxProtocol,
|
presentChoice: (value) => value.name,
|
||||||
options: MuxProtocol.values,
|
|
||||||
getTitle: (e) => e.name,
|
|
||||||
resetValue: defaultOptions.muxProtocol,
|
|
||||||
).show(context);
|
|
||||||
if (pickedProtocol == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(muxProtocol: pickedProtocol),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.muxMaxStreams),
|
value: ref.watch(ConfigOptions.muxMaxStreams),
|
||||||
subtitle: Text(options.muxMaxStreams.toString()),
|
preferences: ref.watch(ConfigOptions.muxMaxStreams.notifier),
|
||||||
onTap: () async {
|
|
||||||
final maxStreams = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.muxMaxStreams,
|
title: t.settings.config.muxMaxStreams,
|
||||||
initialValue: options.muxMaxStreams,
|
inputToValue: int.tryParse,
|
||||||
resetValue: defaultOptions.muxMaxStreams,
|
|
||||||
mapTo: int.tryParse,
|
|
||||||
digitsOnly: true,
|
digitsOnly: true,
|
||||||
).show(context);
|
|
||||||
if (maxStreams == null || maxStreams < 1) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(muxMaxStreams: maxStreams),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(t.settings.config.section.inbound),
|
SettingsSection(t.settings.config.section.inbound),
|
||||||
ListTile(
|
ChoicePreferenceWidget(
|
||||||
title: Text(t.settings.config.serviceMode),
|
selected: ref.watch(ConfigOptions.serviceMode),
|
||||||
subtitle: Text(options.serviceMode.present(t)),
|
preferences: ref.watch(ConfigOptions.serviceMode.notifier),
|
||||||
onTap: () async {
|
choices: ServiceMode.choices,
|
||||||
final pickedMode = await SettingsPickerDialog(
|
|
||||||
title: t.settings.config.serviceMode,
|
title: t.settings.config.serviceMode,
|
||||||
selected: options.serviceMode,
|
presentChoice: (value) => value.present(t),
|
||||||
options: ServiceMode.choices,
|
|
||||||
getTitle: (e) => e.present(t),
|
|
||||||
resetValue: ServiceMode.defaultMode,
|
|
||||||
).show(context);
|
|
||||||
if (pickedMode == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(serviceMode: pickedMode),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.config.strictRoute),
|
title: Text(t.settings.config.strictRoute),
|
||||||
value: options.strictRoute,
|
value: ref.watch(ConfigOptions.strictRoute),
|
||||||
onChanged: (value) async =>
|
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update,
|
||||||
changeOption(ConfigOptionPatch(strictRoute: value)),
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ChoicePreferenceWidget(
|
||||||
title: Text(t.settings.config.tunImplementation),
|
selected: ref.watch(ConfigOptions.tunImplementation),
|
||||||
subtitle: Text(options.tunImplementation.name),
|
preferences:
|
||||||
onTap: () async {
|
ref.watch(ConfigOptions.tunImplementation.notifier),
|
||||||
final tunImplementation = await SettingsPickerDialog(
|
choices: TunImplementation.values,
|
||||||
title: t.settings.config.tunImplementation,
|
title: t.settings.config.tunImplementation,
|
||||||
selected: options.tunImplementation,
|
presentChoice: (value) => value.name,
|
||||||
options: TunImplementation.values,
|
|
||||||
getTitle: (e) => e.name,
|
|
||||||
resetValue: defaultOptions.tunImplementation,
|
|
||||||
).show(context);
|
|
||||||
if (tunImplementation == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(tunImplementation: tunImplementation),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.mixedPort),
|
value: ref.watch(ConfigOptions.mixedPort),
|
||||||
subtitle: Text(options.mixedPort.toString()),
|
preferences: ref.watch(ConfigOptions.mixedPort.notifier),
|
||||||
onTap: () async {
|
|
||||||
final mixedPort = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.mixedPort,
|
title: t.settings.config.mixedPort,
|
||||||
initialValue: options.mixedPort,
|
inputToValue: int.tryParse,
|
||||||
resetValue: defaultOptions.mixedPort,
|
|
||||||
validator: isPort,
|
|
||||||
mapTo: int.tryParse,
|
|
||||||
digitsOnly: true,
|
digitsOnly: true,
|
||||||
).show(context);
|
validateInput: isPort,
|
||||||
if (mixedPort == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(mixedPort: mixedPort),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.localDnsPort),
|
value: ref.watch(ConfigOptions.localDnsPort),
|
||||||
subtitle: Text(options.localDnsPort.toString()),
|
preferences: ref.watch(ConfigOptions.localDnsPort.notifier),
|
||||||
onTap: () async {
|
|
||||||
final localDnsPort = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.localDnsPort,
|
title: t.settings.config.localDnsPort,
|
||||||
initialValue: options.localDnsPort,
|
inputToValue: int.tryParse,
|
||||||
resetValue: defaultOptions.localDnsPort,
|
|
||||||
validator: isPort,
|
|
||||||
mapTo: int.tryParse,
|
|
||||||
digitsOnly: true,
|
digitsOnly: true,
|
||||||
).show(context);
|
validateInput: isPort,
|
||||||
if (localDnsPort == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(localDnsPort: localDnsPort),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
experimental(t.settings.config.allowConnectionFromLan),
|
experimental(t.settings.config.allowConnectionFromLan),
|
||||||
),
|
),
|
||||||
value: options.allowConnectionFromLan,
|
value: ref.watch(ConfigOptions.allowConnectionFromLan),
|
||||||
onChanged: (value) => changeOption(
|
onChanged: ref
|
||||||
ConfigOptionPatch(allowConnectionFromLan: value),
|
.read(ConfigOptions.allowConnectionFromLan.notifier)
|
||||||
),
|
.update,
|
||||||
),
|
),
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(t.settings.config.section.tlsTricks),
|
SettingsSection(t.settings.config.section.tlsTricks),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title:
|
title: Text(experimental(t.settings.config.enableTlsFragment)),
|
||||||
Text(experimental(t.settings.config.enableTlsFragment)),
|
value: ref.watch(ConfigOptions.enableTlsFragment),
|
||||||
value: options.enableTlsFragment,
|
onChanged:
|
||||||
onChanged: (value) async => changeOption(
|
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
|
||||||
ConfigOptionPatch(enableTlsFragment: value),
|
|
||||||
),
|
),
|
||||||
),
|
ValuePreferenceWidget(
|
||||||
ListTile(
|
value: ref.watch(ConfigOptions.tlsFragmentSize),
|
||||||
title: Text(t.settings.config.tlsFragmentSize),
|
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier),
|
||||||
subtitle: Text(options.tlsFragmentSize.present(t)),
|
|
||||||
onTap: () async {
|
|
||||||
final range = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.tlsFragmentSize,
|
title: t.settings.config.tlsFragmentSize,
|
||||||
initialValue: options.tlsFragmentSize.format(),
|
inputToValue: OptionalRange.tryParse,
|
||||||
resetValue: defaultOptions.tlsFragmentSize.format(),
|
presentValue: (value) => value.present(t),
|
||||||
).show(context);
|
formatInputValue: (value) => value.format(),
|
||||||
if (range == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
tlsFragmentSize: OptionalRange.tryParse(range),
|
|
||||||
),
|
),
|
||||||
);
|
ValuePreferenceWidget(
|
||||||
},
|
value: ref.watch(ConfigOptions.tlsFragmentSleep),
|
||||||
),
|
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
|
||||||
ListTile(
|
|
||||||
title: Text(t.settings.config.tlsFragmentSleep),
|
|
||||||
subtitle: Text(options.tlsFragmentSleep.present(t)),
|
|
||||||
onTap: () async {
|
|
||||||
final range = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.tlsFragmentSleep,
|
title: t.settings.config.tlsFragmentSleep,
|
||||||
initialValue: options.tlsFragmentSleep.format(),
|
inputToValue: OptionalRange.tryParse,
|
||||||
resetValue: defaultOptions.tlsFragmentSleep.format(),
|
presentValue: (value) => value.present(t),
|
||||||
).show(context);
|
formatInputValue: (value) => value.format(),
|
||||||
if (range == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
tlsFragmentSleep: OptionalRange.tryParse(range),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
experimental(t.settings.config.enableTlsMixedSniCase),
|
experimental(t.settings.config.enableTlsMixedSniCase),
|
||||||
),
|
),
|
||||||
value: options.enableTlsMixedSniCase,
|
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
|
||||||
onChanged: (value) async => changeOption(
|
onChanged: ref
|
||||||
ConfigOptionPatch(enableTlsMixedSniCase: value),
|
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
|
||||||
),
|
.update,
|
||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title:
|
title: Text(experimental(t.settings.config.enableTlsPadding)),
|
||||||
Text(experimental(t.settings.config.enableTlsPadding)),
|
value: ref.watch(ConfigOptions.enableTlsPadding),
|
||||||
value: options.enableTlsPadding,
|
onChanged:
|
||||||
onChanged: (value) async => changeOption(
|
ref.watch(ConfigOptions.enableTlsPadding.notifier).update,
|
||||||
ConfigOptionPatch(enableTlsPadding: value),
|
|
||||||
),
|
),
|
||||||
),
|
ValuePreferenceWidget(
|
||||||
ListTile(
|
value: ref.watch(ConfigOptions.tlsPaddingSize),
|
||||||
title: Text(t.settings.config.tlsPaddingSize),
|
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier),
|
||||||
subtitle: Text(options.tlsPaddingSize.present(t)),
|
|
||||||
onTap: () async {
|
|
||||||
final range = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.tlsPaddingSize,
|
title: t.settings.config.tlsPaddingSize,
|
||||||
initialValue: options.tlsPaddingSize.format(),
|
inputToValue: OptionalRange.tryParse,
|
||||||
resetValue: defaultOptions.tlsPaddingSize.format(),
|
presentValue: (value) => value.format(),
|
||||||
).show(context);
|
formatInputValue: (value) => value.format(),
|
||||||
if (range == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
tlsPaddingSize: OptionalRange.tryParse(range),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(experimental(t.settings.config.section.warp)),
|
SettingsSection(experimental(t.settings.config.section.warp)),
|
||||||
WarpOptionsTiles(
|
const WarpOptionsTiles(),
|
||||||
options: options,
|
|
||||||
defaultOptions: defaultOptions,
|
|
||||||
onChange: changeOption,
|
|
||||||
),
|
|
||||||
const SettingsDivider(),
|
const SettingsDivider(),
|
||||||
SettingsSection(t.settings.config.section.misc),
|
SettingsSection(t.settings.config.section.misc),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.connectionTestUrl),
|
value: ref.watch(ConfigOptions.connectionTestUrl),
|
||||||
subtitle: Text(options.connectionTestUrl),
|
preferences:
|
||||||
onTap: () async {
|
ref.watch(ConfigOptions.connectionTestUrl.notifier),
|
||||||
final url = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.connectionTestUrl,
|
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(
|
ListTile(
|
||||||
title: Text(t.settings.config.urlTestInterval),
|
title: Text(t.settings.config.urlTestInterval),
|
||||||
subtitle: Text(
|
subtitle: Text(
|
||||||
options.urlTestInterval
|
ref
|
||||||
|
.watch(ConfigOptions.urlTestInterval)
|
||||||
.toApproximateTime(isRelativeToNow: false),
|
.toApproximateTime(isRelativeToNow: false),
|
||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final urlTestInterval = await SettingsSliderDialog(
|
final urlTestInterval = await SettingsSliderDialog(
|
||||||
title: t.settings.config.urlTestInterval,
|
title: t.settings.config.urlTestInterval,
|
||||||
initialValue: options.urlTestInterval.inMinutes
|
initialValue: ref
|
||||||
|
.watch(ConfigOptions.urlTestInterval)
|
||||||
|
.inMinutes
|
||||||
.coerceIn(0, 60)
|
.coerceIn(0, 60)
|
||||||
.toDouble(),
|
.toDouble(),
|
||||||
resetValue:
|
onReset:
|
||||||
defaultOptions.urlTestInterval.inMinutes.toDouble(),
|
ref.read(ConfigOptions.urlTestInterval.notifier).reset,
|
||||||
min: 1,
|
min: 1,
|
||||||
max: 60,
|
max: 60,
|
||||||
divisions: 60,
|
divisions: 60,
|
||||||
@@ -449,57 +272,22 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
|||||||
.toApproximateTime(isRelativeToNow: false),
|
.toApproximateTime(isRelativeToNow: false),
|
||||||
).show(context);
|
).show(context);
|
||||||
if (urlTestInterval == null) return;
|
if (urlTestInterval == null) return;
|
||||||
await changeOption(
|
await ref
|
||||||
ConfigOptionPatch(
|
.read(ConfigOptions.urlTestInterval.notifier)
|
||||||
urlTestInterval:
|
.update(Duration(minutes: urlTestInterval.toInt()));
|
||||||
Duration(minutes: urlTestInterval.toInt()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.clashApiPort),
|
value: ref.watch(ConfigOptions.clashApiPort),
|
||||||
subtitle: Text(options.clashApiPort.toString()),
|
preferences: ref.watch(ConfigOptions.clashApiPort.notifier),
|
||||||
onTap: () async {
|
|
||||||
final clashApiPort = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.clashApiPort,
|
title: t.settings.config.clashApiPort,
|
||||||
initialValue: options.clashApiPort,
|
validateInput: isPort,
|
||||||
resetValue: defaultOptions.clashApiPort,
|
|
||||||
validator: isPort,
|
|
||||||
mapTo: int.tryParse,
|
|
||||||
digitsOnly: true,
|
digitsOnly: true,
|
||||||
).show(context);
|
inputToValue: int.tryParse,
|
||||||
if (clashApiPort == null) return;
|
|
||||||
await changeOption(
|
|
||||||
ConfigOptionPatch(clashApiPort: clashApiPort),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
const Gap(24),
|
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/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/model/constants.dart';
|
import 'package:hiddify/core/model/constants.dart';
|
||||||
import 'package:hiddify/core/model/optional_range.dart';
|
import 'package:hiddify/core/model/optional_range.dart';
|
||||||
import 'package:hiddify/core/widget/custom_alert_dialog.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/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/singbox/model/singbox_config_enum.dart';
|
||||||
import 'package:hiddify/utils/uri_utils.dart';
|
import 'package:hiddify/utils/uri_utils.dart';
|
||||||
import 'package:hiddify/utils/validators.dart';
|
import 'package:hiddify/utils/validators.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class WarpOptionsTiles extends HookConsumerWidget {
|
class WarpOptionsTiles extends HookConsumerWidget {
|
||||||
const WarpOptionsTiles({
|
const WarpOptionsTiles({super.key});
|
||||||
required this.options,
|
|
||||||
required this.defaultOptions,
|
|
||||||
required this.onChange,
|
|
||||||
super.key,
|
|
||||||
});
|
|
||||||
|
|
||||||
final ConfigOptionEntity options;
|
|
||||||
final ConfigOptionEntity defaultOptions;
|
|
||||||
final Future<void> Function(ConfigOptionPatch patch) onChange;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
@@ -31,7 +21,8 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
|||||||
|
|
||||||
final warpOptions = ref.watch(warpOptionNotifierProvider);
|
final warpOptions = ref.watch(warpOptionNotifierProvider);
|
||||||
final warpPrefaceCompleted = warpOptions.consentGiven;
|
final warpPrefaceCompleted = warpOptions.consentGiven;
|
||||||
final canChangeOptions = warpPrefaceCompleted && options.enableWarp;
|
final enableWarp = ref.watch(ConfigOptions.enableWarp);
|
||||||
|
final canChangeOptions = warpPrefaceCompleted && enableWarp;
|
||||||
|
|
||||||
ref.listen(
|
ref.listen(
|
||||||
warpOptionNotifierProvider.select((value) => value.configGeneration),
|
warpOptionNotifierProvider.select((value) => value.configGeneration),
|
||||||
@@ -49,7 +40,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
|||||||
children: [
|
children: [
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.config.enableWarp),
|
title: Text(t.settings.config.enableWarp),
|
||||||
value: options.enableWarp,
|
value: enableWarp,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
if (!warpPrefaceCompleted) {
|
if (!warpPrefaceCompleted) {
|
||||||
final agreed = await showDialog<bool>(
|
final agreed = await showDialog<bool>(
|
||||||
@@ -58,10 +49,10 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
|||||||
);
|
);
|
||||||
if (agreed ?? false) {
|
if (agreed ?? false) {
|
||||||
await ref.read(warpOptionNotifierProvider.notifier).agree();
|
await ref.read(warpOptionNotifierProvider.notifier).agree();
|
||||||
await onChange(ConfigOptionPatch(enableWarp: value));
|
await ref.read(ConfigOptions.enableWarp.notifier).update(value);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await onChange(ConfigOptionPatch(enableWarp: value));
|
await ref.read(ConfigOptions.enableWarp.notifier).update(value);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -85,112 +76,56 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
|||||||
.generateWarpConfig();
|
.generateWarpConfig();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
ChoicePreferenceWidget(
|
||||||
title: Text(t.settings.config.warpDetourMode),
|
selected: ref.watch(ConfigOptions.warpDetourMode),
|
||||||
subtitle: Text(options.warpDetourMode.present(t)),
|
preferences: ref.watch(ConfigOptions.warpDetourMode.notifier),
|
||||||
enabled: canChangeOptions,
|
enabled: canChangeOptions,
|
||||||
onTap: () async {
|
choices: WarpDetourMode.values,
|
||||||
final warpDetourMode = await SettingsPickerDialog(
|
|
||||||
title: t.settings.config.warpDetourMode,
|
title: t.settings.config.warpDetourMode,
|
||||||
selected: options.warpDetourMode,
|
presentChoice: (value) => value.present(t),
|
||||||
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,
|
|
||||||
),
|
),
|
||||||
|
ValuePreferenceWidget(
|
||||||
|
value: ref.watch(ConfigOptions.warpLicenseKey),
|
||||||
|
preferences: ref.watch(ConfigOptions.warpLicenseKey.notifier),
|
||||||
enabled: canChangeOptions,
|
enabled: canChangeOptions,
|
||||||
onTap: () async {
|
|
||||||
final licenseKey = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.warpLicenseKey,
|
title: t.settings.config.warpLicenseKey,
|
||||||
initialValue: options.warpLicenseKey,
|
presentValue: (value) => value.isEmpty ? t.general.notSet : value,
|
||||||
resetValue: defaultOptions.warpLicenseKey,
|
|
||||||
).show(context);
|
|
||||||
if (licenseKey == null) return;
|
|
||||||
await onChange(ConfigOptionPatch(warpLicenseKey: licenseKey));
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.warpCleanIp),
|
value: ref.watch(ConfigOptions.warpCleanIp),
|
||||||
subtitle: Text(options.warpCleanIp),
|
preferences: ref.watch(ConfigOptions.warpCleanIp.notifier),
|
||||||
enabled: canChangeOptions,
|
enabled: canChangeOptions,
|
||||||
onTap: () async {
|
|
||||||
final warpCleanIp = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.warpCleanIp,
|
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(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.warpPort),
|
value: ref.watch(ConfigOptions.warpPort),
|
||||||
subtitle: Text(options.warpPort.toString()),
|
preferences: ref.watch(ConfigOptions.warpPort.notifier),
|
||||||
enabled: canChangeOptions,
|
enabled: canChangeOptions,
|
||||||
onTap: () async {
|
|
||||||
final warpPort = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.warpPort,
|
title: t.settings.config.warpPort,
|
||||||
initialValue: options.warpPort,
|
inputToValue: int.tryParse,
|
||||||
resetValue: defaultOptions.warpPort,
|
validateInput: isPort,
|
||||||
validator: isPort,
|
|
||||||
mapTo: int.tryParse,
|
|
||||||
digitsOnly: true,
|
digitsOnly: true,
|
||||||
).show(context);
|
|
||||||
if (warpPort == null) return;
|
|
||||||
await onChange(
|
|
||||||
ConfigOptionPatch(warpPort: warpPort),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ValuePreferenceWidget(
|
||||||
title: Text(t.settings.config.warpNoise),
|
value: ref.watch(ConfigOptions.warpNoise),
|
||||||
subtitle: Text(options.warpNoise.present(t)),
|
preferences: ref.watch(ConfigOptions.warpNoise.notifier),
|
||||||
enabled: canChangeOptions,
|
enabled: canChangeOptions,
|
||||||
onTap: () async {
|
|
||||||
final warpNoise = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.warpNoise,
|
title: t.settings.config.warpNoise,
|
||||||
initialValue: options.warpNoise.format(),
|
inputToValue: (input) =>
|
||||||
resetValue: defaultOptions.warpNoise.format(),
|
OptionalRange.tryParse(input, allowEmpty: true),
|
||||||
).show(context);
|
presentValue: (value) => value.present(t),
|
||||||
if (warpNoise == null) return;
|
formatInputValue: (value) => value.format(),
|
||||||
await onChange(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
warpNoise: OptionalRange.tryParse(warpNoise, allowEmpty: true),
|
|
||||||
),
|
),
|
||||||
);
|
ValuePreferenceWidget(
|
||||||
},
|
value: ref.watch(ConfigOptions.warpNoiseDelay),
|
||||||
),
|
preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier),
|
||||||
ListTile(
|
|
||||||
title: Text(t.settings.config.warpNoiseDelay),
|
|
||||||
subtitle: Text(options.warpNoiseDelay.present(t)),
|
|
||||||
enabled: canChangeOptions,
|
enabled: canChangeOptions,
|
||||||
onTap: () async {
|
|
||||||
final warpNoiseDelay = await SettingsInputDialog(
|
|
||||||
title: t.settings.config.warpNoiseDelay,
|
title: t.settings.config.warpNoiseDelay,
|
||||||
initialValue: options.warpNoiseDelay.format(),
|
inputToValue: (input) =>
|
||||||
resetValue: defaultOptions.warpNoiseDelay.format(),
|
OptionalRange.tryParse(input, allowEmpty: true),
|
||||||
).show(context);
|
presentValue: (value) => value.present(t),
|
||||||
if (warpNoiseDelay == null) return;
|
formatInputValue: (value) => value.format(),
|
||||||
await onChange(
|
|
||||||
ConfigOptionPatch(
|
|
||||||
warpNoiseDelay:
|
|
||||||
OptionalRange.tryParse(warpNoiseDelay, allowEmpty: true),
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
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(
|
return ConnectionRepositoryImpl(
|
||||||
directories: ref.watch(appDirectoriesProvider).requireValue,
|
directories: ref.watch(appDirectoriesProvider).requireValue,
|
||||||
singBoxConfigOptionRepository:
|
configOptionRepository: ref.watch(configOptionRepositoryProvider),
|
||||||
ref.watch(singBoxConfigOptionRepositoryProvider),
|
|
||||||
singbox: ref.watch(singboxServiceProvider),
|
singbox: ref.watch(singboxServiceProvider),
|
||||||
platformSource: ConnectionPlatformSourceImpl(),
|
platformSource: ConnectionPlatformSourceImpl(),
|
||||||
profilePathResolver: ref.watch(profilePathResolverProvider),
|
profilePathResolver: ref.watch(profilePathResolverProvider),
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class ConnectionRepositoryImpl
|
|||||||
required this.directories,
|
required this.directories,
|
||||||
required this.singbox,
|
required this.singbox,
|
||||||
required this.platformSource,
|
required this.platformSource,
|
||||||
required this.singBoxConfigOptionRepository,
|
required this.configOptionRepository,
|
||||||
required this.profilePathResolver,
|
required this.profilePathResolver,
|
||||||
required this.geoAssetPathResolver,
|
required this.geoAssetPathResolver,
|
||||||
});
|
});
|
||||||
@@ -46,7 +46,7 @@ class ConnectionRepositoryImpl
|
|||||||
final Directories directories;
|
final Directories directories;
|
||||||
final SingboxService singbox;
|
final SingboxService singbox;
|
||||||
final ConnectionPlatformSource platformSource;
|
final ConnectionPlatformSource platformSource;
|
||||||
final SingBoxConfigOptionRepository singBoxConfigOptionRepository;
|
final ConfigOptionRepository configOptionRepository;
|
||||||
final ProfilePathResolver profilePathResolver;
|
final ProfilePathResolver profilePathResolver;
|
||||||
final GeoAssetPathResolver geoAssetPathResolver;
|
final GeoAssetPathResolver geoAssetPathResolver;
|
||||||
|
|
||||||
@@ -83,7 +83,7 @@ class ConnectionRepositoryImpl
|
|||||||
return TaskEither<ConnectionFailure, SingboxConfigOption>.Do(
|
return TaskEither<ConnectionFailure, SingboxConfigOption>.Do(
|
||||||
($) async {
|
($) async {
|
||||||
final options = await $(
|
final options = await $(
|
||||||
singBoxConfigOptionRepository
|
configOptionRepository
|
||||||
.getFullSingboxConfigOption()
|
.getFullSingboxConfigOption()
|
||||||
.mapLeft((l) => const InvalidConfigOption()),
|
.mapLeft((l) => const InvalidConfigOption()),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:hiddify/core/haptic/haptic_service.dart';
|
import 'package:hiddify/core/haptic/haptic_service.dart';
|
||||||
import 'package:hiddify/core/preferences/general_preferences.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_data_providers.dart';
|
||||||
import 'package:hiddify/features/connection/data/connection_repository.dart';
|
import 'package:hiddify/features/connection/data/connection_repository.dart';
|
||||||
import 'package:hiddify/features/connection/model/connection_status.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) {
|
yield* _connectionRepo.watchConnectionStatus().doOnData((event) {
|
||||||
if (event case Disconnected(connectionFailure: final _?)
|
if (event case Disconnected(connectionFailure: final _?)
|
||||||
when PlatformUtils.isDesktop) {
|
when PlatformUtils.isDesktop) {
|
||||||
ref.read(startedByUserProvider.notifier).update(false);
|
ref.read(Preferences.startedByUser.notifier).update(false);
|
||||||
}
|
}
|
||||||
loggy.info("connection status: ${event.format()}");
|
loggy.info("connection status: ${event.format()}");
|
||||||
});
|
});
|
||||||
@@ -73,11 +72,11 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
|
|||||||
switch (value) {
|
switch (value) {
|
||||||
case Disconnected():
|
case Disconnected():
|
||||||
await haptic.lightImpact();
|
await haptic.lightImpact();
|
||||||
await ref.read(startedByUserProvider.notifier).update(true);
|
await ref.read(Preferences.startedByUser.notifier).update(true);
|
||||||
await _connect();
|
await _connect();
|
||||||
case Connected():
|
case Connected():
|
||||||
await haptic.mediumImpact();
|
await haptic.mediumImpact();
|
||||||
await ref.read(startedByUserProvider.notifier).update(false);
|
await ref.read(Preferences.startedByUser.notifier).update(false);
|
||||||
await _disconnect();
|
await _disconnect();
|
||||||
default:
|
default:
|
||||||
loggy.warning("switching status, debounce");
|
loggy.warning("switching status, debounce");
|
||||||
@@ -92,12 +91,12 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
|
|||||||
return _disconnect();
|
return _disconnect();
|
||||||
}
|
}
|
||||||
loggy.info("active profile changed, reconnecting");
|
loggy.info("active profile changed, reconnecting");
|
||||||
await ref.read(startedByUserProvider.notifier).update(true);
|
await ref.read(Preferences.startedByUser.notifier).update(true);
|
||||||
await _connectionRepo
|
await _connectionRepo
|
||||||
.reconnect(
|
.reconnect(
|
||||||
profile.id,
|
profile.id,
|
||||||
profile.name,
|
profile.name,
|
||||||
ref.read(disableMemoryLimitProvider),
|
ref.read(Preferences.disableMemoryLimit),
|
||||||
)
|
)
|
||||||
.mapLeft((err) {
|
.mapLeft((err) {
|
||||||
loggy.warning("error reconnecting", err);
|
loggy.warning("error reconnecting", err);
|
||||||
@@ -127,7 +126,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
|
|||||||
.connect(
|
.connect(
|
||||||
activeProfile.id,
|
activeProfile.id,
|
||||||
activeProfile.name,
|
activeProfile.name,
|
||||||
ref.read(disableMemoryLimitProvider),
|
ref.read(Preferences.disableMemoryLimit),
|
||||||
)
|
)
|
||||||
.mapLeft((err) async {
|
.mapLeft((err) async {
|
||||||
loggy.warning("error connecting", err);
|
loggy.warning("error connecting", err);
|
||||||
@@ -136,7 +135,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
|
|||||||
if (err.toString().contains("panic")) {
|
if (err.toString().contains("panic")) {
|
||||||
await Sentry.captureException(Exception(err.toString()));
|
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);
|
state = AsyncError(err, StackTrace.current);
|
||||||
}).run();
|
}).run();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:gap/gap.dart';
|
|||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/model/failures.dart';
|
import 'package:hiddify/core/model/failures.dart';
|
||||||
import 'package:hiddify/core/theme/theme_extensions.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/model/connection_status.dart';
|
||||||
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
||||||
import 'package:hiddify/features/connection/widget/experimental_feature_notice.dart';
|
import 'package:hiddify/features/connection/widget/experimental_feature_notice.dart';
|
||||||
@@ -48,10 +48,7 @@ class ConnectionButton extends HookConsumerWidget {
|
|||||||
var canConnect = true;
|
var canConnect = true;
|
||||||
if (status case Disconnected()) {
|
if (status case Disconnected()) {
|
||||||
final hasExperimental =
|
final hasExperimental =
|
||||||
await ref.read(configOptionNotifierProvider.future).then(
|
ref.read(ConfigOptions.hasExperimentalFeatures);
|
||||||
(value) => value.hasExperimentalOptions(),
|
|
||||||
onError: (_) => false,
|
|
||||||
);
|
|
||||||
final canShowNotice =
|
final canShowNotice =
|
||||||
!ref.read(disableExperimentalFeatureNoticeProvider);
|
!ref.read(disableExperimentalFeatureNoticeProvider);
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import 'dart:convert';
|
|
||||||
import 'package:flutter/gestures.dart';
|
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/material.dart';
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hiddify/core/analytics/analytics_controller.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/locale_preferences.dart';
|
||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/model/constants.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/gen/assets.gen.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
import 'package:sliver_tools/sliver_tools.dart';
|
import 'package:sliver_tools/sliver_tools.dart';
|
||||||
|
import 'package:timezone_to_country/timezone_to_country.dart';
|
||||||
|
|
||||||
class IntroPage extends HookConsumerWidget with PresLogger {
|
class IntroPage extends HookConsumerWidget with PresLogger {
|
||||||
bool locationInfoLoaded = false;
|
|
||||||
IntroPage({super.key});
|
IntroPage({super.key});
|
||||||
|
|
||||||
|
bool locationInfoLoaded = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
@@ -101,7 +100,7 @@ class IntroPage extends HookConsumerWidget with PresLogger {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
await ref
|
await ref
|
||||||
.read(introCompletedProvider.notifier)
|
.read(Preferences.introCompleted.notifier)
|
||||||
.update(true);
|
.update(true);
|
||||||
},
|
},
|
||||||
child: isStarting.value
|
child: isStarting.value
|
||||||
@@ -128,9 +127,7 @@ class IntroPage extends HookConsumerWidget with PresLogger {
|
|||||||
loggy.debug(
|
loggy.debug(
|
||||||
'Timezone Region: ${regionLocale.region} Locale: ${regionLocale.locale}',
|
'Timezone Region: ${regionLocale.region} Locale: ${regionLocale.locale}',
|
||||||
);
|
);
|
||||||
await ref
|
await ref.read(Preferences.region.notifier).update(regionLocale.region);
|
||||||
.read(regionNotifierProvider.notifier)
|
|
||||||
.update(regionLocale.region);
|
|
||||||
await ref
|
await ref
|
||||||
.read(localePreferencesProvider.notifier)
|
.read(localePreferencesProvider.notifier)
|
||||||
.changeLocale(regionLocale.locale);
|
.changeLocale(regionLocale.locale);
|
||||||
@@ -147,7 +144,8 @@ class IntroPage extends HookConsumerWidget with PresLogger {
|
|||||||
timeout: const Duration(seconds: 2),
|
timeout: const Duration(seconds: 2),
|
||||||
userAgent:
|
userAgent:
|
||||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:123.0) Gecko/20100101 Firefox/123.0",
|
||||||
debug: true);
|
debug: true,
|
||||||
|
);
|
||||||
final response =
|
final response =
|
||||||
await client.get<Map<String, dynamic>>('https://api.ip.sb/geoip/');
|
await client.get<Map<String, dynamic>>('https://api.ip.sb/geoip/');
|
||||||
|
|
||||||
@@ -159,9 +157,7 @@ class IntroPage extends HookConsumerWidget with PresLogger {
|
|||||||
loggy.debug(
|
loggy.debug(
|
||||||
'Region: ${regionLocale.region} Locale: ${regionLocale.locale}',
|
'Region: ${regionLocale.region} Locale: ${regionLocale.locale}',
|
||||||
);
|
);
|
||||||
await ref
|
await ref.read(Preferences.region.notifier).update(regionLocale.region);
|
||||||
.read(regionNotifierProvider.notifier)
|
|
||||||
.update(regionLocale.region);
|
|
||||||
await ref
|
await ref
|
||||||
.read(localePreferencesProvider.notifier)
|
.read(localePreferencesProvider.notifier)
|
||||||
.changeLocale(regionLocale.locale);
|
.changeLocale(regionLocale.locale);
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
final localizations = MaterialLocalizations.of(context);
|
final localizations = MaterialLocalizations.of(context);
|
||||||
|
|
||||||
final asyncPackages = ref.watch(installedPackagesInfoProvider);
|
final asyncPackages = ref.watch(installedPackagesInfoProvider);
|
||||||
final perAppProxyMode = ref.watch(perAppProxyModeNotifierProvider);
|
final perAppProxyMode = ref.watch(Preferences.perAppProxyMode);
|
||||||
final perAppProxyList = ref.watch(perAppProxyListProvider);
|
final perAppProxyList = ref.watch(perAppProxyListProvider);
|
||||||
|
|
||||||
final showSystemApps = useState(true);
|
final showSystemApps = useState(true);
|
||||||
@@ -130,7 +130,7 @@ class PerAppProxyPage extends HookConsumerWidget with PresLogger {
|
|||||||
groupValue: perAppProxyMode,
|
groupValue: perAppProxyMode,
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await ref
|
await ref
|
||||||
.read(perAppProxyModeNotifierProvider.notifier)
|
.read(Preferences.perAppProxyMode.notifier)
|
||||||
.update(e);
|
.update(e);
|
||||||
if (e == PerAppProxyMode.off && context.mounted) {
|
if (e == PerAppProxyMode.off && context.mounted) {
|
||||||
context.pop();
|
context.pop();
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class AddProfile extends _$AddProfile with AppLogger {
|
|||||||
() async {
|
() async {
|
||||||
final activeProfile = await ref.read(activeProfileProvider.future);
|
final activeProfile = await ref.read(activeProfileProvider.future);
|
||||||
final markAsActive =
|
final markAsActive =
|
||||||
activeProfile == null || ref.read(markNewProfileActiveProvider);
|
activeProfile == null || ref.read(Preferences.markNewProfileActive);
|
||||||
final TaskEither<ProfileFailure, Unit> task;
|
final TaskEither<ProfileFailure, Unit> task;
|
||||||
if (LinkParser.parse(rawInput) case (final link)?) {
|
if (LinkParser.parse(rawInput) case (final link)?) {
|
||||||
loggy.debug("adding profile, url: [${link.url}]");
|
loggy.debug("adding profile, url: [${link.url}]");
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ class ForegroundProfilesUpdateNotifier
|
|||||||
_scheduler = null;
|
_scheduler = null;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ref.watch(introCompletedProvider)) {
|
if (ref.watch(Preferences.introCompleted)) {
|
||||||
loggy.debug("intro done, starting");
|
loggy.debug("intro done, starting");
|
||||||
_scheduler?.start();
|
_scheduler?.start();
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ class IpInfoNotifier extends _$IpInfoNotifier with AppLogger {
|
|||||||
(_, next) => _idle = false,
|
(_, next) => _idle = false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final autoCheck = ref.watch(autoCheckIpProvider);
|
final autoCheck = ref.watch(Preferences.autoCheckIp);
|
||||||
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
final serviceRunning = await ref.watch(serviceRunningProvider.future);
|
||||||
// loggy.debug(
|
// loggy.debug(
|
||||||
// "idle? [$_idle], forced? [$_forceCheck], connected? [$serviceRunning]",
|
// "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/haptic/haptic_service.dart';
|
||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/preferences/preferences_provider.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/connection/notifier/connection_notifier.dart';
|
||||||
import 'package:hiddify/features/proxy/data/proxy_data_providers.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_entity.dart';
|
||||||
import 'package:hiddify/features/proxy/model/proxy_failure.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/riverpod_utils.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -31,24 +31,24 @@ enum ProxiesSort {
|
|||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class ProxiesSortNotifier extends _$ProxiesSortNotifier with AppLogger {
|
class ProxiesSortNotifier extends _$ProxiesSortNotifier with AppLogger {
|
||||||
late final _pref = Pref(
|
late final _pref = PreferencesEntry(
|
||||||
ref.watch(sharedPreferencesProvider).requireValue,
|
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||||
"proxies_sort_mode",
|
key: "proxies_sort_mode",
|
||||||
ProxiesSort.delay,
|
defaultValue: ProxiesSort.delay,
|
||||||
mapFrom: ProxiesSort.values.byName,
|
mapFrom: ProxiesSort.values.byName,
|
||||||
mapTo: (value) => value.name,
|
mapTo: (value) => value.name,
|
||||||
);
|
);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ProxiesSort build() {
|
ProxiesSort build() {
|
||||||
final sortBy = _pref.getValue();
|
final sortBy = _pref.read();
|
||||||
loggy.info("sort proxies by: [${sortBy.name}]");
|
loggy.info("sort proxies by: [${sortBy.name}]");
|
||||||
return sortBy;
|
return sortBy;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> update(ProxiesSort value) {
|
Future<void> update(ProxiesSort value) {
|
||||||
state = 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 t = ref.watch(translationsProvider);
|
||||||
|
|
||||||
final debug = ref.watch(debugModeNotifierProvider);
|
final debug = ref.watch(debugModeNotifierProvider);
|
||||||
final perAppProxy = ref.watch(perAppProxyModeNotifierProvider).enabled;
|
final perAppProxy = ref.watch(Preferences.perAppProxyMode).enabled;
|
||||||
final disableMemoryLimit = ref.watch(disableMemoryLimitProvider);
|
final disableMemoryLimit = ref.watch(Preferences.disableMemoryLimit);
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -43,7 +43,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
|||||||
final newMode =
|
final newMode =
|
||||||
perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude;
|
perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude;
|
||||||
await ref
|
await ref
|
||||||
.read(perAppProxyModeNotifierProvider.notifier)
|
.read(Preferences.perAppProxyMode.notifier)
|
||||||
.update(newMode);
|
.update(newMode);
|
||||||
if (!perAppProxy && context.mounted) {
|
if (!perAppProxy && context.mounted) {
|
||||||
await const PerAppProxyRoute().push(context);
|
await const PerAppProxyRoute().push(context);
|
||||||
@@ -53,7 +53,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
|||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (!perAppProxy) {
|
if (!perAppProxy) {
|
||||||
await ref
|
await ref
|
||||||
.read(perAppProxyModeNotifierProvider.notifier)
|
.read(Preferences.perAppProxyMode.notifier)
|
||||||
.update(PerAppProxyMode.exclude);
|
.update(PerAppProxyMode.exclude);
|
||||||
}
|
}
|
||||||
if (context.mounted) await const PerAppProxyRoute().push(context);
|
if (context.mounted) await const PerAppProxyRoute().push(context);
|
||||||
@@ -66,7 +66,9 @@ class AdvancedSettingTiles extends HookConsumerWidget {
|
|||||||
value: !disableMemoryLimit,
|
value: !disableMemoryLimit,
|
||||||
secondary: const Icon(FluentIcons.developer_board_24_regular),
|
secondary: const Icon(FluentIcons.developer_board_24_regular),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await ref.read(disableMemoryLimitProvider.notifier).update(!value);
|
await ref
|
||||||
|
.read(Preferences.disableMemoryLimit.notifier)
|
||||||
|
.update(!value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
if (Platform.isIOS)
|
if (Platform.isIOS)
|
||||||
|
|||||||
@@ -58,17 +58,17 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
|||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.general.autoIpCheck),
|
title: Text(t.settings.general.autoIpCheck),
|
||||||
secondary: const Icon(FluentIcons.globe_search_24_regular),
|
secondary: const Icon(FluentIcons.globe_search_24_regular),
|
||||||
value: ref.watch(autoCheckIpProvider),
|
value: ref.watch(Preferences.autoCheckIp),
|
||||||
onChanged: ref.read(autoCheckIpProvider.notifier).update,
|
onChanged: ref.read(Preferences.autoCheckIp.notifier).update,
|
||||||
),
|
),
|
||||||
if (Platform.isAndroid) ...[
|
if (Platform.isAndroid) ...[
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.general.dynamicNotification),
|
title: Text(t.settings.general.dynamicNotification),
|
||||||
secondary: const Icon(FluentIcons.top_speed_24_regular),
|
secondary: const Icon(FluentIcons.top_speed_24_regular),
|
||||||
value: ref.watch(dynamicNotificationProvider),
|
value: ref.watch(Preferences.dynamicNotification),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await ref
|
await ref
|
||||||
.read(dynamicNotificationProvider.notifier)
|
.read(Preferences.dynamicNotification.notifier)
|
||||||
.update(value);
|
.update(value);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -94,11 +94,9 @@ class GeneralSettingTiles extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
SwitchListTile(
|
SwitchListTile(
|
||||||
title: Text(t.settings.general.silentStart),
|
title: Text(t.settings.general.silentStart),
|
||||||
value: ref.watch(silentStartNotifierProvider),
|
value: ref.watch(Preferences.silentStart),
|
||||||
onChanged: (value) async {
|
onChanged: (value) async {
|
||||||
await ref
|
await ref.read(Preferences.silentStart.notifier).update(value);
|
||||||
.read(silentStartNotifierProvider.notifier)
|
|
||||||
.update(value);
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
this.mapTo,
|
this.mapTo,
|
||||||
this.validator,
|
this.validator,
|
||||||
this.resetValue,
|
this.valueFormatter,
|
||||||
|
this.onReset,
|
||||||
this.optionalAction,
|
this.optionalAction,
|
||||||
this.icon,
|
this.icon,
|
||||||
this.digitsOnly = false,
|
this.digitsOnly = false,
|
||||||
@@ -22,7 +23,8 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
final T initialValue;
|
final T initialValue;
|
||||||
final T? Function(String value)? mapTo;
|
final T? Function(String value)? mapTo;
|
||||||
final bool Function(String value)? validator;
|
final bool Function(String value)? validator;
|
||||||
final T? resetValue;
|
final String Function(T value)? valueFormatter;
|
||||||
|
final VoidCallback? onReset;
|
||||||
final (String text, VoidCallback)? optionalAction;
|
final (String text, VoidCallback)? optionalAction;
|
||||||
final IconData? icon;
|
final IconData? icon;
|
||||||
final bool digitsOnly;
|
final bool digitsOnly;
|
||||||
@@ -41,7 +43,7 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
final localizations = MaterialLocalizations.of(context);
|
final localizations = MaterialLocalizations.of(context);
|
||||||
|
|
||||||
final textController = useTextEditingController(
|
final textController = useTextEditingController(
|
||||||
text: initialValue?.toString(),
|
text: valueFormatter?.call(initialValue) ?? initialValue.toString(),
|
||||||
);
|
);
|
||||||
|
|
||||||
return FocusTraversalGroup(
|
return FocusTraversalGroup(
|
||||||
@@ -74,12 +76,13 @@ class SettingsInputDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
child: Text(optionalAction!.$1.toUpperCase()),
|
child: Text(optionalAction!.$1.toUpperCase()),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (resetValue != null)
|
if (onReset != null)
|
||||||
FocusTraversalOrder(
|
FocusTraversalOrder(
|
||||||
order: const NumericFocusOrder(4),
|
order: const NumericFocusOrder(4),
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).maybePop(resetValue);
|
onReset!();
|
||||||
|
await Navigator.of(context).maybePop(null);
|
||||||
},
|
},
|
||||||
child: Text(t.general.reset.toUpperCase()),
|
child: Text(t.general.reset.toUpperCase()),
|
||||||
),
|
),
|
||||||
@@ -123,14 +126,14 @@ class SettingsPickerDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
required this.selected,
|
required this.selected,
|
||||||
required this.options,
|
required this.options,
|
||||||
required this.getTitle,
|
required this.getTitle,
|
||||||
this.resetValue,
|
this.onReset,
|
||||||
});
|
});
|
||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final T selected;
|
final T selected;
|
||||||
final List<T> options;
|
final List<T> options;
|
||||||
final String Function(T e) getTitle;
|
final String Function(T e) getTitle;
|
||||||
final T? resetValue;
|
final VoidCallback? onReset;
|
||||||
|
|
||||||
Future<T?> show(BuildContext context) async {
|
Future<T?> show(BuildContext context) async {
|
||||||
return showDialog(
|
return showDialog(
|
||||||
@@ -162,10 +165,11 @@ class SettingsPickerDialog<T> extends HookConsumerWidget with PresLogger {
|
|||||||
.toList(),
|
.toList(),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (resetValue != null)
|
if (onReset != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).maybePop(resetValue);
|
onReset!();
|
||||||
|
await Navigator.of(context).maybePop(null);
|
||||||
},
|
},
|
||||||
child: Text(t.general.reset.toUpperCase()),
|
child: Text(t.general.reset.toUpperCase()),
|
||||||
),
|
),
|
||||||
@@ -186,7 +190,7 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger {
|
|||||||
super.key,
|
super.key,
|
||||||
required this.title,
|
required this.title,
|
||||||
required this.initialValue,
|
required this.initialValue,
|
||||||
this.resetValue,
|
this.onReset,
|
||||||
this.min = 0,
|
this.min = 0,
|
||||||
this.max = 1,
|
this.max = 1,
|
||||||
this.divisions,
|
this.divisions,
|
||||||
@@ -195,7 +199,7 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger {
|
|||||||
|
|
||||||
final String title;
|
final String title;
|
||||||
final double initialValue;
|
final double initialValue;
|
||||||
final double? resetValue;
|
final VoidCallback? onReset;
|
||||||
final double min;
|
final double min;
|
||||||
final double max;
|
final double max;
|
||||||
final int? divisions;
|
final int? divisions;
|
||||||
@@ -229,10 +233,11 @@ class SettingsSliderDialog extends HookConsumerWidget with PresLogger {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: [
|
actions: [
|
||||||
if (resetValue != null)
|
if (onReset != null)
|
||||||
TextButton(
|
TextButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
await Navigator.of(context).maybePop(resetValue);
|
onReset!();
|
||||||
|
await Navigator.of(context).maybePop(null);
|
||||||
},
|
},
|
||||||
child: Text(t.general.reset.toUpperCase()),
|
child: Text(t.general.reset.toUpperCase()),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import 'dart:io';
|
|||||||
import 'package:hiddify/core/localization/translations.dart';
|
import 'package:hiddify/core/localization/translations.dart';
|
||||||
import 'package:hiddify/core/model/constants.dart';
|
import 'package:hiddify/core/model/constants.dart';
|
||||||
import 'package:hiddify/core/router/router.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/data/config_option_repository.dart';
|
||||||
import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart';
|
|
||||||
import 'package:hiddify/features/connection/model/connection_status.dart';
|
import 'package:hiddify/features/connection/model/connection_status.dart';
|
||||||
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
||||||
import 'package:hiddify/features/window/notifier/window_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();
|
connection = const ConnectionStatus.disconnected();
|
||||||
}
|
}
|
||||||
|
|
||||||
final serviceMode = await ref
|
final serviceMode = ref.watch(ConfigOptions.serviceMode);
|
||||||
.watch(configOptionNotifierProvider.future)
|
|
||||||
.then((value) => value.serviceMode);
|
|
||||||
|
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
final destinations = <(String label, String location)>[
|
final destinations = <(String label, String location)>[
|
||||||
@@ -88,8 +85,8 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with AppLogger {
|
|||||||
final newMode = ServiceMode.values.byName(menuItem.key!);
|
final newMode = ServiceMode.values.byName(menuItem.key!);
|
||||||
loggy.debug("switching service mode: [$newMode]");
|
loggy.debug("switching service mode: [$newMode]");
|
||||||
await ref
|
await ref
|
||||||
.read(configOptionNotifierProvider.notifier)
|
.read(ConfigOptions.serviceMode.notifier)
|
||||||
.updateOption(ConfigOptionPatch(serviceMode: newMode));
|
.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