Refactor geo assets
This commit is contained in:
@@ -2,8 +2,6 @@ targets:
|
|||||||
$default:
|
$default:
|
||||||
builders:
|
builders:
|
||||||
drift_dev:
|
drift_dev:
|
||||||
generate_for:
|
|
||||||
- "lib/data/local/**"
|
|
||||||
options:
|
options:
|
||||||
store_date_time_values_as_text: true
|
store_date_time_values_as_text: true
|
||||||
slang_build_runner:
|
slang_build_runner:
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import 'package:hiddify/data/repository/app_repository_impl.dart';
|
|||||||
import 'package:hiddify/domain/environment.dart';
|
import 'package:hiddify/domain/environment.dart';
|
||||||
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
|
import 'package:hiddify/features/common/active_profile/active_profile_notifier.dart';
|
||||||
import 'package:hiddify/features/common/window/window_controller.dart';
|
import 'package:hiddify/features/common/window/window_controller.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
|
||||||
import 'package:hiddify/features/system_tray/system_tray_controller.dart';
|
import 'package:hiddify/features/system_tray/system_tray_controller.dart';
|
||||||
import 'package:hiddify/services/auto_start_service.dart';
|
import 'package:hiddify/services/auto_start_service.dart';
|
||||||
import 'package:hiddify/services/deep_link_service.dart';
|
import 'package:hiddify/services/deep_link_service.dart';
|
||||||
@@ -86,6 +87,7 @@ Future<void> _lazyBootstrap(
|
|||||||
|
|
||||||
final filesEditor = container.read(filesEditorServiceProvider);
|
final filesEditor = container.read(filesEditorServiceProvider);
|
||||||
await filesEditor.init();
|
await filesEditor.init();
|
||||||
|
await container.read(geoAssetRepositoryProvider.future);
|
||||||
|
|
||||||
initLoggers(container.read, debug);
|
initLoggers(container.read, debug);
|
||||||
_logger.info(container.read(appInfoProvider).format());
|
_logger.info(container.read(appInfoProvider).format());
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:hiddify/core/router/routes/shared_routes.dart';
|
import 'package:hiddify/core/router/routes/shared_routes.dart';
|
||||||
import 'package:hiddify/features/about/view/view.dart';
|
import 'package:hiddify/features/about/view/view.dart';
|
||||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
|
||||||
import 'package:hiddify/features/logs/view/view.dart';
|
import 'package:hiddify/features/logs/view/view.dart';
|
||||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart';
|
|
||||||
import 'package:hiddify/features/settings/view/view.dart';
|
import 'package:hiddify/features/settings/view/view.dart';
|
||||||
|
|
||||||
part 'desktop_routes.g.dart';
|
part 'desktop_routes.g.dart';
|
||||||
@@ -117,7 +117,7 @@ class GeoAssetsRoute extends GoRouteData {
|
|||||||
return const MaterialPage(
|
return const MaterialPage(
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
name: name,
|
name: name,
|
||||||
child: GeoAssetsPage(),
|
child: GeoAssetsOverviewPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import 'package:hiddify/core/router/app_router.dart';
|
|||||||
import 'package:hiddify/core/router/routes/shared_routes.dart';
|
import 'package:hiddify/core/router/routes/shared_routes.dart';
|
||||||
import 'package:hiddify/features/about/view/view.dart';
|
import 'package:hiddify/features/about/view/view.dart';
|
||||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
|
||||||
import 'package:hiddify/features/logs/view/view.dart';
|
import 'package:hiddify/features/logs/view/view.dart';
|
||||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_page.dart';
|
|
||||||
import 'package:hiddify/features/settings/view/view.dart';
|
import 'package:hiddify/features/settings/view/view.dart';
|
||||||
|
|
||||||
part 'mobile_routes.g.dart';
|
part 'mobile_routes.g.dart';
|
||||||
@@ -155,7 +155,7 @@ class GeoAssetsRoute extends GoRouteData {
|
|||||||
return const MaterialPage(
|
return const MaterialPage(
|
||||||
fullscreenDialog: true,
|
fullscreenDialog: true,
|
||||||
name: name,
|
name: name,
|
||||||
child: GeoAssetsPage(),
|
child: GeoAssetsOverviewPage(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,18 +4,17 @@ import 'package:dio/dio.dart';
|
|||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/core/prefs/general_prefs.dart';
|
import 'package:hiddify/core/prefs/general_prefs.dart';
|
||||||
import 'package:hiddify/data/api/clash_api.dart';
|
import 'package:hiddify/data/api/clash_api.dart';
|
||||||
import 'package:hiddify/data/local/dao/dao.dart';
|
import 'package:hiddify/data/local/dao/profiles_dao.dart';
|
||||||
import 'package:hiddify/data/local/database.dart';
|
import 'package:hiddify/data/local/database.dart';
|
||||||
import 'package:hiddify/data/repository/app_repository_impl.dart';
|
import 'package:hiddify/data/repository/app_repository_impl.dart';
|
||||||
import 'package:hiddify/data/repository/config_options_store.dart';
|
import 'package:hiddify/data/repository/config_options_store.dart';
|
||||||
import 'package:hiddify/data/repository/geo_assets_repository.dart';
|
|
||||||
import 'package:hiddify/data/repository/repository.dart';
|
import 'package:hiddify/data/repository/repository.dart';
|
||||||
import 'package:hiddify/domain/app/app.dart';
|
import 'package:hiddify/domain/app/app.dart';
|
||||||
import 'package:hiddify/domain/constants.dart';
|
import 'package:hiddify/domain/constants.dart';
|
||||||
import 'package:hiddify/domain/core_facade.dart';
|
import 'package:hiddify/domain/core_facade.dart';
|
||||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_assets_repository.dart';
|
|
||||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
|
||||||
import 'package:hiddify/services/service_providers.dart';
|
import 'package:hiddify/services/service_providers.dart';
|
||||||
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
import 'package:native_dio_adapter/native_dio_adapter.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
@@ -70,38 +69,25 @@ AppRepository appRepository(AppRepositoryRef ref) =>
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort);
|
ClashApi clashApi(ClashApiRef ref) => ClashApi(Defaults.clashApiPort);
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
GeoAssetsDao geoAssetsDao(GeoAssetsDaoRef ref) => GeoAssetsDao(
|
|
||||||
ref.watch(appDatabaseProvider),
|
|
||||||
);
|
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
|
||||||
GeoAssetsRepository geoAssetsRepository(GeoAssetsRepositoryRef ref) {
|
|
||||||
return GeoAssetsRepositoryImpl(
|
|
||||||
geoAssetsDao: ref.watch(geoAssetsDaoProvider),
|
|
||||||
dio: ref.watch(dioProvider),
|
|
||||||
filesEditor: ref.watch(filesEditorServiceProvider),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@riverpod
|
@riverpod
|
||||||
Future<ConfigOptions> configOptions(ConfigOptionsRef ref) async {
|
Future<ConfigOptions> configOptions(ConfigOptionsRef ref) async {
|
||||||
final geoAssets = await ref
|
final geoAssets = await ref
|
||||||
.watch(geoAssetsRepositoryProvider)
|
.watch(geoAssetRepositoryProvider)
|
||||||
|
.requireValue
|
||||||
.getActivePair()
|
.getActivePair()
|
||||||
.getOrElse((l) => throw l)
|
.getOrElse((l) => throw l)
|
||||||
.run();
|
.run();
|
||||||
final filesEditor = ref.watch(filesEditorServiceProvider);
|
final geoAssetsPathResolver = ref.watch(geoAssetPathResolverProvider);
|
||||||
|
|
||||||
final serviceMode = ref.watch(serviceModeStoreProvider);
|
final serviceMode = ref.watch(serviceModeStoreProvider);
|
||||||
return ref.watch(configPreferencesProvider).copyWith(
|
return ref.watch(configPreferencesProvider).copyWith(
|
||||||
enableTun: serviceMode == ServiceMode.tun,
|
enableTun: serviceMode == ServiceMode.tun,
|
||||||
setSystemProxy: serviceMode == ServiceMode.systemProxy,
|
setSystemProxy: serviceMode == ServiceMode.systemProxy,
|
||||||
geoipPath: filesEditor.geoAssetRelativePath(
|
geoipPath: geoAssetsPathResolver.relativePath(
|
||||||
geoAssets.geoip.providerName,
|
geoAssets.geoip.providerName,
|
||||||
geoAssets.geoip.fileName,
|
geoAssets.geoip.fileName,
|
||||||
),
|
),
|
||||||
geositePath: filesEditor.geoAssetRelativePath(
|
geositePath: geoAssetsPathResolver.relativePath(
|
||||||
geoAssets.geosite.providerName,
|
geoAssets.geosite.providerName,
|
||||||
geoAssets.geosite.fileName,
|
geoAssets.geosite.fileName,
|
||||||
),
|
),
|
||||||
@@ -112,6 +98,7 @@ Future<ConfigOptions> configOptions(ConfigOptionsRef ref) async {
|
|||||||
CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
|
CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl(
|
||||||
ref.watch(singboxServiceProvider),
|
ref.watch(singboxServiceProvider),
|
||||||
ref.watch(filesEditorServiceProvider),
|
ref.watch(filesEditorServiceProvider),
|
||||||
|
ref.watch(geoAssetPathResolverProvider),
|
||||||
ref.watch(platformServicesProvider),
|
ref.watch(platformServicesProvider),
|
||||||
ref.watch(clashApiProvider),
|
ref.watch(clashApiProvider),
|
||||||
ref.read(debugModeNotifierProvider),
|
ref.read(debugModeNotifierProvider),
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
export 'geo_assets_dao.dart';
|
|
||||||
export 'profiles_dao.dart';
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
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/rules/geo_asset.dart';
|
|
||||||
import 'package:hiddify/utils/custom_loggers.dart';
|
|
||||||
|
|
||||||
part 'geo_assets_dao.g.dart';
|
|
||||||
|
|
||||||
@DriftAccessor(tables: [GeoAssetEntries])
|
|
||||||
class GeoAssetsDao extends DatabaseAccessor<AppDatabase>
|
|
||||||
with _$GeoAssetsDaoMixin, InfraLogger {
|
|
||||||
GeoAssetsDao(super.db);
|
|
||||||
|
|
||||||
Future<void> add(GeoAsset geoAsset) async {
|
|
||||||
await into(geoAssetEntries).insert(geoAsset.toCompanion());
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<GeoAsset?> getActive(GeoAssetType type) async {
|
|
||||||
return (geoAssetEntries.select()
|
|
||||||
..where((tbl) => tbl.active.equals(true))
|
|
||||||
..where((tbl) => tbl.type.equalsValue(type))
|
|
||||||
..limit(1))
|
|
||||||
.map(GeoAssetMapper.fromEntry)
|
|
||||||
.getSingleOrNull();
|
|
||||||
}
|
|
||||||
|
|
||||||
Stream<List<GeoAsset>> watchAll() {
|
|
||||||
return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> edit(GeoAsset patch) async {
|
|
||||||
await transaction(
|
|
||||||
() async {
|
|
||||||
if (patch.active) {
|
|
||||||
await (update(geoAssetEntries)
|
|
||||||
..where((tbl) => tbl.active.equals(true))
|
|
||||||
..where((tbl) => tbl.type.equalsValue(patch.type)))
|
|
||||||
.write(const GeoAssetEntriesCompanion(active: Value(false)));
|
|
||||||
}
|
|
||||||
await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(patch.id)))
|
|
||||||
.write(patch.toCompanion());
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hiddify/data/local/database.dart';
|
import 'package:hiddify/data/local/database.dart';
|
||||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
|
||||||
|
|
||||||
extension ProfileMapper on Profile {
|
extension ProfileMapper on Profile {
|
||||||
ProfileEntriesCompanion toCompanion() {
|
ProfileEntriesCompanion toCompanion() {
|
||||||
@@ -72,29 +71,3 @@ extension ProfileMapper on Profile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension GeoAssetMapper on GeoAsset {
|
|
||||||
GeoAssetEntriesCompanion toCompanion() {
|
|
||||||
return GeoAssetEntriesCompanion.insert(
|
|
||||||
id: id,
|
|
||||||
type: type,
|
|
||||||
active: active,
|
|
||||||
name: name,
|
|
||||||
providerName: providerName,
|
|
||||||
version: Value(version),
|
|
||||||
lastCheck: Value(lastCheck),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static GeoAsset fromEntry(GeoAssetEntry e) {
|
|
||||||
return GeoAsset(
|
|
||||||
id: e.id,
|
|
||||||
name: e.name,
|
|
||||||
type: e.type,
|
|
||||||
active: e.active,
|
|
||||||
providerName: e.providerName,
|
|
||||||
version: e.version,
|
|
||||||
lastCheck: e.lastCheck,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,22 +2,19 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:drift/native.dart';
|
import 'package:drift/native.dart';
|
||||||
import 'package:hiddify/data/local/dao/dao.dart';
|
|
||||||
import 'package:hiddify/data/local/data_mappers.dart';
|
|
||||||
import 'package:hiddify/data/local/schema_versions.dart';
|
import 'package:hiddify/data/local/schema_versions.dart';
|
||||||
import 'package:hiddify/data/local/tables.dart';
|
import 'package:hiddify/data/local/tables.dart';
|
||||||
import 'package:hiddify/data/local/type_converters.dart';
|
import 'package:hiddify/data/local/type_converters.dart';
|
||||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
import 'package:hiddify/services/files_editor_service.dart';
|
import 'package:hiddify/services/files_editor_service.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
part 'database.g.dart';
|
part 'database.g.dart';
|
||||||
|
|
||||||
@DriftDatabase(
|
@DriftDatabase(tables: [ProfileEntries, GeoAssetEntries])
|
||||||
tables: [ProfileEntries, GeoAssetEntries],
|
|
||||||
daos: [ProfilesDao, GeoAssetsDao],
|
|
||||||
)
|
|
||||||
class AppDatabase extends _$AppDatabase {
|
class AppDatabase extends _$AppDatabase {
|
||||||
AppDatabase({required QueryExecutor connection}) : super(connection);
|
AppDatabase({required QueryExecutor connection}) : super(connection);
|
||||||
|
|
||||||
@@ -57,7 +54,7 @@ class AppDatabase extends _$AppDatabase {
|
|||||||
|
|
||||||
Future<void> _prePopulateGeoAssets() async {
|
Future<void> _prePopulateGeoAssets() async {
|
||||||
await transaction(() async {
|
await transaction(() async {
|
||||||
final geoAssets = defaultGeoAssets.map((e) => e.toCompanion());
|
final geoAssets = defaultGeoAssets.map((e) => e.toEntry());
|
||||||
for (final geoAsset in geoAssets) {
|
for (final geoAsset in geoAssets) {
|
||||||
await into(geoAssetEntries).insert(geoAsset);
|
await into(geoAssetEntries).insert(geoAsset);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:drift/drift.dart';
|
import 'package:drift/drift.dart';
|
||||||
import 'package:hiddify/data/local/type_converters.dart';
|
import 'package:hiddify/data/local/type_converters.dart';
|
||||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
|
||||||
@DataClassName('ProfileEntry')
|
@DataClassName('ProfileEntry')
|
||||||
class ProfileEntries extends Table {
|
class ProfileEntries extends Table {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import 'package:hiddify/domain/constants.dart';
|
|||||||
import 'package:hiddify/domain/core_facade.dart';
|
import 'package:hiddify/domain/core_facade.dart';
|
||||||
import 'package:hiddify/domain/core_service_failure.dart';
|
import 'package:hiddify/domain/core_service_failure.dart';
|
||||||
import 'package:hiddify/domain/singbox/singbox.dart';
|
import 'package:hiddify/domain/singbox/singbox.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart';
|
||||||
import 'package:hiddify/services/files_editor_service.dart';
|
import 'package:hiddify/services/files_editor_service.dart';
|
||||||
import 'package:hiddify/services/platform_services.dart';
|
import 'package:hiddify/services/platform_services.dart';
|
||||||
import 'package:hiddify/services/singbox/singbox_service.dart';
|
import 'package:hiddify/services/singbox/singbox_service.dart';
|
||||||
@@ -19,6 +20,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
|||||||
CoreFacadeImpl(
|
CoreFacadeImpl(
|
||||||
this.singbox,
|
this.singbox,
|
||||||
this.filesEditor,
|
this.filesEditor,
|
||||||
|
this.geoAssetPathResolver,
|
||||||
this.platformServices,
|
this.platformServices,
|
||||||
this.clash,
|
this.clash,
|
||||||
this.debug,
|
this.debug,
|
||||||
@@ -27,6 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
|||||||
|
|
||||||
final SingboxService singbox;
|
final SingboxService singbox;
|
||||||
final FilesEditorService filesEditor;
|
final FilesEditorService filesEditor;
|
||||||
|
final GeoAssetPathResolver geoAssetPathResolver;
|
||||||
final PlatformServices platformServices;
|
final PlatformServices platformServices;
|
||||||
final ClashApi clash;
|
final ClashApi clash;
|
||||||
final bool debug;
|
final bool debug;
|
||||||
@@ -38,8 +41,8 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade {
|
|||||||
return exceptionHandler(
|
return exceptionHandler(
|
||||||
() async {
|
() async {
|
||||||
final options = await configOptions();
|
final options = await configOptions();
|
||||||
final geoip = filesEditor.resolveGeoAssetPath(options.geoipPath);
|
final geoip = geoAssetPathResolver.resolvePath(options.geoipPath);
|
||||||
final geosite = filesEditor.resolveGeoAssetPath(options.geositePath);
|
final geosite = geoAssetPathResolver.resolvePath(options.geositePath);
|
||||||
if (!await File(geoip).exists() || !await File(geosite).exists()) {
|
if (!await File(geoip).exists() || !await File(geosite).exists()) {
|
||||||
return left(const CoreMissingGeoAssets());
|
return left(const CoreMissingGeoAssets());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,168 +0,0 @@
|
|||||||
import 'dart:io';
|
|
||||||
|
|
||||||
import 'package:dartx/dartx_io.dart';
|
|
||||||
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/rules/geo_asset.dart';
|
|
||||||
import 'package:hiddify/domain/rules/geo_asset_failure.dart';
|
|
||||||
import 'package:hiddify/domain/rules/geo_assets_repository.dart';
|
|
||||||
import 'package:hiddify/services/files_editor_service.dart';
|
|
||||||
import 'package:hiddify/utils/custom_loggers.dart';
|
|
||||||
import 'package:rxdart/rxdart.dart';
|
|
||||||
import 'package:watcher/watcher.dart';
|
|
||||||
|
|
||||||
class GeoAssetsRepositoryImpl
|
|
||||||
with ExceptionHandler, InfraLogger
|
|
||||||
implements GeoAssetsRepository {
|
|
||||||
GeoAssetsRepositoryImpl({
|
|
||||||
required this.geoAssetsDao,
|
|
||||||
required this.dio,
|
|
||||||
required this.filesEditor,
|
|
||||||
});
|
|
||||||
|
|
||||||
final GeoAssetsDao geoAssetsDao;
|
|
||||||
final Dio dio;
|
|
||||||
final FilesEditorService filesEditor;
|
|
||||||
|
|
||||||
@override
|
|
||||||
TaskEither<GeoAssetFailure, ({GeoAsset geoip, GeoAsset geosite})>
|
|
||||||
getActivePair() {
|
|
||||||
return exceptionHandler(
|
|
||||||
() async {
|
|
||||||
final geoip = await geoAssetsDao.getActive(GeoAssetType.geoip);
|
|
||||||
final geosite = await geoAssetsDao.getActive(GeoAssetType.geosite);
|
|
||||||
if (geoip == null || geosite == null) {
|
|
||||||
return left(const GeoAssetFailure.activeAssetNotFound());
|
|
||||||
}
|
|
||||||
return right((geoip: geoip, geosite: geosite));
|
|
||||||
},
|
|
||||||
GeoAssetFailure.unexpected,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll() {
|
|
||||||
final persistedStream = geoAssetsDao.watchAll();
|
|
||||||
final filesStream = _watchGeoFiles();
|
|
||||||
|
|
||||||
return Rx.combineLatest2(
|
|
||||||
persistedStream,
|
|
||||||
filesStream,
|
|
||||||
(assets, files) => assets.map(
|
|
||||||
(e) {
|
|
||||||
final path = filesEditor.geoAssetPath(e.providerName, e.fileName);
|
|
||||||
final file = files.firstOrNullWhere((e) => e.path == path);
|
|
||||||
final stat = file?.statSync();
|
|
||||||
return (e, stat?.size);
|
|
||||||
},
|
|
||||||
).toList(),
|
|
||||||
).handleExceptions(GeoAssetUnexpectedFailure.new);
|
|
||||||
}
|
|
||||||
|
|
||||||
Iterable<File> _geoFiles = [];
|
|
||||||
Stream<Iterable<File>> _watchGeoFiles() async* {
|
|
||||||
yield await _readGeoFiles();
|
|
||||||
yield* Watcher(
|
|
||||||
filesEditor.geoAssetsDir.path,
|
|
||||||
pollingDelay: const Duration(seconds: 1),
|
|
||||||
).events.asyncMap((event) async {
|
|
||||||
await _readGeoFiles();
|
|
||||||
return _geoFiles;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<Iterable<File>> _readGeoFiles() async {
|
|
||||||
return _geoFiles = Directory(filesEditor.geoAssetsDir.path)
|
|
||||||
.listSync()
|
|
||||||
.whereType<File>()
|
|
||||||
.where((e) => e.extension == '.db');
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TaskEither<GeoAssetFailure, Unit> update(GeoAsset geoAsset) {
|
|
||||||
return exceptionHandler(
|
|
||||||
() async {
|
|
||||||
loggy.debug(
|
|
||||||
"checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
|
|
||||||
);
|
|
||||||
final response = await dio.get<Map>(geoAsset.repositoryUrl);
|
|
||||||
if (response.statusCode != 200 || response.data == null) {
|
|
||||||
return left(
|
|
||||||
GeoAssetFailure.unexpected("invalid response", StackTrace.current),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final path =
|
|
||||||
filesEditor.geoAssetPath(geoAsset.providerName, geoAsset.name);
|
|
||||||
final tagName = response.data!['tag_name'] as String;
|
|
||||||
loggy.debug("latest release of [${geoAsset.name}]: [$tagName]");
|
|
||||||
if (tagName == geoAsset.version && await File(path).exists()) {
|
|
||||||
await geoAssetsDao.edit(geoAsset.copyWith(lastCheck: DateTime.now()));
|
|
||||||
return left(const GeoAssetFailure.noUpdateAvailable());
|
|
||||||
}
|
|
||||||
|
|
||||||
final assets = (response.data!['assets'] as List)
|
|
||||||
.whereType<Map<String, dynamic>>();
|
|
||||||
final asset =
|
|
||||||
assets.firstOrNullWhere((e) => e["name"] == geoAsset.name);
|
|
||||||
if (asset == null) {
|
|
||||||
return left(
|
|
||||||
GeoAssetFailure.unexpected(
|
|
||||||
"couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
|
|
||||||
StackTrace.current,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final downloadUrl = asset["browser_download_url"] as String;
|
|
||||||
loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]");
|
|
||||||
final tempPath = "$path.tmp";
|
|
||||||
await File(path).parent.create(recursive: true);
|
|
||||||
await dio.download(downloadUrl, tempPath);
|
|
||||||
await File(tempPath).rename(path);
|
|
||||||
|
|
||||||
await geoAssetsDao.edit(
|
|
||||||
geoAsset.copyWith(
|
|
||||||
version: tagName,
|
|
||||||
lastCheck: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
return right(unit);
|
|
||||||
},
|
|
||||||
GeoAssetFailure.unexpected,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAsset geoAsset) {
|
|
||||||
return exceptionHandler(
|
|
||||||
() async {
|
|
||||||
await geoAssetsDao.edit(geoAsset.copyWith(active: true));
|
|
||||||
return right(unit);
|
|
||||||
},
|
|
||||||
GeoAssetFailure.unexpected,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
TaskEither<GeoAssetFailure, Unit> addRecommended() {
|
|
||||||
return exceptionHandler(
|
|
||||||
() async {
|
|
||||||
final persistedIds = await geoAssetsDao
|
|
||||||
.watchAll()
|
|
||||||
.first
|
|
||||||
.then((value) => value.map((e) => e.id));
|
|
||||||
final missing =
|
|
||||||
recommendedGeoAssets.where((e) => !persistedIds.contains(e.id));
|
|
||||||
for (final geoAsset in missing) {
|
|
||||||
await geoAssetsDao.add(geoAsset);
|
|
||||||
}
|
|
||||||
return right(unit);
|
|
||||||
},
|
|
||||||
GeoAssetFailure.unexpected,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,7 @@ import 'dart:io';
|
|||||||
|
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:fpdart/fpdart.dart';
|
import 'package:fpdart/fpdart.dart';
|
||||||
import 'package:hiddify/data/local/dao/dao.dart';
|
import 'package:hiddify/data/local/dao/profiles_dao.dart';
|
||||||
import 'package:hiddify/data/repository/exception_handlers.dart';
|
import 'package:hiddify/data/repository/exception_handlers.dart';
|
||||||
import 'package:hiddify/domain/enums.dart';
|
import 'package:hiddify/domain/enums.dart';
|
||||||
import 'package:hiddify/domain/profiles/profiles.dart';
|
import 'package:hiddify/domain/profiles/profiles.dart';
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
|
||||||
|
|
||||||
part 'geo_asset.freezed.dart';
|
|
||||||
part 'geo_asset.g.dart';
|
|
||||||
|
|
||||||
enum GeoAssetType { geoip, geosite }
|
|
||||||
|
|
||||||
typedef GeoAssetWithFileSize = (GeoAsset geoAsset, int? size);
|
|
||||||
|
|
||||||
@freezed
|
|
||||||
class GeoAsset with _$GeoAsset {
|
|
||||||
const GeoAsset._();
|
|
||||||
|
|
||||||
const factory GeoAsset({
|
|
||||||
required String id,
|
|
||||||
required String name,
|
|
||||||
required GeoAssetType type,
|
|
||||||
required bool active,
|
|
||||||
required String providerName,
|
|
||||||
String? version,
|
|
||||||
DateTime? lastCheck,
|
|
||||||
}) = _GeoAsset;
|
|
||||||
|
|
||||||
factory GeoAsset.fromJson(Map<String, dynamic> json) =>
|
|
||||||
_$GeoAssetFromJson(json);
|
|
||||||
|
|
||||||
String get fileName => name;
|
|
||||||
|
|
||||||
String get repositoryUrl =>
|
|
||||||
"https://api.github.com/repos/$providerName/releases/latest";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// default geoip asset bundled with the app
|
|
||||||
const defaultGeoip = GeoAsset(
|
|
||||||
id: "sing-box-geoip",
|
|
||||||
name: "geoip.db",
|
|
||||||
type: GeoAssetType.geoip,
|
|
||||||
active: true,
|
|
||||||
providerName: "SagerNet/sing-geoip",
|
|
||||||
);
|
|
||||||
|
|
||||||
/// default geosite asset bundled with the app
|
|
||||||
const defaultGeosite = GeoAsset(
|
|
||||||
id: "sing-box-geosite",
|
|
||||||
name: "geosite.db",
|
|
||||||
type: GeoAssetType.geosite,
|
|
||||||
active: true,
|
|
||||||
providerName: "SagerNet/sing-geosite",
|
|
||||||
);
|
|
||||||
|
|
||||||
const defaultGeoAssets = [defaultGeoip, defaultGeosite];
|
|
||||||
|
|
||||||
const recommendedGeoAssets = [
|
|
||||||
...defaultGeoAssets,
|
|
||||||
GeoAsset(
|
|
||||||
id: "chocolate4U-geoip",
|
|
||||||
name: "geoip.db",
|
|
||||||
type: GeoAssetType.geoip,
|
|
||||||
active: false,
|
|
||||||
providerName: "Chocolate4U/Iran-sing-box-rules",
|
|
||||||
),
|
|
||||||
GeoAsset(
|
|
||||||
id: "chocolate4U-geosite",
|
|
||||||
name: "geosite.db",
|
|
||||||
type: GeoAssetType.geosite,
|
|
||||||
active: false,
|
|
||||||
providerName: "Chocolate4U/Iran-sing-box-rules",
|
|
||||||
),
|
|
||||||
];
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
import 'package:fpdart/fpdart.dart';
|
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
|
||||||
import 'package:hiddify/domain/rules/geo_asset_failure.dart';
|
|
||||||
|
|
||||||
abstract interface class GeoAssetsRepository {
|
|
||||||
TaskEither<GeoAssetFailure, ({GeoAsset geoip, GeoAsset geosite})>
|
|
||||||
getActivePair();
|
|
||||||
|
|
||||||
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll();
|
|
||||||
|
|
||||||
TaskEither<GeoAssetFailure, Unit> update(GeoAsset geoAsset);
|
|
||||||
|
|
||||||
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAsset geoAsset);
|
|
||||||
|
|
||||||
TaskEither<GeoAssetFailure, Unit> addRecommended();
|
|
||||||
}
|
|
||||||
31
lib/features/geo_asset/data/geo_asset_data_mapper.dart
Normal file
31
lib/features/geo_asset/data/geo_asset_data_mapper.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hiddify/data/local/database.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
|
||||||
|
extension GeoAssetEntityMapper on GeoAssetEntity {
|
||||||
|
GeoAssetEntriesCompanion toEntry() {
|
||||||
|
return GeoAssetEntriesCompanion.insert(
|
||||||
|
id: id,
|
||||||
|
type: type,
|
||||||
|
active: active,
|
||||||
|
name: name,
|
||||||
|
providerName: providerName,
|
||||||
|
version: Value(version),
|
||||||
|
lastCheck: Value(lastCheck),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension GeoAssetEntryMapper on GeoAssetEntry {
|
||||||
|
GeoAssetEntity toEntity() {
|
||||||
|
return GeoAssetEntity(
|
||||||
|
id: id,
|
||||||
|
name: name,
|
||||||
|
type: type,
|
||||||
|
active: active,
|
||||||
|
providerName: providerName,
|
||||||
|
version: version,
|
||||||
|
lastCheck: lastCheck,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/features/geo_asset/data/geo_asset_data_providers.dart
Normal file
31
lib/features/geo_asset/data/geo_asset_data_providers.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:hiddify/data/data_providers.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.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/services/service_providers.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'geo_asset_data_providers.g.dart';
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
Future<GeoAssetRepository> geoAssetRepository(GeoAssetRepositoryRef ref) async {
|
||||||
|
final repo = GeoAssetRepositoryImpl(
|
||||||
|
geoAssetDataSource: ref.watch(geoAssetDataSourceProvider),
|
||||||
|
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
|
||||||
|
dio: ref.watch(dioProvider),
|
||||||
|
);
|
||||||
|
await repo.init().getOrElse((l) => throw l).run();
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
GeoAssetDataSource geoAssetDataSource(GeoAssetDataSourceRef ref) {
|
||||||
|
return GeoAssetsDao(ref.watch(appDatabaseProvider));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Riverpod(keepAlive: true)
|
||||||
|
GeoAssetPathResolver geoAssetPathResolver(GeoAssetPathResolverRef ref) {
|
||||||
|
return GeoAssetPathResolver(
|
||||||
|
ref.watch(filesEditorServiceProvider).dirs.workingDir,
|
||||||
|
);
|
||||||
|
}
|
||||||
59
lib/features/geo_asset/data/geo_asset_data_source.dart
Normal file
59
lib/features/geo_asset/data/geo_asset_data_source.dart
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:hiddify/data/local/database.dart';
|
||||||
|
import 'package:hiddify/data/local/tables.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
import 'package:hiddify/utils/custom_loggers.dart';
|
||||||
|
|
||||||
|
part 'geo_asset_data_source.g.dart';
|
||||||
|
|
||||||
|
abstract interface class GeoAssetDataSource {
|
||||||
|
Future<void> insert(GeoAssetEntriesCompanion entry);
|
||||||
|
Future<GeoAssetEntry?> getActiveAssetByType(GeoAssetType type);
|
||||||
|
Stream<List<GeoAssetEntry>> watchAll();
|
||||||
|
Future<void> patch(String id, GeoAssetEntriesCompanion entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DriftAccessor(tables: [GeoAssetEntries])
|
||||||
|
class GeoAssetsDao extends DatabaseAccessor<AppDatabase>
|
||||||
|
with _$GeoAssetsDaoMixin, InfraLogger
|
||||||
|
implements GeoAssetDataSource {
|
||||||
|
GeoAssetsDao(super.db);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> insert(GeoAssetEntriesCompanion entry) async {
|
||||||
|
await into(geoAssetEntries).insert(entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<GeoAssetEntry?> getActiveAssetByType(GeoAssetType type) async {
|
||||||
|
return (geoAssetEntries.select()
|
||||||
|
..where((tbl) => tbl.active.equals(true))
|
||||||
|
..where((tbl) => tbl.type.equalsValue(type))
|
||||||
|
..limit(1))
|
||||||
|
.getSingleOrNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<List<GeoAssetEntry>> watchAll() {
|
||||||
|
return geoAssetEntries.select().watch();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> patch(String id, GeoAssetEntriesCompanion entry) async {
|
||||||
|
await transaction(
|
||||||
|
() async {
|
||||||
|
if (entry.active.present && entry.active.value) {
|
||||||
|
final baseEntry = await (select(geoAssetEntries)
|
||||||
|
..where((tbl) => tbl.id.equals(id)))
|
||||||
|
.getSingle();
|
||||||
|
await (update(geoAssetEntries)
|
||||||
|
..where((tbl) => tbl.active.equals(true))
|
||||||
|
..where((tbl) => tbl.type.equalsValue(baseEntry.type)))
|
||||||
|
.write(const GeoAssetEntriesCompanion(active: Value(false)));
|
||||||
|
}
|
||||||
|
await (update(geoAssetEntries)..where((tbl) => tbl.id.equals(id)))
|
||||||
|
.write(entry);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
31
lib/features/geo_asset/data/geo_asset_path_resolver.dart
Normal file
31
lib/features/geo_asset/data/geo_asset_path_resolver.dart
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:path/path.dart' as p;
|
||||||
|
|
||||||
|
class GeoAssetPathResolver {
|
||||||
|
const GeoAssetPathResolver(this._workingDir);
|
||||||
|
|
||||||
|
final Directory _workingDir;
|
||||||
|
|
||||||
|
Directory get directory => Directory(p.join(_workingDir.path, "geo-assets"));
|
||||||
|
|
||||||
|
File file(String providerName, String fileName) {
|
||||||
|
final prefix = providerName.replaceAll("/", "-").toLowerCase().trim();
|
||||||
|
return File(
|
||||||
|
p.join(
|
||||||
|
directory.path,
|
||||||
|
"$prefix${prefix.isEmpty ? "" : "-"}$fileName",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// geoasset's path relative to working directory
|
||||||
|
String relativePath(String providerName, String fileName) {
|
||||||
|
final fullPath = file(providerName, fileName).path;
|
||||||
|
return p.relative(fullPath, from: _workingDir.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
String resolvePath(String path) {
|
||||||
|
return p.absolute(_workingDir.path, path);
|
||||||
|
}
|
||||||
|
}
|
||||||
232
lib/features/geo_asset/data/geo_asset_repository.dart
Normal file
232
lib/features/geo_asset/data/geo_asset_repository.dart
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
import 'package:dartx/dartx_io.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import 'package:drift/drift.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import 'package:hiddify/data/local/database.dart';
|
||||||
|
import 'package:hiddify/data/repository/exception_handlers.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_mapper.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart';
|
||||||
|
import 'package:hiddify/gen/assets.gen.dart';
|
||||||
|
import 'package:hiddify/utils/custom_loggers.dart';
|
||||||
|
import 'package:rxdart/rxdart.dart';
|
||||||
|
import 'package:watcher/watcher.dart';
|
||||||
|
|
||||||
|
abstract interface class GeoAssetRepository {
|
||||||
|
/// populate bundled geo assets directory with bundled files if needed
|
||||||
|
TaskEither<GeoAssetFailure, Unit> init();
|
||||||
|
TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})>
|
||||||
|
getActivePair();
|
||||||
|
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll();
|
||||||
|
TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset);
|
||||||
|
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset);
|
||||||
|
TaskEither<GeoAssetFailure, Unit> addRecommended();
|
||||||
|
}
|
||||||
|
|
||||||
|
class GeoAssetRepositoryImpl
|
||||||
|
with ExceptionHandler, InfraLogger
|
||||||
|
implements GeoAssetRepository {
|
||||||
|
GeoAssetRepositoryImpl({
|
||||||
|
required this.geoAssetDataSource,
|
||||||
|
required this.geoAssetPathResolver,
|
||||||
|
required this.dio,
|
||||||
|
});
|
||||||
|
|
||||||
|
final GeoAssetDataSource geoAssetDataSource;
|
||||||
|
final GeoAssetPathResolver geoAssetPathResolver;
|
||||||
|
final Dio dio;
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<GeoAssetFailure, Unit> init() {
|
||||||
|
return exceptionHandler(
|
||||||
|
() async {
|
||||||
|
loggy.debug("initializing");
|
||||||
|
final geoipFile = geoAssetPathResolver.file(
|
||||||
|
defaultGeoip.providerName,
|
||||||
|
defaultGeoip.fileName,
|
||||||
|
);
|
||||||
|
final geositeFile = geoAssetPathResolver.file(
|
||||||
|
defaultGeosite.providerName,
|
||||||
|
defaultGeosite.fileName,
|
||||||
|
);
|
||||||
|
|
||||||
|
final dirExists = await geoAssetPathResolver.directory.exists();
|
||||||
|
if (!dirExists) {
|
||||||
|
await geoAssetPathResolver.directory.create(recursive: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!dirExists || !await geoipFile.exists()) {
|
||||||
|
final bundledGeoip = await rootBundle.load(Assets.core.geoip);
|
||||||
|
await geoipFile.writeAsBytes(bundledGeoip.buffer.asInt8List());
|
||||||
|
}
|
||||||
|
if (!dirExists || !await geositeFile.exists()) {
|
||||||
|
final bundledGeosite = await rootBundle.load(Assets.core.geosite);
|
||||||
|
await geositeFile.writeAsBytes(bundledGeosite.buffer.asInt8List());
|
||||||
|
}
|
||||||
|
return right(unit);
|
||||||
|
},
|
||||||
|
GeoAssetUnexpectedFailure.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})>
|
||||||
|
getActivePair() {
|
||||||
|
return exceptionHandler(
|
||||||
|
() async {
|
||||||
|
final geoip =
|
||||||
|
await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geoip);
|
||||||
|
final geosite =
|
||||||
|
await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geosite);
|
||||||
|
if (geoip == null || geosite == null) {
|
||||||
|
return left(const GeoAssetFailure.activeAssetNotFound());
|
||||||
|
}
|
||||||
|
return right((geoip: geoip.toEntity(), geosite: geosite.toEntity()));
|
||||||
|
},
|
||||||
|
GeoAssetUnexpectedFailure.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll() {
|
||||||
|
final persistedStream = geoAssetDataSource
|
||||||
|
.watchAll()
|
||||||
|
.map((event) => event.map((e) => e.toEntity()));
|
||||||
|
final filesStream = _watchGeoFiles();
|
||||||
|
|
||||||
|
return Rx.combineLatest2(
|
||||||
|
persistedStream,
|
||||||
|
filesStream,
|
||||||
|
(assets, files) => assets.map(
|
||||||
|
(e) {
|
||||||
|
final path =
|
||||||
|
geoAssetPathResolver.file(e.providerName, e.fileName).path;
|
||||||
|
final file = files.firstOrNullWhere((e) => e.path == path);
|
||||||
|
final stat = file?.statSync();
|
||||||
|
return (e, stat?.size);
|
||||||
|
},
|
||||||
|
).toList(),
|
||||||
|
).handleExceptions(GeoAssetUnexpectedFailure.new);
|
||||||
|
}
|
||||||
|
|
||||||
|
Iterable<File> _geoFiles = [];
|
||||||
|
Stream<Iterable<File>> _watchGeoFiles() async* {
|
||||||
|
yield await _readGeoFiles();
|
||||||
|
yield* Watcher(
|
||||||
|
geoAssetPathResolver.directory.path,
|
||||||
|
pollingDelay: const Duration(seconds: 1),
|
||||||
|
).events.asyncMap((event) async {
|
||||||
|
await _readGeoFiles();
|
||||||
|
return _geoFiles;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Iterable<File>> _readGeoFiles() async {
|
||||||
|
return _geoFiles = Directory(geoAssetPathResolver.directory.path)
|
||||||
|
.listSync()
|
||||||
|
.whereType<File>()
|
||||||
|
.where((e) => e.extension == '.db');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset) {
|
||||||
|
return exceptionHandler(
|
||||||
|
() async {
|
||||||
|
loggy.debug(
|
||||||
|
"checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
|
||||||
|
);
|
||||||
|
final response = await dio.get<Map>(geoAsset.repositoryUrl);
|
||||||
|
if (response.statusCode != 200 || response.data == null) {
|
||||||
|
return left(
|
||||||
|
GeoAssetUnexpectedFailure.new(
|
||||||
|
"invalid response",
|
||||||
|
StackTrace.current,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final file =
|
||||||
|
geoAssetPathResolver.file(geoAsset.providerName, geoAsset.name);
|
||||||
|
final tagName = response.data!['tag_name'] as String;
|
||||||
|
loggy.debug("latest release of [${geoAsset.name}]: [$tagName]");
|
||||||
|
if (tagName == geoAsset.version && await file.exists()) {
|
||||||
|
await geoAssetDataSource.patch(
|
||||||
|
geoAsset.id,
|
||||||
|
GeoAssetEntriesCompanion(lastCheck: Value(DateTime.now())),
|
||||||
|
);
|
||||||
|
return left(const GeoAssetFailure.noUpdateAvailable());
|
||||||
|
}
|
||||||
|
|
||||||
|
final assets = (response.data!['assets'] as List)
|
||||||
|
.whereType<Map<String, dynamic>>();
|
||||||
|
final asset =
|
||||||
|
assets.firstOrNullWhere((e) => e["name"] == geoAsset.name);
|
||||||
|
if (asset == null) {
|
||||||
|
return left(
|
||||||
|
GeoAssetUnexpectedFailure.new(
|
||||||
|
"couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
|
||||||
|
StackTrace.current,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
final downloadUrl = asset["browser_download_url"] as String;
|
||||||
|
loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]");
|
||||||
|
final tempPath = "${file.path}.tmp";
|
||||||
|
await file.parent.create(recursive: true);
|
||||||
|
await dio.download(downloadUrl, tempPath);
|
||||||
|
await File(tempPath).rename(file.path);
|
||||||
|
|
||||||
|
await geoAssetDataSource.patch(
|
||||||
|
geoAsset.id,
|
||||||
|
GeoAssetEntriesCompanion(
|
||||||
|
version: Value(tagName),
|
||||||
|
lastCheck: Value(DateTime.now()),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
return right(unit);
|
||||||
|
},
|
||||||
|
GeoAssetUnexpectedFailure.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset) {
|
||||||
|
return exceptionHandler(
|
||||||
|
() async {
|
||||||
|
await geoAssetDataSource.patch(
|
||||||
|
geoAsset.id,
|
||||||
|
const GeoAssetEntriesCompanion(active: Value(true)),
|
||||||
|
);
|
||||||
|
return right(unit);
|
||||||
|
},
|
||||||
|
GeoAssetUnexpectedFailure.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
TaskEither<GeoAssetFailure, Unit> addRecommended() {
|
||||||
|
return exceptionHandler(
|
||||||
|
() async {
|
||||||
|
final persistedIds = await geoAssetDataSource
|
||||||
|
.watchAll()
|
||||||
|
.first
|
||||||
|
.then((value) => value.map((e) => e.id));
|
||||||
|
final missing =
|
||||||
|
recommendedGeoAssets.where((e) => !persistedIds.contains(e.id));
|
||||||
|
for (final geoAsset in missing) {
|
||||||
|
await geoAssetDataSource.insert(geoAsset.toEntry());
|
||||||
|
}
|
||||||
|
return right(unit);
|
||||||
|
},
|
||||||
|
GeoAssetUnexpectedFailure.new,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/features/geo_asset/model/default_geo_assets.dart
Normal file
39
lib/features/geo_asset/model/default_geo_assets.dart
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
|
||||||
|
/// default geoip asset bundled with the app
|
||||||
|
const defaultGeoip = GeoAssetEntity(
|
||||||
|
id: "sing-box-geoip",
|
||||||
|
name: "geoip.db",
|
||||||
|
type: GeoAssetType.geoip,
|
||||||
|
active: true,
|
||||||
|
providerName: "SagerNet/sing-geoip",
|
||||||
|
);
|
||||||
|
|
||||||
|
/// default geosite asset bundled with the app
|
||||||
|
const defaultGeosite = GeoAssetEntity(
|
||||||
|
id: "sing-box-geosite",
|
||||||
|
name: "geosite.db",
|
||||||
|
type: GeoAssetType.geosite,
|
||||||
|
active: true,
|
||||||
|
providerName: "SagerNet/sing-geosite",
|
||||||
|
);
|
||||||
|
|
||||||
|
const defaultGeoAssets = [defaultGeoip, defaultGeosite];
|
||||||
|
|
||||||
|
const recommendedGeoAssets = [
|
||||||
|
...defaultGeoAssets,
|
||||||
|
GeoAssetEntity(
|
||||||
|
id: "chocolate4U-geoip",
|
||||||
|
name: "geoip.db",
|
||||||
|
type: GeoAssetType.geoip,
|
||||||
|
active: false,
|
||||||
|
providerName: "Chocolate4U/Iran-sing-box-rules",
|
||||||
|
),
|
||||||
|
GeoAssetEntity(
|
||||||
|
id: "chocolate4U-geosite",
|
||||||
|
name: "geosite.db",
|
||||||
|
type: GeoAssetType.geosite,
|
||||||
|
active: false,
|
||||||
|
providerName: "Chocolate4U/Iran-sing-box-rules",
|
||||||
|
),
|
||||||
|
];
|
||||||
27
lib/features/geo_asset/model/geo_asset_entity.dart
Normal file
27
lib/features/geo_asset/model/geo_asset_entity.dart
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
|
part 'geo_asset_entity.freezed.dart';
|
||||||
|
|
||||||
|
enum GeoAssetType { geoip, geosite }
|
||||||
|
|
||||||
|
typedef GeoAssetWithFileSize = (GeoAssetEntity geoAsset, int? size);
|
||||||
|
|
||||||
|
@freezed
|
||||||
|
class GeoAssetEntity with _$GeoAssetEntity {
|
||||||
|
const GeoAssetEntity._();
|
||||||
|
|
||||||
|
const factory GeoAssetEntity({
|
||||||
|
required String id,
|
||||||
|
required String name,
|
||||||
|
required GeoAssetType type,
|
||||||
|
required bool active,
|
||||||
|
required String providerName,
|
||||||
|
String? version,
|
||||||
|
DateTime? lastCheck,
|
||||||
|
}) = _GeoAssetEntity;
|
||||||
|
|
||||||
|
String get fileName => name;
|
||||||
|
|
||||||
|
String get repositoryUrl =>
|
||||||
|
"https://api.github.com/repos/$providerName/releases/latest";
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ part 'geo_asset_failure.freezed.dart';
|
|||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
sealed class GeoAssetFailure with _$GeoAssetFailure, Failure {
|
sealed class GeoAssetFailure with _$GeoAssetFailure, Failure {
|
||||||
const GeoAssetFailure._();
|
const GeoAssetFailure._();
|
||||||
|
|
||||||
const factory GeoAssetFailure.unexpected([
|
const factory GeoAssetFailure.unexpected([
|
||||||
Object? error,
|
Object? error,
|
||||||
33
lib/features/geo_asset/notifier/geo_asset_notifier.dart
Normal file
33
lib/features/geo_asset/notifier/geo_asset_notifier.dart
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
import 'package:hiddify/utils/custom_loggers.dart';
|
||||||
|
import 'package:hiddify/utils/riverpod_utils.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'geo_asset_notifier.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class FetchGeoAsset extends _$FetchGeoAsset with AppLogger {
|
||||||
|
@override
|
||||||
|
Future<Unit?> build(String id) async {
|
||||||
|
ref.disposeDelay(const Duration(seconds: 10));
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> fetch(GeoAssetEntity geoAsset) async {
|
||||||
|
state = const AsyncLoading();
|
||||||
|
state = await AsyncValue.guard(
|
||||||
|
() => ref
|
||||||
|
.read(geoAssetRepositoryProvider)
|
||||||
|
.requireValue
|
||||||
|
.update(geoAsset)
|
||||||
|
.getOrElse(
|
||||||
|
(failure) {
|
||||||
|
loggy.warning("error updating geo asset $failure", failure);
|
||||||
|
throw failure;
|
||||||
|
},
|
||||||
|
).run(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart';
|
||||||
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
|
import 'package:hiddify/utils/custom_loggers.dart';
|
||||||
|
import 'package:hiddify/utils/riverpod_utils.dart';
|
||||||
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
part 'geo_assets_overview_notifier.g.dart';
|
||||||
|
|
||||||
|
@riverpod
|
||||||
|
class GeoAssetsOverviewNotifier extends _$GeoAssetsOverviewNotifier
|
||||||
|
with AppLogger {
|
||||||
|
@override
|
||||||
|
Stream<List<GeoAssetWithFileSize>> build() {
|
||||||
|
ref.disposeDelay(const Duration(seconds: 5));
|
||||||
|
return ref
|
||||||
|
.watch(geoAssetRepositoryProvider)
|
||||||
|
.requireValue
|
||||||
|
.watchAll()
|
||||||
|
.map((event) => event.getOrElse((l) => throw l));
|
||||||
|
}
|
||||||
|
|
||||||
|
GeoAssetRepository get _geoAssetRepo =>
|
||||||
|
ref.read(geoAssetRepositoryProvider).requireValue;
|
||||||
|
|
||||||
|
Future<void> markAsActive(GeoAssetEntity geoAsset) async {
|
||||||
|
await _geoAssetRepo.markAsActive(geoAsset).getOrElse(
|
||||||
|
(f) {
|
||||||
|
loggy.warning("error marking geo asset as active", f);
|
||||||
|
throw f;
|
||||||
|
},
|
||||||
|
).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> addRecommended() async {
|
||||||
|
await _geoAssetRepo.addRecommended().getOrElse(
|
||||||
|
(f) {
|
||||||
|
loggy.warning("error adding recommended geo assets", f);
|
||||||
|
throw f;
|
||||||
|
},
|
||||||
|
).run();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart';
|
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart';
|
||||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart';
|
import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
class GeoAssetsPage extends HookConsumerWidget {
|
class GeoAssetsOverviewPage extends HookConsumerWidget {
|
||||||
const GeoAssetsPage({super.key});
|
const GeoAssetsOverviewPage({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
final state = ref.watch(geoAssetsNotifierProvider);
|
final state = ref.watch(geoAssetsOverviewNotifierProvider);
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
body: CustomScrollView(
|
body: CustomScrollView(
|
||||||
@@ -25,7 +25,7 @@ class GeoAssetsPage extends HookConsumerWidget {
|
|||||||
child: Text(t.settings.geoAssets.addRecommended),
|
child: Text(t.settings.geoAssets.addRecommended),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ref
|
ref
|
||||||
.read(geoAssetsNotifierProvider.notifier)
|
.read(geoAssetsOverviewNotifierProvider.notifier)
|
||||||
.addRecommended();
|
.addRecommended();
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
@@ -38,7 +38,12 @@ class GeoAssetsPage extends HookConsumerWidget {
|
|||||||
AsyncData(value: final geoAssets) => SliverList.builder(
|
AsyncData(value: final geoAssets) => SliverList.builder(
|
||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final geoAsset = geoAssets[index];
|
final geoAsset = geoAssets[index];
|
||||||
return GeoAssetTile(geoAsset);
|
return GeoAssetTile(
|
||||||
|
geoAsset,
|
||||||
|
onMarkAsActive: () => ref
|
||||||
|
.read(geoAssetsOverviewNotifierProvider.notifier)
|
||||||
|
.markAsActive(geoAsset.$1),
|
||||||
|
);
|
||||||
},
|
},
|
||||||
itemCount: geoAssets.length,
|
itemCount: geoAssets.length,
|
||||||
),
|
),
|
||||||
@@ -2,40 +2,44 @@ import 'package:dartx/dartx.dart';
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:hiddify/core/core_providers.dart';
|
import 'package:hiddify/core/core_providers.dart';
|
||||||
import 'package:hiddify/domain/failures.dart';
|
import 'package:hiddify/domain/failures.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_asset_failure.dart';
|
import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart';
|
||||||
import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart';
|
import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart';
|
||||||
import 'package:hiddify/utils/alerts.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:hiddify/utils/async_mutation.dart';
|
|
||||||
import 'package:hiddify/utils/date_time_formatter.dart';
|
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:humanizer/humanizer.dart';
|
import 'package:humanizer/humanizer.dart';
|
||||||
|
|
||||||
class GeoAssetTile extends HookConsumerWidget {
|
class GeoAssetTile extends HookConsumerWidget {
|
||||||
GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key})
|
GeoAssetTile(
|
||||||
: geoAsset = geoAssetWithFileSize.$1,
|
GeoAssetWithFileSize geoAssetWithFileSize, {
|
||||||
|
super.key,
|
||||||
|
required this.onMarkAsActive,
|
||||||
|
}) : geoAsset = geoAssetWithFileSize.$1,
|
||||||
size = geoAssetWithFileSize.$2;
|
size = geoAssetWithFileSize.$2;
|
||||||
|
|
||||||
final GeoAsset geoAsset;
|
final GeoAssetEntity geoAsset;
|
||||||
final int? size;
|
final int? size;
|
||||||
|
final VoidCallback onMarkAsActive;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final t = ref.watch(translationsProvider);
|
final t = ref.watch(translationsProvider);
|
||||||
|
final fetchState = ref.watch(fetchGeoAssetProvider(geoAsset.id));
|
||||||
final fileMissing = size == null;
|
final fileMissing = size == null;
|
||||||
|
|
||||||
final updateMutation = useMutation(
|
ref.listen(
|
||||||
initialOnFailure: (err) {
|
fetchGeoAssetProvider(geoAsset.id),
|
||||||
if (err case GeoAssetNoUpdateAvailable()) {
|
(_, next) {
|
||||||
CustomToast(t.failure.geoAssets.notUpdate).show(context);
|
switch (next) {
|
||||||
} else {
|
case AsyncError(:final error):
|
||||||
CustomAlertDialog.fromErr(
|
if (error case GeoAssetNoUpdateAvailable()) {
|
||||||
t.presentError(err, action: t.settings.geoAssets.failureMsg),
|
return CustomToast(t.failure.geoAssets.notUpdate).show(context);
|
||||||
).show(context);
|
}
|
||||||
|
CustomAlertDialog.fromErr(t.presentError(error)).show(context);
|
||||||
|
case AsyncData(value: final _?):
|
||||||
|
CustomToast.success(t.settings.geoAssets.successMsg).show(context);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
initialOnSuccess: () =>
|
|
||||||
CustomToast.success(t.settings.geoAssets.successMsg).show(context),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return ListTile(
|
return ListTile(
|
||||||
@@ -49,7 +53,7 @@ class GeoAssetTile extends HookConsumerWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
isThreeLine: true,
|
isThreeLine: true,
|
||||||
subtitle: updateMutation.state.isInProgress
|
subtitle: fetchState.isLoading
|
||||||
? const LinearProgressIndicator()
|
? const LinearProgressIndicator()
|
||||||
: Row(
|
: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
@@ -89,26 +93,15 @@ class GeoAssetTile extends HookConsumerWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
selected: geoAsset.active,
|
selected: geoAsset.active,
|
||||||
onTap: () async {
|
onTap: onMarkAsActive,
|
||||||
await ref
|
|
||||||
.read(geoAssetsNotifierProvider.notifier)
|
|
||||||
.markAsActive(geoAsset);
|
|
||||||
},
|
|
||||||
trailing: PopupMenuButton(
|
trailing: PopupMenuButton(
|
||||||
itemBuilder: (context) {
|
itemBuilder: (context) {
|
||||||
return [
|
return [
|
||||||
PopupMenuItem(
|
PopupMenuItem(
|
||||||
enabled: !updateMutation.state.isInProgress,
|
enabled: !fetchState.isLoading,
|
||||||
onTap: () {
|
onTap: () => ref
|
||||||
if (updateMutation.state.isInProgress) {
|
.read(FetchGeoAssetProvider(geoAsset.id).notifier)
|
||||||
return;
|
.fetch(geoAsset),
|
||||||
}
|
|
||||||
updateMutation.setFuture(
|
|
||||||
ref
|
|
||||||
.read(geoAssetsNotifierProvider.notifier)
|
|
||||||
.updateGeoAsset(geoAsset),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
child: fileMissing
|
child: fileMissing
|
||||||
? Text(t.settings.geoAssets.download)
|
? Text(t.settings.geoAssets.download)
|
||||||
: Text(t.settings.geoAssets.update),
|
: Text(t.settings.geoAssets.update),
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
import 'package:hiddify/data/data_providers.dart';
|
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
|
||||||
import 'package:hiddify/utils/custom_loggers.dart';
|
|
||||||
import 'package:hiddify/utils/riverpod_utils.dart';
|
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
||||||
|
|
||||||
part 'geo_assets_notifier.g.dart';
|
|
||||||
|
|
||||||
@riverpod
|
|
||||||
class GeoAssetsNotifier extends _$GeoAssetsNotifier with AppLogger {
|
|
||||||
@override
|
|
||||||
Stream<List<GeoAssetWithFileSize>> build() {
|
|
||||||
ref.disposeDelay(const Duration(seconds: 5));
|
|
||||||
return ref
|
|
||||||
.watch(geoAssetsRepositoryProvider)
|
|
||||||
.watchAll()
|
|
||||||
.map((event) => event.getOrElse((l) => throw l));
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> updateGeoAsset(GeoAsset geoAsset) async {
|
|
||||||
await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse(
|
|
||||||
(f) {
|
|
||||||
loggy.warning("error updating geo asset", f);
|
|
||||||
throw f;
|
|
||||||
},
|
|
||||||
).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> markAsActive(GeoAsset geoAsset) async {
|
|
||||||
await ref
|
|
||||||
.read(geoAssetsRepositoryProvider)
|
|
||||||
.markAsActive(geoAsset)
|
|
||||||
.getOrElse(
|
|
||||||
(f) {
|
|
||||||
loggy.warning("error marking geo asset as active", f);
|
|
||||||
throw f;
|
|
||||||
},
|
|
||||||
).run();
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> addRecommended() async {
|
|
||||||
await ref.read(geoAssetsRepositoryProvider).addRecommended().getOrElse(
|
|
||||||
(f) {
|
|
||||||
loggy.warning("error adding recommended geo assets", f);
|
|
||||||
throw f;
|
|
||||||
},
|
|
||||||
).run();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,6 @@
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dartx/dartx.dart';
|
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:hiddify/domain/constants.dart';
|
import 'package:hiddify/domain/constants.dart';
|
||||||
import 'package:hiddify/domain/rules/geo_asset.dart';
|
|
||||||
import 'package:hiddify/gen/assets.gen.dart';
|
|
||||||
import 'package:hiddify/services/platform_services.dart';
|
import 'package:hiddify/services/platform_services.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
import 'package:path/path.dart' as p;
|
import 'package:path/path.dart' as p;
|
||||||
@@ -28,9 +24,6 @@ class FilesEditorService with InfraLogger {
|
|||||||
Directory(p.join(workingDir.path, Constants.configsFolderName));
|
Directory(p.join(workingDir.path, Constants.configsFolderName));
|
||||||
Directory get logsDir => dirs.workingDir;
|
Directory get logsDir => dirs.workingDir;
|
||||||
|
|
||||||
Directory get geoAssetsDir =>
|
|
||||||
Directory(p.join(workingDir.path, "geo-assets"));
|
|
||||||
|
|
||||||
File get appLogsFile => File(p.join(logsDir.path, "app.log"));
|
File get appLogsFile => File(p.join(logsDir.path, "app.log"));
|
||||||
File get coreLogsFile => File(p.join(logsDir.path, "box.log"));
|
File get coreLogsFile => File(p.join(logsDir.path, "box.log"));
|
||||||
|
|
||||||
@@ -53,9 +46,6 @@ class FilesEditorService with InfraLogger {
|
|||||||
if (!await configsDir.exists()) {
|
if (!await configsDir.exists()) {
|
||||||
await configsDir.create(recursive: true);
|
await configsDir.create(recursive: true);
|
||||||
}
|
}
|
||||||
if (!await geoAssetsDir.exists()) {
|
|
||||||
await geoAssetsDir.create(recursive: true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await appLogsFile.exists()) {
|
if (await appLogsFile.exists()) {
|
||||||
await appLogsFile.writeAsString("");
|
await appLogsFile.writeAsString("");
|
||||||
@@ -68,8 +58,6 @@ class FilesEditorService with InfraLogger {
|
|||||||
} else {
|
} else {
|
||||||
await coreLogsFile.create(recursive: true);
|
await coreLogsFile.create(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _populateGeoAssets();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static Future<Directory> getDatabaseDirectory() async {
|
static Future<Directory> getDatabaseDirectory() async {
|
||||||
@@ -85,44 +73,9 @@ class FilesEditorService with InfraLogger {
|
|||||||
return p.join(configsDir.path, "$fileName.json");
|
return p.join(configsDir.path, "$fileName.json");
|
||||||
}
|
}
|
||||||
|
|
||||||
String geoAssetPath(String providerName, String fileName) {
|
|
||||||
final prefix = providerName.replaceAll("/", "-").toLowerCase();
|
|
||||||
return p.join(
|
|
||||||
geoAssetsDir.path,
|
|
||||||
"$prefix${prefix.isBlank ? "" : "-"}$fileName",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// geoasset's path relative to working directory
|
|
||||||
String geoAssetRelativePath(String providerName, String fileName) {
|
|
||||||
final fullPath = geoAssetPath(providerName, fileName);
|
|
||||||
return p.relative(fullPath, from: workingDir.path);
|
|
||||||
}
|
|
||||||
|
|
||||||
String resolveGeoAssetPath(String path) {
|
|
||||||
return p.absolute(workingDir.path, path);
|
|
||||||
}
|
|
||||||
|
|
||||||
String tempConfigPath(String fileName) => configPath("temp_$fileName");
|
String tempConfigPath(String fileName) => configPath("temp_$fileName");
|
||||||
|
|
||||||
Future<void> deleteConfig(String fileName) {
|
Future<void> deleteConfig(String fileName) {
|
||||||
return File(configPath(fileName)).delete();
|
return File(configPath(fileName)).delete();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _populateGeoAssets() async {
|
|
||||||
loggy.debug('populating geo assets');
|
|
||||||
final geoipPath =
|
|
||||||
geoAssetPath(defaultGeoip.providerName, defaultGeoip.fileName);
|
|
||||||
if (!await File(geoipPath).exists()) {
|
|
||||||
final bundledGeoip = await rootBundle.load(Assets.core.geoip);
|
|
||||||
await File(geoipPath).writeAsBytes(bundledGeoip.buffer.asInt8List());
|
|
||||||
}
|
|
||||||
|
|
||||||
final geositePath =
|
|
||||||
geoAssetPath(defaultGeosite.providerName, defaultGeosite.fileName);
|
|
||||||
if (!await File(geositePath).exists()) {
|
|
||||||
final bundledGeosite = await rootBundle.load(Assets.core.geosite);
|
|
||||||
await File(geositePath).writeAsBytes(bundledGeosite.buffer.asInt8List());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user