initial
This commit is contained in:
42
lib/data/data_providers.dart
Normal file
42
lib/data/data_providers.dart
Normal file
@@ -0,0 +1,42 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/data/repository/repository.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/services/service_providers.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
part 'data_providers.g.dart';
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect();
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
SharedPreferences sharedPreferences(SharedPreferencesRef ref) =>
|
||||
throw UnimplementedError('sharedPreferences must be overridden');
|
||||
|
||||
// TODO: set options for dio
|
||||
@Riverpod(keepAlive: true)
|
||||
Dio dio(DioRef ref) => Dio();
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ProfilesDao profilesDao(ProfilesDaoRef ref) => ProfilesDao(
|
||||
ref.watch(appDatabaseProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ClashFacade clashFacade(ClashFacadeRef ref) => ClashFacadeImpl(
|
||||
clashService: ref.watch(clashServiceProvider),
|
||||
filesEditor: ref.watch(filesEditorServiceProvider),
|
||||
);
|
||||
|
||||
@Riverpod(keepAlive: true)
|
||||
ProfilesRepository profilesRepository(ProfilesRepositoryRef ref) =>
|
||||
ProfilesRepositoryImpl(
|
||||
profilesDao: ref.watch(profilesDaoProvider),
|
||||
filesEditor: ref.watch(filesEditorServiceProvider),
|
||||
clashFacade: ref.watch(clashFacadeProvider),
|
||||
dio: ref.watch(dioProvider),
|
||||
);
|
||||
1
lib/data/local/dao/dao.dart
Normal file
1
lib/data/local/dao/dao.dart
Normal file
@@ -0,0 +1 @@
|
||||
export 'profiles_dao.dart';
|
||||
83
lib/data/local/dao/profiles_dao.dart
Normal file
83
lib/data/local/dao/profiles_dao.dart
Normal file
@@ -0,0 +1,83 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/data_mappers.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/data/local/tables.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
|
||||
part 'profiles_dao.g.dart';
|
||||
|
||||
@DriftAccessor(tables: [ProfileEntries])
|
||||
class ProfilesDao extends DatabaseAccessor<AppDatabase>
|
||||
with _$ProfilesDaoMixin, InfraLogger {
|
||||
ProfilesDao(super.db);
|
||||
|
||||
Future<Profile?> getById(String id) async {
|
||||
return (profileEntries.select()..where((tbl) => tbl.id.equals(id)))
|
||||
.map(ProfileMapper.fromEntry)
|
||||
.getSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<Profile?> watchActiveProfile() {
|
||||
return (profileEntries.select()..where((tbl) => tbl.active.equals(true)))
|
||||
.map(ProfileMapper.fromEntry)
|
||||
.watchSingleOrNull();
|
||||
}
|
||||
|
||||
Stream<int> watchProfileCount() {
|
||||
final count = profileEntries.id.count();
|
||||
return (profileEntries.selectOnly()..addColumns([count]))
|
||||
.map((exp) => exp.read(count)!)
|
||||
.watchSingle();
|
||||
}
|
||||
|
||||
Stream<List<Profile>> watchAll() {
|
||||
return (profileEntries.select()
|
||||
..orderBy(
|
||||
[(tbl) => OrderingTerm.desc(tbl.active)],
|
||||
))
|
||||
.map(ProfileMapper.fromEntry)
|
||||
.watch();
|
||||
}
|
||||
|
||||
Future<void> create(Profile profile) async {
|
||||
await transaction(
|
||||
() async {
|
||||
if (profile.active) {
|
||||
await (update(profileEntries)
|
||||
..where((tbl) => tbl.id.isNotValue(profile.id)))
|
||||
.write(const ProfileEntriesCompanion(active: Value(false)));
|
||||
}
|
||||
await into(profileEntries).insert(profile.toCompanion());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> edit(Profile patch) async {
|
||||
await transaction(
|
||||
() async {
|
||||
await (update(profileEntries)..where((tbl) => tbl.id.equals(patch.id)))
|
||||
.write(patch.toCompanion());
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setAsActive(String id) async {
|
||||
await transaction(
|
||||
() async {
|
||||
await (update(profileEntries)..where((tbl) => tbl.id.isNotValue(id)))
|
||||
.write(const ProfileEntriesCompanion(active: Value(false)));
|
||||
await (update(profileEntries)..where((tbl) => tbl.id.equals(id)))
|
||||
.write(const ProfileEntriesCompanion(active: Value(true)));
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> removeById(String id) async {
|
||||
await transaction(
|
||||
() async {
|
||||
await (delete(profileEntries)..where((tbl) => tbl.id.equals(id))).go();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
37
lib/data/local/data_mappers.dart
Normal file
37
lib/data/local/data_mappers.dart
Normal file
@@ -0,0 +1,37 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/database.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
|
||||
extension ProfileMapper on Profile {
|
||||
ProfileEntriesCompanion toCompanion() {
|
||||
return ProfileEntriesCompanion.insert(
|
||||
id: id,
|
||||
active: active,
|
||||
name: name,
|
||||
url: url,
|
||||
lastUpdate: lastUpdate,
|
||||
upload: Value(subInfo?.upload),
|
||||
download: Value(subInfo?.download),
|
||||
total: Value(subInfo?.total),
|
||||
expire: Value(subInfo?.expire),
|
||||
updateInterval: Value(updateInterval),
|
||||
);
|
||||
}
|
||||
|
||||
static Profile fromEntry(ProfileEntry entry) {
|
||||
return Profile(
|
||||
id: entry.id,
|
||||
active: entry.active,
|
||||
name: entry.name,
|
||||
url: entry.url,
|
||||
lastUpdate: entry.lastUpdate,
|
||||
updateInterval: entry.updateInterval,
|
||||
subInfo: SubscriptionInfo(
|
||||
upload: entry.upload,
|
||||
download: entry.download,
|
||||
total: entry.total,
|
||||
expire: entry.expire,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
29
lib/data/local/database.dart
Normal file
29
lib/data/local/database.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:drift/native.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/local/tables.dart';
|
||||
import 'package:hiddify/data/local/type_converters.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@DriftDatabase(tables: [ProfileEntries], daos: [ProfilesDao])
|
||||
class AppDatabase extends _$AppDatabase {
|
||||
AppDatabase({required QueryExecutor connection}) : super(connection);
|
||||
|
||||
AppDatabase.connect() : super(_openConnection());
|
||||
|
||||
@override
|
||||
int get schemaVersion => 1;
|
||||
}
|
||||
|
||||
LazyDatabase _openConnection() {
|
||||
return LazyDatabase(() async {
|
||||
final dbFolder = await getApplicationDocumentsDirectory();
|
||||
final file = File(p.join(dbFolder.path, 'db.sqlite'));
|
||||
return NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
20
lib/data/local/tables.dart
Normal file
20
lib/data/local/tables.dart
Normal file
@@ -0,0 +1,20 @@
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:hiddify/data/local/type_converters.dart';
|
||||
|
||||
@DataClassName('ProfileEntry')
|
||||
class ProfileEntries extends Table {
|
||||
TextColumn get id => text()();
|
||||
BoolColumn get active => boolean()();
|
||||
TextColumn get name => text().withLength(min: 1)();
|
||||
TextColumn get url => text()();
|
||||
IntColumn get upload => integer().nullable()();
|
||||
IntColumn get download => integer().nullable()();
|
||||
IntColumn get total => integer().nullable()();
|
||||
DateTimeColumn get expire => dateTime().nullable()();
|
||||
IntColumn get updateInterval =>
|
||||
integer().nullable().map(DurationTypeConverter())();
|
||||
DateTimeColumn get lastUpdate => dateTime()();
|
||||
|
||||
@override
|
||||
Set<Column> get primaryKey => {id};
|
||||
}
|
||||
13
lib/data/local/type_converters.dart
Normal file
13
lib/data/local/type_converters.dart
Normal file
@@ -0,0 +1,13 @@
|
||||
import 'package:drift/drift.dart';
|
||||
|
||||
class DurationTypeConverter extends TypeConverter<Duration, int> {
|
||||
@override
|
||||
Duration fromSql(int fromDb) {
|
||||
return Duration(seconds: fromDb);
|
||||
}
|
||||
|
||||
@override
|
||||
int toSql(Duration value) {
|
||||
return value.inSeconds;
|
||||
}
|
||||
}
|
||||
116
lib/data/repository/clash_facade_impl.dart
Normal file
116
lib/data/repository/clash_facade_impl.dart
Normal file
@@ -0,0 +1,116 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/data/repository/exception_handlers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/constants.dart';
|
||||
import 'package:hiddify/services/clash/clash.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
|
||||
class ClashFacadeImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements ClashFacade {
|
||||
ClashFacadeImpl({
|
||||
required ClashService clashService,
|
||||
required FilesEditorService filesEditor,
|
||||
}) : _clash = clashService,
|
||||
_filesEditor = filesEditor;
|
||||
|
||||
final ClashService _clash;
|
||||
final FilesEditorService _filesEditor;
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, ClashConfig> getConfigs() {
|
||||
return exceptionHandler(
|
||||
() async => _clash.getConfigs().mapLeft(ClashFailure.core).run(),
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, bool> validateConfig(String configFileName) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final path = _filesEditor.configPath(configFileName);
|
||||
return _clash.validateConfig(path).mapLeft(ClashFailure.core).run();
|
||||
},
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, Unit> changeConfigs(String configFileName) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
loggy.debug("changing config, file name: [$configFileName]");
|
||||
final path = _filesEditor.configPath(configFileName);
|
||||
return _clash.updateConfigs(path).mapLeft(ClashFailure.core).run();
|
||||
},
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, Unit> patchOverrides(ClashConfig overrides) {
|
||||
return exceptionHandler(
|
||||
() async =>
|
||||
_clash.patchConfigs(overrides).mapLeft(ClashFailure.core).run(),
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, List<ClashProxy>> getProxies() {
|
||||
return exceptionHandler(
|
||||
() async => _clash.getProxies().mapLeft(ClashFailure.core).run(),
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, Unit> changeProxy(
|
||||
String selectorName,
|
||||
String proxyName,
|
||||
) {
|
||||
return exceptionHandler(
|
||||
() async => _clash
|
||||
.changeProxy(selectorName, proxyName)
|
||||
.mapLeft(ClashFailure.core)
|
||||
.run(),
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, ClashTraffic> getTraffic() {
|
||||
return exceptionHandler(
|
||||
() async => _clash.getTraffic().mapLeft(ClashFailure.core).run(),
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ClashFailure, int> testDelay(
|
||||
String proxyName, {
|
||||
String testUrl = Constants.delayTestUrl,
|
||||
}) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
final result = _clash
|
||||
.getProxyDelay(proxyName, testUrl)
|
||||
.mapLeft(ClashFailure.core)
|
||||
.run();
|
||||
return result;
|
||||
},
|
||||
ClashFailure.unexpected,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Either<ClashFailure, ClashLog>> watchLogs() {
|
||||
return _clash
|
||||
.watchLogs(LogLevel.info)
|
||||
.handleExceptions(ClashFailure.unexpected);
|
||||
}
|
||||
}
|
||||
32
lib/data/repository/exception_handlers.dart
Normal file
32
lib/data/repository/exception_handlers.dart
Normal file
@@ -0,0 +1,32 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:rxdart/rxdart.dart';
|
||||
|
||||
mixin ExceptionHandler implements LoggerMixin {
|
||||
TaskEither<F, R> exceptionHandler<F, R>(
|
||||
Future<Either<F, R>> Function() run,
|
||||
F Function(Object error, StackTrace stackTrace) onError,
|
||||
) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
try {
|
||||
return await run();
|
||||
} catch (error, stackTrace) {
|
||||
return Left(onError(error, stackTrace));
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
extension StreamExceptionHandler<R extends Object?> on Stream<R> {
|
||||
Stream<Either<F, R>> handleExceptions<F>(
|
||||
F Function(Object error, StackTrace stackTrace) onError,
|
||||
) {
|
||||
return map(right<F, R>).onErrorReturnWith(
|
||||
(error, stackTrace) {
|
||||
return Left(onError(error, stackTrace));
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
155
lib/data/repository/profiles_repository_impl.dart
Normal file
155
lib/data/repository/profiles_repository_impl.dart
Normal file
@@ -0,0 +1,155 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import 'package:hiddify/data/local/dao/dao.dart';
|
||||
import 'package:hiddify/data/repository/exception_handlers.dart';
|
||||
import 'package:hiddify/domain/clash/clash.dart';
|
||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||
import 'package:hiddify/services/files_editor_service.dart';
|
||||
import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
class ProfilesRepositoryImpl
|
||||
with ExceptionHandler, InfraLogger
|
||||
implements ProfilesRepository {
|
||||
ProfilesRepositoryImpl({
|
||||
required this.profilesDao,
|
||||
required this.filesEditor,
|
||||
required this.clashFacade,
|
||||
required this.dio,
|
||||
});
|
||||
|
||||
final ProfilesDao profilesDao;
|
||||
final FilesEditorService filesEditor;
|
||||
final ClashFacade clashFacade;
|
||||
final Dio dio;
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Profile?> get(String id) {
|
||||
return TaskEither.tryCatch(
|
||||
() => profilesDao.getById(id),
|
||||
ProfileUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Either<ProfileFailure, Profile?>> watchActiveProfile() {
|
||||
return profilesDao
|
||||
.watchActiveProfile()
|
||||
.handleExceptions(ProfileUnexpectedFailure.new);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Either<ProfileFailure, bool>> watchHasAnyProfile() {
|
||||
return profilesDao
|
||||
.watchProfileCount()
|
||||
.map((event) => event != 0)
|
||||
.handleExceptions(ProfileUnexpectedFailure.new);
|
||||
}
|
||||
|
||||
@override
|
||||
Stream<Either<ProfileFailure, List<Profile>>> watchAll() {
|
||||
return profilesDao
|
||||
.watchAll()
|
||||
.handleExceptions(ProfileUnexpectedFailure.new);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> add(Profile baseProfile) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
return fetch(baseProfile.url, baseProfile.id)
|
||||
.flatMap(
|
||||
(subInfo) => TaskEither(() async {
|
||||
await profilesDao.create(
|
||||
baseProfile.copyWith(
|
||||
subInfo: subInfo,
|
||||
lastUpdate: DateTime.now(),
|
||||
),
|
||||
);
|
||||
return right(unit);
|
||||
}),
|
||||
)
|
||||
.run();
|
||||
},
|
||||
ProfileUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> update(Profile baseProfile) {
|
||||
return exceptionHandler(
|
||||
() async {
|
||||
return fetch(baseProfile.url, baseProfile.id)
|
||||
.flatMap(
|
||||
(subInfo) => TaskEither(() async {
|
||||
await profilesDao.edit(
|
||||
baseProfile.copyWith(
|
||||
subInfo: subInfo,
|
||||
lastUpdate: DateTime.now(),
|
||||
),
|
||||
);
|
||||
return right(unit);
|
||||
}),
|
||||
)
|
||||
.run();
|
||||
},
|
||||
ProfileUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> setAsActive(String id) {
|
||||
return TaskEither.tryCatch(
|
||||
() async {
|
||||
await profilesDao.setAsActive(id);
|
||||
return unit;
|
||||
},
|
||||
ProfileUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
TaskEither<ProfileFailure, Unit> delete(String id) {
|
||||
return TaskEither.tryCatch(
|
||||
() async {
|
||||
await profilesDao.removeById(id);
|
||||
await filesEditor.deleteConfig(id);
|
||||
return unit;
|
||||
},
|
||||
ProfileUnexpectedFailure.new,
|
||||
);
|
||||
}
|
||||
|
||||
@visibleForTesting
|
||||
TaskEither<ProfileFailure, SubscriptionInfo?> fetch(
|
||||
String url,
|
||||
String fileName,
|
||||
) {
|
||||
return TaskEither(
|
||||
() async {
|
||||
final path = filesEditor.configPath(fileName);
|
||||
final response = await dio.download(url, path);
|
||||
if (response.statusCode != 200) {
|
||||
await File(path).delete();
|
||||
return left(const ProfileUnexpectedFailure());
|
||||
}
|
||||
final isValid = await clashFacade
|
||||
.validateConfig(fileName)
|
||||
.getOrElse((_) => false)
|
||||
.run();
|
||||
if (!isValid) {
|
||||
await File(path).delete();
|
||||
return left(const ProfileFailure.invalidConfig());
|
||||
}
|
||||
final subInfoString =
|
||||
response.headers.map['subscription-userinfo']?.single;
|
||||
final subInfo = subInfoString != null
|
||||
? SubscriptionInfo.fromResponseHeader(subInfoString)
|
||||
: null;
|
||||
return right(subInfo);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
2
lib/data/repository/repository.dart
Normal file
2
lib/data/repository/repository.dart
Normal file
@@ -0,0 +1,2 @@
|
||||
export 'clash_facade_impl.dart';
|
||||
export 'profiles_repository_impl.dart';
|
||||
Reference in New Issue
Block a user