Change mapping and bug fixes

This commit is contained in:
problematicconsumer
2024-02-15 19:39:35 +03:30
parent bd4c5eed7e
commit 702c59c3bc
21 changed files with 501 additions and 422 deletions

View File

@@ -15,11 +15,9 @@ DioHttpClient httpClient(HttpClientRef ref) {
);
ref.listen(
configOptionNotifierProvider,
(_, next) {
if (next case AsyncData(value: final options)) {
client.setProxyPort(options.mixedPort);
}
configOptionNotifierProvider.selectAsync((data) => data.mixedPort),
(_, next) async {
client.setProxyPort(await next);
},
fireImmediately: true,
);

View File

@@ -0,0 +1,54 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:dartx/dartx.dart';
import 'package:hiddify/core/localization/translations.dart';
part 'optional_range.mapper.dart';
@MappableClass()
class OptionalRange with OptionalRangeMappable {
const OptionalRange({this.min, this.max});
final int? min;
final int? max;
String format() => [min, max].whereNotNull().join("-");
String present(TranslationsEn t) =>
format().isEmpty ? t.general.notSet : format();
factory OptionalRange._fromString(
String input, {
bool allowEmpty = true,
}) =>
switch (input.split("-")) {
[final String val] when val.isEmpty && allowEmpty =>
const OptionalRange(),
[final String min] => OptionalRange(min: int.parse(min)),
[final String min, final String max] => OptionalRange(
min: int.parse(min),
max: int.parse(max),
),
_ => throw Exception("Invalid range: $input"),
};
static OptionalRange? tryParse(
String input, {
bool allowEmpty = false,
}) {
try {
return OptionalRange._fromString(input);
} catch (_) {
return null;
}
}
}
class OptionalRangeJsonMapper extends SimpleMapper<OptionalRange> {
const OptionalRangeJsonMapper();
@override
OptionalRange decode(dynamic value) =>
OptionalRange._fromString(value as String);
@override
dynamic encode(OptionalRange self) => self.format();
}

View File

@@ -1,57 +0,0 @@
import 'package:dartx/dartx.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/localization/translations.dart';
part 'range.freezed.dart';
@freezed
class RangeWithOptionalCeil with _$RangeWithOptionalCeil {
const RangeWithOptionalCeil._();
const factory RangeWithOptionalCeil({
int? min,
int? max,
}) = _RangeWithOptionalCeil;
String format() => [min, max].whereNotNull().join("-");
String present(TranslationsEn t) =>
format().isEmpty ? t.general.notSet : format();
factory RangeWithOptionalCeil._fromString(
String input, {
bool allowEmpty = true,
}) =>
switch (input.split("-")) {
[final String val] when val.isEmpty && allowEmpty =>
const RangeWithOptionalCeil(),
[final String min] => RangeWithOptionalCeil(min: int.parse(min)),
[final String min, final String max] => RangeWithOptionalCeil(
min: int.parse(min),
max: int.parse(max),
),
_ => throw Exception("Invalid range: $input"),
};
static RangeWithOptionalCeil? tryParse(
String input, {
bool allowEmpty = false,
}) {
try {
return RangeWithOptionalCeil._fromString(input);
} catch (_) {
return null;
}
}
}
class RangeWithOptionalCeilJsonConverter
implements JsonConverter<RangeWithOptionalCeil, String> {
const RangeWithOptionalCeilJsonConverter();
@override
RangeWithOptionalCeil fromJson(String json) =>
RangeWithOptionalCeil._fromString(json);
@override
String toJson(RangeWithOptionalCeil object) => object.format();
}

View File

@@ -1,11 +1,11 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
class IntervalInSecondsConverter implements JsonConverter<Duration, int> {
const IntervalInSecondsConverter();
class IntervalInSecondsMapper extends SimpleMapper<Duration> {
const IntervalInSecondsMapper();
@override
Duration fromJson(int json) => Duration(seconds: json);
Duration decode(dynamic value) => Duration(seconds: value as int);
@override
int toJson(Duration object) => object.inSeconds;
dynamic encode(Duration self) => self.inSeconds;
}

View File

@@ -3,10 +3,8 @@ 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/features/config_option/model/config_option_failure.dart';
import 'package:hiddify/features/config_option/model/config_option_patch.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/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/utils.dart';
@@ -36,7 +34,7 @@ class ConfigOptionRepositoryImpl
@override
Either<ConfigOptionFailure, ConfigOptionEntity> getConfigOption() {
try {
final map = ConfigOptionEntity.initial.toJson();
final map = ConfigOptionEntity.initial().toMap();
for (final key in map.keys) {
final persisted = preferences.get(key);
if (persisted != null) {
@@ -51,7 +49,7 @@ class ConfigOptionRepositoryImpl
map[key] = persisted;
}
}
final options = ConfigOptionEntity.fromJson(map);
final options = ConfigOptionEntityMapper.fromMap(map);
return right(options);
} catch (error, stackTrace) {
return left(ConfigOptionUnexpectedFailure(error, stackTrace));
@@ -64,7 +62,7 @@ class ConfigOptionRepositoryImpl
) {
return exceptionHandler(
() async {
final map = patch.toJson();
final map = patch.toMap();
await updateByJson(map);
return right(unit);
},
@@ -76,7 +74,7 @@ class ConfigOptionRepositoryImpl
TaskEither<ConfigOptionFailure, Unit> resetConfigOption() {
return exceptionHandler(
() async {
final map = ConfigOptionEntity.initial.toJson();
final map = ConfigOptionEntity.initial().toMap();
await updateByJson(map);
return right(unit);
},
@@ -88,7 +86,7 @@ class ConfigOptionRepositoryImpl
Future<void> updateByJson(
Map<String, dynamic> options,
) async {
final map = ConfigOptionEntity.initial.toJson();
final map = ConfigOptionEntity.initial().toMap();
for (final key in map.keys) {
final value = options[key];
if (value != null) {
@@ -172,48 +170,7 @@ class SingBoxConfigOptionRepositoryImpl
final persisted =
optionsRepository.getConfigOption().getOrElse((l) => throw l);
final singboxConfigOption = SingboxConfigOption(
executeConfigAsIs: false,
logLevel: persisted.logLevel,
resolveDestination: persisted.resolveDestination,
ipv6Mode: persisted.ipv6Mode,
remoteDnsAddress: persisted.remoteDnsAddress,
remoteDnsDomainStrategy: persisted.remoteDnsDomainStrategy,
directDnsAddress: persisted.directDnsAddress,
directDnsDomainStrategy: persisted.directDnsDomainStrategy,
mixedPort: persisted.mixedPort,
localDnsPort: persisted.localDnsPort,
tunImplementation: persisted.tunImplementation,
mtu: persisted.mtu,
strictRoute: persisted.strictRoute,
connectionTestUrl: persisted.connectionTestUrl,
urlTestInterval: persisted.urlTestInterval,
enableClashApi: persisted.enableClashApi,
clashApiPort: persisted.clashApiPort,
enableTun: persisted.serviceMode == ServiceMode.tun,
enableTunService: persisted.serviceMode == ServiceMode.tunService,
setSystemProxy: persisted.serviceMode == ServiceMode.systemProxy,
bypassLan: persisted.bypassLan,
allowConnectionFromLan: persisted.allowConnectionFromLan,
enableFakeDns: persisted.enableFakeDns,
enableDnsRouting: persisted.enableDnsRouting,
independentDnsCache: persisted.independentDnsCache,
enableTlsFragment: persisted.enableTlsFragment,
tlsFragmentSize: persisted.tlsFragmentSize,
tlsFragmentSleep: persisted.tlsFragmentSleep,
enableTlsMixedSniCase: persisted.enableTlsMixedSniCase,
enableTlsPadding: persisted.enableTlsPadding,
tlsPaddingSize: persisted.tlsPaddingSize,
enableMux: persisted.enableMux,
muxPadding: persisted.muxPadding,
muxMaxStreams: persisted.muxMaxStreams,
muxProtocol: persisted.muxProtocol,
enableWarp: persisted.enableWarp,
warpDetourMode: persisted.warpDetourMode,
warpLicenseKey: persisted.warpLicenseKey,
warpCleanIp: persisted.warpCleanIp,
warpPort: persisted.warpPort,
warpNoise: persisted.warpNoise,
final singboxConfigOption = persisted.toSingbox(
geoipPath: geoAssetPathResolver.relativePath(
geoAssets.geoip.providerName,
geoAssets.geoip.fileName,

View File

@@ -1,75 +1,107 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/model/range.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:hiddify/core/model/optional_range.dart';
import 'package:hiddify/core/utils/json_converters.dart';
import 'package:hiddify/features/config_option/model/config_option_patch.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';
part 'config_option_entity.mapper.dart';
@freezed
class ConfigOptionEntity with _$ConfigOptionEntity {
const ConfigOptionEntity._();
@MappableClass(
caseStyle: CaseStyle.paramCase,
includeCustomMappers: [
OptionalRangeJsonMapper(),
IntervalInSecondsMapper(),
],
)
class ConfigOptionEntity with ConfigOptionEntityMappable {
const ConfigOptionEntity({
required this.serviceMode,
this.logLevel = LogLevel.warn,
this.resolveDestination = false,
this.ipv6Mode = IPv6Mode.disable,
this.remoteDnsAddress = "http://1.1.1.1",
this.remoteDnsDomainStrategy = DomainStrategy.auto,
this.directDnsAddress = "1.1.1.1",
this.directDnsDomainStrategy = DomainStrategy.auto,
this.mixedPort = 2334,
this.localDnsPort = 6450,
this.tunImplementation = TunImplementation.mixed,
this.mtu = 9000,
this.strictRoute = true,
this.connectionTestUrl = "http://cp.cloudflare.com/",
this.urlTestInterval = const Duration(minutes: 10),
this.enableClashApi = true,
this.clashApiPort = 6756,
this.bypassLan = false,
this.allowConnectionFromLan = false,
this.enableFakeDns = false,
this.enableDnsRouting = true,
this.independentDnsCache = true,
this.enableTlsFragment = false,
this.tlsFragmentSize = const OptionalRange(min: 10, max: 100),
this.tlsFragmentSleep = const OptionalRange(min: 50, max: 200),
this.enableTlsMixedSniCase = false,
this.enableTlsPadding = false,
this.tlsPaddingSize = const OptionalRange(min: 100, max: 200),
this.enableMux = false,
this.muxPadding = false,
this.muxMaxStreams = 8,
this.muxProtocol = MuxProtocol.h2mux,
this.enableWarp = false,
this.warpDetourMode = WarpDetourMode.outbound,
this.warpLicenseKey = "",
this.warpCleanIp = "auto",
this.warpPort = 0,
this.warpNoise = const OptionalRange(),
});
@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,
@RangeWithOptionalCeilJsonConverter()
@Default(RangeWithOptionalCeil(min: 10, max: 100))
RangeWithOptionalCeil tlsFragmentSize,
@RangeWithOptionalCeilJsonConverter()
@Default(RangeWithOptionalCeil(min: 50, max: 200))
RangeWithOptionalCeil tlsFragmentSleep,
@Default(false) bool enableTlsMixedSniCase,
@Default(false) bool enableTlsPadding,
@RangeWithOptionalCeilJsonConverter()
@Default(RangeWithOptionalCeil(min: 100, max: 200))
RangeWithOptionalCeil 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("auto") String warpCleanIp,
@Default(0) int warpPort,
@RangeWithOptionalCeilJsonConverter()
@Default(RangeWithOptionalCeil())
RangeWithOptionalCeil warpNoise,
}) = _ConfigOptionEntity;
final ServiceMode serviceMode;
final LogLevel logLevel;
final bool resolveDestination;
@MappableField(key: "ipv6-mode")
final IPv6Mode ipv6Mode;
final String remoteDnsAddress;
final DomainStrategy remoteDnsDomainStrategy;
final String directDnsAddress;
final DomainStrategy directDnsDomainStrategy;
final int mixedPort;
final int localDnsPort;
final TunImplementation tunImplementation;
final int mtu;
final bool strictRoute;
final String connectionTestUrl;
final Duration urlTestInterval;
final bool enableClashApi;
final int clashApiPort;
final bool bypassLan;
final bool allowConnectionFromLan;
final bool enableFakeDns;
final bool enableDnsRouting;
final bool independentDnsCache;
final bool enableTlsFragment;
final OptionalRange tlsFragmentSize;
final OptionalRange tlsFragmentSleep;
final bool enableTlsMixedSniCase;
final bool enableTlsPadding;
final OptionalRange tlsPaddingSize;
final bool enableMux;
final bool muxPadding;
final int muxMaxStreams;
final MuxProtocol muxProtocol;
final bool enableWarp;
final WarpDetourMode warpDetourMode;
final String warpLicenseKey;
final String warpCleanIp;
final int warpPort;
final OptionalRange warpNoise;
static ConfigOptionEntity initial = ConfigOptionEntity(
serviceMode: ServiceMode.defaultMode,
);
factory ConfigOptionEntity.initial() =>
ConfigOptionEntity(serviceMode: ServiceMode.defaultMode);
bool hasExperimentalOptions() {
if (PlatformUtils.isDesktop && serviceMode == ServiceMode.tun) {
@@ -88,56 +120,157 @@ class ConfigOptionEntity with _$ConfigOptionEntity {
String format() {
const encoder = JsonEncoder.withIndent(' ');
return encoder.convert(toJson());
return encoder.convert(toMap());
}
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,
warpCleanIp: patch.warpCleanIp ?? warpCleanIp,
warpPort: patch.warpPort ?? warpPort,
warpNoise: patch.warpNoise ?? warpNoise,
);
return copyWith.$delta(patch.delta());
}
factory ConfigOptionEntity.fromJson(Map<String, dynamic> json) =>
_$ConfigOptionEntityFromJson(json);
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,
enableWarp: enableWarp,
warpDetourMode: warpDetourMode,
warpLicenseKey: warpLicenseKey,
warpCleanIp: warpCleanIp,
warpPort: warpPort,
warpNoise: warpNoise,
geoipPath: geoipPath,
geositePath: geositePath,
rules: rules,
);
}
}
@MappableClass(
caseStyle: CaseStyle.paramCase,
ignoreNull: true,
includeCustomMappers: [
OptionalRangeJsonMapper(),
IntervalInSecondsMapper(),
],
)
class ConfigOptionPatch with ConfigOptionPatchMappable {
const ConfigOptionPatch({
this.serviceMode,
this.logLevel,
this.resolveDestination,
this.ipv6Mode,
this.remoteDnsAddress,
this.remoteDnsDomainStrategy,
this.directDnsAddress,
this.directDnsDomainStrategy,
this.mixedPort,
this.localDnsPort,
this.tunImplementation,
this.mtu,
this.strictRoute,
this.connectionTestUrl,
this.urlTestInterval,
this.enableClashApi,
this.clashApiPort,
this.bypassLan,
this.allowConnectionFromLan,
this.enableFakeDns,
this.enableDnsRouting,
this.independentDnsCache,
this.enableTlsFragment,
this.tlsFragmentSize,
this.tlsFragmentSleep,
this.enableTlsMixedSniCase,
this.enableTlsPadding,
this.tlsPaddingSize,
this.enableMux,
this.muxPadding,
this.muxMaxStreams,
this.muxProtocol,
this.enableWarp,
this.warpDetourMode,
this.warpLicenseKey,
this.warpCleanIp,
this.warpPort,
this.warpNoise,
});
final ServiceMode? serviceMode;
final LogLevel? logLevel;
final bool? resolveDestination;
@MappableField(key: "ipv6-mode")
final IPv6Mode? ipv6Mode;
final String? remoteDnsAddress;
final DomainStrategy? remoteDnsDomainStrategy;
final String? directDnsAddress;
final DomainStrategy? directDnsDomainStrategy;
final int? mixedPort;
final int? localDnsPort;
final TunImplementation? tunImplementation;
final int? mtu;
final bool? strictRoute;
final String? connectionTestUrl;
final Duration? urlTestInterval;
final bool? enableClashApi;
final int? clashApiPort;
final bool? bypassLan;
final bool? allowConnectionFromLan;
final bool? enableFakeDns;
final bool? enableDnsRouting;
final bool? independentDnsCache;
final bool? enableTlsFragment;
final OptionalRange? tlsFragmentSize;
final OptionalRange? tlsFragmentSleep;
final bool? enableTlsMixedSniCase;
final bool? enableTlsPadding;
final OptionalRange? tlsPaddingSize;
final bool? enableMux;
final bool? muxPadding;
final int? muxMaxStreams;
final MuxProtocol? muxProtocol;
final bool? enableWarp;
final WarpDetourMode? warpDetourMode;
final String? warpLicenseKey;
final String? warpCleanIp;
final int? warpPort;
final OptionalRange? warpNoise;
Map<String, dynamic> delta() =>
toMap()..removeWhere((key, value) => value == null);
}

View File

@@ -1,60 +0,0 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/model/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';
part 'config_option_patch.freezed.dart';
part 'config_option_patch.g.dart';
@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,
@RangeWithOptionalCeilJsonConverter()
RangeWithOptionalCeil? tlsFragmentSize,
@RangeWithOptionalCeilJsonConverter()
RangeWithOptionalCeil? tlsFragmentSleep,
bool? enableTlsMixedSniCase,
bool? enableTlsPadding,
@RangeWithOptionalCeilJsonConverter() RangeWithOptionalCeil? tlsPaddingSize,
bool? enableMux,
bool? muxPadding,
int? muxMaxStreams,
MuxProtocol? muxProtocol,
bool? enableWarp,
WarpDetourMode? warpDetourMode,
String? warpLicenseKey,
String? warpCleanIp,
int? warpPort,
@RangeWithOptionalCeilJsonConverter() RangeWithOptionalCeil? warpNoise,
}) = _ConfigOptionPatch;
factory ConfigOptionPatch.fromJson(Map<String, dynamic> json) =>
_$ConfigOptionPatchFromJson(json);
}

View File

@@ -1,6 +1,5 @@
import 'package:hiddify/features/config_option/data/config_option_data_providers.dart';
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
import 'package:hiddify/features/config_option/model/config_option_patch.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';

View File

@@ -5,12 +5,11 @@ 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/range.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/model/config_option_patch.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/log/model/log_level.dart';
@@ -28,7 +27,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider);
final defaultOptions = ConfigOptionEntity.initial;
final defaultOptions = ConfigOptionEntity.initial();
final asyncOptions = ref.watch(configOptionNotifierProvider);
Future<void> changeOption(ConfigOptionPatch patch) async {
@@ -349,8 +348,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
if (range == null) return;
await changeOption(
ConfigOptionPatch(
tlsFragmentSize:
RangeWithOptionalCeil.tryParse(range),
tlsFragmentSize: OptionalRange.tryParse(range),
),
);
},
@@ -367,8 +365,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
if (range == null) return;
await changeOption(
ConfigOptionPatch(
tlsFragmentSleep:
RangeWithOptionalCeil.tryParse(range),
tlsFragmentSleep: OptionalRange.tryParse(range),
),
);
},
@@ -402,7 +399,7 @@ class ConfigOptionsPage extends HookConsumerWidget {
if (range == null) return;
await changeOption(
ConfigOptionPatch(
tlsPaddingSize: RangeWithOptionalCeil.tryParse(range),
tlsPaddingSize: OptionalRange.tryParse(range),
),
);
},

View File

@@ -3,9 +3,8 @@ 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/range.dart';
import 'package:hiddify/core/model/optional_range.dart';
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
import 'package:hiddify/features/config_option/model/config_option_patch.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/singbox/model/singbox_config_enum.dart';
@@ -134,10 +133,7 @@ class WarpOptionsTiles extends HookConsumerWidget {
if (warpNoise == null) return;
await onChange(
ConfigOptionPatch(
warpNoise: RangeWithOptionalCeil.tryParse(
warpNoise,
allowEmpty: true,
),
warpNoise: OptionalRange.tryParse(warpNoise, allowEmpty: true),
),
);
},

View File

@@ -1,12 +1,10 @@
import 'dart:async';
import 'dart:convert';
import 'dart:ffi';
import 'dart:io';
import 'package:hiddify/core/utils/ffi_utils.dart';
import 'package:hiddify/utils/custom_loggers.dart';
import 'package:hiddify/utils/utils.dart';
import 'package:path/path.dart' as p;
import 'package:posix/posix.dart';
import 'package:win32/win32.dart';

View File

@@ -1,6 +1,10 @@
import 'package:dart_mappable/dart_mappable.dart';
import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart';
part 'log_level.mapper.dart';
@MappableEnum()
enum LogLevel {
trace,
debug,

View File

@@ -1,7 +1,6 @@
import 'package:dartx/dartx.dart';
import 'package:fluentui_system_icons/fluentui_system_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter/semantics.dart';
import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/widget/animated_visibility.dart';

View File

@@ -8,7 +8,6 @@ import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
import 'package:hiddify/features/proxy/data/proxy_data_providers.dart';
import 'package:hiddify/features/proxy/model/proxy_entity.dart';
import 'package:hiddify/features/proxy/model/proxy_failure.dart';
import 'package:hiddify/singbox/model/singbox_proxy_type.dart';
import 'package:hiddify/utils/pref_notifier.dart';
import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:hiddify/utils/utils.dart';
@@ -94,14 +93,14 @@ class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger {
for (final group in proxies) {
final sortedItems = switch (sortBy) {
ProxiesSort.name => group.items.sortedWith((a, b) {
if(a.type.isGroup && !b.type.isGroup) return -1;
if(!a.type.isGroup && b.type.isGroup) return 1;
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
return a.tag.compareTo(b.tag);
}),
}),
ProxiesSort.delay => group.items.sortedWith((a, b) {
if(a.type.isGroup && !b.type.isGroup) return -1;
if(!a.type.isGroup && b.type.isGroup) return 1;
if (a.type.isGroup && !b.type.isGroup) return -1;
if (!a.type.isGroup && b.type.isGroup) return 1;
final ai = a.urlTestDelay;
final bi = b.urlTestDelay;
if (ai == 0 && bi == 0) return -1;

View File

@@ -3,7 +3,7 @@ import 'dart:io';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/constants.dart';
import 'package:hiddify/core/router/router.dart';
import 'package:hiddify/features/config_option/model/config_option_patch.dart';
import 'package:hiddify/features/config_option/model/config_option_entity.dart';
import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart';
import 'package:hiddify/features/connection/model/connection_status.dart';
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';

View File

@@ -1,19 +1,24 @@
import 'dart:io';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/utils/platform_utils.dart';
import 'package:json_annotation/json_annotation.dart';
@JsonEnum(valueField: 'key')
part 'singbox_config_enum.mapper.dart';
@MappableEnum()
enum ServiceMode {
proxy("proxy"),
systemProxy("system-proxy"),
tun("vpn"),
tunService("vpn-service");
@MappableValue("proxy")
proxy,
const ServiceMode(this.key);
@MappableValue("system-proxy")
systemProxy,
final String key;
@MappableValue("vpn")
tun,
@MappableValue("vpn-service")
tunService;
static ServiceMode get defaultMode =>
PlatformUtils.isDesktop ? systemProxy : tun;
@@ -39,16 +44,19 @@ enum ServiceMode {
};
}
@JsonEnum(valueField: 'key')
@MappableEnum()
enum IPv6Mode {
disable("ipv4_only"),
enable("prefer_ipv4"),
prefer("prefer_ipv6"),
only("ipv6_only");
@MappableValue("ipv4_only")
disable,
const IPv6Mode(this.key);
@MappableValue("prefer_ipv4")
enable,
final String key;
@MappableValue("prefer_ipv6")
prefer,
@MappableValue("ipv6_only")
only;
String present(TranslationsEn t) => switch (this) {
disable => t.settings.config.ipv6Modes.disable,
@@ -58,12 +66,21 @@ enum IPv6Mode {
};
}
@JsonEnum(valueField: 'key')
@MappableEnum()
enum DomainStrategy {
@MappableValue("")
auto(""),
@MappableValue("prefer_ipv6")
preferIpv6("prefer_ipv6"),
@MappableValue("prefer_ipv4")
preferIpv4("prefer_ipv4"),
@MappableValue("ipv4_only")
ipv4Only("ipv4_only"),
@MappableValue("ipv6_only")
ipv6Only("ipv6_only");
const DomainStrategy(this.key);
@@ -76,18 +93,21 @@ enum DomainStrategy {
};
}
@MappableEnum()
enum TunImplementation {
mixed,
system,
gVisor;
}
@MappableEnum()
enum MuxProtocol {
h2mux,
smux,
yamux;
}
@MappableEnum()
enum WarpDetourMode {
outbound,
inbound;

View File

@@ -1,86 +1,127 @@
import 'dart:convert';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/model/range.dart';
import 'package:dart_mappable/dart_mappable.dart';
import 'package:hiddify/core/model/optional_range.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_rule.dart';
part 'singbox_config_option.freezed.dart';
part 'singbox_config_option.g.dart';
part 'singbox_config_option.mapper.dart';
@freezed
class SingboxConfigOption with _$SingboxConfigOption {
const SingboxConfigOption._();
@MappableClass(
caseStyle: CaseStyle.paramCase,
includeCustomMappers: [
OptionalRangeJsonMapper(),
IntervalMapper(),
],
)
class SingboxConfigOption with SingboxConfigOptionMappable {
const SingboxConfigOption({
required this.executeConfigAsIs,
required this.logLevel,
required this.resolveDestination,
required this.ipv6Mode,
required this.remoteDnsAddress,
required this.remoteDnsDomainStrategy,
required this.directDnsAddress,
required this.directDnsDomainStrategy,
required this.mixedPort,
required this.localDnsPort,
required this.tunImplementation,
required this.mtu,
required this.strictRoute,
required this.connectionTestUrl,
required this.urlTestInterval,
required this.enableClashApi,
required this.clashApiPort,
required this.enableTun,
required this.enableTunService,
required this.setSystemProxy,
required this.bypassLan,
required this.allowConnectionFromLan,
required this.enableFakeDns,
required this.enableDnsRouting,
required this.independentDnsCache,
required this.enableTlsFragment,
required this.tlsFragmentSize,
required this.tlsFragmentSleep,
required this.enableTlsMixedSniCase,
required this.enableTlsPadding,
required this.tlsPaddingSize,
required this.enableMux,
required this.muxPadding,
required this.muxMaxStreams,
required this.muxProtocol,
required this.enableWarp,
required this.warpDetourMode,
required this.warpLicenseKey,
required this.warpCleanIp,
required this.warpPort,
required this.warpNoise,
required this.geoipPath,
required this.geositePath,
required this.rules,
});
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxConfigOption({
required bool executeConfigAsIs,
required LogLevel logLevel,
required bool resolveDestination,
required IPv6Mode ipv6Mode,
required String remoteDnsAddress,
required DomainStrategy remoteDnsDomainStrategy,
required String directDnsAddress,
required DomainStrategy directDnsDomainStrategy,
required int mixedPort,
required int localDnsPort,
required TunImplementation tunImplementation,
required int mtu,
required bool strictRoute,
required String connectionTestUrl,
@IntervalConverter() required Duration urlTestInterval,
required bool enableClashApi,
required int clashApiPort,
required bool enableTun,
required bool enableTunService,
required bool setSystemProxy,
required bool bypassLan,
required bool allowConnectionFromLan,
required bool enableFakeDns,
required bool enableDnsRouting,
required bool independentDnsCache,
required bool enableTlsFragment,
@RangeWithOptionalCeilJsonConverter()
required RangeWithOptionalCeil tlsFragmentSize,
@RangeWithOptionalCeilJsonConverter()
required RangeWithOptionalCeil tlsFragmentSleep,
required bool enableTlsMixedSniCase,
required bool enableTlsPadding,
@RangeWithOptionalCeilJsonConverter()
required RangeWithOptionalCeil tlsPaddingSize,
required bool enableMux,
required bool muxPadding,
required int muxMaxStreams,
required MuxProtocol muxProtocol,
required bool enableWarp,
required WarpDetourMode warpDetourMode,
required String warpLicenseKey,
required String warpCleanIp,
required int warpPort,
@RangeWithOptionalCeilJsonConverter()
required RangeWithOptionalCeil warpNoise,
required String geoipPath,
required String geositePath,
required List<SingboxRule> rules,
}) = _SingboxConfigOption;
final bool executeConfigAsIs;
final LogLevel logLevel;
final bool resolveDestination;
@MappableField(key: "ipv6-mode")
final IPv6Mode ipv6Mode;
final String remoteDnsAddress;
final DomainStrategy remoteDnsDomainStrategy;
final String directDnsAddress;
final DomainStrategy directDnsDomainStrategy;
final int mixedPort;
final int localDnsPort;
final TunImplementation tunImplementation;
final int mtu;
final bool strictRoute;
final String connectionTestUrl;
final Duration urlTestInterval;
final bool enableClashApi;
final int clashApiPort;
final bool enableTun;
final bool enableTunService;
final bool setSystemProxy;
final bool bypassLan;
final bool allowConnectionFromLan;
final bool enableFakeDns;
final bool enableDnsRouting;
final bool independentDnsCache;
final bool enableTlsFragment;
final OptionalRange tlsFragmentSize;
final OptionalRange tlsFragmentSleep;
final bool enableTlsMixedSniCase;
final bool enableTlsPadding;
final OptionalRange tlsPaddingSize;
final bool enableMux;
final bool muxPadding;
final int muxMaxStreams;
final MuxProtocol muxProtocol;
final bool enableWarp;
final WarpDetourMode warpDetourMode;
final String warpLicenseKey;
final String warpCleanIp;
final int warpPort;
final OptionalRange warpNoise;
final String geoipPath;
final String geositePath;
final List<SingboxRule> rules;
String format() {
const encoder = JsonEncoder.withIndent(' ');
return encoder.convert(toJson());
return encoder.convert(toMap());
}
factory SingboxConfigOption.fromJson(Map<String, dynamic> json) =>
_$SingboxConfigOptionFromJson(json);
}
class IntervalConverter implements JsonConverter<Duration, String> {
const IntervalConverter();
class IntervalMapper extends SimpleMapper<Duration> {
const IntervalMapper();
@override
Duration fromJson(String json) =>
Duration(minutes: int.parse(json.replaceAll("m", "")));
Duration decode(dynamic value) =>
Duration(minutes: int.parse((value as String).replaceAll("m", "")));
@override
String toJson(Duration object) => "${object.inMinutes}m";
String encode(Duration self) => "${self.inMinutes}m";
}

View File

@@ -1,35 +1,37 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:dart_mappable/dart_mappable.dart';
part 'singbox_rule.freezed.dart';
part 'singbox_rule.g.dart';
part 'singbox_rule.mapper.dart';
@freezed
class SingboxRule with _$SingboxRule {
const SingboxRule._();
@MappableClass()
class SingboxRule with SingboxRuleMappable {
const SingboxRule({
this.domains,
this.ip,
this.port,
this.protocol,
this.network = RuleNetwork.tcpAndUdp,
this.outbound = RuleOutbound.proxy,
});
@JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxRule({
String? domains,
String? ip,
String? port,
String? protocol,
@Default(RuleNetwork.tcpAndUdp) RuleNetwork network,
@Default(RuleOutbound.proxy) RuleOutbound outbound,
}) = _SingboxRule;
factory SingboxRule.fromJson(Map<String, dynamic> json) =>
_$SingboxRuleFromJson(json);
final String? domains;
final String? ip;
final String? port;
final String? protocol;
final RuleNetwork network;
final RuleOutbound outbound;
}
@MappableEnum()
enum RuleOutbound { proxy, bypass, block }
@JsonEnum(valueField: 'key')
@MappableEnum()
enum RuleNetwork {
tcpAndUdp(""),
tcp("tcp"),
udp("udp");
@MappableValue("")
tcpAndUdp,
const RuleNetwork(this.key);
@MappableValue("tcp")
tcp,
final String? key;
@MappableValue("udp")
udp;
}

View File

@@ -121,7 +121,7 @@ class FFISingboxService with InfraLogger implements SingboxService {
return TaskEither(
() => CombineWorker().execute(
() {
final json = jsonEncode(options.toJson());
final json = options.toJson();
final err = _box
.changeConfigOptions(json.toNativeUtf8().cast())
.cast<Utf8>()

View File

@@ -80,7 +80,7 @@ class PlatformSingboxService with InfraLogger implements SingboxService {
loggy.debug("changing options");
await methodChannel.invokeMethod(
"change_config_options",
jsonEncode(options.toJson()),
options.toJson(),
);
return right(unit);
},

View File

@@ -1,7 +1,6 @@
import 'dart:convert';
import 'package:dartx/dartx.dart';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/features/profile/data/profile_parser.dart';
import 'package:hiddify/features/profile/data/profile_repository.dart';
import 'package:hiddify/singbox/model/singbox_proxy_type.dart';
@@ -71,7 +70,7 @@ abstract class LinkParser {
if (subinfo.name.isNotNullOrEmpty && subinfo.name != "Remote Profile") {
name = subinfo.name;
}
return (content: normalContent, name: name ?? ProxyType.unknown.label);
}