diff --git a/build.yaml b/build.yaml index b33aa2b2..c34cb667 100644 --- a/build.yaml +++ b/build.yaml @@ -2,8 +2,6 @@ targets: $default: builders: drift_dev: - generate_for: - - "lib/data/local/**" options: store_date_time_values_as_text: true slang_build_runner: diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 4cfa3d18..7f4c42fe 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -12,6 +12,7 @@ import 'package:hiddify/data/repository/app_repository_impl.dart'; import 'package:hiddify/domain/environment.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/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/services/deep_link_service.dart'; @@ -86,6 +87,7 @@ Future _lazyBootstrap( final filesEditor = container.read(filesEditorServiceProvider); await filesEditor.init(); + await container.read(geoAssetRepositoryProvider.future); initLoggers(container.read, debug); _logger.info(container.read(appInfoProvider).format()); diff --git a/lib/core/router/routes/desktop_routes.dart b/lib/core/router/routes/desktop_routes.dart index 186f7ddd..3830b7c3 100644 --- a/lib/core/router/routes/desktop_routes.dart +++ b/lib/core/router/routes/desktop_routes.dart @@ -3,8 +3,8 @@ import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.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/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'desktop_routes.g.dart'; @@ -117,7 +117,7 @@ class GeoAssetsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: GeoAssetsPage(), + child: GeoAssetsOverviewPage(), ); } } diff --git a/lib/core/router/routes/mobile_routes.dart b/lib/core/router/routes/mobile_routes.dart index 79c28024..5dcf0802 100644 --- a/lib/core/router/routes/mobile_routes.dart +++ b/lib/core/router/routes/mobile_routes.dart @@ -4,8 +4,8 @@ import 'package:hiddify/core/router/app_router.dart'; import 'package:hiddify/core/router/routes/shared_routes.dart'; import 'package:hiddify/features/about/view/view.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/settings/geo_assets/geo_assets_page.dart'; import 'package:hiddify/features/settings/view/view.dart'; part 'mobile_routes.g.dart'; @@ -155,7 +155,7 @@ class GeoAssetsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: GeoAssetsPage(), + child: GeoAssetsOverviewPage(), ); } } diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart index f1c47452..8ffd3327 100644 --- a/lib/data/data_providers.dart +++ b/lib/data/data_providers.dart @@ -4,18 +4,17 @@ import 'package:dio/dio.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/core/prefs/general_prefs.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/repository/app_repository_impl.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/domain/app/app.dart'; import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.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/features/geo_asset/data/geo_asset_data_providers.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:native_dio_adapter/native_dio_adapter.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -70,38 +69,25 @@ AppRepository appRepository(AppRepositoryRef ref) => @Riverpod(keepAlive: true) 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 Future configOptions(ConfigOptionsRef ref) async { final geoAssets = await ref - .watch(geoAssetsRepositoryProvider) + .watch(geoAssetRepositoryProvider) + .requireValue .getActivePair() .getOrElse((l) => throw l) .run(); - final filesEditor = ref.watch(filesEditorServiceProvider); + final geoAssetsPathResolver = ref.watch(geoAssetPathResolverProvider); final serviceMode = ref.watch(serviceModeStoreProvider); return ref.watch(configPreferencesProvider).copyWith( enableTun: serviceMode == ServiceMode.tun, setSystemProxy: serviceMode == ServiceMode.systemProxy, - geoipPath: filesEditor.geoAssetRelativePath( + geoipPath: geoAssetsPathResolver.relativePath( geoAssets.geoip.providerName, geoAssets.geoip.fileName, ), - geositePath: filesEditor.geoAssetRelativePath( + geositePath: geoAssetsPathResolver.relativePath( geoAssets.geosite.providerName, geoAssets.geosite.fileName, ), @@ -112,6 +98,7 @@ Future configOptions(ConfigOptionsRef ref) async { CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( ref.watch(singboxServiceProvider), ref.watch(filesEditorServiceProvider), + ref.watch(geoAssetPathResolverProvider), ref.watch(platformServicesProvider), ref.watch(clashApiProvider), ref.read(debugModeNotifierProvider), diff --git a/lib/data/local/dao/dao.dart b/lib/data/local/dao/dao.dart deleted file mode 100644 index e267403f..00000000 --- a/lib/data/local/dao/dao.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'geo_assets_dao.dart'; -export 'profiles_dao.dart'; diff --git a/lib/data/local/dao/geo_assets_dao.dart b/lib/data/local/dao/geo_assets_dao.dart deleted file mode 100644 index 25ef879a..00000000 --- a/lib/data/local/dao/geo_assets_dao.dart +++ /dev/null @@ -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 - with _$GeoAssetsDaoMixin, InfraLogger { - GeoAssetsDao(super.db); - - Future add(GeoAsset geoAsset) async { - await into(geoAssetEntries).insert(geoAsset.toCompanion()); - } - - Future 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> watchAll() { - return geoAssetEntries.select().map(GeoAssetMapper.fromEntry).watch(); - } - - Future 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()); - }, - ); - } -} diff --git a/lib/data/local/data_mappers.dart b/lib/data/local/data_mappers.dart index 571afd89..0646a749 100644 --- a/lib/data/local/data_mappers.dart +++ b/lib/data/local/data_mappers.dart @@ -1,7 +1,6 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/database.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; extension ProfileMapper on Profile { 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, - ); - } -} diff --git a/lib/data/local/database.dart b/lib/data/local/database.dart index 2800f77e..f1ecaea9 100644 --- a/lib/data/local/database.dart +++ b/lib/data/local/database.dart @@ -2,22 +2,19 @@ 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/data_mappers.dart'; import 'package:hiddify/data/local/schema_versions.dart'; import 'package:hiddify/data/local/tables.dart'; import 'package:hiddify/data/local/type_converters.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:path/path.dart' as p; part 'database.g.dart'; -@DriftDatabase( - tables: [ProfileEntries, GeoAssetEntries], - daos: [ProfilesDao, GeoAssetsDao], -) +@DriftDatabase(tables: [ProfileEntries, GeoAssetEntries]) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); @@ -57,7 +54,7 @@ class AppDatabase extends _$AppDatabase { Future _prePopulateGeoAssets() async { await transaction(() async { - final geoAssets = defaultGeoAssets.map((e) => e.toCompanion()); + final geoAssets = defaultGeoAssets.map((e) => e.toEntry()); for (final geoAsset in geoAssets) { await into(geoAssetEntries).insert(geoAsset); } diff --git a/lib/data/local/tables.dart b/lib/data/local/tables.dart index 18fe4cdc..f8a09291 100644 --- a/lib/data/local/tables.dart +++ b/lib/data/local/tables.dart @@ -1,7 +1,7 @@ import 'package:drift/drift.dart'; import 'package:hiddify/data/local/type_converters.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') class ProfileEntries extends Table { diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart index d254726c..15bdde09 100644 --- a/lib/data/repository/core_facade_impl.dart +++ b/lib/data/repository/core_facade_impl.dart @@ -10,6 +10,7 @@ import 'package:hiddify/domain/constants.dart'; import 'package:hiddify/domain/core_facade.dart'; import 'package:hiddify/domain/core_service_failure.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/platform_services.dart'; import 'package:hiddify/services/singbox/singbox_service.dart'; @@ -19,6 +20,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { CoreFacadeImpl( this.singbox, this.filesEditor, + this.geoAssetPathResolver, this.platformServices, this.clash, this.debug, @@ -27,6 +29,7 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { final SingboxService singbox; final FilesEditorService filesEditor; + final GeoAssetPathResolver geoAssetPathResolver; final PlatformServices platformServices; final ClashApi clash; final bool debug; @@ -38,8 +41,8 @@ class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { return exceptionHandler( () async { final options = await configOptions(); - final geoip = filesEditor.resolveGeoAssetPath(options.geoipPath); - final geosite = filesEditor.resolveGeoAssetPath(options.geositePath); + final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); + final geosite = geoAssetPathResolver.resolvePath(options.geositePath); if (!await File(geoip).exists() || !await File(geosite).exists()) { return left(const CoreMissingGeoAssets()); } diff --git a/lib/data/repository/geo_assets_repository.dart b/lib/data/repository/geo_assets_repository.dart deleted file mode 100644 index 57c7c638..00000000 --- a/lib/data/repository/geo_assets_repository.dart +++ /dev/null @@ -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 - 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>> 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 _geoFiles = []; - Stream> _watchGeoFiles() async* { - yield await _readGeoFiles(); - yield* Watcher( - filesEditor.geoAssetsDir.path, - pollingDelay: const Duration(seconds: 1), - ).events.asyncMap((event) async { - await _readGeoFiles(); - return _geoFiles; - }); - } - - Future> _readGeoFiles() async { - return _geoFiles = Directory(filesEditor.geoAssetsDir.path) - .listSync() - .whereType() - .where((e) => e.extension == '.db'); - } - - @override - TaskEither update(GeoAsset geoAsset) { - return exceptionHandler( - () async { - loggy.debug( - "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", - ); - final response = await dio.get(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>(); - 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 markAsActive(GeoAsset geoAsset) { - return exceptionHandler( - () async { - await geoAssetsDao.edit(geoAsset.copyWith(active: true)); - return right(unit); - }, - GeoAssetFailure.unexpected, - ); - } - - @override - TaskEither 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, - ); - } -} diff --git a/lib/data/repository/profiles_repository_impl.dart b/lib/data/repository/profiles_repository_impl.dart index cc05cd30..d73eb585 100644 --- a/lib/data/repository/profiles_repository_impl.dart +++ b/lib/data/repository/profiles_repository_impl.dart @@ -2,7 +2,7 @@ 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/local/dao/profiles_dao.dart'; import 'package:hiddify/data/repository/exception_handlers.dart'; import 'package:hiddify/domain/enums.dart'; import 'package:hiddify/domain/profiles/profiles.dart'; diff --git a/lib/domain/rules/geo_asset.dart b/lib/domain/rules/geo_asset.dart deleted file mode 100644 index 409b7b82..00000000 --- a/lib/domain/rules/geo_asset.dart +++ /dev/null @@ -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 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", - ), -]; diff --git a/lib/domain/rules/geo_assets_repository.dart b/lib/domain/rules/geo_assets_repository.dart deleted file mode 100644 index 2e55632d..00000000 --- a/lib/domain/rules/geo_assets_repository.dart +++ /dev/null @@ -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 - getActivePair(); - - Stream>> watchAll(); - - TaskEither update(GeoAsset geoAsset); - - TaskEither markAsActive(GeoAsset geoAsset); - - TaskEither addRecommended(); -} diff --git a/lib/features/geo_asset/data/geo_asset_data_mapper.dart b/lib/features/geo_asset/data/geo_asset_data_mapper.dart new file mode 100644 index 00000000..7906ac0e --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_mapper.dart @@ -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, + ); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_data_providers.dart b/lib/features/geo_asset/data/geo_asset_data_providers.dart new file mode 100644 index 00000000..1e9be492 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_providers.dart @@ -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(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, + ); +} diff --git a/lib/features/geo_asset/data/geo_asset_data_source.dart b/lib/features/geo_asset/data/geo_asset_data_source.dart new file mode 100644 index 00000000..b72b751d --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_data_source.dart @@ -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 insert(GeoAssetEntriesCompanion entry); + Future getActiveAssetByType(GeoAssetType type); + Stream> watchAll(); + Future patch(String id, GeoAssetEntriesCompanion entry); +} + +@DriftAccessor(tables: [GeoAssetEntries]) +class GeoAssetsDao extends DatabaseAccessor + with _$GeoAssetsDaoMixin, InfraLogger + implements GeoAssetDataSource { + GeoAssetsDao(super.db); + + @override + Future insert(GeoAssetEntriesCompanion entry) async { + await into(geoAssetEntries).insert(entry); + } + + @override + Future getActiveAssetByType(GeoAssetType type) async { + return (geoAssetEntries.select() + ..where((tbl) => tbl.active.equals(true)) + ..where((tbl) => tbl.type.equalsValue(type)) + ..limit(1)) + .getSingleOrNull(); + } + + @override + Stream> watchAll() { + return geoAssetEntries.select().watch(); + } + + @override + Future 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); + }, + ); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_path_resolver.dart b/lib/features/geo_asset/data/geo_asset_path_resolver.dart new file mode 100644 index 00000000..5dc1d117 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_path_resolver.dart @@ -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); + } +} diff --git a/lib/features/geo_asset/data/geo_asset_repository.dart b/lib/features/geo_asset/data/geo_asset_repository.dart new file mode 100644 index 00000000..74abb850 --- /dev/null +++ b/lib/features/geo_asset/data/geo_asset_repository.dart @@ -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 init(); + TaskEither + getActivePair(); + Stream>> watchAll(); + TaskEither update(GeoAssetEntity geoAsset); + TaskEither markAsActive(GeoAssetEntity geoAsset); + TaskEither 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 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 + 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>> 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 _geoFiles = []; + Stream> _watchGeoFiles() async* { + yield await _readGeoFiles(); + yield* Watcher( + geoAssetPathResolver.directory.path, + pollingDelay: const Duration(seconds: 1), + ).events.asyncMap((event) async { + await _readGeoFiles(); + return _geoFiles; + }); + } + + Future> _readGeoFiles() async { + return _geoFiles = Directory(geoAssetPathResolver.directory.path) + .listSync() + .whereType() + .where((e) => e.extension == '.db'); + } + + @override + TaskEither update(GeoAssetEntity geoAsset) { + return exceptionHandler( + () async { + loggy.debug( + "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", + ); + final response = await dio.get(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>(); + 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 markAsActive(GeoAssetEntity geoAsset) { + return exceptionHandler( + () async { + await geoAssetDataSource.patch( + geoAsset.id, + const GeoAssetEntriesCompanion(active: Value(true)), + ); + return right(unit); + }, + GeoAssetUnexpectedFailure.new, + ); + } + + @override + TaskEither 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, + ); + } +} diff --git a/lib/features/geo_asset/model/default_geo_assets.dart b/lib/features/geo_asset/model/default_geo_assets.dart new file mode 100644 index 00000000..df00d7a2 --- /dev/null +++ b/lib/features/geo_asset/model/default_geo_assets.dart @@ -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", + ), +]; diff --git a/lib/features/geo_asset/model/geo_asset_entity.dart b/lib/features/geo_asset/model/geo_asset_entity.dart new file mode 100644 index 00000000..44a3f9fd --- /dev/null +++ b/lib/features/geo_asset/model/geo_asset_entity.dart @@ -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"; +} diff --git a/lib/domain/rules/geo_asset_failure.dart b/lib/features/geo_asset/model/geo_asset_failure.dart similarity index 97% rename from lib/domain/rules/geo_asset_failure.dart rename to lib/features/geo_asset/model/geo_asset_failure.dart index 7beb8ef2..161b193e 100644 --- a/lib/domain/rules/geo_asset_failure.dart +++ b/lib/features/geo_asset/model/geo_asset_failure.dart @@ -6,7 +6,7 @@ part 'geo_asset_failure.freezed.dart'; @freezed sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { - const GeoAssetFailure._(); + const GeoAssetFailure._(); const factory GeoAssetFailure.unexpected([ Object? error, diff --git a/lib/features/geo_asset/notifier/geo_asset_notifier.dart b/lib/features/geo_asset/notifier/geo_asset_notifier.dart new file mode 100644 index 00000000..94f1ddac --- /dev/null +++ b/lib/features/geo_asset/notifier/geo_asset_notifier.dart @@ -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 build(String id) async { + ref.disposeDelay(const Duration(seconds: 10)); + return null; + } + + Future 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(), + ); + } +} diff --git a/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart b/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart new file mode 100644 index 00000000..d3ead5b9 --- /dev/null +++ b/lib/features/geo_asset/overview/geo_assets_overview_notifier.dart @@ -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> 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 markAsActive(GeoAssetEntity geoAsset) async { + await _geoAssetRepo.markAsActive(geoAsset).getOrElse( + (f) { + loggy.warning("error marking geo asset as active", f); + throw f; + }, + ).run(); + } + + Future addRecommended() async { + await _geoAssetRepo.addRecommended().getOrElse( + (f) { + loggy.warning("error adding recommended geo assets", f); + throw f; + }, + ).run(); + } +} diff --git a/lib/features/settings/geo_assets/geo_assets_page.dart b/lib/features/geo_asset/overview/geo_assets_overview_page.dart similarity index 65% rename from lib/features/settings/geo_assets/geo_assets_page.dart rename to lib/features/geo_asset/overview/geo_assets_overview_page.dart index b53aedf1..a611fabd 100644 --- a/lib/features/settings/geo_assets/geo_assets_page.dart +++ b/lib/features/geo_asset/overview/geo_assets_overview_page.dart @@ -1,16 +1,16 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_asset_tile.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; +import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart'; +import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class GeoAssetsPage extends HookConsumerWidget { - const GeoAssetsPage({super.key}); +class GeoAssetsOverviewPage extends HookConsumerWidget { + const GeoAssetsOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final state = ref.watch(geoAssetsNotifierProvider); + final state = ref.watch(geoAssetsOverviewNotifierProvider); return Scaffold( body: CustomScrollView( @@ -25,7 +25,7 @@ class GeoAssetsPage extends HookConsumerWidget { child: Text(t.settings.geoAssets.addRecommended), onTap: () { ref - .read(geoAssetsNotifierProvider.notifier) + .read(geoAssetsOverviewNotifierProvider.notifier) .addRecommended(); }, ), @@ -38,7 +38,12 @@ class GeoAssetsPage extends HookConsumerWidget { AsyncData(value: final geoAssets) => SliverList.builder( itemBuilder: (context, index) { final geoAsset = geoAssets[index]; - return GeoAssetTile(geoAsset); + return GeoAssetTile( + geoAsset, + onMarkAsActive: () => ref + .read(geoAssetsOverviewNotifierProvider.notifier) + .markAsActive(geoAsset.$1), + ); }, itemCount: geoAssets.length, ), diff --git a/lib/features/settings/geo_assets/geo_asset_tile.dart b/lib/features/geo_asset/widget/geo_asset_tile.dart similarity index 63% rename from lib/features/settings/geo_assets/geo_asset_tile.dart rename to lib/features/geo_asset/widget/geo_asset_tile.dart index db1f6de0..620f1a21 100644 --- a/lib/features/settings/geo_assets/geo_asset_tile.dart +++ b/lib/features/geo_asset/widget/geo_asset_tile.dart @@ -2,40 +2,44 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:hiddify/core/core_providers.dart'; import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/domain/rules/geo_asset.dart'; -import 'package:hiddify/domain/rules/geo_asset_failure.dart'; -import 'package:hiddify/features/settings/geo_assets/geo_assets_notifier.dart'; -import 'package:hiddify/utils/alerts.dart'; -import 'package:hiddify/utils/async_mutation.dart'; -import 'package:hiddify/utils/date_time_formatter.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/features/geo_asset/notifier/geo_asset_notifier.dart'; +import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:humanizer/humanizer.dart'; class GeoAssetTile extends HookConsumerWidget { - GeoAssetTile(GeoAssetWithFileSize geoAssetWithFileSize, {super.key}) - : geoAsset = geoAssetWithFileSize.$1, + GeoAssetTile( + GeoAssetWithFileSize geoAssetWithFileSize, { + super.key, + required this.onMarkAsActive, + }) : geoAsset = geoAssetWithFileSize.$1, size = geoAssetWithFileSize.$2; - final GeoAsset geoAsset; + final GeoAssetEntity geoAsset; final int? size; + final VoidCallback onMarkAsActive; @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); + final fetchState = ref.watch(fetchGeoAssetProvider(geoAsset.id)); final fileMissing = size == null; - final updateMutation = useMutation( - initialOnFailure: (err) { - if (err case GeoAssetNoUpdateAvailable()) { - CustomToast(t.failure.geoAssets.notUpdate).show(context); - } else { - CustomAlertDialog.fromErr( - t.presentError(err, action: t.settings.geoAssets.failureMsg), - ).show(context); + ref.listen( + fetchGeoAssetProvider(geoAsset.id), + (_, next) { + switch (next) { + case AsyncError(:final error): + if (error case GeoAssetNoUpdateAvailable()) { + return CustomToast(t.failure.geoAssets.notUpdate).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( @@ -49,7 +53,7 @@ class GeoAssetTile extends HookConsumerWidget { ), ), isThreeLine: true, - subtitle: updateMutation.state.isInProgress + subtitle: fetchState.isLoading ? const LinearProgressIndicator() : Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -89,26 +93,15 @@ class GeoAssetTile extends HookConsumerWidget { ], ), selected: geoAsset.active, - onTap: () async { - await ref - .read(geoAssetsNotifierProvider.notifier) - .markAsActive(geoAsset); - }, + onTap: onMarkAsActive, trailing: PopupMenuButton( itemBuilder: (context) { return [ PopupMenuItem( - enabled: !updateMutation.state.isInProgress, - onTap: () { - if (updateMutation.state.isInProgress) { - return; - } - updateMutation.setFuture( - ref - .read(geoAssetsNotifierProvider.notifier) - .updateGeoAsset(geoAsset), - ); - }, + enabled: !fetchState.isLoading, + onTap: () => ref + .read(FetchGeoAssetProvider(geoAsset.id).notifier) + .fetch(geoAsset), child: fileMissing ? Text(t.settings.geoAssets.download) : Text(t.settings.geoAssets.update), diff --git a/lib/features/settings/geo_assets/geo_assets_notifier.dart b/lib/features/settings/geo_assets/geo_assets_notifier.dart deleted file mode 100644 index 0e1cb3db..00000000 --- a/lib/features/settings/geo_assets/geo_assets_notifier.dart +++ /dev/null @@ -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> build() { - ref.disposeDelay(const Duration(seconds: 5)); - return ref - .watch(geoAssetsRepositoryProvider) - .watchAll() - .map((event) => event.getOrElse((l) => throw l)); - } - - Future updateGeoAsset(GeoAsset geoAsset) async { - await ref.read(geoAssetsRepositoryProvider).update(geoAsset).getOrElse( - (f) { - loggy.warning("error updating geo asset", f); - throw f; - }, - ).run(); - } - - Future 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 addRecommended() async { - await ref.read(geoAssetsRepositoryProvider).addRecommended().getOrElse( - (f) { - loggy.warning("error adding recommended geo assets", f); - throw f; - }, - ).run(); - } -} diff --git a/lib/services/files_editor_service.dart b/lib/services/files_editor_service.dart index db048e81..8b6995d3 100644 --- a/lib/services/files_editor_service.dart +++ b/lib/services/files_editor_service.dart @@ -1,10 +1,6 @@ import 'dart:io'; -import 'package:dartx/dartx.dart'; -import 'package:flutter/services.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/utils/utils.dart'; import 'package:path/path.dart' as p; @@ -28,9 +24,6 @@ class FilesEditorService with InfraLogger { Directory(p.join(workingDir.path, Constants.configsFolderName)); 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 coreLogsFile => File(p.join(logsDir.path, "box.log")); @@ -53,9 +46,6 @@ class FilesEditorService with InfraLogger { if (!await configsDir.exists()) { await configsDir.create(recursive: true); } - if (!await geoAssetsDir.exists()) { - await geoAssetsDir.create(recursive: true); - } if (await appLogsFile.exists()) { await appLogsFile.writeAsString(""); @@ -68,8 +58,6 @@ class FilesEditorService with InfraLogger { } else { await coreLogsFile.create(recursive: true); } - - await _populateGeoAssets(); } static Future getDatabaseDirectory() async { @@ -85,44 +73,9 @@ class FilesEditorService with InfraLogger { 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"); Future deleteConfig(String fileName) { return File(configPath(fileName)).delete(); } - - Future _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()); - } - } }