Refactor preferences
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
|
||||
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
|
||||
import 'package:hiddify/singbox/service/singbox_service_provider.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'config_option_data_providers.g.dart';
|
||||
@@ -10,19 +9,9 @@ part 'config_option_data_providers.g.dart';
|
||||
ConfigOptionRepository configOptionRepository(
|
||||
ConfigOptionRepositoryRef ref,
|
||||
) {
|
||||
return ConfigOptionRepositoryImpl(
|
||||
return ConfigOptionRepository(
|
||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||
singbox: ref.watch(singboxServiceProvider),
|
||||
);
|
||||
}
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
SingBoxConfigOptionRepository singBoxConfigOptionRepository(
|
||||
SingBoxConfigOptionRepositoryRef ref,
|
||||
) {
|
||||
return SingBoxConfigOptionRepositoryImpl(
|
||||
preferences: ref.watch(sharedPreferencesProvider).requireValue,
|
||||
optionsRepository: ref.watch(configOptionRepositoryProvider),
|
||||
getConfigOptions: () => ConfigOptions.singboxOptions(ref),
|
||||
geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue,
|
||||
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
|
||||
);
|
||||
|
||||
@@ -1,166 +1,402 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/core/model/optional_range.dart';
|
||||
import 'package:hiddify/core/model/region.dart';
|
||||
import 'package:hiddify/core/utils/exception_handler.dart';
|
||||
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
|
||||
import 'package:hiddify/core/utils/json_converters.dart';
|
||||
import 'package:hiddify/core/utils/preferences_utils.dart';
|
||||
import 'package:hiddify/features/config_option/model/config_option_failure.dart';
|
||||
import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart';
|
||||
import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart';
|
||||
import 'package:hiddify/features/log/model/log_level.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_config_enum.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_config_option.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_rule.dart';
|
||||
import 'package:hiddify/singbox/service/singbox_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
abstract interface class ConfigOptionRepository {
|
||||
Either<ConfigOptionFailure, ConfigOptionEntity> getConfigOption();
|
||||
TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
|
||||
ConfigOptionPatch patch,
|
||||
abstract class ConfigOptions {
|
||||
static final serviceMode = PreferencesNotifier.create<ServiceMode, String>(
|
||||
"service-mode",
|
||||
ServiceMode.defaultMode,
|
||||
mapFrom: (value) => ServiceMode.choices.firstWhere((e) => e.key == value),
|
||||
mapTo: (value) => value.key,
|
||||
);
|
||||
TaskEither<ConfigOptionFailure, Unit> resetConfigOption();
|
||||
TaskEither<ConfigOptionFailure, String> generateWarpConfig();
|
||||
}
|
||||
|
||||
abstract interface class SingBoxConfigOptionRepository {
|
||||
TaskEither<ConfigOptionFailure, SingboxConfigOption>
|
||||
getFullSingboxConfigOption();
|
||||
}
|
||||
static final logLevel = PreferencesNotifier.create<LogLevel, String>(
|
||||
"log-level",
|
||||
LogLevel.warn,
|
||||
mapFrom: LogLevel.values.byName,
|
||||
mapTo: (value) => value.name,
|
||||
);
|
||||
|
||||
class ConfigOptionRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements ConfigOptionRepository {
|
||||
ConfigOptionRepositoryImpl({
|
||||
required this.preferences,
|
||||
required this.singbox,
|
||||
});
|
||||
static final resolveDestination = PreferencesNotifier.create<bool, bool>(
|
||||
"resolve-destination",
|
||||
false,
|
||||
);
|
||||
|
||||
final SharedPreferences preferences;
|
||||
final SingboxService singbox;
|
||||
static final ipv6Mode = PreferencesNotifier.create<IPv6Mode, String>(
|
||||
"ipv6-mode",
|
||||
IPv6Mode.disable,
|
||||
mapFrom: (value) => IPv6Mode.values.firstWhere((e) => e.key == value),
|
||||
mapTo: (value) => value.key,
|
||||
);
|
||||
|
||||
@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;
|
||||
}
|
||||
map[key] = persisted;
|
||||
}
|
||||
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;
|
||||
}
|
||||
final options = ConfigOptionEntity.fromJson(map);
|
||||
return right(options);
|
||||
} catch (error, stackTrace) {
|
||||
return left(ConfigOptionUnexpectedFailure(error, stackTrace));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, Unit> updateConfigOption(
|
||||
ConfigOptionPatch patch,
|
||||
) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final map = patch.toJson();
|
||||
await updateByJson(map);
|
||||
return right(unit);
|
||||
},
|
||||
ConfigOptionUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, Unit> resetConfigOption() {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final map = ConfigOptionEntity.initial().toJson();
|
||||
await updateByJson(map);
|
||||
return right(unit);
|
||||
},
|
||||
ConfigOptionUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
Future<void> updateByJson(
|
||||
Map<String, dynamic> options,
|
||||
) async {
|
||||
final map = ConfigOptionEntity.initial().toJson();
|
||||
for (final key in map.keys) {
|
||||
final value = options[key];
|
||||
if (value != null) {
|
||||
loggy.debug("updating [$key] to [$value]");
|
||||
|
||||
switch (value) {
|
||||
case bool _:
|
||||
await preferences.setBool(key, value);
|
||||
case String _:
|
||||
await preferences.setString(key, value);
|
||||
case int _:
|
||||
await preferences.setInt(key, value);
|
||||
case double _:
|
||||
await preferences.setDouble(key, value);
|
||||
default:
|
||||
loggy.warning("unexpected type");
|
||||
}
|
||||
if (ref.watch(enableTlsFragment) ||
|
||||
ref.watch(enableTlsMixedSniCase) ||
|
||||
ref.watch(enableTlsPadding) ||
|
||||
ref.watch(enableMux) ||
|
||||
ref.watch(enableWarp)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, String> generateWarpConfig() {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final options = getConfigOption().getOrElse((l) => throw l);
|
||||
return await singbox
|
||||
.generateWarpConfig(
|
||||
licenseKey: options.warpLicenseKey,
|
||||
previousAccountId: options.warpAccountId,
|
||||
previousAccessToken: options.warpAccessToken,
|
||||
)
|
||||
.mapLeft((l) => ConfigOptionFailure.unexpected(l))
|
||||
.flatMap(
|
||||
(warp) => updateConfigOption(
|
||||
ConfigOptionPatch(
|
||||
warpAccountId: warp.accountId,
|
||||
warpAccessToken: warp.accessToken,
|
||||
warpWireguardConfig: warp.wireguardConfig,
|
||||
),
|
||||
).map((_) => warp.log),
|
||||
)
|
||||
.run();
|
||||
},
|
||||
(error, stackTrace) {
|
||||
loggy.error(error);
|
||||
return ConfigOptionUnexpectedFailure(error, stackTrace);
|
||||
},
|
||||
return false;
|
||||
},
|
||||
);
|
||||
|
||||
/// list of all config option preferences
|
||||
static final preferences = [
|
||||
serviceMode,
|
||||
logLevel,
|
||||
resolveDestination,
|
||||
ipv6Mode,
|
||||
remoteDnsAddress,
|
||||
remoteDnsDomainStrategy,
|
||||
directDnsAddress,
|
||||
directDnsDomainStrategy,
|
||||
mixedPort,
|
||||
localDnsPort,
|
||||
tunImplementation,
|
||||
mtu,
|
||||
strictRoute,
|
||||
connectionTestUrl,
|
||||
urlTestInterval,
|
||||
clashApiPort,
|
||||
bypassLan,
|
||||
allowConnectionFromLan,
|
||||
enableDnsRouting,
|
||||
enableTlsFragment,
|
||||
tlsFragmentSize,
|
||||
tlsFragmentSleep,
|
||||
enableTlsMixedSniCase,
|
||||
enableTlsPadding,
|
||||
tlsPaddingSize,
|
||||
enableMux,
|
||||
muxPadding,
|
||||
muxMaxStreams,
|
||||
muxProtocol,
|
||||
enableWarp,
|
||||
warpDetourMode,
|
||||
warpLicenseKey,
|
||||
warpAccountId,
|
||||
warpAccessToken,
|
||||
warpCleanIp,
|
||||
warpPort,
|
||||
warpNoise,
|
||||
warpWireguardConfig,
|
||||
];
|
||||
|
||||
/// singbox options
|
||||
///
|
||||
/// **this is partial, don't use it directly**
|
||||
static SingboxConfigOption singboxOptions(ProviderRef ref) {
|
||||
final mode = ref.read(serviceMode);
|
||||
return SingboxConfigOption(
|
||||
executeConfigAsIs: false,
|
||||
logLevel: ref.read(logLevel),
|
||||
resolveDestination: ref.read(resolveDestination),
|
||||
ipv6Mode: ref.read(ipv6Mode),
|
||||
remoteDnsAddress: ref.read(remoteDnsAddress),
|
||||
remoteDnsDomainStrategy: ref.read(remoteDnsDomainStrategy),
|
||||
directDnsAddress: ref.read(directDnsAddress),
|
||||
directDnsDomainStrategy: ref.read(directDnsDomainStrategy),
|
||||
mixedPort: ref.read(mixedPort),
|
||||
localDnsPort: ref.read(localDnsPort),
|
||||
tunImplementation: ref.read(tunImplementation),
|
||||
mtu: ref.read(mtu),
|
||||
strictRoute: ref.read(strictRoute),
|
||||
connectionTestUrl: ref.read(connectionTestUrl),
|
||||
urlTestInterval: ref.read(urlTestInterval),
|
||||
enableClashApi: ref.read(enableClashApi),
|
||||
clashApiPort: ref.read(clashApiPort),
|
||||
enableTun: mode == ServiceMode.tun,
|
||||
enableTunService: mode == ServiceMode.tunService,
|
||||
setSystemProxy: mode == ServiceMode.systemProxy,
|
||||
bypassLan: ref.read(bypassLan),
|
||||
allowConnectionFromLan: ref.read(allowConnectionFromLan),
|
||||
enableFakeDns: ref.read(enableFakeDns),
|
||||
enableDnsRouting: ref.read(enableDnsRouting),
|
||||
independentDnsCache: ref.read(independentDnsCache),
|
||||
enableTlsFragment: ref.read(enableTlsFragment),
|
||||
tlsFragmentSize: ref.read(tlsFragmentSize),
|
||||
tlsFragmentSleep: ref.read(tlsFragmentSleep),
|
||||
enableTlsMixedSniCase: ref.read(enableTlsMixedSniCase),
|
||||
enableTlsPadding: ref.read(enableTlsPadding),
|
||||
tlsPaddingSize: ref.read(tlsPaddingSize),
|
||||
enableMux: ref.read(enableMux),
|
||||
muxPadding: ref.read(muxPadding),
|
||||
muxMaxStreams: ref.read(muxMaxStreams),
|
||||
muxProtocol: ref.read(muxProtocol),
|
||||
warp: SingboxWarpOption(
|
||||
enable: ref.read(enableWarp),
|
||||
mode: ref.read(warpDetourMode),
|
||||
wireguardConfig: ref.read(warpWireguardConfig),
|
||||
licenseKey: ref.read(warpLicenseKey),
|
||||
accountId: ref.read(warpAccountId),
|
||||
accessToken: ref.read(warpAccessToken),
|
||||
cleanIp: ref.read(warpCleanIp),
|
||||
cleanPort: ref.read(warpPort),
|
||||
warpNoise: ref.read(warpNoise),
|
||||
warpNoiseDelay: ref.read(warpNoiseDelay),
|
||||
),
|
||||
geoipPath: "",
|
||||
geositePath: "",
|
||||
rules: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SingBoxConfigOptionRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements SingBoxConfigOptionRepository {
|
||||
SingBoxConfigOptionRepositoryImpl({
|
||||
class ConfigOptionRepository with ExceptionHandler, InfraLogger {
|
||||
ConfigOptionRepository({
|
||||
required this.preferences,
|
||||
required this.optionsRepository,
|
||||
required this.getConfigOptions,
|
||||
required this.geoAssetRepository,
|
||||
required this.geoAssetPathResolver,
|
||||
});
|
||||
|
||||
final SharedPreferences preferences;
|
||||
final ConfigOptionRepository optionsRepository;
|
||||
final SingboxConfigOption Function() getConfigOptions;
|
||||
final GeoAssetRepository geoAssetRepository;
|
||||
final GeoAssetPathResolver geoAssetPathResolver;
|
||||
|
||||
@override
|
||||
TaskEither<ConfigOptionFailure, SingboxConfigOption>
|
||||
getFullSingboxConfigOption() {
|
||||
return exceptionHandler(
|
||||
@@ -204,9 +440,7 @@ class SingBoxConfigOptionRepositoryImpl
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
|
||||
final persisted =
|
||||
optionsRepository.getConfigOption().getOrElse((l) => throw l);
|
||||
final singboxConfigOption = persisted.toSingbox(
|
||||
final singboxConfigOption = getConfigOptions().copyWith(
|
||||
geoipPath: geoAssetPathResolver.relativePath(
|
||||
geoAssets.geoip.providerName,
|
||||
geoAssets.geoip.fileName,
|
||||
|
||||
@@ -1,267 +0,0 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/model/optional_range.dart';
|
||||
import 'package:hiddify/core/utils/json_converters.dart';
|
||||
import 'package:hiddify/features/log/model/log_level.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_config_enum.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_config_option.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_rule.dart';
|
||||
import 'package:hiddify/utils/platform_utils.dart';
|
||||
|
||||
part 'config_option_entity.freezed.dart';
|
||||
part 'config_option_entity.g.dart';
|
||||
|
||||
@freezed
|
||||
class ConfigOptionEntity with _$ConfigOptionEntity {
|
||||
const ConfigOptionEntity._();
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.kebab)
|
||||
const factory ConfigOptionEntity({
|
||||
required ServiceMode serviceMode,
|
||||
@Default(LogLevel.warn) LogLevel logLevel,
|
||||
@Default(false) bool resolveDestination,
|
||||
@Default(IPv6Mode.disable) IPv6Mode ipv6Mode,
|
||||
@Default("udp://1.1.1.1") String remoteDnsAddress,
|
||||
@Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy,
|
||||
@Default("1.1.1.1") String directDnsAddress,
|
||||
@Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy,
|
||||
@Default(2334) int mixedPort,
|
||||
@Default(6450) int localDnsPort,
|
||||
@Default(TunImplementation.mixed) TunImplementation tunImplementation,
|
||||
@Default(9000) int mtu,
|
||||
@Default(true) bool strictRoute,
|
||||
@Default("http://cp.cloudflare.com/") String connectionTestUrl,
|
||||
@IntervalInSecondsConverter()
|
||||
@Default(Duration(minutes: 10))
|
||||
Duration urlTestInterval,
|
||||
@Default(true) bool enableClashApi,
|
||||
@Default(6756) int clashApiPort,
|
||||
@Default(false) bool bypassLan,
|
||||
@Default(false) bool allowConnectionFromLan,
|
||||
@Default(false) bool enableFakeDns,
|
||||
@Default(true) bool enableDnsRouting,
|
||||
@Default(true) bool independentDnsCache,
|
||||
@Default(false) bool enableTlsFragment,
|
||||
@OptionalRangeJsonConverter()
|
||||
@Default(OptionalRange(min: 1, max: 500))
|
||||
OptionalRange tlsFragmentSize,
|
||||
@OptionalRangeJsonConverter()
|
||||
@Default(OptionalRange(min: 0, max: 500))
|
||||
OptionalRange tlsFragmentSleep,
|
||||
@Default(false) bool enableTlsMixedSniCase,
|
||||
@Default(false) bool enableTlsPadding,
|
||||
@OptionalRangeJsonConverter()
|
||||
@Default(OptionalRange(min: 1, max: 1500))
|
||||
OptionalRange tlsPaddingSize,
|
||||
@Default(false) bool enableMux,
|
||||
@Default(false) bool muxPadding,
|
||||
@Default(8) int muxMaxStreams,
|
||||
@Default(MuxProtocol.h2mux) MuxProtocol muxProtocol,
|
||||
@Default(false) bool enableWarp,
|
||||
@Default(WarpDetourMode.outbound) WarpDetourMode warpDetourMode,
|
||||
@Default("") String warpLicenseKey,
|
||||
@Default("") String warpAccountId,
|
||||
@Default("") String warpAccessToken,
|
||||
@Default("auto") String warpCleanIp,
|
||||
@Default(0) int warpPort,
|
||||
@OptionalRangeJsonConverter()
|
||||
@Default(OptionalRange(min: 5, max: 10))
|
||||
OptionalRange warpNoise,
|
||||
@OptionalRangeJsonConverter()
|
||||
@Default(OptionalRange(min: 20, max: 200))
|
||||
OptionalRange warpNoiseDelay,
|
||||
@Default("") String warpWireguardConfig,
|
||||
}) = _ConfigOptionEntity;
|
||||
|
||||
factory ConfigOptionEntity.initial() => ConfigOptionEntity(
|
||||
serviceMode: ServiceMode.defaultMode,
|
||||
);
|
||||
|
||||
bool hasExperimentalOptions() {
|
||||
if (PlatformUtils.isDesktop && serviceMode == ServiceMode.tun) {
|
||||
return true;
|
||||
}
|
||||
if (enableTlsFragment ||
|
||||
enableTlsMixedSniCase ||
|
||||
enableTlsPadding ||
|
||||
enableMux ||
|
||||
enableWarp) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
String format() {
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
return encoder.convert(toJson());
|
||||
}
|
||||
|
||||
ConfigOptionEntity patch(ConfigOptionPatch patch) {
|
||||
return copyWith(
|
||||
serviceMode: patch.serviceMode ?? serviceMode,
|
||||
logLevel: patch.logLevel ?? logLevel,
|
||||
resolveDestination: patch.resolveDestination ?? resolveDestination,
|
||||
ipv6Mode: patch.ipv6Mode ?? ipv6Mode,
|
||||
remoteDnsAddress: patch.remoteDnsAddress ?? remoteDnsAddress,
|
||||
remoteDnsDomainStrategy:
|
||||
patch.remoteDnsDomainStrategy ?? remoteDnsDomainStrategy,
|
||||
directDnsAddress: patch.directDnsAddress ?? directDnsAddress,
|
||||
directDnsDomainStrategy:
|
||||
patch.directDnsDomainStrategy ?? directDnsDomainStrategy,
|
||||
mixedPort: patch.mixedPort ?? mixedPort,
|
||||
localDnsPort: patch.localDnsPort ?? localDnsPort,
|
||||
tunImplementation: patch.tunImplementation ?? tunImplementation,
|
||||
mtu: patch.mtu ?? mtu,
|
||||
strictRoute: patch.strictRoute ?? strictRoute,
|
||||
connectionTestUrl: patch.connectionTestUrl ?? connectionTestUrl,
|
||||
urlTestInterval: patch.urlTestInterval ?? urlTestInterval,
|
||||
enableClashApi: patch.enableClashApi ?? enableClashApi,
|
||||
clashApiPort: patch.clashApiPort ?? clashApiPort,
|
||||
bypassLan: patch.bypassLan ?? bypassLan,
|
||||
allowConnectionFromLan:
|
||||
patch.allowConnectionFromLan ?? allowConnectionFromLan,
|
||||
enableFakeDns: patch.enableFakeDns ?? enableFakeDns,
|
||||
enableDnsRouting: patch.enableDnsRouting ?? enableDnsRouting,
|
||||
independentDnsCache: patch.independentDnsCache ?? independentDnsCache,
|
||||
enableTlsFragment: patch.enableTlsFragment ?? enableTlsFragment,
|
||||
tlsFragmentSize: patch.tlsFragmentSize ?? tlsFragmentSize,
|
||||
tlsFragmentSleep: patch.tlsFragmentSleep ?? tlsFragmentSleep,
|
||||
enableTlsMixedSniCase:
|
||||
patch.enableTlsMixedSniCase ?? enableTlsMixedSniCase,
|
||||
enableTlsPadding: patch.enableTlsPadding ?? enableTlsPadding,
|
||||
tlsPaddingSize: patch.tlsPaddingSize ?? tlsPaddingSize,
|
||||
enableMux: patch.enableMux ?? enableMux,
|
||||
muxPadding: patch.muxPadding ?? muxPadding,
|
||||
muxMaxStreams: patch.muxMaxStreams ?? muxMaxStreams,
|
||||
muxProtocol: patch.muxProtocol ?? muxProtocol,
|
||||
enableWarp: patch.enableWarp ?? enableWarp,
|
||||
warpDetourMode: patch.warpDetourMode ?? warpDetourMode,
|
||||
warpLicenseKey: patch.warpLicenseKey ?? warpLicenseKey,
|
||||
warpAccountId: patch.warpAccountId ?? warpAccountId,
|
||||
warpAccessToken: patch.warpAccessToken ?? warpAccessToken,
|
||||
warpCleanIp: patch.warpCleanIp ?? warpCleanIp,
|
||||
warpPort: patch.warpPort ?? warpPort,
|
||||
warpNoise: patch.warpNoise ?? warpNoise,
|
||||
warpNoiseDelay: patch.warpNoiseDelay ?? warpNoiseDelay,
|
||||
warpWireguardConfig: patch.warpWireguardConfig ?? warpWireguardConfig,
|
||||
);
|
||||
}
|
||||
|
||||
SingboxConfigOption toSingbox({
|
||||
required String geoipPath,
|
||||
required String geositePath,
|
||||
required List<SingboxRule> rules,
|
||||
}) {
|
||||
return SingboxConfigOption(
|
||||
executeConfigAsIs: false,
|
||||
logLevel: logLevel,
|
||||
resolveDestination: resolveDestination,
|
||||
ipv6Mode: ipv6Mode,
|
||||
remoteDnsAddress: remoteDnsAddress,
|
||||
remoteDnsDomainStrategy: remoteDnsDomainStrategy,
|
||||
directDnsAddress: directDnsAddress,
|
||||
directDnsDomainStrategy: directDnsDomainStrategy,
|
||||
mixedPort: mixedPort,
|
||||
localDnsPort: localDnsPort,
|
||||
tunImplementation: tunImplementation,
|
||||
mtu: mtu,
|
||||
strictRoute: strictRoute,
|
||||
connectionTestUrl: connectionTestUrl,
|
||||
urlTestInterval: urlTestInterval,
|
||||
enableClashApi: enableClashApi,
|
||||
clashApiPort: clashApiPort,
|
||||
enableTun: serviceMode == ServiceMode.tun,
|
||||
enableTunService: serviceMode == ServiceMode.tunService,
|
||||
setSystemProxy: serviceMode == ServiceMode.systemProxy,
|
||||
bypassLan: bypassLan,
|
||||
allowConnectionFromLan: allowConnectionFromLan,
|
||||
enableFakeDns: enableFakeDns,
|
||||
enableDnsRouting: enableDnsRouting,
|
||||
independentDnsCache: independentDnsCache,
|
||||
enableTlsFragment: enableTlsFragment,
|
||||
tlsFragmentSize: tlsFragmentSize,
|
||||
tlsFragmentSleep: tlsFragmentSleep,
|
||||
enableTlsMixedSniCase: enableTlsMixedSniCase,
|
||||
enableTlsPadding: enableTlsPadding,
|
||||
tlsPaddingSize: tlsPaddingSize,
|
||||
enableMux: enableMux,
|
||||
muxPadding: muxPadding,
|
||||
muxMaxStreams: muxMaxStreams,
|
||||
muxProtocol: muxProtocol,
|
||||
geoipPath: geoipPath,
|
||||
geositePath: geositePath,
|
||||
rules: rules,
|
||||
warp: SingboxWarpOption(
|
||||
enable: enableWarp,
|
||||
mode: warpDetourMode,
|
||||
licenseKey: warpLicenseKey,
|
||||
accountId: warpAccountId,
|
||||
accessToken: warpAccessToken,
|
||||
cleanIp: warpCleanIp,
|
||||
cleanPort: warpPort,
|
||||
warpNoise: warpNoise,
|
||||
warpNoiseDelay: warpNoiseDelay,
|
||||
wireguardConfig: warpWireguardConfig,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
factory ConfigOptionEntity.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConfigOptionEntityFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
class ConfigOptionPatch with _$ConfigOptionPatch {
|
||||
const ConfigOptionPatch._();
|
||||
|
||||
@JsonSerializable(fieldRename: FieldRename.kebab)
|
||||
const factory ConfigOptionPatch({
|
||||
ServiceMode? serviceMode,
|
||||
LogLevel? logLevel,
|
||||
bool? resolveDestination,
|
||||
IPv6Mode? ipv6Mode,
|
||||
String? remoteDnsAddress,
|
||||
DomainStrategy? remoteDnsDomainStrategy,
|
||||
String? directDnsAddress,
|
||||
DomainStrategy? directDnsDomainStrategy,
|
||||
int? mixedPort,
|
||||
int? localDnsPort,
|
||||
TunImplementation? tunImplementation,
|
||||
int? mtu,
|
||||
bool? strictRoute,
|
||||
String? connectionTestUrl,
|
||||
@IntervalInSecondsConverter() Duration? urlTestInterval,
|
||||
bool? enableClashApi,
|
||||
int? clashApiPort,
|
||||
bool? bypassLan,
|
||||
bool? allowConnectionFromLan,
|
||||
bool? enableFakeDns,
|
||||
bool? enableDnsRouting,
|
||||
bool? independentDnsCache,
|
||||
bool? enableTlsFragment,
|
||||
@OptionalRangeJsonConverter() OptionalRange? tlsFragmentSize,
|
||||
@OptionalRangeJsonConverter() OptionalRange? tlsFragmentSleep,
|
||||
bool? enableTlsMixedSniCase,
|
||||
bool? enableTlsPadding,
|
||||
@OptionalRangeJsonConverter() OptionalRange? tlsPaddingSize,
|
||||
bool? enableMux,
|
||||
bool? muxPadding,
|
||||
int? muxMaxStreams,
|
||||
MuxProtocol? muxProtocol,
|
||||
bool? enableWarp,
|
||||
WarpDetourMode? warpDetourMode,
|
||||
String? warpLicenseKey,
|
||||
String? warpAccountId,
|
||||
String? warpAccessToken,
|
||||
String? warpCleanIp,
|
||||
int? warpPort,
|
||||
@OptionalRangeJsonConverter() OptionalRange? warpNoise,
|
||||
@OptionalRangeJsonConverter() OptionalRange? warpNoiseDelay,
|
||||
String? warpWireguardConfig,
|
||||
}) = _ConfigOptionPatch;
|
||||
|
||||
factory ConfigOptionPatch.fromJson(Map<String, dynamic> json) =>
|
||||
_$ConfigOptionPatchFromJson(json);
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'package:hiddify/features/config_option/data/config_option_data_providers.dart';
|
||||
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
|
||||
import 'package:hiddify/utils/custom_loggers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
@@ -8,28 +10,22 @@ part 'config_option_notifier.g.dart';
|
||||
@Riverpod(keepAlive: true)
|
||||
class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
|
||||
@override
|
||||
Future<ConfigOptionEntity> build() async {
|
||||
return ref
|
||||
.watch(configOptionRepositoryProvider)
|
||||
.getConfigOption()
|
||||
.getOrElse((l) {
|
||||
loggy.error("error getting persisted options $l", l);
|
||||
throw l;
|
||||
});
|
||||
}
|
||||
Future<void> build() async {}
|
||||
|
||||
Future<void> updateOption(ConfigOptionPatch patch) async {
|
||||
if (state case AsyncData(value: final options)) {
|
||||
await ref
|
||||
.read(configOptionRepositoryProvider)
|
||||
.updateConfigOption(patch)
|
||||
.map((_) => state = AsyncData(options.patch(patch)))
|
||||
.run();
|
||||
}
|
||||
Future<void> exportJsonToClipboard() async {
|
||||
final map = {
|
||||
for (final option in ConfigOptions.preferences)
|
||||
ref.read(option.notifier).entry.key: ref.read(option.notifier).raw(),
|
||||
};
|
||||
const encoder = JsonEncoder.withIndent(' ');
|
||||
final json = encoder.convert(map);
|
||||
await Clipboard.setData(ClipboardData(text: json));
|
||||
}
|
||||
|
||||
Future<void> resetOption() async {
|
||||
await ref.read(configOptionRepositoryProvider).resetConfigOption().run();
|
||||
for (final option in ConfigOptions.preferences) {
|
||||
await ref.read(option.notifier).reset();
|
||||
}
|
||||
ref.invalidateSelf();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hiddify/core/preferences/preferences_provider.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_data_providers.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
|
||||
import 'package:hiddify/features/config_option/model/config_option_failure.dart';
|
||||
import 'package:hiddify/singbox/service/singbox_service_provider.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
@@ -46,15 +47,28 @@ class WarpOptionNotifier extends _$WarpOptionNotifier with AppLogger {
|
||||
Future<void> generateWarpConfig() async {
|
||||
if (state.configGeneration.isLoading) return;
|
||||
state = state.copyWith(configGeneration: const AsyncLoading());
|
||||
final result = await AsyncValue.guard(
|
||||
() async => await ref
|
||||
.read(configOptionRepositoryProvider)
|
||||
.generateWarpConfig()
|
||||
.getOrElse((l) {
|
||||
loggy.warning("error generating warp config: $l", l);
|
||||
throw l;
|
||||
}).run(),
|
||||
);
|
||||
|
||||
final result = await AsyncValue.guard(() async {
|
||||
final warp = await ref
|
||||
.read(singboxServiceProvider)
|
||||
.generateWarpConfig(
|
||||
licenseKey: ref.read(ConfigOptions.warpLicenseKey),
|
||||
previousAccountId: ref.read(ConfigOptions.warpAccountId),
|
||||
previousAccessToken: ref.read(ConfigOptions.warpAccessToken),
|
||||
)
|
||||
.getOrElse((l) => throw l)
|
||||
.run();
|
||||
|
||||
await ref
|
||||
.read(ConfigOptions.warpAccountId.notifier)
|
||||
.update(warp.accountId);
|
||||
await ref
|
||||
.read(ConfigOptions.warpAccessToken.notifier)
|
||||
.update(warp.accessToken);
|
||||
|
||||
return warp.log;
|
||||
});
|
||||
|
||||
state = state.copyWith(configGeneration: result);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +1,15 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/failures.dart';
|
||||
import 'package:hiddify/core/model/optional_range.dart';
|
||||
import 'package:hiddify/core/widget/adaptive_icon.dart';
|
||||
import 'package:hiddify/core/widget/tip_card.dart';
|
||||
import 'package:hiddify/features/common/nested_app_bar.dart';
|
||||
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
|
||||
import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart';
|
||||
import 'package:hiddify/features/config_option/overview/warp_options_widgets.dart';
|
||||
import 'package:hiddify/features/config_option/widget/preference_tile.dart';
|
||||
import 'package:hiddify/features/log/model/log_level.dart';
|
||||
import 'package:hiddify/features/settings/widgets/sections_widgets.dart';
|
||||
import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart';
|
||||
@@ -27,13 +25,6 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final t = ref.watch(translationsProvider);
|
||||
|
||||
final defaultOptions = ConfigOptionEntity.initial();
|
||||
final asyncOptions = ref.watch(configOptionNotifierProvider);
|
||||
|
||||
Future<void> changeOption(ConfigOptionPatch patch) async {
|
||||
await ref.read(configOptionNotifierProvider.notifier).updateOption(patch);
|
||||
}
|
||||
|
||||
String experimental(String txt) {
|
||||
return "$txt (${t.settings.experimental})";
|
||||
}
|
||||
@@ -44,462 +35,259 @@ class ConfigOptionsPage extends HookConsumerWidget {
|
||||
NestedAppBar(
|
||||
title: Text(t.settings.config.pageTitle),
|
||||
actions: [
|
||||
if (asyncOptions case AsyncData(value: final options))
|
||||
PopupMenuButton(
|
||||
icon: Icon(AdaptiveIcon(context).more),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Text(t.general.addToClipboard),
|
||||
onTap: () {
|
||||
Clipboard.setData(
|
||||
ClipboardData(text: options.format()),
|
||||
);
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.config.resetBtn),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(configOptionNotifierProvider.notifier)
|
||||
.resetOption();
|
||||
},
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
switch (asyncOptions) {
|
||||
AsyncData(value: final options) => SliverList.list(
|
||||
children: [
|
||||
TipCard(message: t.settings.experimentalMsg),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.logLevel),
|
||||
subtitle: Text(options.logLevel.name.toUpperCase()),
|
||||
onTap: () async {
|
||||
final logLevel = await SettingsPickerDialog(
|
||||
title: t.settings.config.logLevel,
|
||||
selected: options.logLevel,
|
||||
options: LogLevel.choices,
|
||||
getTitle: (e) => e.name.toUpperCase(),
|
||||
resetValue: defaultOptions.logLevel,
|
||||
).show(context);
|
||||
if (logLevel == null) return;
|
||||
await changeOption(ConfigOptionPatch(logLevel: logLevel));
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.route),
|
||||
SwitchListTile(
|
||||
title: Text(experimental(t.settings.config.bypassLan)),
|
||||
value: options.bypassLan,
|
||||
onChanged: (value) async =>
|
||||
changeOption(ConfigOptionPatch(bypassLan: value)),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.resolveDestination),
|
||||
value: options.resolveDestination,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(resolveDestination: value),
|
||||
PopupMenuButton(
|
||||
icon: Icon(AdaptiveIcon(context).more),
|
||||
itemBuilder: (context) {
|
||||
return [
|
||||
PopupMenuItem(
|
||||
onTap: ref
|
||||
.read(configOptionNotifierProvider.notifier)
|
||||
.exportJsonToClipboard,
|
||||
child: Text(t.general.addToClipboard),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.ipv6Mode),
|
||||
subtitle: Text(options.ipv6Mode.present(t)),
|
||||
onTap: () async {
|
||||
final ipv6Mode = await SettingsPickerDialog(
|
||||
title: t.settings.config.ipv6Mode,
|
||||
selected: options.ipv6Mode,
|
||||
options: IPv6Mode.values,
|
||||
getTitle: (e) => e.present(t),
|
||||
resetValue: defaultOptions.ipv6Mode,
|
||||
).show(context);
|
||||
if (ipv6Mode == null) return;
|
||||
await changeOption(ConfigOptionPatch(ipv6Mode: ipv6Mode));
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.dns),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.remoteDnsAddress),
|
||||
subtitle: Text(options.remoteDnsAddress),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
title: t.settings.config.remoteDnsAddress,
|
||||
initialValue: options.remoteDnsAddress,
|
||||
resetValue: defaultOptions.remoteDnsAddress,
|
||||
).show(context);
|
||||
if (url == null || url.isEmpty) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(remoteDnsAddress: url),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.remoteDnsDomainStrategy),
|
||||
subtitle: Text(options.remoteDnsDomainStrategy.displayName),
|
||||
onTap: () async {
|
||||
final domainStrategy = await SettingsPickerDialog(
|
||||
title: t.settings.config.remoteDnsDomainStrategy,
|
||||
selected: options.remoteDnsDomainStrategy,
|
||||
options: DomainStrategy.values,
|
||||
getTitle: (e) => e.displayName,
|
||||
resetValue: defaultOptions.remoteDnsDomainStrategy,
|
||||
).show(context);
|
||||
if (domainStrategy == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
remoteDnsDomainStrategy: domainStrategy,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.directDnsAddress),
|
||||
subtitle: Text(options.directDnsAddress),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
title: t.settings.config.directDnsAddress,
|
||||
initialValue: options.directDnsAddress,
|
||||
resetValue: defaultOptions.directDnsAddress,
|
||||
).show(context);
|
||||
if (url == null || url.isEmpty) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(directDnsAddress: url),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.directDnsDomainStrategy),
|
||||
subtitle: Text(options.directDnsDomainStrategy.displayName),
|
||||
onTap: () async {
|
||||
final domainStrategy = await SettingsPickerDialog(
|
||||
title: t.settings.config.directDnsDomainStrategy,
|
||||
selected: options.directDnsDomainStrategy,
|
||||
options: DomainStrategy.values,
|
||||
getTitle: (e) => e.displayName,
|
||||
resetValue: defaultOptions.directDnsDomainStrategy,
|
||||
).show(context);
|
||||
if (domainStrategy == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
directDnsDomainStrategy: domainStrategy,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.enableDnsRouting),
|
||||
value: options.enableDnsRouting,
|
||||
onChanged: (value) => changeOption(
|
||||
ConfigOptionPatch(enableDnsRouting: value),
|
||||
),
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(experimental(t.settings.config.section.mux)),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.enableMux),
|
||||
value: options.enableMux,
|
||||
onChanged: (value) => changeOption(
|
||||
ConfigOptionPatch(enableMux: value),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.muxProtocol),
|
||||
subtitle: Text(options.muxProtocol.name),
|
||||
onTap: () async {
|
||||
final pickedProtocol = await SettingsPickerDialog(
|
||||
title: t.settings.config.muxProtocol,
|
||||
selected: options.muxProtocol,
|
||||
options: MuxProtocol.values,
|
||||
getTitle: (e) => e.name,
|
||||
resetValue: defaultOptions.muxProtocol,
|
||||
).show(context);
|
||||
if (pickedProtocol == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(muxProtocol: pickedProtocol),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.muxMaxStreams),
|
||||
subtitle: Text(options.muxMaxStreams.toString()),
|
||||
onTap: () async {
|
||||
final maxStreams = await SettingsInputDialog(
|
||||
title: t.settings.config.muxMaxStreams,
|
||||
initialValue: options.muxMaxStreams,
|
||||
resetValue: defaultOptions.muxMaxStreams,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (maxStreams == null || maxStreams < 1) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(muxMaxStreams: maxStreams),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.inbound),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.serviceMode),
|
||||
subtitle: Text(options.serviceMode.present(t)),
|
||||
onTap: () async {
|
||||
final pickedMode = await SettingsPickerDialog(
|
||||
title: t.settings.config.serviceMode,
|
||||
selected: options.serviceMode,
|
||||
options: ServiceMode.choices,
|
||||
getTitle: (e) => e.present(t),
|
||||
resetValue: ServiceMode.defaultMode,
|
||||
).show(context);
|
||||
if (pickedMode == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(serviceMode: pickedMode),
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.strictRoute),
|
||||
value: options.strictRoute,
|
||||
onChanged: (value) async =>
|
||||
changeOption(ConfigOptionPatch(strictRoute: value)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tunImplementation),
|
||||
subtitle: Text(options.tunImplementation.name),
|
||||
onTap: () async {
|
||||
final tunImplementation = await SettingsPickerDialog(
|
||||
title: t.settings.config.tunImplementation,
|
||||
selected: options.tunImplementation,
|
||||
options: TunImplementation.values,
|
||||
getTitle: (e) => e.name,
|
||||
resetValue: defaultOptions.tunImplementation,
|
||||
).show(context);
|
||||
if (tunImplementation == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(tunImplementation: tunImplementation),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.mixedPort),
|
||||
subtitle: Text(options.mixedPort.toString()),
|
||||
onTap: () async {
|
||||
final mixedPort = await SettingsInputDialog(
|
||||
title: t.settings.config.mixedPort,
|
||||
initialValue: options.mixedPort,
|
||||
resetValue: defaultOptions.mixedPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (mixedPort == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(mixedPort: mixedPort),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.localDnsPort),
|
||||
subtitle: Text(options.localDnsPort.toString()),
|
||||
onTap: () async {
|
||||
final localDnsPort = await SettingsInputDialog(
|
||||
title: t.settings.config.localDnsPort,
|
||||
initialValue: options.localDnsPort,
|
||||
resetValue: defaultOptions.localDnsPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (localDnsPort == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(localDnsPort: localDnsPort),
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
experimental(t.settings.config.allowConnectionFromLan),
|
||||
),
|
||||
value: options.allowConnectionFromLan,
|
||||
onChanged: (value) => changeOption(
|
||||
ConfigOptionPatch(allowConnectionFromLan: value),
|
||||
),
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.tlsTricks),
|
||||
SwitchListTile(
|
||||
title:
|
||||
Text(experimental(t.settings.config.enableTlsFragment)),
|
||||
value: options.enableTlsFragment,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(enableTlsFragment: value),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tlsFragmentSize),
|
||||
subtitle: Text(options.tlsFragmentSize.present(t)),
|
||||
onTap: () async {
|
||||
final range = await SettingsInputDialog(
|
||||
title: t.settings.config.tlsFragmentSize,
|
||||
initialValue: options.tlsFragmentSize.format(),
|
||||
resetValue: defaultOptions.tlsFragmentSize.format(),
|
||||
).show(context);
|
||||
if (range == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
tlsFragmentSize: OptionalRange.tryParse(range),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tlsFragmentSleep),
|
||||
subtitle: Text(options.tlsFragmentSleep.present(t)),
|
||||
onTap: () async {
|
||||
final range = await SettingsInputDialog(
|
||||
title: t.settings.config.tlsFragmentSleep,
|
||||
initialValue: options.tlsFragmentSleep.format(),
|
||||
resetValue: defaultOptions.tlsFragmentSleep.format(),
|
||||
).show(context);
|
||||
if (range == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
tlsFragmentSleep: OptionalRange.tryParse(range),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
experimental(t.settings.config.enableTlsMixedSniCase),
|
||||
),
|
||||
value: options.enableTlsMixedSniCase,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(enableTlsMixedSniCase: value),
|
||||
),
|
||||
),
|
||||
SwitchListTile(
|
||||
title:
|
||||
Text(experimental(t.settings.config.enableTlsPadding)),
|
||||
value: options.enableTlsPadding,
|
||||
onChanged: (value) async => changeOption(
|
||||
ConfigOptionPatch(enableTlsPadding: value),
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.tlsPaddingSize),
|
||||
subtitle: Text(options.tlsPaddingSize.present(t)),
|
||||
onTap: () async {
|
||||
final range = await SettingsInputDialog(
|
||||
title: t.settings.config.tlsPaddingSize,
|
||||
initialValue: options.tlsPaddingSize.format(),
|
||||
resetValue: defaultOptions.tlsPaddingSize.format(),
|
||||
).show(context);
|
||||
if (range == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
tlsPaddingSize: OptionalRange.tryParse(range),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(experimental(t.settings.config.section.warp)),
|
||||
WarpOptionsTiles(
|
||||
options: options,
|
||||
defaultOptions: defaultOptions,
|
||||
onChange: changeOption,
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.misc),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.connectionTestUrl),
|
||||
subtitle: Text(options.connectionTestUrl),
|
||||
onTap: () async {
|
||||
final url = await SettingsInputDialog(
|
||||
title: t.settings.config.connectionTestUrl,
|
||||
initialValue: options.connectionTestUrl,
|
||||
resetValue: defaultOptions.connectionTestUrl,
|
||||
).show(context);
|
||||
if (url == null || url.isEmpty || !isUrl(url)) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(connectionTestUrl: url),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.urlTestInterval),
|
||||
subtitle: Text(
|
||||
options.urlTestInterval
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
),
|
||||
onTap: () async {
|
||||
final urlTestInterval = await SettingsSliderDialog(
|
||||
title: t.settings.config.urlTestInterval,
|
||||
initialValue: options.urlTestInterval.inMinutes
|
||||
.coerceIn(0, 60)
|
||||
.toDouble(),
|
||||
resetValue:
|
||||
defaultOptions.urlTestInterval.inMinutes.toDouble(),
|
||||
min: 1,
|
||||
max: 60,
|
||||
divisions: 60,
|
||||
labelGen: (value) => Duration(minutes: value.toInt())
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
).show(context);
|
||||
if (urlTestInterval == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(
|
||||
urlTestInterval:
|
||||
Duration(minutes: urlTestInterval.toInt()),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.clashApiPort),
|
||||
subtitle: Text(options.clashApiPort.toString()),
|
||||
onTap: () async {
|
||||
final clashApiPort = await SettingsInputDialog(
|
||||
title: t.settings.config.clashApiPort,
|
||||
initialValue: options.clashApiPort,
|
||||
resetValue: defaultOptions.clashApiPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (clashApiPort == null) return;
|
||||
await changeOption(
|
||||
ConfigOptionPatch(clashApiPort: clashApiPort),
|
||||
);
|
||||
},
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
AsyncError(:final error) => SliverFillRemaining(
|
||||
hasScrollBody: false,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(FluentIcons.error_circle_24_regular),
|
||||
const Gap(2),
|
||||
Text(t.presentShortError(error)),
|
||||
const Gap(2),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
PopupMenuItem(
|
||||
child: Text(t.settings.config.resetBtn),
|
||||
onTap: () async {
|
||||
await ref
|
||||
.read(configOptionNotifierProvider.notifier)
|
||||
.resetOption();
|
||||
},
|
||||
child: Text(t.settings.config.resetBtn),
|
||||
),
|
||||
],
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
_ => const SliverToBoxAdapter(),
|
||||
},
|
||||
],
|
||||
),
|
||||
SliverList.list(
|
||||
children: [
|
||||
TipCard(message: t.settings.experimentalMsg),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.logLevel),
|
||||
preferences: ref.watch(ConfigOptions.logLevel.notifier),
|
||||
choices: LogLevel.choices,
|
||||
title: t.settings.config.logLevel,
|
||||
presentChoice: (value) => value.name.toUpperCase(),
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.route),
|
||||
SwitchListTile(
|
||||
title: Text(experimental(t.settings.config.bypassLan)),
|
||||
value: ref.watch(ConfigOptions.bypassLan),
|
||||
onChanged: ref.watch(ConfigOptions.bypassLan.notifier).update,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.resolveDestination),
|
||||
value: ref.watch(ConfigOptions.resolveDestination),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.resolveDestination.notifier).update,
|
||||
),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.ipv6Mode),
|
||||
preferences: ref.watch(ConfigOptions.ipv6Mode.notifier),
|
||||
choices: IPv6Mode.values,
|
||||
title: t.settings.config.ipv6Mode,
|
||||
presentChoice: (value) => value.present(t),
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.dns),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.remoteDnsAddress),
|
||||
preferences: ref.watch(ConfigOptions.remoteDnsAddress.notifier),
|
||||
title: t.settings.config.remoteDnsAddress,
|
||||
),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.remoteDnsDomainStrategy),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.remoteDnsDomainStrategy.notifier),
|
||||
choices: DomainStrategy.values,
|
||||
title: t.settings.config.remoteDnsDomainStrategy,
|
||||
presentChoice: (value) => value.displayName,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.directDnsAddress),
|
||||
preferences: ref.watch(ConfigOptions.directDnsAddress.notifier),
|
||||
title: t.settings.config.directDnsAddress,
|
||||
),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.directDnsDomainStrategy),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.directDnsDomainStrategy.notifier),
|
||||
choices: DomainStrategy.values,
|
||||
title: t.settings.config.directDnsDomainStrategy,
|
||||
presentChoice: (value) => value.displayName,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.enableDnsRouting),
|
||||
value: ref.watch(ConfigOptions.enableDnsRouting),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.enableDnsRouting.notifier).update,
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(experimental(t.settings.config.section.mux)),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.enableMux),
|
||||
value: ref.watch(ConfigOptions.enableMux),
|
||||
onChanged: ref.watch(ConfigOptions.enableMux.notifier).update,
|
||||
),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.muxProtocol),
|
||||
preferences: ref.watch(ConfigOptions.muxProtocol.notifier),
|
||||
choices: MuxProtocol.values,
|
||||
title: t.settings.config.muxProtocol,
|
||||
presentChoice: (value) => value.name,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.muxMaxStreams),
|
||||
preferences: ref.watch(ConfigOptions.muxMaxStreams.notifier),
|
||||
title: t.settings.config.muxMaxStreams,
|
||||
inputToValue: int.tryParse,
|
||||
digitsOnly: true,
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.inbound),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.serviceMode),
|
||||
preferences: ref.watch(ConfigOptions.serviceMode.notifier),
|
||||
choices: ServiceMode.choices,
|
||||
title: t.settings.config.serviceMode,
|
||||
presentChoice: (value) => value.present(t),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.strictRoute),
|
||||
value: ref.watch(ConfigOptions.strictRoute),
|
||||
onChanged: ref.watch(ConfigOptions.strictRoute.notifier).update,
|
||||
),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.tunImplementation),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.tunImplementation.notifier),
|
||||
choices: TunImplementation.values,
|
||||
title: t.settings.config.tunImplementation,
|
||||
presentChoice: (value) => value.name,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.mixedPort),
|
||||
preferences: ref.watch(ConfigOptions.mixedPort.notifier),
|
||||
title: t.settings.config.mixedPort,
|
||||
inputToValue: int.tryParse,
|
||||
digitsOnly: true,
|
||||
validateInput: isPort,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.localDnsPort),
|
||||
preferences: ref.watch(ConfigOptions.localDnsPort.notifier),
|
||||
title: t.settings.config.localDnsPort,
|
||||
inputToValue: int.tryParse,
|
||||
digitsOnly: true,
|
||||
validateInput: isPort,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
experimental(t.settings.config.allowConnectionFromLan),
|
||||
),
|
||||
value: ref.watch(ConfigOptions.allowConnectionFromLan),
|
||||
onChanged: ref
|
||||
.read(ConfigOptions.allowConnectionFromLan.notifier)
|
||||
.update,
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.tlsTricks),
|
||||
SwitchListTile(
|
||||
title: Text(experimental(t.settings.config.enableTlsFragment)),
|
||||
value: ref.watch(ConfigOptions.enableTlsFragment),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.enableTlsFragment.notifier).update,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.tlsFragmentSize),
|
||||
preferences: ref.watch(ConfigOptions.tlsFragmentSize.notifier),
|
||||
title: t.settings.config.tlsFragmentSize,
|
||||
inputToValue: OptionalRange.tryParse,
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.tlsFragmentSleep),
|
||||
preferences: ref.watch(ConfigOptions.tlsFragmentSleep.notifier),
|
||||
title: t.settings.config.tlsFragmentSleep,
|
||||
inputToValue: OptionalRange.tryParse,
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(
|
||||
experimental(t.settings.config.enableTlsMixedSniCase),
|
||||
),
|
||||
value: ref.watch(ConfigOptions.enableTlsMixedSniCase),
|
||||
onChanged: ref
|
||||
.watch(ConfigOptions.enableTlsMixedSniCase.notifier)
|
||||
.update,
|
||||
),
|
||||
SwitchListTile(
|
||||
title: Text(experimental(t.settings.config.enableTlsPadding)),
|
||||
value: ref.watch(ConfigOptions.enableTlsPadding),
|
||||
onChanged:
|
||||
ref.watch(ConfigOptions.enableTlsPadding.notifier).update,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.tlsPaddingSize),
|
||||
preferences: ref.watch(ConfigOptions.tlsPaddingSize.notifier),
|
||||
title: t.settings.config.tlsPaddingSize,
|
||||
inputToValue: OptionalRange.tryParse,
|
||||
presentValue: (value) => value.format(),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(experimental(t.settings.config.section.warp)),
|
||||
const WarpOptionsTiles(),
|
||||
const SettingsDivider(),
|
||||
SettingsSection(t.settings.config.section.misc),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.connectionTestUrl),
|
||||
preferences:
|
||||
ref.watch(ConfigOptions.connectionTestUrl.notifier),
|
||||
title: t.settings.config.connectionTestUrl,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.urlTestInterval),
|
||||
subtitle: Text(
|
||||
ref
|
||||
.watch(ConfigOptions.urlTestInterval)
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
),
|
||||
onTap: () async {
|
||||
final urlTestInterval = await SettingsSliderDialog(
|
||||
title: t.settings.config.urlTestInterval,
|
||||
initialValue: ref
|
||||
.watch(ConfigOptions.urlTestInterval)
|
||||
.inMinutes
|
||||
.coerceIn(0, 60)
|
||||
.toDouble(),
|
||||
onReset:
|
||||
ref.read(ConfigOptions.urlTestInterval.notifier).reset,
|
||||
min: 1,
|
||||
max: 60,
|
||||
divisions: 60,
|
||||
labelGen: (value) => Duration(minutes: value.toInt())
|
||||
.toApproximateTime(isRelativeToNow: false),
|
||||
).show(context);
|
||||
if (urlTestInterval == null) return;
|
||||
await ref
|
||||
.read(ConfigOptions.urlTestInterval.notifier)
|
||||
.update(Duration(minutes: urlTestInterval.toInt()));
|
||||
},
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.clashApiPort),
|
||||
preferences: ref.watch(ConfigOptions.clashApiPort.notifier),
|
||||
title: t.settings.config.clashApiPort,
|
||||
validateInput: isPort,
|
||||
digitsOnly: true,
|
||||
inputToValue: int.tryParse,
|
||||
),
|
||||
const Gap(24),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,29 +1,19 @@
|
||||
import 'package:dartx/dartx.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hiddify/core/localization/translations.dart';
|
||||
import 'package:hiddify/core/model/constants.dart';
|
||||
import 'package:hiddify/core/model/optional_range.dart';
|
||||
import 'package:hiddify/core/widget/custom_alert_dialog.dart';
|
||||
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
|
||||
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
|
||||
import 'package:hiddify/features/config_option/notifier/warp_option_notifier.dart';
|
||||
import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart';
|
||||
import 'package:hiddify/features/config_option/widget/preference_tile.dart';
|
||||
import 'package:hiddify/singbox/model/singbox_config_enum.dart';
|
||||
import 'package:hiddify/utils/uri_utils.dart';
|
||||
import 'package:hiddify/utils/validators.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
||||
class WarpOptionsTiles extends HookConsumerWidget {
|
||||
const WarpOptionsTiles({
|
||||
required this.options,
|
||||
required this.defaultOptions,
|
||||
required this.onChange,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final ConfigOptionEntity options;
|
||||
final ConfigOptionEntity defaultOptions;
|
||||
final Future<void> Function(ConfigOptionPatch patch) onChange;
|
||||
const WarpOptionsTiles({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
@@ -31,7 +21,8 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
||||
|
||||
final warpOptions = ref.watch(warpOptionNotifierProvider);
|
||||
final warpPrefaceCompleted = warpOptions.consentGiven;
|
||||
final canChangeOptions = warpPrefaceCompleted && options.enableWarp;
|
||||
final enableWarp = ref.watch(ConfigOptions.enableWarp);
|
||||
final canChangeOptions = warpPrefaceCompleted && enableWarp;
|
||||
|
||||
ref.listen(
|
||||
warpOptionNotifierProvider.select((value) => value.configGeneration),
|
||||
@@ -49,7 +40,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
||||
children: [
|
||||
SwitchListTile(
|
||||
title: Text(t.settings.config.enableWarp),
|
||||
value: options.enableWarp,
|
||||
value: enableWarp,
|
||||
onChanged: (value) async {
|
||||
if (!warpPrefaceCompleted) {
|
||||
final agreed = await showDialog<bool>(
|
||||
@@ -58,10 +49,10 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
||||
);
|
||||
if (agreed ?? false) {
|
||||
await ref.read(warpOptionNotifierProvider.notifier).agree();
|
||||
await onChange(ConfigOptionPatch(enableWarp: value));
|
||||
await ref.read(ConfigOptions.enableWarp.notifier).update(value);
|
||||
}
|
||||
} else {
|
||||
await onChange(ConfigOptionPatch(enableWarp: value));
|
||||
await ref.read(ConfigOptions.enableWarp.notifier).update(value);
|
||||
}
|
||||
},
|
||||
),
|
||||
@@ -85,112 +76,56 @@ class WarpOptionsTiles extends HookConsumerWidget {
|
||||
.generateWarpConfig();
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpDetourMode),
|
||||
subtitle: Text(options.warpDetourMode.present(t)),
|
||||
ChoicePreferenceWidget(
|
||||
selected: ref.watch(ConfigOptions.warpDetourMode),
|
||||
preferences: ref.watch(ConfigOptions.warpDetourMode.notifier),
|
||||
enabled: canChangeOptions,
|
||||
onTap: () async {
|
||||
final warpDetourMode = await SettingsPickerDialog(
|
||||
title: t.settings.config.warpDetourMode,
|
||||
selected: options.warpDetourMode,
|
||||
options: WarpDetourMode.values,
|
||||
getTitle: (e) => e.present(t),
|
||||
resetValue: defaultOptions.warpDetourMode,
|
||||
).show(context);
|
||||
if (warpDetourMode == null) return;
|
||||
await onChange(
|
||||
ConfigOptionPatch(warpDetourMode: warpDetourMode),
|
||||
);
|
||||
},
|
||||
choices: WarpDetourMode.values,
|
||||
title: t.settings.config.warpDetourMode,
|
||||
presentChoice: (value) => value.present(t),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpLicenseKey),
|
||||
subtitle: Text(
|
||||
options.warpLicenseKey.isEmpty
|
||||
? t.general.notSet
|
||||
: options.warpLicenseKey,
|
||||
),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.warpLicenseKey),
|
||||
preferences: ref.watch(ConfigOptions.warpLicenseKey.notifier),
|
||||
enabled: canChangeOptions,
|
||||
onTap: () async {
|
||||
final licenseKey = await SettingsInputDialog(
|
||||
title: t.settings.config.warpLicenseKey,
|
||||
initialValue: options.warpLicenseKey,
|
||||
resetValue: defaultOptions.warpLicenseKey,
|
||||
).show(context);
|
||||
if (licenseKey == null) return;
|
||||
await onChange(ConfigOptionPatch(warpLicenseKey: licenseKey));
|
||||
},
|
||||
title: t.settings.config.warpLicenseKey,
|
||||
presentValue: (value) => value.isEmpty ? t.general.notSet : value,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpCleanIp),
|
||||
subtitle: Text(options.warpCleanIp),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.warpCleanIp),
|
||||
preferences: ref.watch(ConfigOptions.warpCleanIp.notifier),
|
||||
enabled: canChangeOptions,
|
||||
onTap: () async {
|
||||
final warpCleanIp = await SettingsInputDialog(
|
||||
title: t.settings.config.warpCleanIp,
|
||||
initialValue: options.warpCleanIp,
|
||||
resetValue: defaultOptions.warpCleanIp,
|
||||
).show(context);
|
||||
if (warpCleanIp == null || warpCleanIp.isBlank) return;
|
||||
await onChange(ConfigOptionPatch(warpCleanIp: warpCleanIp));
|
||||
},
|
||||
title: t.settings.config.warpCleanIp,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpPort),
|
||||
subtitle: Text(options.warpPort.toString()),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.warpPort),
|
||||
preferences: ref.watch(ConfigOptions.warpPort.notifier),
|
||||
enabled: canChangeOptions,
|
||||
onTap: () async {
|
||||
final warpPort = await SettingsInputDialog(
|
||||
title: t.settings.config.warpPort,
|
||||
initialValue: options.warpPort,
|
||||
resetValue: defaultOptions.warpPort,
|
||||
validator: isPort,
|
||||
mapTo: int.tryParse,
|
||||
digitsOnly: true,
|
||||
).show(context);
|
||||
if (warpPort == null) return;
|
||||
await onChange(
|
||||
ConfigOptionPatch(warpPort: warpPort),
|
||||
);
|
||||
},
|
||||
title: t.settings.config.warpPort,
|
||||
inputToValue: int.tryParse,
|
||||
validateInput: isPort,
|
||||
digitsOnly: true,
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpNoise),
|
||||
subtitle: Text(options.warpNoise.present(t)),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.warpNoise),
|
||||
preferences: ref.watch(ConfigOptions.warpNoise.notifier),
|
||||
enabled: canChangeOptions,
|
||||
onTap: () async {
|
||||
final warpNoise = await SettingsInputDialog(
|
||||
title: t.settings.config.warpNoise,
|
||||
initialValue: options.warpNoise.format(),
|
||||
resetValue: defaultOptions.warpNoise.format(),
|
||||
).show(context);
|
||||
if (warpNoise == null) return;
|
||||
await onChange(
|
||||
ConfigOptionPatch(
|
||||
warpNoise: OptionalRange.tryParse(warpNoise, allowEmpty: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
title: t.settings.config.warpNoise,
|
||||
inputToValue: (input) =>
|
||||
OptionalRange.tryParse(input, allowEmpty: true),
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
ListTile(
|
||||
title: Text(t.settings.config.warpNoiseDelay),
|
||||
subtitle: Text(options.warpNoiseDelay.present(t)),
|
||||
ValuePreferenceWidget(
|
||||
value: ref.watch(ConfigOptions.warpNoiseDelay),
|
||||
preferences: ref.watch(ConfigOptions.warpNoiseDelay.notifier),
|
||||
enabled: canChangeOptions,
|
||||
onTap: () async {
|
||||
final warpNoiseDelay = await SettingsInputDialog(
|
||||
title: t.settings.config.warpNoiseDelay,
|
||||
initialValue: options.warpNoiseDelay.format(),
|
||||
resetValue: defaultOptions.warpNoiseDelay.format(),
|
||||
).show(context);
|
||||
if (warpNoiseDelay == null) return;
|
||||
await onChange(
|
||||
ConfigOptionPatch(
|
||||
warpNoiseDelay:
|
||||
OptionalRange.tryParse(warpNoiseDelay, allowEmpty: true),
|
||||
),
|
||||
);
|
||||
},
|
||||
)
|
||||
title: t.settings.config.warpNoiseDelay,
|
||||
inputToValue: (input) =>
|
||||
OptionalRange.tryParse(input, allowEmpty: true),
|
||||
presentValue: (value) => value.present(t),
|
||||
formatInputValue: (value) => value.format(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user