initial
This commit is contained in:
7
lib/domain/clash/clash.dart
Normal file
7
lib/domain/clash/clash.dart
Normal 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';
|
||||
72
lib/domain/clash/clash_config.dart
Normal file
72
lib/domain/clash/clash_config.dart
Normal 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;
|
||||
}
|
||||
61
lib/domain/clash/clash_enums.dart
Normal file
61
lib/domain/clash/clash_enums.dart
Normal 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,
|
||||
];
|
||||
}
|
||||
32
lib/domain/clash/clash_facade.dart
Normal file
32
lib/domain/clash/clash_facade.dart
Normal 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();
|
||||
}
|
||||
27
lib/domain/clash/clash_failures.dart
Normal file
27
lib/domain/clash/clash_failures.dart
Normal 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 ?? ""),
|
||||
};
|
||||
}
|
||||
}
|
||||
22
lib/domain/clash/clash_log.dart
Normal file
22
lib/domain/clash/clash_log.dart
Normal 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);
|
||||
}
|
||||
59
lib/domain/clash/clash_proxy.dart
Normal file
59
lib/domain/clash/clash_proxy.dart
Normal 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);
|
||||
}
|
||||
17
lib/domain/clash/clash_traffic.dart
Normal file
17
lib/domain/clash/clash_traffic.dart
Normal 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);
|
||||
}
|
||||
43
lib/domain/connectivity/connection_status.dart
Normal file
43
lib/domain/connectivity/connection_status.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
4
lib/domain/connectivity/connectivity.dart
Normal file
4
lib/domain/connectivity/connectivity.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
export 'connection_status.dart';
|
||||
export 'connectivity_failure.dart';
|
||||
export 'network_prefs.dart';
|
||||
export 'traffic.dart';
|
||||
21
lib/domain/connectivity/connectivity_failure.dart
Normal file
21
lib/domain/connectivity/connectivity_failure.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
17
lib/domain/connectivity/network_prefs.dart
Normal file
17
lib/domain/connectivity/network_prefs.dart
Normal 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);
|
||||
}
|
||||
13
lib/domain/connectivity/traffic.dart
Normal file
13
lib/domain/connectivity/traffic.dart
Normal 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;
|
||||
}
|
||||
7
lib/domain/constants.dart
Normal file
7
lib/domain/constants.dart
Normal 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
13
lib/domain/failures.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
25
lib/domain/profiles/profile.dart
Normal file
25
lib/domain/profiles/profile.dart
Normal 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);
|
||||
}
|
||||
4
lib/domain/profiles/profiles.dart
Normal file
4
lib/domain/profiles/profiles.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
export 'profile.dart';
|
||||
export 'profiles_failure.dart';
|
||||
export 'profiles_repository.dart';
|
||||
export 'subscription_info.dart';
|
||||
28
lib/domain/profiles/profiles_failure.dart
Normal file
28
lib/domain/profiles/profiles_failure.dart
Normal 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,
|
||||
};
|
||||
}
|
||||
}
|
||||
20
lib/domain/profiles/profiles_repository.dart
Normal file
20
lib/domain/profiles/profiles_repository.dart
Normal 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);
|
||||
}
|
||||
44
lib/domain/profiles/subscription_info.dart
Normal file
44
lib/domain/profiles/subscription_info.dart
Normal 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);
|
||||
Reference in New Issue
Block a user