This commit is contained in:
problematicconsumer
2023-07-06 17:18:41 +03:30
commit b617c95f62
352 changed files with 21017 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
export 'clash_config.dart';
export 'clash_enums.dart';
export 'clash_facade.dart';
export 'clash_failures.dart';
export 'clash_log.dart';
export 'clash_proxy.dart';
export 'clash_traffic.dart';

View File

@@ -0,0 +1,72 @@
import 'package:fpdart/fpdart.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/clash/clash_enums.dart';
part 'clash_config.freezed.dart';
part 'clash_config.g.dart';
@freezed
class ClashConfig with _$ClashConfig {
const ClashConfig._();
@JsonSerializable(includeIfNull: false, fieldRename: FieldRename.kebab)
const factory ClashConfig({
@JsonKey(name: 'port') int? httpPort,
int? socksPort,
int? redirPort,
int? tproxyPort,
int? mixedPort,
List<String>? authentication,
bool? allowLan,
String? bindAddress,
TunnelMode? mode,
LogLevel? logLevel,
bool? ipv6,
}) = _ClashConfig;
static const initial = ClashConfig(
httpPort: 12346,
socksPort: 12347,
mixedPort: 12348,
);
ClashConfig patch(ClashConfigPatch patch) {
return copyWith(
httpPort: (patch.httpPort ?? optionOf(httpPort)).toNullable(),
socksPort: (patch.socksPort ?? optionOf(socksPort)).toNullable(),
redirPort: (patch.redirPort ?? optionOf(redirPort)).toNullable(),
tproxyPort: (patch.tproxyPort ?? optionOf(tproxyPort)).toNullable(),
mixedPort: (patch.mixedPort ?? optionOf(mixedPort)).toNullable(),
authentication:
(patch.authentication ?? optionOf(authentication)).toNullable(),
allowLan: (patch.allowLan ?? optionOf(allowLan)).toNullable(),
bindAddress: (patch.bindAddress ?? optionOf(bindAddress)).toNullable(),
mode: (patch.mode ?? optionOf(mode)).toNullable(),
logLevel: (patch.logLevel ?? optionOf(logLevel)).toNullable(),
ipv6: (patch.ipv6 ?? optionOf(ipv6)).toNullable(),
);
}
factory ClashConfig.fromJson(Map<String, dynamic> json) =>
_$ClashConfigFromJson(json);
}
@freezed
class ClashConfigPatch with _$ClashConfigPatch {
const ClashConfigPatch._();
@JsonSerializable(includeIfNull: false)
const factory ClashConfigPatch({
Option<int>? httpPort,
Option<int>? socksPort,
Option<int>? redirPort,
Option<int>? tproxyPort,
Option<int>? mixedPort,
Option<List<String>>? authentication,
Option<bool>? allowLan,
Option<String>? bindAddress,
Option<TunnelMode>? mode,
Option<LogLevel>? logLevel,
Option<bool>? ipv6,
}) = _ClashConfigPatch;
}

View File

@@ -0,0 +1,61 @@
import 'package:flutter/material.dart';
enum TunnelMode {
rule,
global,
direct;
}
enum LogLevel {
info,
warning,
error,
debug,
silent;
Color get color => switch (this) {
info => Colors.lightGreen,
warning => Colors.orangeAccent,
error => Colors.redAccent,
debug => Colors.lightBlue,
_ => Colors.white,
};
}
enum ProxyType {
direct("Direct"),
reject("Reject"),
compatible("Compatible"),
pass("Pass"),
shadowSocks("ShadowSocks"),
shadowSocksR("ShadowSocksR"),
snell("Snell"),
socks5("Socks5"),
http("Http"),
vmess("Vmess"),
vless("Vless"),
trojan("Trojan"),
hysteria("Hysteria"),
wireGuard("WireGuard"),
tuic("Tuic"),
relay("Relay"),
selector("Selector"),
fallback("Fallback"),
urlTest("URLTest", "urltest"),
loadBalance("LoadBalance"),
unknown("Unknown");
const ProxyType(this.label, [this._key]);
final String? _key;
final String label;
String get key => _key ?? name;
static List<ProxyType> groupValues = [
selector,
fallback,
urlTest,
loadBalance,
];
}

View File

@@ -0,0 +1,32 @@
import 'dart:async';
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/clash/clash.dart';
import 'package:hiddify/domain/constants.dart';
abstract class ClashFacade {
TaskEither<ClashFailure, ClashConfig> getConfigs();
TaskEither<ClashFailure, bool> validateConfig(String configFileName);
/// change active configuration file by [configFileName]
TaskEither<ClashFailure, Unit> changeConfigs(String configFileName);
TaskEither<ClashFailure, Unit> patchOverrides(ClashConfig overrides);
TaskEither<ClashFailure, List<ClashProxy>> getProxies();
TaskEither<ClashFailure, Unit> changeProxy(
String selectorName,
String proxyName,
);
TaskEither<ClashFailure, int> testDelay(
String proxyName, {
String testUrl = Constants.delayTestUrl,
});
TaskEither<ClashFailure, ClashTraffic> getTraffic();
Stream<Either<ClashFailure, ClashLog>> watchLogs();
}

View File

@@ -0,0 +1,27 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/failures.dart';
part 'clash_failures.freezed.dart';
// TODO: rewrite
@freezed
sealed class ClashFailure with _$ClashFailure, Failure {
const ClashFailure._();
const factory ClashFailure.unexpected(
Object error,
StackTrace stackTrace,
) = ClashUnexpectedFailure;
const factory ClashFailure.core([String? error]) = ClashCoreFailure;
@override
String present(TranslationsEn t) {
return switch (this) {
ClashUnexpectedFailure() => t.failure.clash.unexpected,
ClashCoreFailure(:final error) =>
t.failure.clash.core(reason: error ?? ""),
};
}
}

View File

@@ -0,0 +1,22 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/clash/clash_enums.dart';
part 'clash_log.freezed.dart';
part 'clash_log.g.dart';
@freezed
class ClashLog with _$ClashLog {
const ClashLog._();
const factory ClashLog({
@JsonKey(name: 'type') required LogLevel level,
@JsonKey(name: 'payload') required String message,
@JsonKey(defaultValue: DateTime.now) required DateTime time,
}) = _ClashLog;
String get timeStamp =>
"${time.month}-${time.day} ${time.hour}:${time.minute}:${time.second}";
factory ClashLog.fromJson(Map<String, dynamic> json) =>
_$ClashLogFromJson(json);
}

View File

@@ -0,0 +1,59 @@
import 'package:dartx/dartx.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/clash/clash_enums.dart';
part 'clash_proxy.freezed.dart';
part 'clash_proxy.g.dart';
// TODO: test and improve
@Freezed(fromJson: true)
class ClashProxy with _$ClashProxy {
const ClashProxy._();
const factory ClashProxy.group({
required String name,
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
required List<String> all,
required String now,
List<ClashHistory>? history,
@JsonKey(includeFromJson: false, includeToJson: false) int? delay,
}) = ClashProxyGroup;
const factory ClashProxy.item({
required String name,
@JsonKey(fromJson: _typeFromJson) required ProxyType type,
List<ClashHistory>? history,
@JsonKey(includeFromJson: false, includeToJson: false) int? delay,
}) = ClashProxyItem;
factory ClashProxy.fromJson(Map<String, dynamic> json) {
final isGroup = json.containsKey('all') ||
json.containsKey('now') ||
ProxyType.groupValues.any(
(e) => e.label == json.getOrElse('type', () => null),
);
if (isGroup) {
return ClashProxyGroup.fromJson(json);
} else {
return ClashProxyItem.fromJson(json);
}
}
}
ProxyType _typeFromJson(dynamic type) =>
ProxyType.values
.firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ??
ProxyType.unknown;
@freezed
class ClashHistory with _$ClashHistory {
const ClashHistory._();
const factory ClashHistory({
required String time,
required int delay,
}) = _ClashHistory;
factory ClashHistory.fromJson(Map<String, dynamic> json) =>
_$ClashHistoryFromJson(json);
}

View File

@@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'clash_traffic.freezed.dart';
part 'clash_traffic.g.dart';
@freezed
class ClashTraffic with _$ClashTraffic {
const ClashTraffic._();
const factory ClashTraffic({
@JsonKey(name: 'up') required int upload,
@JsonKey(name: 'down') required int download,
}) = _ClashTraffic;
factory ClashTraffic.fromJson(Map<String, dynamic> json) =>
_$ClashTrafficFromJson(json);
}

View File

@@ -0,0 +1,43 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/connectivity/connectivity_failure.dart';
part 'connection_status.freezed.dart';
@freezed
sealed class ConnectionStatus with _$ConnectionStatus {
const ConnectionStatus._();
const factory ConnectionStatus.disconnected([
ConnectivityFailure? connectFailure,
]) = Disconnected;
const factory ConnectionStatus.connecting() = Connecting;
const factory ConnectionStatus.connected([
ConnectivityFailure? disconnectFailure,
]) = Connected;
const factory ConnectionStatus.disconnecting() = Disconnecting;
factory ConnectionStatus.fromBool(bool connected) {
return connected
? const ConnectionStatus.connected()
: const Disconnected();
}
bool get isConnected => switch (this) {
Connected() => true,
_ => false,
};
bool get isSwitching => switch (this) {
Connecting() => true,
Disconnecting() => true,
_ => false,
};
String present(TranslationsEn t) => switch (this) {
Disconnected() => t.home.connection.tapToConnect,
Connecting() => t.home.connection.connecting,
Connected() => t.home.connection.connected,
Disconnecting() => t.home.connection.disconnecting,
};
}

View File

@@ -0,0 +1,4 @@
export 'connection_status.dart';
export 'connectivity_failure.dart';
export 'network_prefs.dart';
export 'traffic.dart';

View File

@@ -0,0 +1,21 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/failures.dart';
part 'connectivity_failure.freezed.dart';
// TODO: rewrite
@freezed
sealed class ConnectivityFailure with _$ConnectivityFailure, Failure {
const ConnectivityFailure._();
const factory ConnectivityFailure.unexpected([
Object? error,
StackTrace? stackTrace,
]) = ConnectivityUnexpectedFailure;
@override
String present(TranslationsEn t) {
return t.failure.connectivity.unexpected;
}
}

View File

@@ -0,0 +1,17 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'network_prefs.freezed.dart';
part 'network_prefs.g.dart';
@freezed
class NetworkPrefs with _$NetworkPrefs {
const NetworkPrefs._();
const factory NetworkPrefs({
@Default(true) bool systemProxy,
@Default(true) bool bypassPrivateNetworks,
}) = _NetworkPrefs;
factory NetworkPrefs.fromJson(Map<String, dynamic> json) =>
_$NetworkPrefsFromJson(json);
}

View File

@@ -0,0 +1,13 @@
import 'package:freezed_annotation/freezed_annotation.dart';
part 'traffic.freezed.dart';
@freezed
class Traffic with _$Traffic {
const Traffic._();
const factory Traffic({
required int upload,
required int download,
}) = _Traffic;
}

View File

@@ -0,0 +1,7 @@
abstract class Constants {
static const localHost = '127.0.0.1';
static const clashFolderName = "clash";
static const delayTestUrl = "https://www.google.com";
static const configFileName = "config";
static const countryMMDBFileName = "Country";
}

13
lib/domain/failures.dart Normal file
View File

@@ -0,0 +1,13 @@
import 'package:hiddify/core/locale/locale.dart';
// TODO: rewrite
mixin Failure {
String present(TranslationsEn t);
}
extension ErrorPresenter on TranslationsEn {
String presentError(Object error) {
if (error case Failure()) return error.present(this);
return failure.unexpected;
}
}

View File

@@ -0,0 +1,25 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
part 'profile.freezed.dart';
part 'profile.g.dart';
@freezed
class Profile with _$Profile {
const Profile._();
const factory Profile({
required String id,
required bool active,
required String name,
required String url,
SubscriptionInfo? subInfo,
Duration? updateInterval,
required DateTime lastUpdate,
}) = _Profile;
bool get hasSubscriptionInfo => subInfo?.isValid ?? false;
factory Profile.fromJson(Map<String, dynamic> json) =>
_$ProfileFromJson(json);
}

View File

@@ -0,0 +1,4 @@
export 'profile.dart';
export 'profiles_failure.dart';
export 'profiles_repository.dart';
export 'subscription_info.dart';

View File

@@ -0,0 +1,28 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:hiddify/core/locale/locale.dart';
import 'package:hiddify/domain/failures.dart';
part 'profiles_failure.freezed.dart';
@freezed
sealed class ProfileFailure with _$ProfileFailure, Failure {
const ProfileFailure._();
const factory ProfileFailure.unexpected([
Object? error,
StackTrace? stackTrace,
]) = ProfileUnexpectedFailure;
const factory ProfileFailure.notFound() = ProfileNotFoundFailure;
const factory ProfileFailure.invalidConfig() = ProfileInvalidConfigFailure;
@override
String present(TranslationsEn t) {
return switch (this) {
ProfileUnexpectedFailure() => t.failure.profiles.unexpected,
ProfileNotFoundFailure() => t.failure.profiles.notFound,
ProfileInvalidConfigFailure() => t.failure.profiles.invalidConfig,
};
}
}

View File

@@ -0,0 +1,20 @@
import 'package:fpdart/fpdart.dart';
import 'package:hiddify/domain/profiles/profiles.dart';
abstract class ProfilesRepository {
TaskEither<ProfileFailure, Profile?> get(String id);
Stream<Either<ProfileFailure, Profile?>> watchActiveProfile();
Stream<Either<ProfileFailure, bool>> watchHasAnyProfile();
Stream<Either<ProfileFailure, List<Profile>>> watchAll();
TaskEither<ProfileFailure, Unit> add(Profile baseProfile);
TaskEither<ProfileFailure, Unit> update(Profile baseProfile);
TaskEither<ProfileFailure, Unit> setAsActive(String id);
TaskEither<ProfileFailure, Unit> delete(String id);
}

View File

@@ -0,0 +1,44 @@
import 'package:dartx/dartx.dart';
import 'package:freezed_annotation/freezed_annotation.dart';
part 'subscription_info.freezed.dart';
part 'subscription_info.g.dart';
// TODO: test and improve
@freezed
class SubscriptionInfo with _$SubscriptionInfo {
const SubscriptionInfo._();
const factory SubscriptionInfo({
int? upload,
int? download,
int? total,
@JsonKey(fromJson: _dateTimeFromSecondsSinceEpoch) DateTime? expire,
}) = _SubscriptionInfo;
bool get isValid =>
total != null && download != null && upload != null && expire != null;
bool get isExpired => expire! <= DateTime.now();
int get consumption => upload! + download!;
double get ratio => consumption / total!;
Duration get remaining => expire!.difference(DateTime.now());
factory SubscriptionInfo.fromResponseHeader(String header) {
final values = header.split(';');
final map = {
for (final v in values)
v.split('=').first: int.tryParse(v.split('=').second)
};
return SubscriptionInfo.fromJson(map);
}
factory SubscriptionInfo.fromJson(Map<String, dynamic> json) =>
_$SubscriptionInfoFromJson(json);
}
DateTime? _dateTimeFromSecondsSinceEpoch(dynamic expire) =>
DateTime.fromMillisecondsSinceEpoch((expire as int) * 1000);