From ed614988a2324f4a2b2fe0cefd7bafcd0334c147 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Fri, 1 Dec 2023 12:56:24 +0330 Subject: [PATCH] Refactor --- lib/bootstrap.dart | 24 +- lib/core/app_info/app_info_provider.dart | 30 ++ lib/core/core_providers.dart | 23 -- .../database/app_database.dart} | 24 +- .../connection/database_connection.dart | 14 + .../converters/duration_converter.dart} | 0 lib/core/database/database_provider.dart | 7 + .../database}/schema_versions.dart | 0 .../database}/schemas/drift_schema_v1.json | 0 .../database}/schemas/drift_schema_v2.json | 0 .../database/schemas/drift_schema_v3.json | 286 ++++++++++++++++ .../database/tables/database_tables.dart} | 2 +- .../http_client/http_client_provider.dart | 28 ++ lib/core/localization/locale_extensions.dart | 13 + lib/core/localization/locale_preferences.dart | 28 ++ lib/core/localization/translations.dart | 11 + lib/core/model/app_info_entity.dart | 32 ++ lib/{domain => core/model}/constants.dart | 11 - lib/core/model/directories.dart | 7 + lib/{domain => core/model}/environment.dart | 0 lib/{domain => core/model}/failures.dart | 2 +- lib/core/model/region.dart | 15 + .../in_app_notification_controller.dart | 2 +- .../general_preferences.dart} | 40 ++- .../preferences/preferences_provider.dart | 8 + .../service_preferences.dart} | 11 +- lib/core/prefs/locale_prefs.dart | 45 --- lib/core/prefs/prefs.dart | 4 - lib/core/prefs/theme_prefs.dart | 25 -- lib/core/router/app_router.dart | 3 +- lib/core/router/routes.dart | 20 +- lib/core/{prefs => theme}/app_theme.dart | 67 +--- lib/core/theme/app_theme_mode.dart | 25 ++ lib/core/theme/theme_extensions.dart | 40 +++ lib/core/theme/theme_preferences.dart | 26 ++ .../utils/exception_handler.dart} | 0 lib/{ => core}/utils/ffi_utils.dart | 0 lib/core/utils/json_converters.dart | 11 + lib/core/widget/custom_alert_dialog.dart | 2 +- lib/data/data_providers.dart | 86 ----- lib/data/local/schemas/drift_schema_v3.json | 1 - lib/data/repository/app_repository_impl.dart | 62 ---- lib/data/repository/config_options_store.dart | 155 --------- lib/data/repository/core_facade_impl.dart | 246 ------------- lib/data/repository/repository.dart | 1 - lib/domain/app/app.dart | 3 - lib/domain/app/app_failure.dart | 23 -- lib/domain/app/app_info.dart | 87 ----- lib/domain/app/app_repository.dart | 9 - lib/domain/connectivity/connectivity.dart | 2 - lib/domain/core_facade.dart | 3 - lib/domain/core_service_failure.dart | 101 ------ lib/domain/enums.dart | 1 - lib/domain/singbox/config_options.dart | 109 ------ lib/domain/singbox/core_status.dart | 31 -- lib/domain/singbox/outbounds.dart | 44 --- lib/domain/singbox/rules.dart | 73 ---- lib/domain/singbox/service_mode.dart | 24 -- lib/domain/singbox/singbox.dart | 7 - lib/domain/singbox/singbox_facade.dart | 49 --- lib/features/about/view/view.dart | 1 - .../app/widget/app.dart} | 24 +- .../data/app_update_data_providers.dart | 12 + .../data/app_update_repository.dart | 55 +++ .../data/github_release_parser.dart | 36 ++ .../app_update/model/app_update_failure.dart | 26 ++ .../model/remote_version_entity.dart | 22 ++ .../notifier}/app_update_notifier.dart | 52 +-- .../app_update/notifier/app_update_state.dart | 19 ++ .../widget}/new_version_dialog.dart | 9 +- .../common/adaptive_root_scaffold.dart | 4 +- lib/features/common/common_controllers.dart | 6 +- lib/features/common/general_pref_tiles.dart | 14 +- .../common/qr_code_scanner_screen.dart | 2 +- lib/features/common/stats_provider.dart | 23 -- .../common/window/window_controller.dart | 14 +- .../data/config_option_data_providers.dart | 17 + .../data/config_option_repository.dart | 172 ++++++++++ .../model/config_option_entity.dart | 80 +++++ .../model/config_option_failure.dart | 26 ++ .../model/config_option_patch.dart | 39 +++ .../notifier/config_option_notifier.dart | 31 ++ .../overview/config_options_page.dart | 323 ++++++++++++++++++ .../data/connection_data_providers.dart | 24 ++ .../data/connection_platform_source.dart | 67 ++++ .../data/connection_repository.dart | 214 ++++++++++++ .../connection/model}/connection_failure.dart | 39 ++- .../connection/model}/connection_status.dart | 4 +- .../notifier/connection_notifier.dart} | 31 +- .../geo_asset/data/geo_asset_data_mapper.dart | 2 +- .../data/geo_asset_data_providers.dart | 5 +- .../geo_asset/data/geo_asset_data_source.dart | 4 +- .../geo_asset/data/geo_asset_repository.dart | 4 +- .../geo_asset/model/geo_asset_failure.dart | 6 +- .../overview/geo_assets_overview_page.dart | 2 +- .../geo_asset/widget/geo_asset_tile.dart | 4 +- lib/features/home/view/view.dart | 1 - .../connection_button.dart | 24 +- .../empty_profiles_home_body.dart | 2 +- .../home/{view => widget}/home_page.dart | 11 +- lib/features/home/widgets/widgets.dart | 2 - .../intro/{ => widget}/intro_page.dart | 6 +- lib/features/log/data/log_data_providers.dart | 1 + lib/features/log/data/log_repository.dart | 4 +- lib/features/log/model/log_failure.dart | 7 +- .../log/overview/logs_overview_page.dart | 6 +- .../data/per_app_proxy_data_providers.dart | 9 + .../data/per_app_proxy_repository.dart | 55 +++ .../model/installed_package_info.dart | 17 + .../model/per_app_proxy_mode.dart | 24 ++ .../overview/per_app_proxy_notifier.dart | 36 ++ .../overview}/per_app_proxy_page.dart | 46 +-- .../profile/add/add_profile_modal.dart | 2 +- .../profile/data/profile_data_mapper.dart | 2 +- .../profile/data/profile_data_providers.dart | 8 +- .../profile/data/profile_data_source.dart | 4 +- .../profile/data/profile_repository.dart | 100 +++--- .../profile/details/profile_details_page.dart | 4 +- .../profile/model/profile_failure.dart | 4 +- .../profile/model/profile_sort_enum.dart | 2 +- .../profile/notifier/profile_notifier.dart | 10 +- .../notifier/profiles_update_notifier.dart | 6 +- .../overview/profiles_overview_notifier.dart | 3 +- .../overview/profiles_overview_page.dart | 4 +- lib/features/profile/widget/profile_tile.dart | 5 +- lib/features/proxies/notifier/notifier.dart | 1 - lib/features/proxies/view/view.dart | 1 - lib/features/proxies/widgets/widgets.dart | 1 - .../proxy/data/proxy_data_providers.dart | 12 + lib/features/proxy/data/proxy_repository.dart | 78 +++++ lib/features/proxy/model/proxy_entity.dart | 37 ++ lib/features/proxy/model/proxy_failure.dart | 33 ++ .../overview/proxies_overview_notifier.dart} | 50 +-- .../overview/proxies_overview_page.dart} | 16 +- .../widgets => proxy/widget}/proxy_tile.dart | 12 +- .../view => settings/about}/about_page.dart | 14 +- .../data/settings_data_providers.dart | 9 + .../settings/data/settings_repository.dart | 44 +++ .../settings/model/settings_failure.dart | 26 ++ .../notifier/platform_settings_notifier.dart | 25 ++ .../settings_overview_page.dart} | 6 +- .../settings/view/config_options_page.dart | 299 ---------------- lib/features/settings/view/view.dart | 3 - .../widgets/advanced_setting_tiles.dart | 6 +- .../widgets/general_setting_tiles.dart | 16 +- .../widgets/platform_settings_tiles.dart | 26 +- .../widgets/settings_input_dialog.dart | 2 +- .../stats/data/stats_data_providers.dart | 10 + lib/features/stats/data/stats_repository.dart | 33 ++ lib/features/stats/model/stats_entity.dart | 22 ++ lib/features/stats/model/stats_failure.dart | 26 ++ .../stats/notifier/stats_notifier.dart | 23 ++ .../widget}/side_bar_stats_overview.dart | 11 +- .../system_tray/system_tray_controller.dart | 27 +- lib/main_dev.dart | 2 +- lib/main_prod.dart | 2 +- lib/services/auto_start_service.dart | 4 +- lib/services/platform_services.dart | 135 -------- lib/services/service_providers.dart | 4 - lib/services/singbox/shared.dart | 46 --- lib/services/singbox/singbox_service.dart | 59 ---- lib/singbox/model/singbox_config_enum.dart | 68 ++++ lib/singbox/model/singbox_config_option.dart | 62 ++++ lib/singbox/model/singbox_outbound.dart | 40 +++ .../model/singbox_proxy_type.dart} | 0 lib/singbox/model/singbox_rule.dart | 35 ++ lib/singbox/model/singbox_stats.dart | 22 ++ lib/singbox/model/singbox_status.dart | 48 +++ .../service}/ffi_singbox_service.dart | 66 ++-- .../service/platform_singbox_service.dart} | 69 ++-- lib/singbox/service/singbox_service.dart | 60 ++++ .../service/singbox_service_provider.dart | 9 + lib/utils/link_parsers.dart | 2 +- lib/utils/mutation_state.dart | 2 +- lib/utils/pref_notifier.dart | 4 +- lib/utils/sentry_utils.dart | 2 +- .../generated_migrations/schema.dart | 0 .../generated_migrations/schema_v1.dart | 0 .../generated_migrations/schema_v2.dart | 0 .../generated_migrations/schema_v3.dart | 0 .../database}/migrations_test.dart | 2 +- 181 files changed, 3092 insertions(+), 2341 deletions(-) create mode 100644 lib/core/app_info/app_info_provider.dart delete mode 100644 lib/core/core_providers.dart rename lib/{data/local/database.dart => core/database/app_database.dart} (71%) create mode 100644 lib/core/database/connection/database_connection.dart rename lib/{data/local/type_converters.dart => core/database/converters/duration_converter.dart} (100%) create mode 100644 lib/core/database/database_provider.dart rename lib/{data/local => core/database}/schema_versions.dart (100%) rename lib/{data/local => core/database}/schemas/drift_schema_v1.json (100%) rename lib/{data/local => core/database}/schemas/drift_schema_v2.json (100%) create mode 100644 lib/core/database/schemas/drift_schema_v3.json rename lib/{data/local/tables.dart => core/database/tables/database_tables.dart} (95%) create mode 100644 lib/core/http_client/http_client_provider.dart create mode 100644 lib/core/localization/locale_extensions.dart create mode 100644 lib/core/localization/locale_preferences.dart create mode 100644 lib/core/localization/translations.dart create mode 100644 lib/core/model/app_info_entity.dart rename lib/{domain => core/model}/constants.dart (63%) create mode 100644 lib/core/model/directories.dart rename lib/{domain => core/model}/environment.dart (100%) rename lib/{domain => core/model}/failures.dart (97%) create mode 100644 lib/core/model/region.dart rename lib/core/{prefs/general_prefs.dart => preferences/general_preferences.dart} (76%) create mode 100644 lib/core/preferences/preferences_provider.dart rename lib/core/{prefs/service_prefs.dart => preferences/service_preferences.dart} (63%) delete mode 100644 lib/core/prefs/locale_prefs.dart delete mode 100644 lib/core/prefs/prefs.dart delete mode 100644 lib/core/prefs/theme_prefs.dart rename lib/core/{prefs => theme}/app_theme.dart (77%) create mode 100644 lib/core/theme/app_theme_mode.dart create mode 100644 lib/core/theme/theme_extensions.dart create mode 100644 lib/core/theme/theme_preferences.dart rename lib/{data/repository/exception_handlers.dart => core/utils/exception_handler.dart} (100%) rename lib/{ => core}/utils/ffi_utils.dart (100%) create mode 100644 lib/core/utils/json_converters.dart delete mode 100644 lib/data/data_providers.dart delete mode 100644 lib/data/local/schemas/drift_schema_v3.json delete mode 100644 lib/data/repository/app_repository_impl.dart delete mode 100644 lib/data/repository/config_options_store.dart delete mode 100644 lib/data/repository/core_facade_impl.dart delete mode 100644 lib/data/repository/repository.dart delete mode 100644 lib/domain/app/app.dart delete mode 100644 lib/domain/app/app_failure.dart delete mode 100644 lib/domain/app/app_info.dart delete mode 100644 lib/domain/app/app_repository.dart delete mode 100644 lib/domain/connectivity/connectivity.dart delete mode 100644 lib/domain/core_facade.dart delete mode 100644 lib/domain/core_service_failure.dart delete mode 100644 lib/domain/enums.dart delete mode 100644 lib/domain/singbox/config_options.dart delete mode 100644 lib/domain/singbox/core_status.dart delete mode 100644 lib/domain/singbox/outbounds.dart delete mode 100644 lib/domain/singbox/rules.dart delete mode 100644 lib/domain/singbox/service_mode.dart delete mode 100644 lib/domain/singbox/singbox.dart delete mode 100644 lib/domain/singbox/singbox_facade.dart delete mode 100644 lib/features/about/view/view.dart rename lib/{core/app/app_view.dart => features/app/widget/app.dart} (65%) create mode 100644 lib/features/app_update/data/app_update_data_providers.dart create mode 100644 lib/features/app_update/data/app_update_repository.dart create mode 100644 lib/features/app_update/data/github_release_parser.dart create mode 100644 lib/features/app_update/model/app_update_failure.dart create mode 100644 lib/features/app_update/model/remote_version_entity.dart rename lib/features/{common => app_update/notifier}/app_update_notifier.dart (58%) create mode 100644 lib/features/app_update/notifier/app_update_state.dart rename lib/features/{common => app_update/widget}/new_version_dialog.dart (91%) delete mode 100644 lib/features/common/stats_provider.dart create mode 100644 lib/features/config_option/data/config_option_data_providers.dart create mode 100644 lib/features/config_option/data/config_option_repository.dart create mode 100644 lib/features/config_option/model/config_option_entity.dart create mode 100644 lib/features/config_option/model/config_option_failure.dart create mode 100644 lib/features/config_option/model/config_option_patch.dart create mode 100644 lib/features/config_option/notifier/config_option_notifier.dart create mode 100644 lib/features/config_option/overview/config_options_page.dart create mode 100644 lib/features/connection/data/connection_data_providers.dart create mode 100644 lib/features/connection/data/connection_platform_source.dart create mode 100644 lib/features/connection/data/connection_repository.dart rename lib/{domain/connectivity => features/connection/model}/connection_failure.dart (51%) rename lib/{domain/connectivity => features/connection/model}/connection_status.dart (90%) rename lib/features/{common/connectivity/connectivity_controller.dart => connection/notifier/connection_notifier.dart} (75%) delete mode 100644 lib/features/home/view/view.dart rename lib/features/home/{widgets => widget}/connection_button.dart (84%) rename lib/features/home/{widgets => widget}/empty_profiles_home_body.dart (96%) rename lib/features/home/{view => widget}/home_page.dart (90%) delete mode 100644 lib/features/home/widgets/widgets.dart rename lib/features/intro/{ => widget}/intro_page.dart (95%) create mode 100644 lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart create mode 100644 lib/features/per_app_proxy/data/per_app_proxy_repository.dart create mode 100644 lib/features/per_app_proxy/model/installed_package_info.dart create mode 100644 lib/features/per_app_proxy/model/per_app_proxy_mode.dart create mode 100644 lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart rename lib/features/{settings/view => per_app_proxy/overview}/per_app_proxy_page.dart (85%) delete mode 100644 lib/features/proxies/notifier/notifier.dart delete mode 100644 lib/features/proxies/view/view.dart delete mode 100644 lib/features/proxies/widgets/widgets.dart create mode 100644 lib/features/proxy/data/proxy_data_providers.dart create mode 100644 lib/features/proxy/data/proxy_repository.dart create mode 100644 lib/features/proxy/model/proxy_entity.dart create mode 100644 lib/features/proxy/model/proxy_failure.dart rename lib/features/{proxies/notifier/proxies_notifier.dart => proxy/overview/proxies_overview_notifier.dart} (72%) rename lib/features/{proxies/view/proxies_page.dart => proxy/overview/proxies_overview_page.dart} (91%) rename lib/features/{proxies/widgets => proxy/widget}/proxy_tile.dart (88%) rename lib/features/{about/view => settings/about}/about_page.dart (91%) create mode 100644 lib/features/settings/data/settings_data_providers.dart create mode 100644 lib/features/settings/data/settings_repository.dart create mode 100644 lib/features/settings/model/settings_failure.dart create mode 100644 lib/features/settings/notifier/platform_settings_notifier.dart rename lib/features/settings/{view/settings_page.dart => overview/settings_overview_page.dart} (85%) delete mode 100644 lib/features/settings/view/config_options_page.dart delete mode 100644 lib/features/settings/view/view.dart create mode 100644 lib/features/stats/data/stats_data_providers.dart create mode 100644 lib/features/stats/data/stats_repository.dart create mode 100644 lib/features/stats/model/stats_entity.dart create mode 100644 lib/features/stats/model/stats_failure.dart create mode 100644 lib/features/stats/notifier/stats_notifier.dart rename lib/features/{common => stats/widget}/side_bar_stats_overview.dart (89%) delete mode 100644 lib/services/singbox/shared.dart delete mode 100644 lib/services/singbox/singbox_service.dart create mode 100644 lib/singbox/model/singbox_config_enum.dart create mode 100644 lib/singbox/model/singbox_config_option.dart create mode 100644 lib/singbox/model/singbox_outbound.dart rename lib/{domain/singbox/proxy_type.dart => singbox/model/singbox_proxy_type.dart} (100%) create mode 100644 lib/singbox/model/singbox_rule.dart create mode 100644 lib/singbox/model/singbox_stats.dart create mode 100644 lib/singbox/model/singbox_status.dart rename lib/{services/singbox => singbox/service}/ffi_singbox_service.dart (84%) rename lib/{services/singbox/mobile_singbox_service.dart => singbox/service/platform_singbox_service.dart} (68%) create mode 100644 lib/singbox/service/singbox_service.dart create mode 100644 lib/singbox/service/singbox_service_provider.dart rename test/{data/local => core/database}/generated_migrations/schema.dart (100%) rename test/{data/local => core/database}/generated_migrations/schema_v1.dart (100%) rename test/{data/local => core/database}/generated_migrations/schema_v2.dart (100%) rename test/{data/local => core/database}/generated_migrations/schema_v3.dart (100%) rename test/{data/local => core/database}/migrations_test.dart (95%) diff --git a/lib/bootstrap.dart b/lib/bootstrap.dart index 3a610431..2458c244 100644 --- a/lib/bootstrap.dart +++ b/lib/bootstrap.dart @@ -4,12 +4,11 @@ import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_native_splash/flutter_native_splash.dart'; -import 'package:hiddify/core/app/app_view.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/data/repository/app_repository_impl.dart'; -import 'package:hiddify/domain/environment.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/app/widget/app.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/log/data/log_data_providers.dart'; @@ -19,11 +18,11 @@ 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'; import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:loggy/loggy.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:shared_preferences/shared_preferences.dart'; import 'package:window_manager/window_manager.dart'; final _logger = Loggy('bootstrap'); @@ -41,15 +40,14 @@ Future lazyBootstrap( _loggers.addPrinter(sentryLogger); Loggy.initLoggy(); - final appInfo = await AppRepositoryImpl.getAppInfo(env); - final sharedPreferences = await SharedPreferences.getInstance(); final container = ProviderContainer( overrides: [ - appInfoProvider.overrideWithValue(appInfo), - sharedPreferencesProvider.overrideWithValue(sharedPreferences), + environmentProvider.overrideWithValue(env), ], ); + final appInfo = await container.read(appInfoProvider.future); + await container.read(sharedPreferencesProvider.future); final enableAnalytics = container.read(enableAnalyticsProvider); await SentryFlutter.init( @@ -94,7 +92,7 @@ Future _lazyBootstrap( await container.read(profileRepositoryProvider.future); initLoggers(container.read, debug); - _logger.info(container.read(appInfoProvider).format()); + _logger.info(container.read(appInfoProvider).requireValue.format()); final silentStart = container.read(silentStartNotifierProvider); if (silentStart) { @@ -131,7 +129,7 @@ Future _lazyBootstrap( ProviderScope( parent: container, child: SentryUserInteractionWidget( - child: const AppView(), + child: const App(), ), ), ); diff --git a/lib/core/app_info/app_info_provider.dart b/lib/core/app_info/app_info_provider.dart new file mode 100644 index 00000000..48042618 --- /dev/null +++ b/lib/core/app_info/app_info_provider.dart @@ -0,0 +1,30 @@ +import 'dart:io'; + +import 'package:hiddify/core/model/app_info_entity.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_info_provider.g.dart'; + +@Riverpod(keepAlive: true) +Environment environment(EnvironmentRef ref) => + throw Exception("override environmentProvider"); + +@Riverpod(keepAlive: true) +class AppInfo extends _$AppInfo { + @override + Future build() async { + final packageInfo = await PackageInfo.fromPlatform(); + final environment = ref.watch(environmentProvider); + return AppInfoEntity( + name: packageInfo.appName, + version: packageInfo.version, + buildNumber: packageInfo.buildNumber, + release: Release.read(), + operatingSystem: Platform.operatingSystem, + operatingSystemVersion: Platform.operatingSystemVersion, + environment: environment, + ); + } +} diff --git a/lib/core/core_providers.dart b/lib/core/core_providers.dart deleted file mode 100644 index 35ce17e4..00000000 --- a/lib/core/core_providers.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/environment.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'core_providers.g.dart'; - -@Riverpod(keepAlive: true) -AppInfo appInfo(AppInfoRef ref) => - throw UnimplementedError('AppInfo must be overridden'); - -@Riverpod(keepAlive: true) -Environment env(EnvRef ref) => ref.watch(appInfoProvider).environment; - -@Riverpod(keepAlive: true) -TranslationsEn translations(TranslationsRef ref) => - ref.watch(localeNotifierProvider).build(); - -@Riverpod(keepAlive: true) -AppTheme theme(ThemeRef ref) => AppTheme( - ref.watch(themeModeNotifierProvider), - ref.watch(localeNotifierProvider).preferredFontFamily, - ); diff --git a/lib/data/local/database.dart b/lib/core/database/app_database.dart similarity index 71% rename from lib/data/local/database.dart rename to lib/core/database/app_database.dart index e18897d6..5130d37a 100644 --- a/lib/data/local/database.dart +++ b/lib/core/database/app_database.dart @@ -1,24 +1,20 @@ -import 'dart:io'; - import 'package:drift/drift.dart'; -import 'package:drift/native.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/core/database/connection/database_connection.dart'; +import 'package:hiddify/core/database/converters/duration_converter.dart'; +import 'package:hiddify/core/database/schema_versions.dart'; +import 'package:hiddify/core/database/tables/database_tables.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/features/profile/model/profile_entity.dart'; -import 'package:hiddify/services/files_editor_service.dart'; -import 'package:path/path.dart' as p; -part 'database.g.dart'; +part 'app_database.g.dart'; @DriftDatabase(tables: [ProfileEntries, GeoAssetEntries]) class AppDatabase extends _$AppDatabase { AppDatabase({required QueryExecutor connection}) : super(connection); - AppDatabase.connect() : super(_openConnection()); + AppDatabase.connect() : super(openConnection()); @override int get schemaVersion => 3; @@ -61,11 +57,3 @@ class AppDatabase extends _$AppDatabase { }); } } - -LazyDatabase _openConnection() { - return LazyDatabase(() async { - final dbDir = await FilesEditorService.getDatabaseDirectory(); - final file = File(p.join(dbDir.path, 'db.sqlite')); - return NativeDatabase.createInBackground(file); - }); -} diff --git a/lib/core/database/connection/database_connection.dart b/lib/core/database/connection/database_connection.dart new file mode 100644 index 00000000..f05450e5 --- /dev/null +++ b/lib/core/database/connection/database_connection.dart @@ -0,0 +1,14 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:hiddify/services/files_editor_service.dart'; +import 'package:path/path.dart' as p; + +LazyDatabase openConnection() { + return LazyDatabase(() async { + final dbDir = await FilesEditorService.getDatabaseDirectory(); + final file = File(p.join(dbDir.path, 'db.sqlite')); + return NativeDatabase.createInBackground(file); + }); +} diff --git a/lib/data/local/type_converters.dart b/lib/core/database/converters/duration_converter.dart similarity index 100% rename from lib/data/local/type_converters.dart rename to lib/core/database/converters/duration_converter.dart diff --git a/lib/core/database/database_provider.dart b/lib/core/database/database_provider.dart new file mode 100644 index 00000000..fb388e20 --- /dev/null +++ b/lib/core/database/database_provider.dart @@ -0,0 +1,7 @@ +import 'package:hiddify/core/database/app_database.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'database_provider.g.dart'; + +@Riverpod(keepAlive: true) +AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect(); diff --git a/lib/data/local/schema_versions.dart b/lib/core/database/schema_versions.dart similarity index 100% rename from lib/data/local/schema_versions.dart rename to lib/core/database/schema_versions.dart diff --git a/lib/data/local/schemas/drift_schema_v1.json b/lib/core/database/schemas/drift_schema_v1.json similarity index 100% rename from lib/data/local/schemas/drift_schema_v1.json rename to lib/core/database/schemas/drift_schema_v1.json diff --git a/lib/data/local/schemas/drift_schema_v2.json b/lib/core/database/schemas/drift_schema_v2.json similarity index 100% rename from lib/data/local/schemas/drift_schema_v2.json rename to lib/core/database/schemas/drift_schema_v2.json diff --git a/lib/core/database/schemas/drift_schema_v3.json b/lib/core/database/schemas/drift_schema_v3.json new file mode 100644 index 00000000..3cfc5fb9 --- /dev/null +++ b/lib/core/database/schemas/drift_schema_v3.json @@ -0,0 +1,286 @@ +{ + "_meta": { + "description": "This file contains a serialized version of schema entities for drift.", + "version": "1.1.0" + }, + "options": { + "store_date_time_values_as_text": true + }, + "entities": [ + { + "id": 0, + "references": [], + "type": "table", + "data": { + "name": "profile_entries", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(ProfileType.values)", + "dart_type_name": "ProfileType" + } + }, + { + "name": "active", + "getter_name": "active", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"active\" IN (0, 1))", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": null + } + } + ] + }, + { + "name": "url", + "getter_name": "url", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_update", + "getter_name": "lastUpdate", + "moor_type": "dateTime", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "update_interval", + "getter_name": "updateInterval", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "DurationTypeConverter()", + "dart_type_name": "Duration" + } + }, + { + "name": "upload", + "getter_name": "upload", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "download", + "getter_name": "download", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "total", + "getter_name": "total", + "moor_type": "int", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "expire", + "getter_name": "expire", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "web_page_url", + "getter_name": "webPageUrl", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "support_url", + "getter_name": "supportUrl", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ] + } + }, + { + "id": 1, + "references": [], + "type": "table", + "data": { + "name": "geo_asset_entries", + "was_declared_in_moor": false, + "columns": [ + { + "name": "id", + "getter_name": "id", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "type", + "getter_name": "type", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [], + "type_converter": { + "dart_expr": "const EnumNameConverter(GeoAssetType.values)", + "dart_type_name": "GeoAssetType" + } + }, + { + "name": "active", + "getter_name": "active", + "moor_type": "bool", + "nullable": false, + "customConstraints": null, + "defaultConstraints": "CHECK (\"active\" IN (0, 1))", + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "name", + "getter_name": "name", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": null + } + } + ] + }, + { + "name": "provider_name", + "getter_name": "providerName", + "moor_type": "string", + "nullable": false, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [ + { + "allowed-lengths": { + "min": 1, + "max": null + } + } + ] + }, + { + "name": "version", + "getter_name": "version", + "moor_type": "string", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + }, + { + "name": "last_check", + "getter_name": "lastCheck", + "moor_type": "dateTime", + "nullable": true, + "customConstraints": null, + "default_dart": null, + "default_client_dart": null, + "dsl_features": [] + } + ], + "is_virtual": false, + "without_rowid": false, + "constraints": [], + "explicit_pk": [ + "id" + ], + "unique_keys": [ + [ + "name", + "provider_name" + ] + ] + } + } + ] +} \ No newline at end of file diff --git a/lib/data/local/tables.dart b/lib/core/database/tables/database_tables.dart similarity index 95% rename from lib/data/local/tables.dart rename to lib/core/database/tables/database_tables.dart index ab1d9a59..469f33de 100644 --- a/lib/data/local/tables.dart +++ b/lib/core/database/tables/database_tables.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/type_converters.dart'; +import 'package:hiddify/core/database/converters/duration_converter.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; diff --git a/lib/core/http_client/http_client_provider.dart b/lib/core/http_client/http_client_provider.dart new file mode 100644 index 00000000..0798a9d4 --- /dev/null +++ b/lib/core/http_client/http_client_provider.dart @@ -0,0 +1,28 @@ +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:native_dio_adapter/native_dio_adapter.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'http_client_provider.g.dart'; + +@Riverpod(keepAlive: true) +Dio httpClient(HttpClientRef ref) { + final dio = Dio( + BaseOptions( + connectTimeout: const Duration(seconds: 15), + sendTimeout: const Duration(seconds: 15), + receiveTimeout: const Duration(seconds: 15), + headers: { + "User-Agent": ref.watch(appInfoProvider).requireValue.userAgent, + }, + ), + ); + final debug = ref.read(debugModeNotifierProvider); + if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { + dio.httpClientAdapter = NativeAdapter(); + } + return dio; +} diff --git a/lib/core/localization/locale_extensions.dart b/lib/core/localization/locale_extensions.dart new file mode 100644 index 00000000..e42d81e8 --- /dev/null +++ b/lib/core/localization/locale_extensions.dart @@ -0,0 +1,13 @@ +import 'package:flutter_localized_locales/flutter_localized_locales.dart'; +import 'package:hiddify/gen/fonts.gen.dart'; +import 'package:hiddify/gen/translations.g.dart'; + +extension AppLocaleX on AppLocale { + String get preferredFontFamily => + this == AppLocale.fa ? FontFamily.shabnam : ""; + + String get localeName => + LocaleNamesLocalizationsDelegate + .nativeLocaleNames[flutterLocale.toString()] ?? + name; +} diff --git a/lib/core/localization/locale_preferences.dart b/lib/core/localization/locale_preferences.dart new file mode 100644 index 00000000..28da4eff --- /dev/null +++ b/lib/core/localization/locale_preferences.dart @@ -0,0 +1,28 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/gen/translations.g.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'locale_preferences.g.dart'; + +@Riverpod(keepAlive: true) +class LocalePreferences extends _$LocalePreferences { + @override + AppLocale build() { + final persisted = + ref.watch(sharedPreferencesProvider).requireValue.getString("locale"); + if (persisted == null) return AppLocaleUtils.findDeviceLocale(); + // keep backward compatibility with chinese after changing zh to zh_CN + if (persisted == "zh") { + return AppLocale.zhCn; + } + return AppLocale.values.byName(persisted); + } + + Future changeLocale(AppLocale value) async { + state = value; + await ref + .read(sharedPreferencesProvider) + .requireValue + .setString("locale", value.name); + } +} diff --git a/lib/core/localization/translations.dart b/lib/core/localization/translations.dart new file mode 100644 index 00000000..461ffab2 --- /dev/null +++ b/lib/core/localization/translations.dart @@ -0,0 +1,11 @@ +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/gen/translations.g.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +export 'package:hiddify/gen/translations.g.dart'; + +part 'translations.g.dart'; + +@Riverpod(keepAlive: true) +TranslationsEn translations(TranslationsRef ref) => + ref.watch(localePreferencesProvider).build(); diff --git a/lib/core/model/app_info_entity.dart b/lib/core/model/app_info_entity.dart new file mode 100644 index 00000000..c239d7fa --- /dev/null +++ b/lib/core/model/app_info_entity.dart @@ -0,0 +1,32 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/model/environment.dart'; + +part 'app_info_entity.freezed.dart'; + +@freezed +class AppInfoEntity with _$AppInfoEntity { + const AppInfoEntity._(); + + const factory AppInfoEntity({ + required String name, + required String version, + required String buildNumber, + required Release release, + required String operatingSystem, + required String operatingSystemVersion, + required Environment environment, + }) = _AppInfoEntity; + + String get userAgent => + "HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box"; + + String get presentVersion => environment == Environment.prod + ? version + : "$version ${environment.name}"; + + /// formats app info for sharing + String format() => ''' +$name v$version ($buildNumber) [${environment.name}] +${release.name} release +$operatingSystem [$operatingSystemVersion]'''; +} diff --git a/lib/domain/constants.dart b/lib/core/model/constants.dart similarity index 63% rename from lib/domain/constants.dart rename to lib/core/model/constants.dart index da4e62dd..e2ffb59f 100644 --- a/lib/domain/constants.dart +++ b/lib/core/model/constants.dart @@ -1,9 +1,5 @@ abstract class Constants { static const appName = "Hiddify Next"; - static const geoipFileName = "geoip.db"; - static const geositeFileName = "geosite.db"; - static const configsFolderName = "configs"; - static const localHost = "127.0.0.1"; static const githubUrl = "https://github.com/hiddify/hiddify-next"; static const githubReleasesApiUrl = "https://api.github.com/repos/hiddify/hiddify-next/releases"; @@ -15,10 +11,3 @@ abstract class Constants { static const privacyPolicyUrl = "https://hiddify.com/en/privacy-policy/"; static const termsAndConditionsUrl = "https://hiddify.com/terms/"; } - -abstract class Defaults { - static const clashApiPort = 9090; - static const mixedPort = 2334; - static const connectionTestUrl = "https://www.gstatic.com/generate_204"; - static const concurrentTestCount = 5; -} diff --git a/lib/core/model/directories.dart b/lib/core/model/directories.dart new file mode 100644 index 00000000..b940b75d --- /dev/null +++ b/lib/core/model/directories.dart @@ -0,0 +1,7 @@ +import 'dart:io'; + +typedef Directories = ({ + Directory baseDir, + Directory workingDir, + Directory tempDir +}); diff --git a/lib/domain/environment.dart b/lib/core/model/environment.dart similarity index 100% rename from lib/domain/environment.dart rename to lib/core/model/environment.dart diff --git a/lib/domain/failures.dart b/lib/core/model/failures.dart similarity index 97% rename from lib/domain/failures.dart rename to lib/core/model/failures.dart index 1640ac61..1f28b6d9 100644 --- a/lib/domain/failures.dart +++ b/lib/core/model/failures.dart @@ -1,5 +1,5 @@ import 'package:dio/dio.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; typedef PresentableError = ({String type, String? message}); diff --git a/lib/core/model/region.dart b/lib/core/model/region.dart new file mode 100644 index 00000000..4b4a65eb --- /dev/null +++ b/lib/core/model/region.dart @@ -0,0 +1,15 @@ +import 'package:hiddify/core/localization/translations.dart'; + +enum Region { + ir, + cn, + ru, + other; + + String present(TranslationsEn t) => switch (this) { + ir => t.settings.general.regions.ir, + cn => t.settings.general.regions.cn, + ru => t.settings.general.regions.ru, + other => t.settings.general.regions.other, + }; +} diff --git a/lib/core/notification/in_app_notification_controller.dart b/lib/core/notification/in_app_notification_controller.dart index 1894e13e..7ef2113c 100644 --- a/lib/core/notification/in_app_notification_controller.dart +++ b/lib/core/notification/in_app_notification_controller.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; diff --git a/lib/core/prefs/general_prefs.dart b/lib/core/preferences/general_preferences.dart similarity index 76% rename from lib/core/prefs/general_prefs.dart rename to lib/core/preferences/general_preferences.dart index 656ffb41..af0a8dbe 100644 --- a/lib/core/prefs/general_prefs.dart +++ b/lib/core/preferences/general_preferences.dart @@ -1,19 +1,22 @@ import 'package:flutter/foundation.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/environment.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'general_prefs.g.dart'; +part 'general_preferences.g.dart'; + +// TODO refactor bool _debugIntroPage = false; @Riverpod(keepAlive: true) class IntroCompleted extends _$IntroCompleted { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "intro_completed", false, ); @@ -33,7 +36,7 @@ class IntroCompleted extends _$IntroCompleted { @Riverpod(keepAlive: true) class RegionNotifier extends _$RegionNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "region", Region.other, mapFrom: Region.values.byName, @@ -51,8 +54,11 @@ class RegionNotifier extends _$RegionNotifier { @Riverpod(keepAlive: true) class SilentStartNotifier extends _$SilentStartNotifier { - late final _pref = - Pref(ref.watch(sharedPreferencesProvider), "silent_start", false); + late final _pref = Pref( + ref.watch(sharedPreferencesProvider).requireValue, + "silent_start", + false, + ); @override bool build() => _pref.getValue(); @@ -66,7 +72,7 @@ class SilentStartNotifier extends _$SilentStartNotifier { @Riverpod(keepAlive: true) class EnableAnalytics extends _$EnableAnalytics { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "enable_analytics", true, ); @@ -83,7 +89,7 @@ class EnableAnalytics extends _$EnableAnalytics { @Riverpod(keepAlive: true) class DisableMemoryLimit extends _$DisableMemoryLimit { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "disable_memory_limit", false, ); @@ -100,9 +106,9 @@ class DisableMemoryLimit extends _$DisableMemoryLimit { @Riverpod(keepAlive: true) class DebugModeNotifier extends _$DebugModeNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "debug_mode", - ref.read(envProvider) == Environment.dev, + ref.read(environmentProvider) == Environment.dev, ); @override @@ -117,7 +123,7 @@ class DebugModeNotifier extends _$DebugModeNotifier { @Riverpod(keepAlive: true) class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "per_app_proxy_mode", PerAppProxyMode.off, mapFrom: PerAppProxyMode.values.byName, @@ -136,13 +142,13 @@ class PerAppProxyModeNotifier extends _$PerAppProxyModeNotifier { @Riverpod(keepAlive: true) class PerAppProxyList extends _$PerAppProxyList { late final _include = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "per_app_proxy_include_list", [], ); late final _exclude = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "per_app_proxy_exclude_list", [], ); @@ -165,7 +171,7 @@ class PerAppProxyList extends _$PerAppProxyList { @riverpod class MarkNewProfileActive extends _$MarkNewProfileActive { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "mark_new_profile_active", true, ); diff --git a/lib/core/preferences/preferences_provider.dart b/lib/core/preferences/preferences_provider.dart new file mode 100644 index 00000000..533f6bd8 --- /dev/null +++ b/lib/core/preferences/preferences_provider.dart @@ -0,0 +1,8 @@ +import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +part 'preferences_provider.g.dart'; + +@Riverpod(keepAlive: true) +Future sharedPreferences(SharedPreferencesRef ref) async => + SharedPreferences.getInstance(); diff --git a/lib/core/prefs/service_prefs.dart b/lib/core/preferences/service_preferences.dart similarity index 63% rename from lib/core/prefs/service_prefs.dart rename to lib/core/preferences/service_preferences.dart index d9cc2cbb..847c614f 100644 --- a/lib/core/prefs/service_prefs.dart +++ b/lib/core/preferences/service_preferences.dart @@ -1,14 +1,17 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'service_prefs.g.dart'; +part 'service_preferences.g.dart'; @Riverpod(keepAlive: true) class StartedByUser extends _$StartedByUser with AppLogger { - late final _pref = - Pref(ref.watch(sharedPreferencesProvider), "started_by_user", false); + late final _pref = Pref( + ref.watch(sharedPreferencesProvider).requireValue, + "started_by_user", + false, + ); @override bool build() => _pref.getValue(); diff --git a/lib/core/prefs/locale_prefs.dart b/lib/core/prefs/locale_prefs.dart deleted file mode 100644 index 95352687..00000000 --- a/lib/core/prefs/locale_prefs.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter_localized_locales/flutter_localized_locales.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/gen/fonts.gen.dart'; -import 'package:hiddify/gen/translations.g.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -export 'package:hiddify/gen/translations.g.dart'; - -part 'locale_prefs.g.dart'; - -@Riverpod(keepAlive: true) -class LocaleNotifier extends _$LocaleNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider), - "locale", - AppLocaleUtils.findDeviceLocale(), - mapFrom: (String value) { - // keep backward compatibility with chinese after changing zh to zh_CN - if (value == "zh") { - return AppLocale.zhCn; - } - return AppLocale.values.byName(value); - }, - mapTo: (value) => value.name, - ); - - @override - AppLocale build() => _pref.getValue(); - - Future update(AppLocale value) { - state = value; - return _pref.update(value); - } -} - -extension AppLocaleX on AppLocale { - String get preferredFontFamily => - this == AppLocale.fa ? FontFamily.shabnam : ""; - - String get localeName => - LocaleNamesLocalizationsDelegate - .nativeLocaleNames[flutterLocale.toString()] ?? - name; -} diff --git a/lib/core/prefs/prefs.dart b/lib/core/prefs/prefs.dart deleted file mode 100644 index 2d8fa8fc..00000000 --- a/lib/core/prefs/prefs.dart +++ /dev/null @@ -1,4 +0,0 @@ -export 'app_theme.dart'; -export 'general_prefs.dart'; -export 'locale_prefs.dart'; -export 'theme_prefs.dart'; diff --git a/lib/core/prefs/theme_prefs.dart b/lib/core/prefs/theme_prefs.dart deleted file mode 100644 index a9f21b3b..00000000 --- a/lib/core/prefs/theme_prefs.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:hiddify/core/prefs/app_theme.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'theme_prefs.g.dart'; - -@Riverpod(keepAlive: true) -class ThemeModeNotifier extends _$ThemeModeNotifier { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider), - "theme_mode", - AppThemeMode.system, - mapFrom: AppThemeMode.values.byName, - mapTo: (value) => value.name, - ); - - @override - AppThemeMode build() => _pref.getValue(); - - Future update(AppThemeMode value) { - state = value; - return _pref.update(value); - } -} diff --git a/lib/core/router/app_router.dart b/lib/core/router/app_router.dart index 1339c2d9..44e14eac 100644 --- a/lib/core/router/app_router.dart +++ b/lib/core/router/app_router.dart @@ -1,7 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/router/routes.dart'; import 'package:hiddify/services/deep_link_service.dart'; import 'package:hiddify/utils/utils.dart'; @@ -92,6 +92,7 @@ class RouterListenable extends _$RouterListenable }); } +// ignore: avoid_build_context_in_providers String? redirect(BuildContext context, GoRouterState state) { // if (this.state.isLoading || this.state.hasError) return null; diff --git a/lib/core/router/routes.dart b/lib/core/router/routes.dart index 06c50e7f..d5ec6869 100644 --- a/lib/core/router/routes.dart +++ b/lib/core/router/routes.dart @@ -1,19 +1,19 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hiddify/core/router/app_router.dart'; -import 'package:hiddify/features/about/view/about_page.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; +import 'package:hiddify/features/config_option/overview/config_options_page.dart'; import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart'; -import 'package:hiddify/features/home/view/view.dart'; -import 'package:hiddify/features/intro/intro_page.dart'; +import 'package:hiddify/features/home/widget/home_page.dart'; +import 'package:hiddify/features/intro/widget/intro_page.dart'; import 'package:hiddify/features/log/overview/logs_overview_page.dart'; +import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_page.dart'; import 'package:hiddify/features/profile/add/add_profile_modal.dart'; import 'package:hiddify/features/profile/details/profile_details_page.dart'; import 'package:hiddify/features/profile/overview/profiles_overview_page.dart'; -import 'package:hiddify/features/proxies/view/view.dart'; -import 'package:hiddify/features/settings/view/config_options_page.dart'; -import 'package:hiddify/features/settings/view/per_app_proxy_page.dart'; -import 'package:hiddify/features/settings/view/settings_page.dart'; +import 'package:hiddify/features/proxy/overview/proxies_overview_page.dart'; +import 'package:hiddify/features/settings/about/about_page.dart'; +import 'package:hiddify/features/settings/overview/settings_overview_page.dart'; import 'package:hiddify/utils/utils.dart'; part 'routes.g.dart'; @@ -184,7 +184,7 @@ class ProxiesRoute extends GoRouteData { Page buildPage(BuildContext context, GoRouterState state) { return const NoTransitionPage( name: name, - child: ProxiesPage(), + child: ProxiesOverviewPage(), ); } } @@ -291,10 +291,10 @@ class SettingsRoute extends GoRouteData { return const MaterialPage( fullscreenDialog: true, name: name, - child: SettingsPage(), + child: SettingsOverviewPage(), ); } - return const NoTransitionPage(name: name, child: SettingsPage()); + return const NoTransitionPage(name: name, child: SettingsOverviewPage()); } } diff --git a/lib/core/prefs/app_theme.dart b/lib/core/theme/app_theme.dart similarity index 77% rename from lib/core/prefs/app_theme.dart rename to lib/core/theme/app_theme.dart index 786be4f5..60bce95b 100644 --- a/lib/core/prefs/app_theme.dart +++ b/lib/core/theme/app_theme.dart @@ -1,31 +1,9 @@ +// mostly exact copy of flex color scheme 7.1's fabulous 12 theme import 'package:flex_color_scheme/flex_color_scheme.dart'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/prefs/locale_prefs.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/theme/theme_extensions.dart'; -enum AppThemeMode { - system, - light, - dark, - black; - - String present(TranslationsEn t) => switch (this) { - system => t.settings.general.themeModes.system, - light => t.settings.general.themeModes.light, - dark => t.settings.general.themeModes.dark, - black => t.settings.general.themeModes.black, - }; - - ThemeMode get flutterThemeMode => switch (this) { - system => ThemeMode.system, - light => ThemeMode.light, - dark => ThemeMode.dark, - black => ThemeMode.dark, - }; - - bool get trueBlack => this == black; -} - -// mostly exact copy of flex color scheme 7.1's fabulous 12 theme class AppTheme { AppTheme( this.mode, @@ -160,42 +138,3 @@ class AppTheme { ); } } - -class ConnectionButtonTheme extends ThemeExtension { - const ConnectionButtonTheme({ - this.idleColor, - this.connectedColor, - }); - - final Color? idleColor; - final Color? connectedColor; - - static const ConnectionButtonTheme light = ConnectionButtonTheme( - idleColor: Color(0xFF4a4d8b), - connectedColor: Color(0xFF44a334), - ); - - @override - ThemeExtension copyWith({ - Color? idleColor, - Color? connectedColor, - }) => - ConnectionButtonTheme( - idleColor: idleColor ?? this.idleColor, - connectedColor: connectedColor ?? this.connectedColor, - ); - - @override - ThemeExtension lerp( - covariant ThemeExtension? other, - double t, - ) { - if (other is! ConnectionButtonTheme) { - return this; - } - return ConnectionButtonTheme( - idleColor: Color.lerp(idleColor, other.idleColor, t), - connectedColor: Color.lerp(connectedColor, other.connectedColor, t), - ); - } -} diff --git a/lib/core/theme/app_theme_mode.dart b/lib/core/theme/app_theme_mode.dart new file mode 100644 index 00000000..5696f70c --- /dev/null +++ b/lib/core/theme/app_theme_mode.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; + +enum AppThemeMode { + system, + light, + dark, + black; + + String present(TranslationsEn t) => switch (this) { + system => t.settings.general.themeModes.system, + light => t.settings.general.themeModes.light, + dark => t.settings.general.themeModes.dark, + black => t.settings.general.themeModes.black, + }; + + ThemeMode get flutterThemeMode => switch (this) { + system => ThemeMode.system, + light => ThemeMode.light, + dark => ThemeMode.dark, + black => ThemeMode.dark, + }; + + bool get trueBlack => this == black; +} diff --git a/lib/core/theme/theme_extensions.dart b/lib/core/theme/theme_extensions.dart new file mode 100644 index 00000000..7a7895c2 --- /dev/null +++ b/lib/core/theme/theme_extensions.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; + +class ConnectionButtonTheme extends ThemeExtension { + const ConnectionButtonTheme({ + this.idleColor, + this.connectedColor, + }); + + final Color? idleColor; + final Color? connectedColor; + + static const ConnectionButtonTheme light = ConnectionButtonTheme( + idleColor: Color(0xFF4a4d8b), + connectedColor: Color(0xFF44a334), + ); + + @override + ThemeExtension copyWith({ + Color? idleColor, + Color? connectedColor, + }) => + ConnectionButtonTheme( + idleColor: idleColor ?? this.idleColor, + connectedColor: connectedColor ?? this.connectedColor, + ); + + @override + ThemeExtension lerp( + covariant ThemeExtension? other, + double t, + ) { + if (other is! ConnectionButtonTheme) { + return this; + } + return ConnectionButtonTheme( + idleColor: Color.lerp(idleColor, other.idleColor, t), + connectedColor: Color.lerp(connectedColor, other.connectedColor, t), + ); + } +} diff --git a/lib/core/theme/theme_preferences.dart b/lib/core/theme/theme_preferences.dart new file mode 100644 index 00000000..29fd4d61 --- /dev/null +++ b/lib/core/theme/theme_preferences.dart @@ -0,0 +1,26 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'theme_preferences.g.dart'; + +@Riverpod(keepAlive: true) +class ThemePreferences extends _$ThemePreferences { + @override + AppThemeMode build() { + final persisted = ref + .watch(sharedPreferencesProvider) + .requireValue + .getString("theme_mode"); + if (persisted == null) return AppThemeMode.system; + return AppThemeMode.values.byName(persisted); + } + + Future changeThemeMode(AppThemeMode value) async { + state = value; + await ref + .read(sharedPreferencesProvider) + .requireValue + .setString("theme_mode", value.name); + } +} diff --git a/lib/data/repository/exception_handlers.dart b/lib/core/utils/exception_handler.dart similarity index 100% rename from lib/data/repository/exception_handlers.dart rename to lib/core/utils/exception_handler.dart diff --git a/lib/utils/ffi_utils.dart b/lib/core/utils/ffi_utils.dart similarity index 100% rename from lib/utils/ffi_utils.dart rename to lib/core/utils/ffi_utils.dart diff --git a/lib/core/utils/json_converters.dart b/lib/core/utils/json_converters.dart new file mode 100644 index 00000000..290f6da8 --- /dev/null +++ b/lib/core/utils/json_converters.dart @@ -0,0 +1,11 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +class IntervalInSecondsConverter implements JsonConverter { + const IntervalInSecondsConverter(); + + @override + Duration fromJson(int json) => Duration(seconds: json); + + @override + int toJson(Duration object) => object.inSeconds; +} diff --git a/lib/core/widget/custom_alert_dialog.dart b/lib/core/widget/custom_alert_dialog.dart index ad96ed1f..c3ca3f27 100644 --- a/lib/core/widget/custom_alert_dialog.dart +++ b/lib/core/widget/custom_alert_dialog.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; class CustomAlertDialog extends StatelessWidget { const CustomAlertDialog({ diff --git a/lib/data/data_providers.dart b/lib/data/data_providers.dart deleted file mode 100644 index 0f1b653c..00000000 --- a/lib/data/data_providers.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/general_prefs.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/repository.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/core_facade.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; -import 'package:hiddify/features/profile/data/profile_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'; -import 'package:shared_preferences/shared_preferences.dart'; - -part 'data_providers.g.dart'; - -@Riverpod(keepAlive: true) -AppDatabase appDatabase(AppDatabaseRef ref) => AppDatabase.connect(); - -@Riverpod(keepAlive: true) -SharedPreferences sharedPreferences(SharedPreferencesRef ref) => - throw UnimplementedError('sharedPreferences must be overridden'); - -@Riverpod(keepAlive: true) -Dio dio(DioRef ref) { - final dio = Dio( - BaseOptions( - connectTimeout: const Duration(seconds: 15), - sendTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 15), - headers: { - "User-Agent": ref.watch(appInfoProvider).userAgent, - }, - ), - ); - final debug = ref.read(debugModeNotifierProvider); - if (debug && (Platform.isAndroid || Platform.isIOS || Platform.isMacOS)) { - dio.httpClientAdapter = NativeAdapter(); - } - return dio; -} - -@Riverpod(keepAlive: true) -AppRepository appRepository(AppRepositoryRef ref) => - AppRepositoryImpl(ref.watch(dioProvider)); - -@riverpod -Future configOptions(ConfigOptionsRef ref) async { - final geoAssets = await ref - .watch(geoAssetRepositoryProvider) - .requireValue - .getActivePair() - .getOrElse((l) => throw l) - .run(); - final geoAssetsPathResolver = ref.watch(geoAssetPathResolverProvider); - - final serviceMode = ref.watch(serviceModeStoreProvider); - return ref.watch(configPreferencesProvider).copyWith( - enableTun: serviceMode == ServiceMode.tun, - setSystemProxy: serviceMode == ServiceMode.systemProxy, - geoipPath: geoAssetsPathResolver.relativePath( - geoAssets.geoip.providerName, - geoAssets.geoip.fileName, - ), - geositePath: geoAssetsPathResolver.relativePath( - geoAssets.geosite.providerName, - geoAssets.geosite.fileName, - ), - ); -} - -@Riverpod(keepAlive: true) -CoreFacade coreFacade(CoreFacadeRef ref) => CoreFacadeImpl( - ref.watch(singboxServiceProvider), - ref.watch(filesEditorServiceProvider), - ref.watch(geoAssetPathResolverProvider), - ref.watch(profilePathResolverProvider), - ref.watch(platformServicesProvider), - ref.read(debugModeNotifierProvider), - () => ref.read(configOptionsProvider.future), - ); diff --git a/lib/data/local/schemas/drift_schema_v3.json b/lib/data/local/schemas/drift_schema_v3.json deleted file mode 100644 index 4e845f77..00000000 --- a/lib/data/local/schemas/drift_schema_v3.json +++ /dev/null @@ -1 +0,0 @@ -{"_meta":{"description":"This file contains a serialized version of schema entities for drift.","version":"1.1.0"},"options":{"store_date_time_values_as_text":true},"entities":[{"id":0,"references":[],"type":"table","data":{"name":"profile_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(ProfileType.values)","dart_type_name":"ProfileType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"url","getter_name":"url","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_update","getter_name":"lastUpdate","moor_type":"dateTime","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"update_interval","getter_name":"updateInterval","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"DurationTypeConverter()","dart_type_name":"Duration"}},{"name":"upload","getter_name":"upload","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"download","getter_name":"download","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"total","getter_name":"total","moor_type":"int","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"expire","getter_name":"expire","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"web_page_url","getter_name":"webPageUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"support_url","getter_name":"supportUrl","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"]}},{"id":1,"references":[],"type":"table","data":{"name":"geo_asset_entries","was_declared_in_moor":false,"columns":[{"name":"id","getter_name":"id","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"type","getter_name":"type","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[],"type_converter":{"dart_expr":"const EnumNameConverter(GeoAssetType.values)","dart_type_name":"GeoAssetType"}},{"name":"active","getter_name":"active","moor_type":"bool","nullable":false,"customConstraints":null,"defaultConstraints":"CHECK (\"active\" IN (0, 1))","default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"name","getter_name":"name","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"provider_name","getter_name":"providerName","moor_type":"string","nullable":false,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[{"allowed-lengths":{"min":1,"max":null}}]},{"name":"version","getter_name":"version","moor_type":"string","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]},{"name":"last_check","getter_name":"lastCheck","moor_type":"dateTime","nullable":true,"customConstraints":null,"default_dart":null,"default_client_dart":null,"dsl_features":[]}],"is_virtual":false,"without_rowid":false,"constraints":[],"explicit_pk":["id"],"unique_keys":[["name","provider_name"]]}}]} \ No newline at end of file diff --git a/lib/data/repository/app_repository_impl.dart b/lib/data/repository/app_repository_impl.dart deleted file mode 100644 index f353da0e..00000000 --- a/lib/data/repository/app_repository_impl.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'dart:io'; - -import 'package:dio/dio.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/environment.dart'; -import 'package:hiddify/utils/custom_loggers.dart'; -import 'package:package_info_plus/package_info_plus.dart'; - -class AppRepositoryImpl - with ExceptionHandler, InfraLogger - implements AppRepository { - AppRepositoryImpl(this.dio); - - final Dio dio; - - static Future getAppInfo(Environment environment) async { - final packageInfo = await PackageInfo.fromPlatform(); - return AppInfo( - name: packageInfo.appName, - version: packageInfo.version, - buildNumber: packageInfo.buildNumber, - release: Release.read(), - operatingSystem: Platform.operatingSystem, - operatingSystemVersion: Platform.operatingSystemVersion, - environment: environment, - ); - } - - // TODO add market-specific update checking - @override - TaskEither getLatestVersion({ - bool includePreReleases = false, - Release release = Release.general, - }) { - return exceptionHandler( - () async { - if (!release.allowCustomUpdateChecker) { - throw Exception("custom update checkers are not supported"); - } - final response = await dio.get(Constants.githubReleasesApiUrl); - if (response.statusCode != 200 || response.data == null) { - loggy.warning("failed to fetch latest version info"); - return left(const AppFailure.unexpected()); - } - - final releases = response.data! - .map((e) => RemoteVersionInfo.fromJson(e as Map)); - late RemoteVersionInfo latest; - if (includePreReleases) { - latest = releases.first; - } else { - latest = releases.firstWhere((e) => e.preRelease == false); - } - return right(latest); - }, - AppFailure.unexpected, - ); - } -} diff --git a/lib/data/repository/config_options_store.dart b/lib/data/repository/config_options_store.dart deleted file mode 100644 index 55cb31b4..00000000 --- a/lib/data/repository/config_options_store.dart +++ /dev/null @@ -1,155 +0,0 @@ -// ignore_for_file: avoid_manual_providers_as_generated_provider_dependency -import 'package:flutter/foundation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; -import 'package:hiddify/utils/pref_notifier.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'config_options_store.g.dart'; - -bool _debugConfigBuilder = false; -final _default = ConfigOptions.initial; - -@Riverpod(keepAlive: true) -class ServiceModeStore extends _$ServiceModeStore { - late final _pref = Pref( - ref.watch(sharedPreferencesProvider), - "service-mode", - ServiceMode.defaultMode, - mapFrom: ServiceMode.values.byName, - mapTo: (value) => value.name, - ); - - @override - ServiceMode build() => _pref.getValue(); - - Future update(ServiceMode value) { - state = value; - return _pref.update(value); - } -} - -final logLevelStore = PrefNotifier.provider( - "log-level", - _default.logLevel, - mapFrom: LogLevel.values.byName, - mapTo: (value) => value.name, -); -final resolveDestinationStore = - PrefNotifier.provider("resolve-destination", _default.resolveDestination); -final ipv6ModeStore = PrefNotifier.provider( - "ipv6-mode", - _default.ipv6Mode, - mapFrom: IPv6Mode.values.byName, - mapTo: (value) => value.name, -); -final remoteDnsAddressStore = - PrefNotifier.provider("remote-dns-address", _default.remoteDnsAddress); -final remoteDnsDomainStrategyStore = PrefNotifier.provider( - "remote-domain-dns-strategy", - _default.remoteDnsDomainStrategy, - mapFrom: DomainStrategy.values.byName, - mapTo: (value) => value.name, -); -final directDnsAddressStore = - PrefNotifier.provider("direct-dns-address", _default.directDnsAddress); -final directDnsDomainStrategyStore = PrefNotifier.provider( - "direct-domain-dns-strategy", - _default.directDnsDomainStrategy, - mapFrom: DomainStrategy.values.byName, - mapTo: (value) => value.name, -); -final mixedPortStore = PrefNotifier.provider("mixed-port", _default.mixedPort); -final localDnsPortStore = - PrefNotifier.provider("localDns-port", _default.localDnsPort); -final tunImplementationStore = PrefNotifier.provider( - "tun-implementation", - _default.tunImplementation, - mapFrom: TunImplementation.values.byName, - mapTo: (value) => value.name, -); -final mtuStore = PrefNotifier.provider("mtu", _default.mtu); -final connectionTestUrlStore = - PrefNotifier.provider("connection-test-url", _default.connectionTestUrl); -final urlTestIntervalStore = PrefNotifier.provider( - "url-test-interval", - _default.urlTestInterval, - mapFrom: (value) => Duration(seconds: value), - mapTo: (value) => value.inSeconds, -); -final enableClashApiStore = - PrefNotifier.provider("enable-clash-api", _default.enableClashApi); -final clashApiPortStore = - PrefNotifier.provider("clash-api-port", _default.clashApiPort); -// final enableTunStore = PrefNotifier.provider("enable-tun", _default.enableTun); -// final setSystemProxyStore = -// PrefNotifier.provider("set-system-proxy", _default.setSystemProxy); -final strictRouteStore = - PrefNotifier.provider("strict-route", _default.strictRoute); -final bypassLanStore = PrefNotifier.provider("bypass-lan", _default.bypassLan); -final enableFakeDnsStore = - PrefNotifier.provider("enable-fake-dns", _default.enableFakeDns); - -// HACK temporary -@riverpod -List rules(RulesRef ref) => switch (ref.watch(regionNotifierProvider)) { - Region.ir => [ - const Rule( - id: "id", - name: "name", - enabled: true, - domains: "domain:.ir,geosite:ir", - ip: "geoip:ir", - outbound: RuleOutbound.bypass, - ), - ], - Region.cn => [ - const Rule( - id: "id", - name: "name", - enabled: true, - domains: "domain:.cn,geosite:cn", - ip: "geoip:cn", - outbound: RuleOutbound.bypass, - ), - ], - Region.ru => [ - const Rule( - id: "id", - name: "name", - enabled: true, - domains: "domain:.ru", - ip: "geoip:ru", - outbound: RuleOutbound.bypass, - ), - ], - _ => [], - }; - -@riverpod -ConfigOptions configPreferences(ConfigPreferencesRef ref) { - return ConfigOptions( - executeConfigAsIs: kDebugMode && _debugConfigBuilder, - logLevel: ref.watch(logLevelStore), - resolveDestination: ref.watch(resolveDestinationStore), - ipv6Mode: ref.watch(ipv6ModeStore), - remoteDnsAddress: ref.watch(remoteDnsAddressStore), - remoteDnsDomainStrategy: ref.watch(remoteDnsDomainStrategyStore), - directDnsAddress: ref.watch(directDnsAddressStore), - directDnsDomainStrategy: ref.watch(directDnsDomainStrategyStore), - mixedPort: ref.watch(mixedPortStore), - localDnsPort: ref.watch(localDnsPortStore), - tunImplementation: ref.watch(tunImplementationStore), - mtu: ref.watch(mtuStore), - strictRoute: ref.watch(strictRouteStore), - connectionTestUrl: ref.watch(connectionTestUrlStore), - urlTestInterval: ref.watch(urlTestIntervalStore), - enableClashApi: ref.watch(enableClashApiStore), - clashApiPort: ref.watch(clashApiPortStore), - bypassLan: ref.watch(bypassLanStore), - enableFakeDns: ref.watch(enableFakeDnsStore), - rules: ref.watch(rulesProvider), - ); -} diff --git a/lib/data/repository/core_facade_impl.dart b/lib/data/repository/core_facade_impl.dart deleted file mode 100644 index eea0b4df..00000000 --- a/lib/data/repository/core_facade_impl.dart +++ /dev/null @@ -1,246 +0,0 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/connectivity/connection_status.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/features/profile/data/profile_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'; -import 'package:hiddify/utils/utils.dart'; - -class CoreFacadeImpl with ExceptionHandler, InfraLogger implements CoreFacade { - CoreFacadeImpl( - this.singbox, - this.filesEditor, - this.geoAssetPathResolver, - this.profilePathResolver, - this.platformServices, - this.debug, - this.configOptions, - ); - - final SingboxService singbox; - final FilesEditorService filesEditor; - final GeoAssetPathResolver geoAssetPathResolver; - final ProfilePathResolver profilePathResolver; - final PlatformServices platformServices; - final bool debug; - final Future Function() configOptions; - - bool _initialized = false; - - TaskEither _getConfigOptions() { - return exceptionHandler( - () async { - final options = await configOptions(); - 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()); - } - return right(options); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither setup() { - if (_initialized) return TaskEither.of(unit); - return exceptionHandler( - () { - loggy.debug("setting up singbox"); - return singbox - .setup( - filesEditor.dirs.baseDir.path, - filesEditor.dirs.workingDir.path, - filesEditor.dirs.tempDir.path, - debug, - ) - .map((r) { - loggy.debug("setup complete"); - _initialized = true; - return r; - }) - .mapLeft(CoreServiceFailure.other) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither parseConfig( - String path, - String tempPath, - bool debug, - ) { - return exceptionHandler( - () { - return singbox - .parseConfig(path, tempPath, debug) - .mapLeft(CoreServiceFailure.invalidConfig) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither changeConfigOptions( - ConfigOptions options, - ) { - return exceptionHandler( - () { - return singbox - .changeConfigOptions(options) - .mapLeft(CoreServiceFailure.invalidConfigOptions) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither generateConfig( - String fileName, - ) { - return TaskEither.Do( - ($) async { - final configFile = profilePathResolver.file(fileName); - final options = await $(_getConfigOptions()); - await $(setup()); - await $(changeConfigOptions(options)); - return await $( - singbox - .generateConfig(configFile.path) - .mapLeft(CoreServiceFailure.other), - ); - }, - ).handleExceptions(CoreServiceFailure.unexpected); - } - - @override - TaskEither start( - String fileName, - bool disableMemoryLimit, - ) { - return TaskEither.Do( - ($) async { - final configFile = profilePathResolver.file(fileName); - final options = await $(_getConfigOptions()); - loggy.info( - "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", - ); - - await $( - TaskEither(() async { - if (options.enableTun) { - final hasPrivilege = await platformServices.hasPrivilege(); - if (!hasPrivilege) { - loggy.warning("missing privileges for tun mode"); - return left(const CoreMissingPrivilege()); - } - } - return right(unit); - }), - ); - await $(setup()); - await $(changeConfigOptions(options)); - return await $( - singbox - .start(configFile.path, disableMemoryLimit) - .mapLeft(CoreServiceFailure.start), - ); - }, - ).handleExceptions(CoreServiceFailure.unexpected); - } - - @override - TaskEither stop() { - return exceptionHandler( - () => singbox.stop().mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither restart( - String fileName, - bool disableMemoryLimit, - ) { - return exceptionHandler( - () async { - final configFile = profilePathResolver.file(fileName); - return _getConfigOptions() - .flatMap((options) => changeConfigOptions(options)) - .andThen( - () => singbox - .restart(configFile.path, disableMemoryLimit) - .mapLeft(CoreServiceFailure.start), - ) - .run(); - }, - CoreServiceFailure.unexpected, - ); - } - - @override - Stream>> watchOutbounds() { - return singbox.watchOutbounds().map((event) { - return (jsonDecode(event) as List).map((e) { - return OutboundGroup.fromJson(e as Map); - }).toList(); - }).handleExceptions( - (error, stackTrace) { - loggy.error("error watching outbounds", error, stackTrace); - return CoreServiceFailure.unexpected(error, stackTrace); - }, - ); - } - - @override - TaskEither selectOutbound( - String groupTag, - String outboundTag, - ) { - return exceptionHandler( - () => singbox - .selectOutbound(groupTag, outboundTag) - .mapLeft(CoreServiceFailure.other) - .run(), - CoreServiceFailure.unexpected, - ); - } - - @override - TaskEither urlTest(String groupTag) { - return exceptionHandler( - () => singbox.urlTest(groupTag).mapLeft(CoreServiceFailure.other).run(), - CoreServiceFailure.unexpected, - ); - } - - @override - Stream> watchCoreStatus() { - return singbox.watchStats().map((event) { - final json = jsonDecode(event); - return CoreStatus.fromJson(json as Map); - }).handleExceptions( - (error, stackTrace) { - loggy.warning("error watching status", error, stackTrace); - return CoreServiceFailure.unexpected(error, stackTrace); - }, - ); - } - - @override - Stream watchConnectionStatus() => - singbox.watchConnectionStatus(); -} diff --git a/lib/data/repository/repository.dart b/lib/data/repository/repository.dart deleted file mode 100644 index a5687644..00000000 --- a/lib/data/repository/repository.dart +++ /dev/null @@ -1 +0,0 @@ -export 'core_facade_impl.dart'; diff --git a/lib/domain/app/app.dart b/lib/domain/app/app.dart deleted file mode 100644 index 1853b271..00000000 --- a/lib/domain/app/app.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'app_failure.dart'; -export 'app_info.dart'; -export 'app_repository.dart'; diff --git a/lib/domain/app/app_failure.dart b/lib/domain/app/app_failure.dart deleted file mode 100644 index b340cbbb..00000000 --- a/lib/domain/app/app_failure.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; - -part 'app_failure.freezed.dart'; - -@freezed -sealed class AppFailure with _$AppFailure, Failure { - const AppFailure._(); - - @With() - const factory AppFailure.unexpected([ - Object? error, - StackTrace? stackTrace, - ]) = UpdateUnexpectedFailure; - - @override - ({String type, String? message}) present(TranslationsEn t) { - return switch (this) { - UpdateUnexpectedFailure() => (type: t.failure.unexpected, message: null), - }; - } -} diff --git a/lib/domain/app/app_info.dart b/lib/domain/app/app_info.dart deleted file mode 100644 index 51d09283..00000000 --- a/lib/domain/app/app_info.dart +++ /dev/null @@ -1,87 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/environment.dart'; - -part 'app_info.freezed.dart'; -part 'app_info.g.dart'; - -@freezed -class AppInfo with _$AppInfo { - const AppInfo._(); - - const factory AppInfo({ - required String name, - required String version, - required String buildNumber, - required Release release, - required String operatingSystem, - required String operatingSystemVersion, - required Environment environment, - }) = _AppInfo; - - String get userAgent => "HiddifyNext/$version ($operatingSystem) like ClashMeta v2ray sing-box"; - - String get presentVersion => environment == Environment.prod - ? version - : "$version ${environment.name}"; - - /// formats app info for sharing - String format() => ''' -$name v$version ($buildNumber) [${environment.name}] -${release.name} release -$operatingSystem [$operatingSystemVersion]'''; - - factory AppInfo.fromJson(Map json) => - _$AppInfoFromJson(json); -} - -// TODO ignore drafts -@Freezed() -class RemoteVersionInfo with _$RemoteVersionInfo { - const RemoteVersionInfo._(); - - const factory RemoteVersionInfo({ - required String version, - required String buildNumber, - required String releaseTag, - required bool preRelease, - required String url, - required DateTime publishedAt, - required Environment flavor, - }) = _RemoteVersionInfo; - - String get presentVersion => - flavor == Environment.prod ? version : "$version ${flavor.name}"; - - // ignore: prefer_constructors_over_static_methods - static RemoteVersionInfo fromJson(Map json) { - final fullTag = json['tag_name'] as String; - final fullVersion = fullTag.removePrefix("v").split("-").first.split("+"); - var version = fullVersion.first; - var buildNumber = fullVersion.elementAtOrElse(1, (index) => ""); - var flavor = Environment.prod; - for (final env in Environment.values) { - final suffix = ".${env.name}"; - if (version.endsWith(suffix)) { - version = version.removeSuffix(suffix); - flavor = env; - break; - } else if (buildNumber.endsWith(suffix)) { - buildNumber = buildNumber.removeSuffix(suffix); - flavor = env; - break; - } - } - final preRelease = json["prerelease"] as bool; - final publishedAt = DateTime.parse(json["published_at"] as String); - return RemoteVersionInfo( - version: version, - buildNumber: buildNumber, - releaseTag: fullTag, - preRelease: preRelease, - url: json["html_url"] as String, - publishedAt: publishedAt, - flavor: flavor, - ); - } -} diff --git a/lib/domain/app/app_repository.dart b/lib/domain/app/app_repository.dart deleted file mode 100644 index e51bcb35..00000000 --- a/lib/domain/app/app_repository.dart +++ /dev/null @@ -1,9 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/app/app_failure.dart'; -import 'package:hiddify/domain/app/app_info.dart'; - -abstract interface class AppRepository { - TaskEither getLatestVersion({ - bool includePreReleases = false, - }); -} diff --git a/lib/domain/connectivity/connectivity.dart b/lib/domain/connectivity/connectivity.dart deleted file mode 100644 index 8f2bb7cc..00000000 --- a/lib/domain/connectivity/connectivity.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'connection_failure.dart'; -export 'connection_status.dart'; diff --git a/lib/domain/core_facade.dart b/lib/domain/core_facade.dart deleted file mode 100644 index 15d0ba48..00000000 --- a/lib/domain/core_facade.dart +++ /dev/null @@ -1,3 +0,0 @@ -import 'package:hiddify/domain/singbox/singbox.dart'; - -abstract interface class CoreFacade implements SingboxFacade {} diff --git a/lib/domain/core_service_failure.dart b/lib/domain/core_service_failure.dart deleted file mode 100644 index 87ab9830..00000000 --- a/lib/domain/core_service_failure.dart +++ /dev/null @@ -1,101 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; - -part 'core_service_failure.freezed.dart'; - -@freezed -sealed class CoreServiceFailure with _$CoreServiceFailure, Failure { - const CoreServiceFailure._(); - - @With() - const factory CoreServiceFailure.unexpected( - Object? error, - StackTrace? stackTrace, - ) = UnexpectedCoreServiceFailure; - - @With() - const factory CoreServiceFailure.serviceNotRunning([String? message]) = - CoreServiceNotRunning; - - @With() - const factory CoreServiceFailure.missingPrivilege() = CoreMissingPrivilege; - - @With() - const factory CoreServiceFailure.missingGeoAssets() = CoreMissingGeoAssets; - - const factory CoreServiceFailure.invalidConfigOptions([ - String? message, - ]) = InvalidConfigOptions; - - @With() - const factory CoreServiceFailure.invalidConfig([ - String? message, - ]) = InvalidConfig; - - const factory CoreServiceFailure.create([ - String? message, - ]) = CoreServiceCreateFailure; - - const factory CoreServiceFailure.start([ - String? message, - ]) = CoreServiceStartFailure; - - const factory CoreServiceFailure.other([ - String? message, - ]) = CoreServiceOtherFailure; - - String? get msg => switch (this) { - UnexpectedCoreServiceFailure() => null, - CoreServiceNotRunning(:final message) => message, - CoreMissingPrivilege() => null, - CoreMissingGeoAssets() => null, - InvalidConfigOptions(:final message) => message, - InvalidConfig(:final message) => message, - CoreServiceCreateFailure(:final message) => message, - CoreServiceStartFailure(:final message) => message, - CoreServiceOtherFailure(:final message) => message, - }; - - @override - ({String type, String? message}) present(TranslationsEn t) { - return switch (this) { - UnexpectedCoreServiceFailure() => ( - type: t.failure.singbox.unexpected, - message: null, - ), - CoreServiceNotRunning(:final message) => ( - type: t.failure.singbox.serviceNotRunning, - message: message - ), - CoreMissingPrivilege() => ( - type: t.failure.singbox.missingPrivilege, - message: t.failure.singbox.missingPrivilegeMsg, - ), - CoreMissingGeoAssets() => ( - type: t.failure.singbox.missingGeoAssets, - message: t.failure.singbox.missingGeoAssetsMsg, - ), - InvalidConfigOptions(:final message) => ( - type: t.failure.singbox.invalidConfigOptions, - message: message - ), - InvalidConfig(:final message) => ( - type: t.failure.singbox.invalidConfig, - message: message - ), - CoreServiceCreateFailure(:final message) => ( - type: t.failure.singbox.create, - message: message - ), - CoreServiceStartFailure(:final message) => ( - type: t.failure.singbox.start, - message: message - ), - CoreServiceOtherFailure(:final message) => ( - type: t.failure.singbox.unexpected, - message: message - ), - }; - } -} diff --git a/lib/domain/enums.dart b/lib/domain/enums.dart deleted file mode 100644 index e5ce3ee4..00000000 --- a/lib/domain/enums.dart +++ /dev/null @@ -1 +0,0 @@ -enum SortMode { ascending, descending } diff --git a/lib/domain/singbox/config_options.dart b/lib/domain/singbox/config_options.dart deleted file mode 100644 index f350e79e..00000000 --- a/lib/domain/singbox/config_options.dart +++ /dev/null @@ -1,109 +0,0 @@ -import 'dart:convert'; - -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/singbox/rules.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; - -part 'config_options.freezed.dart'; -part 'config_options.g.dart'; - -@freezed -class ConfigOptions with _$ConfigOptions { - const ConfigOptions._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory ConfigOptions({ - @Default(false) bool executeConfigAsIs, - @Default(LogLevel.warn) LogLevel logLevel, - @Default(false) bool resolveDestination, - @Default(IPv6Mode.disable) IPv6Mode ipv6Mode, - @Default("tcp://8.8.8.8") String remoteDnsAddress, - @Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy, - @Default("8.8.8.8") String directDnsAddress, - @Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy, - @Default(2334) int mixedPort, - @Default(6450) int localDnsPort, - @Default(TunImplementation.mixed) TunImplementation tunImplementation, - @Default(9000) int mtu, - @Default(true) bool strictRoute, - @Default("http://cp.cloudflare.com/") String connectionTestUrl, - @IntervalConverter() - @Default(Duration(minutes: 10)) - Duration urlTestInterval, - @Default(true) bool enableClashApi, - @Default(6756) int clashApiPort, - @Default(false) bool enableTun, - @Default(false) bool setSystemProxy, - @Default(false) bool bypassLan, - @Default(false) bool enableFakeDns, - @Default(true) bool independentDnsCache, - @Default("geoip.db") String geoipPath, - @Default("geosite.db") String geositePath, - List? rules, - }) = _ConfigOptions; - - static ConfigOptions initial = const ConfigOptions(); - - String format() { - const encoder = JsonEncoder.withIndent(' '); - return encoder.convert(toJson()); - } - - factory ConfigOptions.fromJson(Map json) => - _$ConfigOptionsFromJson(json); -} - -@JsonEnum(valueField: 'key') -enum IPv6Mode { - disable("ipv4_only"), - enable("prefer_ipv4"), - prefer("prefer_ipv6"), - only("ipv6_only"); - - const IPv6Mode(this.key); - - final String key; - - String present(TranslationsEn t) => switch (this) { - disable => t.settings.config.ipv6Modes.disable, - enable => t.settings.config.ipv6Modes.enable, - prefer => t.settings.config.ipv6Modes.prefer, - only => t.settings.config.ipv6Modes.only, - }; -} - -@JsonEnum(valueField: 'key') -enum DomainStrategy { - auto(""), - preferIpv6("prefer_ipv6"), - preferIpv4("prefer_ipv4"), - ipv4Only("ipv4_only"), - ipv6Only("ipv6_only"); - - const DomainStrategy(this.key); - - final String key; - - String get displayName => switch (this) { - auto => "auto", - _ => key, - }; -} - -enum TunImplementation { - mixed, - system, - gVisor; -} - -class IntervalConverter implements JsonConverter { - const IntervalConverter(); - - @override - Duration fromJson(String json) => - Duration(minutes: int.parse(json.replaceAll("m", ""))); - - @override - String toJson(Duration object) => "${object.inMinutes}m"; -} diff --git a/lib/domain/singbox/core_status.dart b/lib/domain/singbox/core_status.dart deleted file mode 100644 index 979e57df..00000000 --- a/lib/domain/singbox/core_status.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; - -part 'core_status.freezed.dart'; -part 'core_status.g.dart'; - -@freezed -class CoreStatus with _$CoreStatus { - const CoreStatus._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory CoreStatus({ - required int connectionsIn, - required int connectionsOut, - required int uplink, - required int downlink, - required int uplinkTotal, - required int downlinkTotal, - }) = _CoreStatus; - - factory CoreStatus.empty() => const CoreStatus( - connectionsIn: 0, - connectionsOut: 0, - uplink: 0, - downlink: 0, - uplinkTotal: 0, - downlinkTotal: 0, - ); - - factory CoreStatus.fromJson(Map json) => - _$CoreStatusFromJson(json); -} diff --git a/lib/domain/singbox/outbounds.dart b/lib/domain/singbox/outbounds.dart deleted file mode 100644 index e99d0b11..00000000 --- a/lib/domain/singbox/outbounds.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:dartx/dartx.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/singbox/proxy_type.dart'; - -part 'outbounds.freezed.dart'; -part 'outbounds.g.dart'; - -@freezed -class OutboundGroup with _$OutboundGroup { - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory OutboundGroup({ - required String tag, - @JsonKey(fromJson: _typeFromJson) required ProxyType type, - required String selected, - @Default([]) List items, - }) = _OutboundGroup; - - factory OutboundGroup.fromJson(Map json) => - _$OutboundGroupFromJson(json); -} - -@freezed -class OutboundGroupItem with _$OutboundGroupItem { - const OutboundGroupItem._(); - - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory OutboundGroupItem({ - required String tag, - @JsonKey(fromJson: _typeFromJson) required ProxyType type, - required int urlTestDelay, - String? selectedTag, - }) = _OutboundGroupItem; - - factory OutboundGroupItem.fromJson(Map json) => - _$OutboundGroupItemFromJson(json); -} - -ProxyType _typeFromJson(dynamic type) => - ProxyType.values - .firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ?? - ProxyType.unknown; - -String sanitizedTag(String tag) => - tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight(); diff --git a/lib/domain/singbox/rules.dart b/lib/domain/singbox/rules.dart deleted file mode 100644 index 686474d2..00000000 --- a/lib/domain/singbox/rules.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/locale_prefs.dart'; - -part 'rules.freezed.dart'; -part 'rules.g.dart'; - -@freezed -class Rule with _$Rule { - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory Rule({ - @JsonKey(includeToJson: false) required String id, - @JsonKey(includeToJson: false) required String name, - @JsonKey(includeToJson: false) @Default(false) bool enabled, - String? domains, - String? ip, - String? port, - String? protocol, - @Default(RuleNetwork.tcpAndUdp) RuleNetwork network, - @Default(RuleOutbound.proxy) RuleOutbound outbound, - }) = _Rule; - - factory Rule.fromJson(Map json) => _$RuleFromJson(json); -} - -enum RuleOutbound { proxy, bypass, block } - -@JsonEnum(valueField: 'key') -enum RuleNetwork { - tcpAndUdp(""), - tcp("tcp"), - udp("udp"); - - const RuleNetwork(this.key); - - final String? key; -} - -enum PerAppProxyMode { - off, - include, - exclude; - - bool get enabled => this != off; - - ({String title, String message}) present(TranslationsEn t) => switch (this) { - off => ( - title: t.settings.network.perAppProxyModes.off, - message: t.settings.network.perAppProxyModes.offMsg, - ), - include => ( - title: t.settings.network.perAppProxyModes.include, - message: t.settings.network.perAppProxyModes.includeMsg, - ), - exclude => ( - title: t.settings.network.perAppProxyModes.exclude, - message: t.settings.network.perAppProxyModes.excludeMsg, - ), - }; -} - -enum Region { - ir, - cn, - ru, - other; - - String present(TranslationsEn t) => switch (this) { - ir => t.settings.general.regions.ir, - cn => t.settings.general.regions.cn, - ru => t.settings.general.regions.ru, - other => t.settings.general.regions.other, - }; -} diff --git a/lib/domain/singbox/service_mode.dart b/lib/domain/singbox/service_mode.dart deleted file mode 100644 index 34481e2d..00000000 --- a/lib/domain/singbox/service_mode.dart +++ /dev/null @@ -1,24 +0,0 @@ -import 'package:hiddify/core/prefs/locale_prefs.dart'; -import 'package:hiddify/utils/platform_utils.dart'; - -enum ServiceMode { - proxy, - systemProxy, - tun; - - static ServiceMode get defaultMode => - PlatformUtils.isDesktop ? systemProxy : tun; - - static List get choices { - if (PlatformUtils.isDesktop) { - return values; - } - return [proxy, tun]; - } - - String present(TranslationsEn t) => switch (this) { - proxy => t.settings.config.serviceModes.proxy, - systemProxy => t.settings.config.serviceModes.systemProxy, - tun => t.settings.config.serviceModes.tun, - }; -} diff --git a/lib/domain/singbox/singbox.dart b/lib/domain/singbox/singbox.dart deleted file mode 100644 index 441637cd..00000000 --- a/lib/domain/singbox/singbox.dart +++ /dev/null @@ -1,7 +0,0 @@ -export 'config_options.dart'; -export 'core_status.dart'; -export 'outbounds.dart'; -export 'proxy_type.dart'; -export 'rules.dart'; -export 'service_mode.dart'; -export 'singbox_facade.dart'; diff --git a/lib/domain/singbox/singbox_facade.dart b/lib/domain/singbox/singbox_facade.dart deleted file mode 100644 index 4231afb0..00000000 --- a/lib/domain/singbox/singbox_facade.dart +++ /dev/null @@ -1,49 +0,0 @@ -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/singbox/config_options.dart'; -import 'package:hiddify/domain/singbox/core_status.dart'; -import 'package:hiddify/domain/singbox/outbounds.dart'; - -abstract interface class SingboxFacade { - TaskEither setup(); - - TaskEither parseConfig( - String path, - String tempPath, - bool debug, - ); - - TaskEither changeConfigOptions( - ConfigOptions options, - ); - - TaskEither generateConfig( - String fileName, - ); - - TaskEither start( - String fileName, - bool disableMemoryLimit, - ); - - TaskEither stop(); - - TaskEither restart( - String fileName, - bool disableMemoryLimit, - ); - - Stream>> watchOutbounds(); - - TaskEither selectOutbound( - String groupTag, - String outboundTag, - ); - - TaskEither urlTest(String groupTag); - - Stream watchConnectionStatus(); - - Stream> watchCoreStatus(); -} diff --git a/lib/features/about/view/view.dart b/lib/features/about/view/view.dart deleted file mode 100644 index 8f120a21..00000000 --- a/lib/features/about/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'about_page.dart'; diff --git a/lib/core/app/app_view.dart b/lib/features/app/widget/app.dart similarity index 65% rename from lib/core/app/app_view.dart rename to lib/features/app/widget/app.dart index f4e11bd3..0ab58bbe 100644 --- a/lib/core/app/app_view.dart +++ b/lib/features/app/widget/app.dart @@ -2,11 +2,14 @@ import 'package:accessibility_tools/accessibility_tools.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/features/common/app_update_notifier.dart'; +import 'package:hiddify/core/theme/app_theme.dart'; +import 'package:hiddify/core/theme/theme_preferences.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; import 'package:hiddify/features/common/common_controllers.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -14,14 +17,15 @@ import 'package:upgrader/upgrader.dart'; bool _debugAccessibility = false; -class AppView extends HookConsumerWidget with PresLogger { - const AppView({super.key}); +class App extends HookConsumerWidget with PresLogger { + const App({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final router = ref.watch(routerProvider); - final locale = ref.watch(localeNotifierProvider).flutterLocale; - final theme = ref.watch(themeProvider); + final locale = ref.watch(localePreferencesProvider); + final themeMode = ref.watch(themePreferencesProvider); + final theme = AppTheme(themeMode, locale.preferredFontFamily); ref.watch(commonControllersProvider); @@ -29,11 +33,11 @@ class AppView extends HookConsumerWidget with PresLogger { return MaterialApp.router( routerConfig: router, - locale: locale, + locale: locale.flutterLocale, supportedLocales: AppLocaleUtils.supportedLocales, localizationsDelegates: GlobalMaterialLocalizations.delegates, debugShowCheckedModeBanner: false, - themeMode: theme.mode.flutterThemeMode, + themeMode: themeMode.flutterThemeMode, theme: theme.light(), darkTheme: theme.dark(), title: Constants.appName, diff --git a/lib/features/app_update/data/app_update_data_providers.dart b/lib/features/app_update/data/app_update_data_providers.dart new file mode 100644 index 00000000..834c4c10 --- /dev/null +++ b/lib/features/app_update/data/app_update_data_providers.dart @@ -0,0 +1,12 @@ +import 'package:hiddify/core/http_client/http_client_provider.dart'; +import 'package:hiddify/features/app_update/data/app_update_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'app_update_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +AppUpdateRepository appUpdateRepository( + AppUpdateRepositoryRef ref, +) { + return AppUpdateRepositoryImpl(dio: ref.watch(httpClientProvider)); +} diff --git a/lib/features/app_update/data/app_update_repository.dart b/lib/features/app_update/data/app_update_repository.dart new file mode 100644 index 00000000..6242b271 --- /dev/null +++ b/lib/features/app_update/data/app_update_repository.dart @@ -0,0 +1,55 @@ +import 'package:dio/dio.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/app_update/data/github_release_parser.dart'; +import 'package:hiddify/features/app_update/model/app_update_failure.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; +import 'package:hiddify/utils/utils.dart'; + +abstract interface class AppUpdateRepository { + TaskEither getLatestVersion({ + bool includePreReleases = false, + Release release = Release.general, + }); +} + +class AppUpdateRepositoryImpl + with ExceptionHandler, InfraLogger + implements AppUpdateRepository { + AppUpdateRepositoryImpl({required this.dio}); + + final Dio dio; + + @override + TaskEither getLatestVersion({ + bool includePreReleases = false, + Release release = Release.general, + }) { + return exceptionHandler( + () async { + if (!release.allowCustomUpdateChecker) { + throw Exception("custom update checkers are not supported"); + } + final response = await dio.get(Constants.githubReleasesApiUrl); + if (response.statusCode != 200 || response.data == null) { + loggy.warning("failed to fetch latest version info"); + return left(const AppUpdateFailure.unexpected()); + } + + final releases = response.data!.map( + (e) => GithubReleaseParser.parse(e as Map), + ); + late RemoteVersionEntity latest; + if (includePreReleases) { + latest = releases.first; + } else { + latest = releases.firstWhere((e) => e.preRelease == false); + } + return right(latest); + }, + AppUpdateFailure.unexpected, + ); + } +} diff --git a/lib/features/app_update/data/github_release_parser.dart b/lib/features/app_update/data/github_release_parser.dart new file mode 100644 index 00000000..2dd07d9d --- /dev/null +++ b/lib/features/app_update/data/github_release_parser.dart @@ -0,0 +1,36 @@ +import 'package:dartx/dartx.dart'; +import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; + +abstract class GithubReleaseParser { + static RemoteVersionEntity parse(Map json) { + final fullTag = json['tag_name'] as String; + final fullVersion = fullTag.removePrefix("v").split("-").first.split("+"); + var version = fullVersion.first; + var buildNumber = fullVersion.elementAtOrElse(1, (index) => ""); + var flavor = Environment.prod; + for (final env in Environment.values) { + final suffix = ".${env.name}"; + if (version.endsWith(suffix)) { + version = version.removeSuffix(suffix); + flavor = env; + break; + } else if (buildNumber.endsWith(suffix)) { + buildNumber = buildNumber.removeSuffix(suffix); + flavor = env; + break; + } + } + final preRelease = json["prerelease"] as bool; + final publishedAt = DateTime.parse(json["published_at"] as String); + return RemoteVersionEntity( + version: version, + buildNumber: buildNumber, + releaseTag: fullTag, + preRelease: preRelease, + url: json["html_url"] as String, + publishedAt: publishedAt, + flavor: flavor, + ); + } +} diff --git a/lib/features/app_update/model/app_update_failure.dart b/lib/features/app_update/model/app_update_failure.dart new file mode 100644 index 00000000..f83f3382 --- /dev/null +++ b/lib/features/app_update/model/app_update_failure.dart @@ -0,0 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; + +part 'app_update_failure.freezed.dart'; + +@freezed +sealed class AppUpdateFailure with _$AppUpdateFailure, Failure { + const AppUpdateFailure._(); + + @With() + const factory AppUpdateFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = AppUpdateUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + AppUpdateUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/app_update/model/remote_version_entity.dart b/lib/features/app_update/model/remote_version_entity.dart new file mode 100644 index 00000000..c57434d1 --- /dev/null +++ b/lib/features/app_update/model/remote_version_entity.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/model/environment.dart'; + +part 'remote_version_entity.freezed.dart'; + +@Freezed() +class RemoteVersionEntity with _$RemoteVersionEntity { + const RemoteVersionEntity._(); + + const factory RemoteVersionEntity({ + required String version, + required String buildNumber, + required String releaseTag, + required bool preRelease, + required String url, + required DateTime publishedAt, + required Environment flavor, + }) = _RemoteVersionEntity; + + String get presentVersion => + flavor == Environment.prod ? version : "$version ${flavor.name}"; +} diff --git a/lib/features/common/app_update_notifier.dart b/lib/features/app_update/notifier/app_update_notifier.dart similarity index 58% rename from lib/features/common/app_update_notifier.dart rename to lib/features/app_update/notifier/app_update_notifier.dart index a3a39092..357b95bb 100644 --- a/lib/features/common/app_update_notifier.dart +++ b/lib/features/app_update/notifier/app_update_notifier.dart @@ -1,17 +1,18 @@ import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/app_update/data/app_update_data_providers.dart'; +import 'package:hiddify/features/app_update/model/app_update_failure.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_state.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:upgrader/upgrader.dart'; import 'package:version/version.dart'; -part 'app_update_notifier.freezed.dart'; part 'app_update_notifier.g.dart'; const _debugUpgrader = true; @@ -22,33 +23,17 @@ Upgrader upgrader(UpgraderRef ref) => Upgrader( debugLogging: _debugUpgrader && kDebugMode, durationUntilAlertAgain: const Duration(hours: 12), messages: UpgraderMessages( - code: ref.watch(localeNotifierProvider).languageCode, + code: ref.watch(localePreferencesProvider).languageCode, ), ); -@freezed -class AppUpdateState with _$AppUpdateState { - const factory AppUpdateState.initial() = AppUpdateStateInitial; - const factory AppUpdateState.disabled() = AppUpdateStateDisabled; - const factory AppUpdateState.checking() = AppUpdateStateChecking; - const factory AppUpdateState.error(AppFailure error) = AppUpdateStateError; - const factory AppUpdateState.available(RemoteVersionInfo versionInfo) = - AppUpdateStateAvailable; - const factory AppUpdateState.ignored(RemoteVersionInfo versionInfo) = - AppUpdateStateIgnored; - const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable; -} - @Riverpod(keepAlive: true) class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { @override - AppUpdateState build() { - // _schedule(); - return const AppUpdateState.initial(); - } + AppUpdateState build() => const AppUpdateState.initial(); Pref get _ignoreReleasePref => Pref( - ref.read(sharedPreferencesProvider), + ref.read(sharedPreferencesProvider).requireValue, 'ignored_release_version', null, ); @@ -56,15 +41,14 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { Future check() async { loggy.debug("checking for update"); state = const AppUpdateState.checking(); - final appInfo = ref.watch(appInfoProvider); - // TODO use market-specific update checkers + final appInfo = ref.watch(appInfoProvider).requireValue; if (!appInfo.release.allowCustomUpdateChecker) { loggy.debug( "custom update checkers are not allowed for [${appInfo.release.name}] release", ); return state = const AppUpdateState.disabled(); } - return ref.watch(appRepositoryProvider).getLatestVersion().match( + return ref.watch(appUpdateRepositoryProvider).getLatestVersion().match( (err) { loggy.warning("failed to get latest version", err); return state = AppUpdateState.error(err); @@ -88,16 +72,16 @@ class AppUpdateNotifier extends _$AppUpdateNotifier with AppLogger { } catch (error, stackTrace) { loggy.warning("error parsing versions", error, stackTrace); return state = AppUpdateState.error( - AppFailure.unexpected(error, stackTrace), + AppUpdateFailure.unexpected(error, stackTrace), ); } }, ).run(); } - Future ignoreRelease(RemoteVersionInfo versionInfo) async { - loggy.debug("ignoring release [${versionInfo.version}]"); - await _ignoreReleasePref.update(versionInfo.version); - state = AppUpdateStateIgnored(versionInfo); + Future ignoreRelease(RemoteVersionEntity version) async { + loggy.debug("ignoring release [${version.version}]"); + await _ignoreReleasePref.update(version.version); + state = AppUpdateStateIgnored(version); } } diff --git a/lib/features/app_update/notifier/app_update_state.dart b/lib/features/app_update/notifier/app_update_state.dart new file mode 100644 index 00000000..fe00d9ec --- /dev/null +++ b/lib/features/app_update/notifier/app_update_state.dart @@ -0,0 +1,19 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/app_update/model/app_update_failure.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; + +part 'app_update_state.freezed.dart'; + +@freezed +class AppUpdateState with _$AppUpdateState { + const factory AppUpdateState.initial() = AppUpdateStateInitial; + const factory AppUpdateState.disabled() = AppUpdateStateDisabled; + const factory AppUpdateState.checking() = AppUpdateStateChecking; + const factory AppUpdateState.error(AppUpdateFailure error) = + AppUpdateStateError; + const factory AppUpdateState.available(RemoteVersionEntity versionInfo) = + AppUpdateStateAvailable; + const factory AppUpdateState.ignored(RemoteVersionEntity versionInfo) = + AppUpdateStateIgnored; + const factory AppUpdateState.notAvailable() = AppUpdateStateNotAvailable; +} diff --git a/lib/features/common/new_version_dialog.dart b/lib/features/app_update/widget/new_version_dialog.dart similarity index 91% rename from lib/features/common/new_version_dialog.dart rename to lib/features/app_update/widget/new_version_dialog.dart index 197b32c4..e636fd8c 100644 --- a/lib/features/common/new_version_dialog.dart +++ b/lib/features/app_update/widget/new_version_dialog.dart @@ -1,13 +1,12 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/app/app.dart'; -import 'package:hiddify/features/common/app_update_notifier.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/app_update/model/remote_version_entity.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -// TODO add release notes class NewVersionDialog extends HookConsumerWidget with PresLogger { NewVersionDialog( this.currentVersion, @@ -16,7 +15,7 @@ class NewVersionDialog extends HookConsumerWidget with PresLogger { }) : super(key: _dialogKey); final String currentVersion; - final RemoteVersionInfo newVersion; + final RemoteVersionEntity newVersion; final bool canIgnore; static final _dialogKey = GlobalKey(debugLabel: 'new version dialog'); diff --git a/lib/features/common/adaptive_root_scaffold.dart b/lib/features/common/adaptive_root_scaffold.dart index 5dea563b..91d15336 100644 --- a/lib/features/common/adaptive_root_scaffold.dart +++ b/lib/features/common/adaptive_root_scaffold.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_adaptive_scaffold/flutter_adaptive_scaffold.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/features/common/side_bar_stats_overview.dart'; +import 'package:hiddify/features/stats/widget/side_bar_stats_overview.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; abstract interface class RootScaffold { diff --git a/lib/features/common/common_controllers.dart b/lib/features/common/common_controllers.dart index a8172a49..b0e63882 100644 --- a/lib/features/common/common_controllers.dart +++ b/lib/features/common/common_controllers.dart @@ -1,6 +1,6 @@ -import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/profile/notifier/profiles_update_notifier.dart'; import 'package:hiddify/features/system_tray/system_tray_controller.dart'; import 'package:hiddify/utils/platform_utils.dart'; @@ -22,7 +22,7 @@ void commonControllers(CommonControllersRef ref) { fireImmediately: true, ); ref.listen( - connectivityControllerProvider, + connectionNotifierProvider, (previous, next) {}, fireImmediately: true, ); diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index 69a354cb..1ac1c373 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/core/localization/locale_extensions.dart'; +import 'package:hiddify/core/localization/locale_preferences.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class LocalePrefTile extends HookConsumerWidget { @@ -12,7 +14,7 @@ class LocalePrefTile extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final locale = ref.watch(localeNotifierProvider); + final locale = ref.watch(localePreferencesProvider); return ListTile( title: Text(t.settings.general.locale), @@ -39,8 +41,8 @@ class LocalePrefTile extends HookConsumerWidget { ); if (selectedLocale != null) { await ref - .read(localeNotifierProvider.notifier) - .update(selectedLocale); + .read(localePreferencesProvider.notifier) + .changeLocale(selectedLocale); } }, ); diff --git a/lib/features/common/qr_code_scanner_screen.dart b/lib/features/common/qr_code_scanner_screen.dart index 14ef3071..152df8a0 100644 --- a/lib/features/common/qr_code_scanner_screen.dart +++ b/lib/features/common/qr_code_scanner_screen.dart @@ -1,7 +1,7 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:mobile_scanner/mobile_scanner.dart'; diff --git a/lib/features/common/stats_provider.dart b/lib/features/common/stats_provider.dart deleted file mode 100644 index 5ddbd43b..00000000 --- a/lib/features/common/stats_provider.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'stats_provider.g.dart'; - -@riverpod -class Stats extends _$Stats with AppLogger { - @override - Stream build() async* { - final serviceRunning = await ref.watch(serviceRunningProvider.future); - if (serviceRunning) { - yield* ref - .watch(coreFacadeProvider) - .watchCoreStatus() - .map((event) => event.getOrElse((_) => CoreStatus.empty())); - } else { - yield* Stream.value(CoreStatus.empty()); - } - } -} diff --git a/lib/features/common/window/window_controller.dart b/lib/features/common/window/window_controller.dart index 7b4d09de..bc8884dd 100644 --- a/lib/features/common/window/window_controller.dart +++ b/lib/features/common/window/window_controller.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/prefs/service_prefs.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/preferences/service_preferences.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:window_manager/window_manager.dart'; @@ -16,10 +16,10 @@ class WindowController extends _$WindowController Future build() async { await windowManager.ensureInitialized(); const size = Size(868, 668); - const minumumSize = Size(368, 568); + const minimumSize = Size(368, 568); const windowOptions = WindowOptions( size: size, - minimumSize: minumumSize, + minimumSize: minimumSize, center: true, ); await windowManager.setPreventClose(true); @@ -35,9 +35,7 @@ class WindowController extends _$WindowController () async { if (ref.read(startedByUserProvider)) { loggy.debug("previously started by user, trying to connect"); - return ref - .read(connectivityControllerProvider.notifier) - .mayConnect(); + return ref.read(connectionNotifierProvider.notifier).mayConnect(); } }, ); diff --git a/lib/features/config_option/data/config_option_data_providers.dart b/lib/features/config_option/data/config_option_data_providers.dart new file mode 100644 index 00000000..c9b5f9b4 --- /dev/null +++ b/lib/features/config_option/data/config_option_data_providers.dart @@ -0,0 +1,17 @@ +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'config_option_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +ConfigOptionRepository configOptionRepository( + ConfigOptionRepositoryRef ref, +) { + return ConfigOptionRepositoryImpl( + preferences: ref.watch(sharedPreferencesProvider).requireValue, + geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue, + geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), + ); +} diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart new file mode 100644 index 00000000..f302b1ce --- /dev/null +++ b/lib/features/config_option/data/config_option_repository.dart @@ -0,0 +1,172 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_failure.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.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/singbox/model/singbox_config_enum.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_rule.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +abstract interface class ConfigOptionRepository { + TaskEither + getFullSingboxConfigOption(); + TaskEither getConfigOption(); + TaskEither updateConfigOption( + ConfigOptionPatch patch, + ); +} + +class ConfigOptionRepositoryImpl + with ExceptionHandler, InfraLogger + implements ConfigOptionRepository { + ConfigOptionRepositoryImpl({ + required this.preferences, + required this.geoAssetRepository, + required this.geoAssetPathResolver, + }); + + final SharedPreferences preferences; + final GeoAssetRepository geoAssetRepository; + final GeoAssetPathResolver geoAssetPathResolver; + + @override + TaskEither + getFullSingboxConfigOption() { + return exceptionHandler( + () async { + final region = + Region.values.byName(preferences.getString("region") ?? "other"); + final rules = switch (region) { + Region.ir => [ + const SingboxRule( + domains: "domain:.ir,geosite:ir", + ip: "geoip:ir", + outbound: RuleOutbound.bypass, + ), + ], + Region.cn => [ + const SingboxRule( + domains: "domain:.cn,geosite:cn", + ip: "geoip:cn", + outbound: RuleOutbound.bypass, + ), + ], + Region.ru => [ + const SingboxRule( + domains: "domain:.ru", + ip: "geoip:ru", + outbound: RuleOutbound.bypass, + ), + ], + _ => [], + }; + + final geoAssets = await geoAssetRepository + .getActivePair() + .getOrElse((l) => throw l) + .run(); + + final persisted = + await getConfigOption().getOrElse((l) => throw l).run(); + final singboxConfigOption = SingboxConfigOption( + executeConfigAsIs: false, + logLevel: persisted.logLevel, + resolveDestination: persisted.resolveDestination, + ipv6Mode: persisted.ipv6Mode, + remoteDnsAddress: persisted.remoteDnsAddress, + remoteDnsDomainStrategy: persisted.remoteDnsDomainStrategy, + directDnsAddress: persisted.directDnsAddress, + directDnsDomainStrategy: persisted.directDnsDomainStrategy, + mixedPort: persisted.mixedPort, + localDnsPort: persisted.localDnsPort, + tunImplementation: persisted.tunImplementation, + mtu: persisted.mtu, + strictRoute: persisted.strictRoute, + connectionTestUrl: persisted.connectionTestUrl, + urlTestInterval: persisted.urlTestInterval, + enableClashApi: persisted.enableClashApi, + clashApiPort: persisted.clashApiPort, + enableTun: persisted.serviceMode == ServiceMode.tun, + setSystemProxy: persisted.serviceMode == ServiceMode.systemProxy, + bypassLan: persisted.bypassLan, + enableFakeDns: persisted.enableFakeDns, + independentDnsCache: persisted.independentDnsCache, + geoipPath: geoAssetPathResolver.relativePath( + geoAssets.geoip.providerName, + geoAssets.geoip.fileName, + ), + geositePath: geoAssetPathResolver.relativePath( + geoAssets.geosite.providerName, + geoAssets.geosite.fileName, + ), + rules: rules, + ); + return right(singboxConfigOption); + }, + ConfigOptionUnexpectedFailure.new, + ); + } + + @override + TaskEither getConfigOption() { + return exceptionHandler( + () async { + final map = ConfigOptionEntity.initial.toJson(); + for (final key in map.keys) { + final persisted = preferences.get(key); + if (persisted != null) { + final defaultValue = map[key]; + if (defaultValue != null && + persisted.runtimeType != defaultValue.runtimeType) { + loggy.warning( + "error getting preference[$key], expected type: [${defaultValue.runtimeType}] - received value: [$persisted](${persisted.runtimeType})", + ); + continue; + } + map[key] = persisted; + } + } + final options = ConfigOptionEntity.fromJson(map); + return right(options); + }, + ConfigOptionUnexpectedFailure.new, + ); + } + + @override + TaskEither updateConfigOption( + ConfigOptionPatch patch, + ) { + return exceptionHandler( + () async { + final map = patch.toJson(); + for (final key in map.keys) { + final value = map[key]; + if (value != null) { + loggy.debug("updating [$key] to [$value]"); + + switch (value) { + case bool _: + await preferences.setBool(key, value); + case String _: + await preferences.setString(key, value); + case int _: + await preferences.setInt(key, value); + case double _: + await preferences.setDouble(key, value); + default: + loggy.warning("unexpected type"); + } + } + } + return right(unit); + }, + ConfigOptionUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/config_option/model/config_option_entity.dart b/lib/features/config_option/model/config_option_entity.dart new file mode 100644 index 00000000..9e95f62e --- /dev/null +++ b/lib/features/config_option/model/config_option_entity.dart @@ -0,0 +1,80 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/utils/json_converters.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; + +part 'config_option_entity.freezed.dart'; +part 'config_option_entity.g.dart'; + +@freezed +class ConfigOptionEntity with _$ConfigOptionEntity { + const ConfigOptionEntity._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory ConfigOptionEntity({ + required ServiceMode serviceMode, + @Default(LogLevel.warn) LogLevel logLevel, + @Default(false) bool resolveDestination, + @Default(IPv6Mode.disable) IPv6Mode ipv6Mode, + @Default("tcp://8.8.8.8") String remoteDnsAddress, + @Default(DomainStrategy.auto) DomainStrategy remoteDnsDomainStrategy, + @Default("8.8.8.8") String directDnsAddress, + @Default(DomainStrategy.auto) DomainStrategy directDnsDomainStrategy, + @Default(2334) int mixedPort, + @Default(6450) int localDnsPort, + @Default(TunImplementation.mixed) TunImplementation tunImplementation, + @Default(9000) int mtu, + @Default(true) bool strictRoute, + @Default("http://cp.cloudflare.com/") String connectionTestUrl, + @IntervalInSecondsConverter() + @Default(Duration(minutes: 10)) + Duration urlTestInterval, + @Default(true) bool enableClashApi, + @Default(6756) int clashApiPort, + @Default(false) bool bypassLan, + @Default(false) bool enableFakeDns, + @Default(true) bool independentDnsCache, + }) = _ConfigOptionEntity; + + static ConfigOptionEntity initial = ConfigOptionEntity( + serviceMode: ServiceMode.defaultMode, + ); + + String format() { + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert(toJson()); + } + + ConfigOptionEntity patch(ConfigOptionPatch patch) { + return copyWith( + serviceMode: patch.serviceMode ?? serviceMode, + logLevel: patch.logLevel ?? logLevel, + resolveDestination: patch.resolveDestination ?? resolveDestination, + ipv6Mode: patch.ipv6Mode ?? ipv6Mode, + remoteDnsAddress: patch.remoteDnsAddress ?? remoteDnsAddress, + remoteDnsDomainStrategy: + patch.remoteDnsDomainStrategy ?? remoteDnsDomainStrategy, + directDnsAddress: patch.directDnsAddress ?? directDnsAddress, + directDnsDomainStrategy: + patch.directDnsDomainStrategy ?? directDnsDomainStrategy, + mixedPort: patch.mixedPort ?? mixedPort, + localDnsPort: patch.localDnsPort ?? localDnsPort, + tunImplementation: patch.tunImplementation ?? tunImplementation, + mtu: patch.mtu ?? mtu, + strictRoute: patch.strictRoute ?? strictRoute, + connectionTestUrl: patch.connectionTestUrl ?? connectionTestUrl, + urlTestInterval: patch.urlTestInterval ?? urlTestInterval, + enableClashApi: patch.enableClashApi ?? enableClashApi, + clashApiPort: patch.clashApiPort ?? clashApiPort, + bypassLan: patch.bypassLan ?? bypassLan, + enableFakeDns: patch.enableFakeDns ?? enableFakeDns, + independentDnsCache: patch.independentDnsCache ?? independentDnsCache, + ); + } + + factory ConfigOptionEntity.fromJson(Map json) => + _$ConfigOptionEntityFromJson(json); +} diff --git a/lib/features/config_option/model/config_option_failure.dart b/lib/features/config_option/model/config_option_failure.dart new file mode 100644 index 00000000..bc5c9ab5 --- /dev/null +++ b/lib/features/config_option/model/config_option_failure.dart @@ -0,0 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; + +part 'config_option_failure.freezed.dart'; + +@freezed +sealed class ConfigOptionFailure with _$ConfigOptionFailure, Failure { + const ConfigOptionFailure._(); + + @With() + const factory ConfigOptionFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = ConfigOptionUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + ConfigOptionUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/config_option/model/config_option_patch.dart b/lib/features/config_option/model/config_option_patch.dart new file mode 100644 index 00000000..b19c1dc8 --- /dev/null +++ b/lib/features/config_option/model/config_option_patch.dart @@ -0,0 +1,39 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/utils/json_converters.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; + +part 'config_option_patch.freezed.dart'; +part 'config_option_patch.g.dart'; + +@freezed +class ConfigOptionPatch with _$ConfigOptionPatch { + const ConfigOptionPatch._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory ConfigOptionPatch({ + ServiceMode? serviceMode, + LogLevel? logLevel, + bool? resolveDestination, + IPv6Mode? ipv6Mode, + String? remoteDnsAddress, + DomainStrategy? remoteDnsDomainStrategy, + String? directDnsAddress, + DomainStrategy? directDnsDomainStrategy, + int? mixedPort, + int? localDnsPort, + TunImplementation? tunImplementation, + int? mtu, + bool? strictRoute, + String? connectionTestUrl, + @IntervalInSecondsConverter() Duration? urlTestInterval, + bool? enableClashApi, + int? clashApiPort, + bool? bypassLan, + bool? enableFakeDns, + bool? independentDnsCache, + }) = _ConfigOptionPatch; + + factory ConfigOptionPatch.fromJson(Map json) => + _$ConfigOptionPatchFromJson(json); +} diff --git a/lib/features/config_option/notifier/config_option_notifier.dart b/lib/features/config_option/notifier/config_option_notifier.dart new file mode 100644 index 00000000..fba59b9c --- /dev/null +++ b/lib/features/config_option/notifier/config_option_notifier.dart @@ -0,0 +1,31 @@ +import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'config_option_notifier.g.dart'; + +@Riverpod(keepAlive: true) +class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { + @override + Future build() { + return ref + .watch(configOptionRepositoryProvider) + .getConfigOption() + .getOrElse((l) { + loggy.error("error getting persisted options $l", l); + throw l; + }).run(); + } + + Future updateOption(ConfigOptionPatch patch) async { + if (state case AsyncData(value: final options)) { + await ref + .read(configOptionRepositoryProvider) + .updateConfigOption(patch) + .map((_) => state = AsyncData(options.patch(patch))) + .run(); + } + } +} diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart new file mode 100644 index 00000000..db639f57 --- /dev/null +++ b/lib/features/config_option/overview/config_options_page.dart @@ -0,0 +1,323 @@ +import 'package:dartx/dartx.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:gap/gap.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/config_option/model/config_option_entity.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/features/settings/widgets/sections_widgets.dart'; +import 'package:hiddify/features/settings/widgets/settings_input_dialog.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:humanizer/humanizer.dart'; + +class ConfigOptionsPage extends HookConsumerWidget { + const ConfigOptionsPage({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final defaultOptions = ConfigOptionEntity.initial; + final asyncOptions = ref.watch(configOptionNotifierProvider); + + Future changeOption(ConfigOptionPatch patch) async { + await ref.read(configOptionNotifierProvider.notifier).updateOption(patch); + } + + return Scaffold( + appBar: AppBar( + title: Text(t.settings.config.pageTitle), + actions: [ + if (asyncOptions case AsyncData(value: final options)) + PopupMenuButton( + itemBuilder: (context) { + return [ + PopupMenuItem( + child: Text(t.general.addToClipboard), + onTap: () { + Clipboard.setData( + ClipboardData(text: options.format()), + ); + }, + ), + ]; + }, + ), + ], + ), + body: switch (asyncOptions) { + AsyncData(value: final options) => ListView( + children: [ + ListTile( + title: Text(t.settings.config.logLevel), + subtitle: Text(options.logLevel.name.toUpperCase()), + onTap: () async { + final logLevel = await SettingsPickerDialog( + title: t.settings.config.logLevel, + selected: options.logLevel, + options: LogLevel.choices, + getTitle: (e) => e.name.toUpperCase(), + resetValue: defaultOptions.logLevel, + ).show(context); + if (logLevel == null) return; + await changeOption(ConfigOptionPatch(logLevel: logLevel)); + }, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.route), + // SwitchListTile( + // title: Text(t.settings.config.bypassLan), + // value: options.bypassLan, + // onChanged: ref.read(bypassLanStore.notifier).update, + // ), + SwitchListTile( + title: Text(t.settings.config.resolveDestination), + value: options.resolveDestination, + onChanged: (value) async => + changeOption(ConfigOptionPatch(resolveDestination: value)), + ), + ListTile( + title: Text(t.settings.config.ipv6Mode), + subtitle: Text(options.ipv6Mode.present(t)), + onTap: () async { + final ipv6Mode = await SettingsPickerDialog( + title: t.settings.config.ipv6Mode, + selected: options.ipv6Mode, + options: IPv6Mode.values, + getTitle: (e) => e.present(t), + resetValue: defaultOptions.ipv6Mode, + ).show(context); + if (ipv6Mode == null) return; + await changeOption(ConfigOptionPatch(ipv6Mode: ipv6Mode)); + }, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.dns), + ListTile( + title: Text(t.settings.config.remoteDnsAddress), + subtitle: Text(options.remoteDnsAddress), + onTap: () async { + final url = await SettingsInputDialog( + title: t.settings.config.remoteDnsAddress, + initialValue: options.remoteDnsAddress, + resetValue: defaultOptions.remoteDnsAddress, + ).show(context); + if (url == null || url.isEmpty) return; + await changeOption(ConfigOptionPatch(remoteDnsAddress: url)); + }, + ), + ListTile( + title: Text(t.settings.config.remoteDnsDomainStrategy), + subtitle: Text(options.remoteDnsDomainStrategy.displayName), + onTap: () async { + final domainStrategy = await SettingsPickerDialog( + title: t.settings.config.remoteDnsDomainStrategy, + selected: options.remoteDnsDomainStrategy, + options: DomainStrategy.values, + getTitle: (e) => e.displayName, + resetValue: defaultOptions.remoteDnsDomainStrategy, + ).show(context); + if (domainStrategy == null) return; + await changeOption( + ConfigOptionPatch(remoteDnsDomainStrategy: domainStrategy), + ); + }, + ), + ListTile( + title: Text(t.settings.config.directDnsAddress), + subtitle: Text(options.directDnsAddress), + onTap: () async { + final url = await SettingsInputDialog( + title: t.settings.config.directDnsAddress, + initialValue: options.directDnsAddress, + resetValue: defaultOptions.directDnsAddress, + ).show(context); + if (url == null || url.isEmpty) return; + await changeOption(ConfigOptionPatch(directDnsAddress: url)); + }, + ), + ListTile( + title: Text(t.settings.config.directDnsDomainStrategy), + subtitle: Text(options.directDnsDomainStrategy.displayName), + onTap: () async { + final domainStrategy = await SettingsPickerDialog( + title: t.settings.config.directDnsDomainStrategy, + selected: options.directDnsDomainStrategy, + options: DomainStrategy.values, + getTitle: (e) => e.displayName, + resetValue: defaultOptions.directDnsDomainStrategy, + ).show(context); + if (domainStrategy == null) return; + await changeOption( + ConfigOptionPatch(directDnsDomainStrategy: domainStrategy), + ); + }, + ), + // SwitchListTile( + // title: Text(t.settings.config.enableFakeDns), + // value: options.enableFakeDns, + // onChanged: ref.read(enableFakeDnsStore.notifier).update, + // ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.inbound), + // if (PlatformUtils.isDesktop) ...[ + // SwitchListTile( + // title: Text(t.settings.config.enableTun), + // value: options.enableTun, + // onChanged: ref.read(enableTunStore.notifier).update, + // ), + // SwitchListTile( + // title: Text(t.settings.config.setSystemProxy), + // value: options.setSystemProxy, + // onChanged: ref.read(setSystemProxyStore.notifier).update, + // ), + // ], + ListTile( + title: Text(t.settings.config.serviceMode), + subtitle: Text(options.serviceMode.present(t)), + onTap: () async { + final pickedMode = await SettingsPickerDialog( + title: t.settings.config.serviceMode, + selected: options.serviceMode, + options: ServiceMode.choices, + getTitle: (e) => e.present(t), + resetValue: ServiceMode.defaultMode, + ).show(context); + if (pickedMode == null) return; + await changeOption( + ConfigOptionPatch(serviceMode: pickedMode), + ); + }, + ), + SwitchListTile( + title: Text(t.settings.config.strictRoute), + value: options.strictRoute, + onChanged: (value) async => + changeOption(ConfigOptionPatch(strictRoute: value)), + ), + ListTile( + title: Text(t.settings.config.tunImplementation), + subtitle: Text(options.tunImplementation.name), + onTap: () async { + final tunImplementation = await SettingsPickerDialog( + title: t.settings.config.tunImplementation, + selected: options.tunImplementation, + options: TunImplementation.values, + getTitle: (e) => e.name, + resetValue: defaultOptions.tunImplementation, + ).show(context); + if (tunImplementation == null) return; + await changeOption( + ConfigOptionPatch(tunImplementation: tunImplementation), + ); + }, + ), + ListTile( + title: Text(t.settings.config.mixedPort), + subtitle: Text(options.mixedPort.toString()), + onTap: () async { + final mixedPort = await SettingsInputDialog( + title: t.settings.config.mixedPort, + initialValue: options.mixedPort, + resetValue: defaultOptions.mixedPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (mixedPort == null) return; + await changeOption(ConfigOptionPatch(mixedPort: mixedPort)); + }, + ), + ListTile( + title: Text(t.settings.config.localDnsPort), + subtitle: Text(options.localDnsPort.toString()), + onTap: () async { + final localDnsPort = await SettingsInputDialog( + title: t.settings.config.localDnsPort, + initialValue: options.localDnsPort, + resetValue: defaultOptions.localDnsPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (localDnsPort == null) return; + await changeOption( + ConfigOptionPatch(localDnsPort: localDnsPort), + ); + }, + ), + const SettingsDivider(), + SettingsSection(t.settings.config.section.misc), + ListTile( + title: Text(t.settings.config.connectionTestUrl), + subtitle: Text(options.connectionTestUrl), + onTap: () async { + final url = await SettingsInputDialog( + title: t.settings.config.connectionTestUrl, + initialValue: options.connectionTestUrl, + resetValue: defaultOptions.connectionTestUrl, + ).show(context); + if (url == null || url.isEmpty || !isUrl(url)) return; + await changeOption(ConfigOptionPatch(connectionTestUrl: url)); + }, + ), + ListTile( + title: Text(t.settings.config.urlTestInterval), + subtitle: Text( + options.urlTestInterval + .toApproximateTime(isRelativeToNow: false), + ), + onTap: () async { + final urlTestInterval = await SettingsSliderDialog( + title: t.settings.config.urlTestInterval, + initialValue: options.urlTestInterval.inMinutes + .coerceIn(0, 60) + .toDouble(), + resetValue: + defaultOptions.urlTestInterval.inMinutes.toDouble(), + min: 1, + max: 60, + divisions: 60, + labelGen: (value) => Duration(minutes: value.toInt()) + .toApproximateTime(isRelativeToNow: false), + ).show(context); + if (urlTestInterval == null) return; + await changeOption( + ConfigOptionPatch( + urlTestInterval: + Duration(minutes: urlTestInterval.toInt()), + ), + ); + }, + ), + ListTile( + title: Text(t.settings.config.clashApiPort), + subtitle: Text(options.clashApiPort.toString()), + onTap: () async { + final clashApiPort = await SettingsInputDialog( + title: t.settings.config.clashApiPort, + initialValue: options.clashApiPort, + resetValue: defaultOptions.clashApiPort, + validator: isPort, + mapTo: int.tryParse, + digitsOnly: true, + ).show(context); + if (clashApiPort == null) return; + await changeOption( + ConfigOptionPatch(clashApiPort: clashApiPort), + ); + }, + ), + const Gap(24), + ], + ), + // TODO show appropriate error/loading widgets + _ => const SizedBox(), + }, + ); + } +} diff --git a/lib/features/connection/data/connection_data_providers.dart b/lib/features/connection/data/connection_data_providers.dart new file mode 100644 index 00000000..a33f150d --- /dev/null +++ b/lib/features/connection/data/connection_data_providers.dart @@ -0,0 +1,24 @@ +import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; +import 'package:hiddify/features/connection/data/connection_platform_source.dart'; +import 'package:hiddify/features/connection/data/connection_repository.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; +import 'package:hiddify/features/profile/data/profile_data_providers.dart'; +import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'connection_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +ConnectionRepository connectionRepository( + ConnectionRepositoryRef ref, +) { + return ConnectionRepositoryImpl( + directories: ref.watch(filesEditorServiceProvider).dirs, + configOptionRepository: ref.watch(configOptionRepositoryProvider), + singbox: ref.watch(singboxServiceProvider), + platformSource: ConnectionPlatformSourceImpl(), + profilePathResolver: ref.watch(profilePathResolverProvider), + geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), + ); +} diff --git a/lib/features/connection/data/connection_platform_source.dart b/lib/features/connection/data/connection_platform_source.dart new file mode 100644 index 00000000..7304abb6 --- /dev/null +++ b/lib/features/connection/data/connection_platform_source.dart @@ -0,0 +1,67 @@ +import 'dart:ffi'; +import 'dart:io'; + +import 'package:hiddify/core/utils/ffi_utils.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:posix/posix.dart'; +import 'package:win32/win32.dart'; + +abstract interface class ConnectionPlatformSource { + Future checkPrivilege(); +} + +class ConnectionPlatformSourceImpl + with InfraLogger + implements ConnectionPlatformSource { + @override + Future checkPrivilege() async { + try { + if (Platform.isWindows) { + bool isElevated = false; + withMemory(sizeOf(), (phToken) { + withMemory(sizeOf(), (pReturnedSize) { + withMemory(sizeOf<_TokenElevation>(), + (pElevation) { + if (OpenProcessToken( + GetCurrentProcess(), + TOKEN_QUERY, + phToken.cast(), + ) == + 1) { + if (GetTokenInformation( + phToken.value, + TOKEN_INFORMATION_CLASS.TokenElevation, + pElevation, + sizeOf<_TokenElevation>(), + pReturnedSize, + ) == + 1) { + isElevated = pElevation.ref.tokenIsElevated != 0; + } + } + if (phToken.value != 0) { + CloseHandle(phToken.value); + } + }); + }); + }); + return isElevated; + } else if (Platform.isLinux || Platform.isMacOS) { + final euid = geteuid(); + return euid == 0; + } else { + return true; + } + } catch (e) { + loggy.warning("error checking privilege", e); + return true; // return true so core handles it + } + } +} + +sealed class _TokenElevation extends Struct { + /// A nonzero value if the token has elevated privileges; + /// otherwise, a zero value. + @Int32() + external int tokenIsElevated; +} diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart new file mode 100644 index 00000000..8561253a --- /dev/null +++ b/lib/features/connection/data/connection_repository.dart @@ -0,0 +1,214 @@ +import 'dart:io'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/directories.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/connection/data/connection_platform_source.dart'; +import 'package:hiddify/features/connection/model/connection_failure.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.dart'; +import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/utils.dart'; +import 'package:meta/meta.dart'; + +abstract interface class ConnectionRepository { + Stream watchConnectionStatus(); + TaskEither connect( + String fileName, + bool disableMemoryLimit, + ); + TaskEither disconnect(); + TaskEither reconnect( + String fileName, + bool disableMemoryLimit, + ); +} + +class ConnectionRepositoryImpl + with ExceptionHandler, InfraLogger + implements ConnectionRepository { + ConnectionRepositoryImpl({ + required this.directories, + required this.singbox, + required this.platformSource, + required this.configOptionRepository, + required this.profilePathResolver, + required this.geoAssetPathResolver, + }); + + final Directories directories; + final SingboxService singbox; + final ConnectionPlatformSource platformSource; + final ConfigOptionRepository configOptionRepository; + final ProfilePathResolver profilePathResolver; + final GeoAssetPathResolver geoAssetPathResolver; + + bool _initialized = false; + + @override + Stream watchConnectionStatus() { + return singbox.watchStatus().map( + (event) => switch (event) { + SingboxStopped(:final alert?, :final message) => Disconnected( + switch (alert) { + SingboxAlert.emptyConfiguration => + ConnectionFailure.invalidConfig(message), + SingboxAlert.requestNotificationPermission => + ConnectionFailure.missingNotificationPermission(message), + SingboxAlert.requestVPNPermission => + ConnectionFailure.missingVpnPermission(message), + SingboxAlert.startCommandServer || + SingboxAlert.createService || + SingboxAlert.startService => + ConnectionFailure.unexpected(message), + }, + ), + SingboxStopped() => const Disconnected(), + SingboxStarting() => const Connecting(), + SingboxStarted() => const Connected(), + SingboxStopping() => const Disconnecting(), + }, + ); + } + + @visibleForTesting + TaskEither getConfigOption() { + return TaskEither.Do( + ($) async { + final options = await $( + configOptionRepository + .getFullSingboxConfigOption() + .mapLeft((l) => const InvalidConfigOption()), + ); + + return $( + TaskEither( + () async { + final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); + final geosite = + geoAssetPathResolver.resolvePath(options.geositePath); + if (!await File(geoip).exists() || + !await File(geosite).exists()) { + return left(const ConnectionFailure.missingGeoAssets()); + } + return right(options); + }, + ), + ); + }, + ).handleExceptions(UnexpectedConnectionFailure.new); + } + + @visibleForTesting + TaskEither applyConfigOption( + SingboxConfigOption options, + ) { + return exceptionHandler( + () { + return singbox + .changeOptions(options) + .mapLeft(InvalidConfigOption.new) + .run(); + }, + UnexpectedConnectionFailure.new, + ); + } + + @visibleForTesting + TaskEither setup() { + if (_initialized) return TaskEither.of(unit); + return exceptionHandler( + () { + loggy.debug("setting up singbox"); + return singbox + .setup( + directories, + false, + ) + .map((r) { + loggy.debug("setup complete"); + _initialized = true; + return r; + }) + .mapLeft(UnexpectedConnectionFailure.new) + .run(); + }, + UnexpectedConnectionFailure.new, + ); + } + + @override + TaskEither connect( + String fileName, + bool disableMemoryLimit, + ) { + return TaskEither.Do( + ($) async { + final options = await $(getConfigOption()); + loggy.info( + "config options: ${options.format()}\nMemory Limit: ${!disableMemoryLimit}", + ); + + await $( + TaskEither(() async { + if (options.enableTun) { + final hasPrivilege = await platformSource.checkPrivilege(); + if (!hasPrivilege) { + loggy.warning("missing privileges for tun mode"); + return left(const MissingPrivilege()); + } + } + return right(unit); + }), + ); + await $(setup()); + loggy.debug("after setup"); + await $(applyConfigOption(options)); + loggy.debug("after apply"); + return await $( + singbox + .start( + profilePathResolver.file(fileName).path, + disableMemoryLimit, + ) + .mapLeft(UnexpectedConnectionFailure.new), + ); + }, + ).handleExceptions(UnexpectedConnectionFailure.new); + } + + @override + TaskEither disconnect() { + return exceptionHandler( + () => singbox.stop().mapLeft(UnexpectedConnectionFailure.new).run(), + UnexpectedConnectionFailure.new, + ); + } + + @override + TaskEither reconnect( + String fileName, + bool disableMemoryLimit, + ) { + return exceptionHandler( + () async { + return getConfigOption() + .flatMap((options) => applyConfigOption(options)) + .andThen( + () => singbox + .restart( + profilePathResolver.file(fileName).path, + disableMemoryLimit, + ) + .mapLeft(UnexpectedConnectionFailure.new), + ) + .run(); + }, + UnexpectedConnectionFailure.new, + ); + } +} diff --git a/lib/domain/connectivity/connection_failure.dart b/lib/features/connection/model/connection_failure.dart similarity index 51% rename from lib/domain/connectivity/connection_failure.dart rename to lib/features/connection/model/connection_failure.dart index b8b39f04..4586f2be 100644 --- a/lib/domain/connectivity/connection_failure.dart +++ b/lib/features/connection/model/connection_failure.dart @@ -1,7 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'connection_failure.freezed.dart'; @@ -24,8 +23,21 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure { String? message, ]) = MissingNotificationPermission; - const factory ConnectionFailure.core(CoreServiceFailure failure) = - CoreConnectionFailure; + @With() + const factory ConnectionFailure.missingPrivilege() = MissingPrivilege; + + @With() + const factory ConnectionFailure.missingGeoAssets() = MissingGeoAssets; + + @With() + const factory ConnectionFailure.invalidConfigOption([ + String? message, + ]) = InvalidConfigOption; + + @With() + const factory ConnectionFailure.invalidConfig([ + String? message, + ]) = InvalidConfig; @override ({String type, String? message}) present(TranslationsEn t) { @@ -42,7 +54,22 @@ sealed class ConnectionFailure with _$ConnectionFailure, Failure { type: t.failure.connectivity.missingNotificationPermission, message: message ), - CoreConnectionFailure(:final failure) => failure.present(t), + MissingPrivilege() => ( + type: t.failure.singbox.missingPrivilege, + message: t.failure.singbox.missingPrivilegeMsg, + ), + MissingGeoAssets() => ( + type: t.failure.singbox.missingGeoAssets, + message: t.failure.singbox.missingGeoAssetsMsg, + ), + InvalidConfigOption(:final message) => ( + type: t.failure.singbox.invalidConfigOptions, + message: message, + ), + InvalidConfig(:final message) => ( + type: t.failure.singbox.invalidConfig, + message: message, + ), }; } } diff --git a/lib/domain/connectivity/connection_status.dart b/lib/features/connection/model/connection_status.dart similarity index 90% rename from lib/domain/connectivity/connection_status.dart rename to lib/features/connection/model/connection_status.dart index df84b363..c6497db1 100644 --- a/lib/domain/connectivity/connection_status.dart +++ b/lib/features/connection/model/connection_status.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/connectivity/connection_failure.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/connection/model/connection_failure.dart'; part 'connection_status.freezed.dart'; diff --git a/lib/features/common/connectivity/connectivity_controller.dart b/lib/features/connection/notifier/connection_notifier.dart similarity index 75% rename from lib/features/common/connectivity/connectivity_controller.dart rename to lib/features/connection/notifier/connection_notifier.dart index 8f835b6b..21c22339 100644 --- a/lib/features/common/connectivity/connectivity_controller.dart +++ b/lib/features/connection/notifier/connection_notifier.dart @@ -1,17 +1,17 @@ -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/core/prefs/service_prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_facade.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/preferences/service_preferences.dart'; +import 'package:hiddify/features/connection/data/connection_data_providers.dart'; +import 'package:hiddify/features/connection/data/connection_repository.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; -part 'connectivity_controller.g.dart'; +part 'connection_notifier.g.dart'; @Riverpod(keepAlive: true) -class ConnectivityController extends _$ConnectivityController with AppLogger { +class ConnectionNotifier extends _$ConnectionNotifier with AppLogger { @override Stream build() { ref.listen( @@ -24,7 +24,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } }, ); - return _core.watchConnectionStatus().doOnData((event) { + return _connectionRepo.watchConnectionStatus().doOnData((event) { if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) { ref.read(startedByUserProvider.notifier).update(false); @@ -33,7 +33,8 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { }); } - CoreFacade get _core => ref.watch(coreFacadeProvider); + ConnectionRepository get _connectionRepo => + ref.read(connectionRepositoryProvider); Future mayConnect() async { if (state case AsyncData(:final value)) { @@ -66,8 +67,8 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } loggy.info("active profile changed, reconnecting"); await ref.read(startedByUserProvider.notifier).update(true); - await _core - .restart(profileId, ref.read(disableMemoryLimitProvider)) + await _connectionRepo + .reconnect(profileId, ref.read(disableMemoryLimitProvider)) .mapLeft((err) { loggy.warning("error reconnecting", err); state = AsyncError(err, StackTrace.current); @@ -88,8 +89,8 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { Future _connect() async { final activeProfile = await ref.read(activeProfileProvider.future); - await _core - .start(activeProfile!.id, ref.read(disableMemoryLimitProvider)) + await _connectionRepo + .connect(activeProfile!.id, ref.read(disableMemoryLimitProvider)) .mapLeft((err) async { loggy.warning("error connecting", err); await ref.read(startedByUserProvider.notifier).update(false); @@ -98,7 +99,7 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { } Future _disconnect() async { - await _core.stop().mapLeft((err) { + await _connectionRepo.disconnect().mapLeft((err) { loggy.warning("error disconnecting", err); state = AsyncError(err, StackTrace.current); }).run(); @@ -108,6 +109,6 @@ class ConnectivityController extends _$ConnectivityController with AppLogger { @Riverpod(keepAlive: true) Future serviceRunning(ServiceRunningRef ref) => ref .watch( - connectivityControllerProvider.selectAsync((data) => data.isConnected), + connectionNotifierProvider.selectAsync((data) => data.isConnected), ) .onError((error, stackTrace) => false); diff --git a/lib/features/geo_asset/data/geo_asset_data_mapper.dart b/lib/features/geo_asset/data/geo_asset_data_mapper.dart index 7906ac0e..0f2401e5 100644 --- a/lib/features/geo_asset/data/geo_asset_data_mapper.dart +++ b/lib/features/geo_asset/data/geo_asset_data_mapper.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/core/database/app_database.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; extension GeoAssetEntityMapper on GeoAssetEntity { diff --git a/lib/features/geo_asset/data/geo_asset_data_providers.dart b/lib/features/geo_asset/data/geo_asset_data_providers.dart index 1e9be492..44a8c51f 100644 --- a/lib/features/geo_asset/data/geo_asset_data_providers.dart +++ b/lib/features/geo_asset/data/geo_asset_data_providers.dart @@ -1,4 +1,5 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/database/database_provider.dart'; +import 'package:hiddify/core/http_client/http_client_provider.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'; @@ -12,7 +13,7 @@ Future geoAssetRepository(GeoAssetRepositoryRef ref) async { final repo = GeoAssetRepositoryImpl( geoAssetDataSource: ref.watch(geoAssetDataSourceProvider), geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), - dio: ref.watch(dioProvider), + dio: ref.watch(httpClientProvider), ); await repo.init().getOrElse((l) => throw l).run(); return repo; diff --git a/lib/features/geo_asset/data/geo_asset_data_source.dart b/lib/features/geo_asset/data/geo_asset_data_source.dart index b72b751d..b3c63aac 100644 --- a/lib/features/geo_asset/data/geo_asset_data_source.dart +++ b/lib/features/geo_asset/data/geo_asset_data_source.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/database/tables/database_tables.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; diff --git a/lib/features/geo_asset/data/geo_asset_repository.dart b/lib/features/geo_asset/data/geo_asset_repository.dart index 74abb850..3d39e13f 100644 --- a/lib/features/geo_asset/data/geo_asset_repository.dart +++ b/lib/features/geo_asset/data/geo_asset_repository.dart @@ -5,8 +5,8 @@ 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/core/database/app_database.dart'; +import 'package:hiddify/core/utils/exception_handler.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'; diff --git a/lib/features/geo_asset/model/geo_asset_failure.dart b/lib/features/geo_asset/model/geo_asset_failure.dart index 161b193e..882864da 100644 --- a/lib/features/geo_asset/model/geo_asset_failure.dart +++ b/lib/features/geo_asset/model/geo_asset_failure.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'geo_asset_failure.freezed.dart'; @@ -8,6 +8,7 @@ part 'geo_asset_failure.freezed.dart'; sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { const GeoAssetFailure._(); + @With() const factory GeoAssetFailure.unexpected([ Object? error, StackTrace? stackTrace, @@ -16,6 +17,7 @@ sealed class GeoAssetFailure with _$GeoAssetFailure, Failure { @With() const factory GeoAssetFailure.noUpdateAvailable() = GeoAssetNoUpdateAvailable; + @With() const factory GeoAssetFailure.activeAssetNotFound() = GeoAssetActiveAssetNotFound; diff --git a/lib/features/geo_asset/overview/geo_assets_overview_page.dart b/lib/features/geo_asset/overview/geo_assets_overview_page.dart index a611fabd..6917721c 100644 --- a/lib/features/geo_asset/overview/geo_assets_overview_page.dart +++ b/lib/features/geo_asset/overview/geo_assets_overview_page.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.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'; diff --git a/lib/features/geo_asset/widget/geo_asset_tile.dart b/lib/features/geo_asset/widget/geo_asset_tile.dart index 620f1a21..61fbbee6 100644 --- a/lib/features/geo_asset/widget/geo_asset_tile.dart +++ b/lib/features/geo_asset/widget/geo_asset_tile.dart @@ -1,7 +1,7 @@ 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/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.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'; diff --git a/lib/features/home/view/view.dart b/lib/features/home/view/view.dart deleted file mode 100644 index e4ff2696..00000000 --- a/lib/features/home/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'home_page.dart'; diff --git a/lib/features/home/widgets/connection_button.dart b/lib/features/home/widget/connection_button.dart similarity index 84% rename from lib/features/home/widgets/connection_button.dart rename to lib/features/home/widget/connection_button.dart index afef3330..f2ff6489 100644 --- a/lib/features/home/widgets/connection_button.dart +++ b/lib/features/home/widget/connection_button.dart @@ -1,11 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/core/theme/theme_extensions.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/alerts.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -17,10 +17,10 @@ class ConnectionButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final connectionStatus = ref.watch(connectivityControllerProvider); + final connectionStatus = ref.watch(connectionNotifierProvider); ref.listen( - connectivityControllerProvider, + connectionNotifierProvider, (_, next) { if (next case AsyncError(:final error)) { CustomAlertDialog.fromErr(t.presentError(error)).show(context); @@ -42,18 +42,16 @@ class ConnectionButton extends HookConsumerWidget { : buttonTheme.idleColor!; return _ConnectionButton( - onTap: () => ref - .read(connectivityControllerProvider.notifier) - .toggleConnection(), + onTap: () => + ref.read(connectionNotifierProvider.notifier).toggleConnection(), enabled: !status.isSwitching, label: status.present(t), buttonColor: connectionLogoColor, ); case AsyncError(): return _ConnectionButton( - onTap: () => ref - .read(connectivityControllerProvider.notifier) - .toggleConnection(), + onTap: () => + ref.read(connectionNotifierProvider.notifier).toggleConnection(), enabled: true, label: const Disconnected().present(t), buttonColor: buttonTheme.idleColor!, diff --git a/lib/features/home/widgets/empty_profiles_home_body.dart b/lib/features/home/widget/empty_profiles_home_body.dart similarity index 96% rename from lib/features/home/widgets/empty_profiles_home_body.dart rename to lib/features/home/widget/empty_profiles_home_body.dart index 8937ce18..eed2b370 100644 --- a/lib/features/home/widgets/empty_profiles_home_body.dart +++ b/lib/features/home/widget/empty_profiles_home_body.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/home/view/home_page.dart b/lib/features/home/widget/home_page.dart similarity index 90% rename from lib/features/home/view/home_page.dart rename to lib/features/home/widget/home_page.dart index 86512ad8..785b0e2b 100644 --- a/lib/features/home/view/home_page.dart +++ b/lib/features/home/widget/home_page.dart @@ -1,15 +1,16 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/home/widgets/widgets.dart'; +import 'package:hiddify/features/home/widget/connection_button.dart'; +import 'package:hiddify/features/home/widget/empty_profiles_home_body.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/profile/widget/profile_tile.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; - import 'package:sliver_tools/sliver_tools.dart'; class HomePage extends HookConsumerWidget { @@ -91,7 +92,7 @@ class AppVersionLabel extends HookConsumerWidget { final t = ref.watch(translationsProvider); final theme = Theme.of(context); - final version = ref.watch(appInfoProvider).presentVersion; + final version = ref.watch(appInfoProvider).requireValue.presentVersion; if (version.isBlank) return const SizedBox(); return Semantics( diff --git a/lib/features/home/widgets/widgets.dart b/lib/features/home/widgets/widgets.dart deleted file mode 100644 index b043254e..00000000 --- a/lib/features/home/widgets/widgets.dart +++ /dev/null @@ -1,2 +0,0 @@ -export 'connection_button.dart'; -export 'empty_profiles_home_body.dart'; diff --git a/lib/features/intro/intro_page.dart b/lib/features/intro/widget/intro_page.dart similarity index 95% rename from lib/features/intro/intro_page.dart rename to lib/features/intro/widget/intro_page.dart index 53da7370..5732a17d 100644 --- a/lib/features/intro/intro_page.dart +++ b/lib/features/intro/widget/intro_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/constants.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/utils/utils.dart'; diff --git a/lib/features/log/data/log_data_providers.dart b/lib/features/log/data/log_data_providers.dart index 199b7dbc..c84aa7b7 100644 --- a/lib/features/log/data/log_data_providers.dart +++ b/lib/features/log/data/log_data_providers.dart @@ -1,6 +1,7 @@ import 'package:hiddify/features/log/data/log_path_resolver.dart'; import 'package:hiddify/features/log/data/log_repository.dart'; import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'log_data_providers.g.dart'; diff --git a/lib/features/log/data/log_repository.dart b/lib/features/log/data/log_repository.dart index bd699d42..4f4cb443 100644 --- a/lib/features/log/data/log_repository.dart +++ b/lib/features/log/data/log_repository.dart @@ -1,10 +1,10 @@ import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/features/log/data/log_parser.dart'; import 'package:hiddify/features/log/data/log_path_resolver.dart'; import 'package:hiddify/features/log/model/log_entity.dart'; import 'package:hiddify/features/log/model/log_failure.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/custom_loggers.dart'; abstract interface class LogRepository { diff --git a/lib/features/log/model/log_failure.dart b/lib/features/log/model/log_failure.dart index f1a4e8fe..5815053e 100644 --- a/lib/features/log/model/log_failure.dart +++ b/lib/features/log/model/log_failure.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/locale_prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'log_failure.freezed.dart'; @@ -8,6 +8,7 @@ part 'log_failure.freezed.dart'; sealed class LogFailure with _$LogFailure, Failure { const LogFailure._(); + @With() const factory LogFailure.unexpected([ Object? error, StackTrace? stackTrace, @@ -17,7 +18,7 @@ sealed class LogFailure with _$LogFailure, Failure { ({String type, String? message}) present(TranslationsEn t) { return switch (this) { LogUnexpectedFailure() => ( - type: "unexpected", + type: t.failure.unexpected, message: null, ), }; diff --git a/lib/features/log/overview/logs_overview_page.dart b/lib/features/log/overview/logs_overview_page.dart index 3c8e4f75..17b1b8c9 100644 --- a/lib/features/log/overview/logs_overview_page.dart +++ b/lib/features/log/overview/logs_overview_page.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fpdart/fpdart.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/log/model/log_level.dart'; diff --git a/lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart b/lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart new file mode 100644 index 00000000..74f01681 --- /dev/null +++ b/lib/features/per_app_proxy/data/per_app_proxy_data_providers.dart @@ -0,0 +1,9 @@ +import 'package:hiddify/features/per_app_proxy/data/per_app_proxy_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'per_app_proxy_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +PerAppProxyRepository perAppProxyRepository(PerAppProxyRepositoryRef ref) { + return PerAppProxyRepositoryImpl(); +} diff --git a/lib/features/per_app_proxy/data/per_app_proxy_repository.dart b/lib/features/per_app_proxy/data/per_app_proxy_repository.dart new file mode 100644 index 00000000..7219727c --- /dev/null +++ b/lib/features/per_app_proxy/data/per_app_proxy_repository.dart @@ -0,0 +1,55 @@ +import 'dart:convert'; + +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart'; +import 'package:hiddify/utils/utils.dart'; + +abstract interface class PerAppProxyRepository { + TaskEither> getInstalledPackages(); + TaskEither getPackageIcon(String packageName); +} + +class PerAppProxyRepositoryImpl + with InfraLogger + implements PerAppProxyRepository { + final _methodChannel = const MethodChannel("app.hiddify.com/platform"); + + @override + TaskEither> getInstalledPackages() { + return TaskEither( + () async { + loggy.debug("getting installed packages info"); + final result = + await _methodChannel.invokeMethod("get_installed_packages"); + if (result == null) return left("null response"); + return right( + (jsonDecode(result) as List).map((e) { + return InstalledPackageInfo.fromJson(e as Map); + }).toList(), + ); + }, + ); + } + + @override + TaskEither getPackageIcon(String packageName) { + return TaskEither( + () async { + loggy.debug("getting package [$packageName] icon"); + final result = await _methodChannel.invokeMethod( + "get_package_icon", + {"packageName": packageName}, + ); + if (result == null) return left("null response"); + final Uint8List decoded; + try { + decoded = base64.decode(result); + } catch (e) { + return left("error parsing base64 response"); + } + return right(decoded); + }, + ); + } +} diff --git a/lib/features/per_app_proxy/model/installed_package_info.dart b/lib/features/per_app_proxy/model/installed_package_info.dart new file mode 100644 index 00000000..b951c170 --- /dev/null +++ b/lib/features/per_app_proxy/model/installed_package_info.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'installed_package_info.freezed.dart'; +part 'installed_package_info.g.dart'; + +@freezed +class InstalledPackageInfo with _$InstalledPackageInfo { + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory InstalledPackageInfo({ + required String packageName, + required String name, + required bool isSystemApp, + }) = _InstalledPackageInfo; + + factory InstalledPackageInfo.fromJson(Map json) => + _$InstalledPackageInfoFromJson(json); +} diff --git a/lib/features/per_app_proxy/model/per_app_proxy_mode.dart b/lib/features/per_app_proxy/model/per_app_proxy_mode.dart new file mode 100644 index 00000000..6a84bb34 --- /dev/null +++ b/lib/features/per_app_proxy/model/per_app_proxy_mode.dart @@ -0,0 +1,24 @@ +import 'package:hiddify/core/localization/translations.dart'; + +enum PerAppProxyMode { + off, + include, + exclude; + + bool get enabled => this != off; + + ({String title, String message}) present(TranslationsEn t) => switch (this) { + off => ( + title: t.settings.network.perAppProxyModes.off, + message: t.settings.network.perAppProxyModes.offMsg, + ), + include => ( + title: t.settings.network.perAppProxyModes.include, + message: t.settings.network.perAppProxyModes.includeMsg, + ), + exclude => ( + title: t.settings.network.perAppProxyModes.exclude, + message: t.settings.network.perAppProxyModes.excludeMsg, + ), + }; +} diff --git a/lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart b/lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart new file mode 100644 index 00000000..0830151f --- /dev/null +++ b/lib/features/per_app_proxy/overview/per_app_proxy_notifier.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/features/per_app_proxy/data/per_app_proxy_data_providers.dart'; +import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart'; +import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'per_app_proxy_notifier.g.dart'; + +@riverpod +Future> installedPackagesInfo( + InstalledPackagesInfoRef ref, +) async { + return ref + .watch(perAppProxyRepositoryProvider) + .getInstalledPackages() + .getOrElse((err) { + // _logger.error("error getting installed packages", err); + throw err; + }).run(); +} + +@riverpod +Future packageIcon( + PackageIconRef ref, + String packageName, +) async { + ref.disposeDelay(const Duration(seconds: 10)); + final bytes = await ref + .watch(perAppProxyRepositoryProvider) + .getPackageIcon(packageName) + .getOrElse((err) { + // _logger.warning("error getting package icon", err); + throw err; + }).run(); + return MemoryImage(bytes); +} diff --git a/lib/features/settings/view/per_app_proxy_page.dart b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart similarity index 85% rename from lib/features/settings/view/per_app_proxy_page.dart rename to lib/features/per_app_proxy/overview/per_app_proxy_page.dart index d885110a..651fa6ff 100644 --- a/lib/features/settings/view/per_app_proxy_page.dart +++ b/lib/features/per_app_proxy/overview/per_app_proxy_page.dart @@ -2,51 +2,15 @@ import 'package:dartx/dartx.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/domain/singbox/rules.dart'; -import 'package:hiddify/services/platform_services.dart'; -import 'package:hiddify/services/service_providers.dart'; -import 'package:hiddify/utils/riverpod_utils.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/features/per_app_proxy/model/installed_package_info.dart'; +import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; +import 'package:hiddify/features/per_app_proxy/overview/per_app_proxy_notifier.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:loggy/loggy.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sliver_tools/sliver_tools.dart'; -part 'per_app_proxy_page.g.dart'; - -final _logger = Loggy("PerAppProxySettings"); - -@riverpod -Future> installedPackagesInfo( - InstalledPackagesInfoRef ref, -) async { - return ref - .watch(platformServicesProvider) - .getInstalledPackages() - .getOrElse((err) { - _logger.error("error getting installed packages", err); - throw err; - }).run(); -} - -@riverpod -Future packageIcon( - PackageIconRef ref, - String packageName, -) async { - ref.disposeDelay(const Duration(seconds: 10)); - final bytes = await ref - .watch(platformServicesProvider) - .getPackageIcon(packageName) - .getOrElse((err) { - _logger.warning("error getting package icon", err); - throw err; - }).run(); - return MemoryImage(bytes); -} - class PerAppProxyPage extends HookConsumerWidget with PresLogger { const PerAppProxyPage({super.key}); diff --git a/lib/features/profile/add/add_profile_modal.dart b/lib/features/profile/add/add_profile_modal.dart index 11217720..1a470601 100644 --- a/lib/features/profile/add/add_profile_modal.dart +++ b/lib/features/profile/add/add_profile_modal.dart @@ -3,7 +3,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/router/router.dart'; import 'package:hiddify/features/common/qr_code_scanner_screen.dart'; import 'package:hiddify/features/profile/notifier/profile_notifier.dart'; diff --git a/lib/features/profile/data/profile_data_mapper.dart b/lib/features/profile/data/profile_data_mapper.dart index 38d15061..6abe7bc3 100644 --- a/lib/features/profile/data/profile_data_mapper.dart +++ b/lib/features/profile/data/profile_data_mapper.dart @@ -1,5 +1,5 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/core/database/app_database.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; extension ProfileEntityMapper on ProfileEntity { diff --git a/lib/features/profile/data/profile_data_providers.dart b/lib/features/profile/data/profile_data_providers.dart index 5ff0f515..3520b6a5 100644 --- a/lib/features/profile/data/profile_data_providers.dart +++ b/lib/features/profile/data/profile_data_providers.dart @@ -1,8 +1,10 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/database/database_provider.dart'; +import 'package:hiddify/core/http_client/http_client_provider.dart'; import 'package:hiddify/features/profile/data/profile_data_source.dart'; import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'profile_data_providers.g.dart'; @@ -12,8 +14,8 @@ Future profileRepository(ProfileRepositoryRef ref) async { final repo = ProfileRepositoryImpl( profileDataSource: ref.watch(profileDataSourceProvider), profilePathResolver: ref.watch(profilePathResolverProvider), - configValidator: ref.watch(coreFacadeProvider).parseConfig, - dio: ref.watch(dioProvider), + singbox: ref.watch(singboxServiceProvider), + dio: ref.watch(httpClientProvider), ); await repo.init().getOrElse((l) => throw l).run(); return repo; diff --git a/lib/features/profile/data/profile_data_source.dart b/lib/features/profile/data/profile_data_source.dart index a5f6b241..8d8bc6bd 100644 --- a/lib/features/profile/data/profile_data_source.dart +++ b/lib/features/profile/data/profile_data_source.dart @@ -1,6 +1,6 @@ import 'package:drift/drift.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/local/tables.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/database/tables/database_tables.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; import 'package:hiddify/utils/utils.dart'; diff --git a/lib/features/profile/data/profile_repository.dart b/lib/features/profile/data/profile_repository.dart index 4a099e1a..4f4494c8 100644 --- a/lib/features/profile/data/profile_repository.dart +++ b/lib/features/profile/data/profile_repository.dart @@ -3,9 +3,8 @@ import 'dart:io'; import 'package:dio/dio.dart'; import 'package:drift/drift.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/local/database.dart'; -import 'package:hiddify/data/repository/exception_handlers.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; +import 'package:hiddify/core/database/app_database.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/features/profile/data/profile_data_mapper.dart'; import 'package:hiddify/features/profile/data/profile_data_source.dart'; import 'package:hiddify/features/profile/data/profile_parser.dart'; @@ -13,6 +12,7 @@ import 'package:hiddify/features/profile/data/profile_path_resolver.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/profile/model/profile_failure.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hiddify/utils/link_parsers.dart'; import 'package:meta/meta.dart'; @@ -43,6 +43,8 @@ abstract interface class ProfileRepository { TaskEither add(RemoteProfileEntity baseProfile); + TaskEither generateConfig(String id); + TaskEither updateSubscription( RemoteProfileEntity baseProfile, ); @@ -58,17 +60,13 @@ class ProfileRepositoryImpl ProfileRepositoryImpl({ required this.profileDataSource, required this.profilePathResolver, - required this.configValidator, + required this.singbox, required this.dio, }); final ProfileDataSource profileDataSource; final ProfilePathResolver profilePathResolver; - final TaskEither Function( - String path, - String tempPath, - bool debug, - ) configValidator; + final SingboxService singbox; final Dio dio; @override @@ -165,6 +163,23 @@ class ProfileRepositoryImpl ); } + @visibleForTesting + TaskEither validateConfig( + String path, + String tempPath, + bool debug, + ) { + return exceptionHandler( + () { + return singbox + .validateConfigByPath(path, tempPath, debug) + .mapLeft(ProfileFailure.invalidConfig) + .run(); + }, + ProfileUnexpectedFailure.new, + ); + } + @override TaskEither addByContent( String content, { @@ -179,24 +194,20 @@ class ProfileRepositoryImpl try { await tempFile.writeAsString(content); - final parseResult = - await configValidator(file.path, tempFile.path, false).run(); - return parseResult.fold( - (err) async { - loggy.warning("error parsing config", err); - return left(ProfileFailure.invalidConfig(err.msg)); - }, - (_) async { - final profile = LocalProfileEntity( - id: profileId, - active: markAsActive, - name: name, - lastUpdate: DateTime.now(), - ); - await profileDataSource.insert(profile.toEntry()); - return right(unit); - }, - ); + return await validateConfig(file.path, tempFile.path, false) + .andThen( + () => TaskEither(() async { + final profile = LocalProfileEntity( + id: profileId, + active: markAsActive, + name: name, + lastUpdate: DateTime.now(), + ); + await profileDataSource.insert(profile.toEntry()); + return right(unit); + }), + ) + .run(); } finally { if (tempFile.existsSync()) tempFile.deleteSync(); } @@ -235,6 +246,21 @@ class ProfileRepositoryImpl ); } + @override + TaskEither generateConfig(String id) { + return TaskEither.Do( + ($) async { + final configFile = profilePathResolver.file(id); + // TODO pass options + return await $( + singbox + .generateFullConfigByPath(configFile.path) + .mapLeft(ProfileFailure.unexpected), + ); + }, + ).handleExceptions(ProfileFailure.unexpected); + } + @override TaskEither updateSubscription( RemoteProfileEntity baseProfile, @@ -333,18 +359,14 @@ class ProfileRepositoryImpl ); final headers = await _populateHeaders(response.headers.map, tempFile.path); - final parseResult = - await configValidator(file.path, tempFile.path, false).run(); - return parseResult.fold( - (err) async { - loggy.warning("error parsing config", err); - return left(ProfileFailure.invalidConfig(err.msg)); - }, - (_) async { - final profile = ProfileParser.parse(url, headers); - return right(profile); - }, - ); + return await validateConfig(file.path, tempFile.path, false) + .andThen( + () => TaskEither(() async { + final profile = ProfileParser.parse(url, headers); + return right(profile); + }), + ) + .run(); } finally { if (tempFile.existsSync()) tempFile.deleteSync(); } diff --git a/lib/features/profile/details/profile_details_page.dart b/lib/features/profile/details/profile_details_page.dart index ce1e5b23..4c55d4ed 100644 --- a/lib/features/profile/details/profile_details_page.dart +++ b/lib/features/profile/details/profile_details_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/profile/details/profile_details_notifier.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; diff --git a/lib/features/profile/model/profile_failure.dart b/lib/features/profile/model/profile_failure.dart index 529b5269..440daf38 100644 --- a/lib/features/profile/model/profile_failure.dart +++ b/lib/features/profile/model/profile_failure.dart @@ -1,6 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'profile_failure.freezed.dart'; diff --git a/lib/features/profile/model/profile_sort_enum.dart b/lib/features/profile/model/profile_sort_enum.dart index 04b8f6d4..5852a515 100644 --- a/lib/features/profile/model/profile_sort_enum.dart +++ b/lib/features/profile/model/profile_sort_enum.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; enum ProfilesSort { lastUpdate, diff --git a/lib/features/profile/notifier/profile_notifier.dart b/lib/features/profile/notifier/profile_notifier.dart index 263fdf85..7514b24b 100644 --- a/lib/features/profile/notifier/profile_notifier.dart +++ b/lib/features/profile/notifier/profile_notifier.dart @@ -1,9 +1,9 @@ import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.dart'; -import 'package:hiddify/core/prefs/general_prefs.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; @@ -127,7 +127,7 @@ class UpdateProfile extends _$UpdateProfile with AppLogger { await ref.read(activeProfileProvider.future).then((active) async { if (active != null && active.id == profile.id) { await ref - .read(connectivityControllerProvider.notifier) + .read(connectionNotifierProvider.notifier) .reconnect(profile.id); } }); diff --git a/lib/features/profile/notifier/profiles_update_notifier.dart b/lib/features/profile/notifier/profiles_update_notifier.dart index 5f4325b3..222f7f8e 100644 --- a/lib/features/profile/notifier/profiles_update_notifier.dart +++ b/lib/features/profile/notifier/profiles_update_notifier.dart @@ -1,5 +1,5 @@ import 'package:dartx/dartx.dart'; -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; @@ -40,7 +40,8 @@ class ForegroundProfilesUpdateNotifier Future updateProfiles() async { try { final previousRun = DateTime.tryParse( - ref.read(sharedPreferencesProvider).getString(prefKey) ?? "", + ref.read(sharedPreferencesProvider).requireValue.getString(prefKey) ?? + "", ); if (previousRun != null && previousRun.add(interval) > DateTime.now()) { @@ -86,6 +87,7 @@ class ForegroundProfilesUpdateNotifier } finally { await ref .read(sharedPreferencesProvider) + .requireValue .setString(prefKey, DateTime.now().toIso8601String()); } } diff --git a/lib/features/profile/overview/profiles_overview_notifier.dart b/lib/features/profile/overview/profiles_overview_notifier.dart index 9434a2d9..7dcc68d4 100644 --- a/lib/features/profile/overview/profiles_overview_notifier.dart +++ b/lib/features/profile/overview/profiles_overview_notifier.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/data/data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_repository.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; @@ -70,7 +69,7 @@ class ProfilesOverviewNotifier extends _$ProfilesOverviewNotifier } Future exportConfigToClipboard(ProfileEntity profile) async { - await ref.read(coreFacadeProvider).generateConfig(profile.id).match( + await _profilesRepo.generateConfig(profile.id).match( (err) { loggy.warning('error generating config', err); throw err; diff --git a/lib/features/profile/overview/profiles_overview_page.dart b/lib/features/profile/overview/profiles_overview_page.dart index 105177d4..3e875b13 100644 --- a/lib/features/profile/overview/profiles_overview_page.dart +++ b/lib/features/profile/overview/profiles_overview_page.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/profile/model/profile_sort_enum.dart'; import 'package:hiddify/features/profile/overview/profiles_overview_notifier.dart'; import 'package:hiddify/features/profile/widget/profile_tile.dart'; diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart index c0000cb4..6d7f6c4b 100644 --- a/lib/features/profile/widget/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -3,10 +3,9 @@ import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:gap/gap.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/failures.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/qr_code_dialog.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart'; diff --git a/lib/features/proxies/notifier/notifier.dart b/lib/features/proxies/notifier/notifier.dart deleted file mode 100644 index 60e74984..00000000 --- a/lib/features/proxies/notifier/notifier.dart +++ /dev/null @@ -1 +0,0 @@ -export 'proxies_notifier.dart'; diff --git a/lib/features/proxies/view/view.dart b/lib/features/proxies/view/view.dart deleted file mode 100644 index b35ebe17..00000000 --- a/lib/features/proxies/view/view.dart +++ /dev/null @@ -1 +0,0 @@ -export 'proxies_page.dart'; diff --git a/lib/features/proxies/widgets/widgets.dart b/lib/features/proxies/widgets/widgets.dart deleted file mode 100644 index 6565a8f3..00000000 --- a/lib/features/proxies/widgets/widgets.dart +++ /dev/null @@ -1 +0,0 @@ -export 'proxy_tile.dart'; diff --git a/lib/features/proxy/data/proxy_data_providers.dart b/lib/features/proxy/data/proxy_data_providers.dart new file mode 100644 index 00000000..1c0c0823 --- /dev/null +++ b/lib/features/proxy/data/proxy_data_providers.dart @@ -0,0 +1,12 @@ +import 'package:hiddify/features/proxy/data/proxy_repository.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'proxy_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +ProxyRepository proxyRepository(ProxyRepositoryRef ref) { + return ProxyRepositoryImpl( + singbox: ref.watch(singboxServiceProvider), + ); +} diff --git a/lib/features/proxy/data/proxy_repository.dart b/lib/features/proxy/data/proxy_repository.dart new file mode 100644 index 00000000..f318ecaf --- /dev/null +++ b/lib/features/proxy/data/proxy_repository.dart @@ -0,0 +1,78 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; +import 'package:hiddify/features/proxy/model/proxy_failure.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +abstract interface class ProxyRepository { + Stream>> watchProxies(); + TaskEither selectProxy( + String groupTag, + String outboundTag, + ); + TaskEither urlTest(String groupTag); +} + +class ProxyRepositoryImpl + with ExceptionHandler, InfraLogger + implements ProxyRepository { + ProxyRepositoryImpl({required this.singbox}); + + final SingboxService singbox; + + @override + Stream>> watchProxies() { + return singbox.watchOutbounds().map((event) { + final groupWithSelected = { + for (final group in event) group.tag: group.selected, + }; + return event + .map( + (e) => ProxyGroupEntity( + tag: e.tag, + type: e.type, + selected: e.selected, + items: e.items + .map( + (e) => ProxyItemEntity( + tag: e.tag, + type: e.type, + urlTestDelay: e.urlTestDelay, + selectedTag: groupWithSelected[e.tag], + ), + ) + .toList(), + ), + ) + .toList(); + }).handleExceptions( + (error, stackTrace) { + loggy.error("error watching proxies", error, stackTrace); + return ProxyUnexpectedFailure(error, stackTrace); + }, + ); + } + + @override + TaskEither selectProxy( + String groupTag, + String outboundTag, + ) { + return exceptionHandler( + () => singbox + .selectOutbound(groupTag, outboundTag) + .mapLeft(ProxyUnexpectedFailure.new) + .run(), + ProxyUnexpectedFailure.new, + ); + } + + @override + TaskEither urlTest(String groupTag) { + return exceptionHandler( + () => singbox.urlTest(groupTag).mapLeft(ProxyUnexpectedFailure.new).run(), + ProxyUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/proxy/model/proxy_entity.dart b/lib/features/proxy/model/proxy_entity.dart new file mode 100644 index 00000000..2dbf96d0 --- /dev/null +++ b/lib/features/proxy/model/proxy_entity.dart @@ -0,0 +1,37 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; + +part 'proxy_entity.freezed.dart'; + +@freezed +class ProxyGroupEntity with _$ProxyGroupEntity { + const ProxyGroupEntity._(); + + const factory ProxyGroupEntity({ + required String tag, + required ProxyType type, + required String selected, + @Default([]) List items, + }) = _ProxyGroupEntity; + + String get name => _sanitizedTag(tag); +} + +@freezed +class ProxyItemEntity with _$ProxyItemEntity { + const ProxyItemEntity._(); + + const factory ProxyItemEntity({ + required String tag, + required ProxyType type, + required int urlTestDelay, + String? selectedTag, + }) = _ProxyItemEntity; + + String get name => _sanitizedTag(tag); + String? get selectedName => + selectedTag == null ? null : _sanitizedTag(selectedTag!); +} + +String _sanitizedTag(String tag) => + tag.replaceFirst(RegExp(r"\§[^]*"), "").trimRight(); diff --git a/lib/features/proxy/model/proxy_failure.dart b/lib/features/proxy/model/proxy_failure.dart new file mode 100644 index 00000000..8fcf1b35 --- /dev/null +++ b/lib/features/proxy/model/proxy_failure.dart @@ -0,0 +1,33 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; + +part 'proxy_failure.freezed.dart'; + +@freezed +sealed class ProxyFailure with _$ProxyFailure, Failure { + const ProxyFailure._(); + + @With() + const factory ProxyFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = ProxyUnexpectedFailure; + + @With() + const factory ProxyFailure.serviceNotRunning() = ServiceNotRunning; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + ProxyUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + ServiceNotRunning() => ( + type: t.failure.singbox.serviceNotRunning, + message: null, + ), + }; + } +} diff --git a/lib/features/proxies/notifier/proxies_notifier.dart b/lib/features/proxy/overview/proxies_overview_notifier.dart similarity index 72% rename from lib/features/proxies/notifier/proxies_notifier.dart rename to lib/features/proxy/overview/proxies_overview_notifier.dart index 743d3d20..6c3fc4a7 100644 --- a/lib/features/proxies/notifier/proxies_notifier.dart +++ b/lib/features/proxy/overview/proxies_overview_notifier.dart @@ -2,18 +2,19 @@ import 'dart:async'; import 'package:combine/combine.dart'; import 'package:dartx/dartx.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; -import 'package:hiddify/data/data_providers.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/features/proxy/data/proxy_data_providers.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; +import 'package:hiddify/features/proxy/model/proxy_failure.dart'; import 'package:hiddify/utils/pref_notifier.dart'; import 'package:hiddify/utils/riverpod_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:rxdart/rxdart.dart'; -part 'proxies_notifier.g.dart'; +part 'proxies_overview_notifier.g.dart'; enum ProxiesSort { unsorted, @@ -30,7 +31,7 @@ enum ProxiesSort { @Riverpod(keepAlive: true) class ProxiesSortNotifier extends _$ProxiesSortNotifier { late final _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, "proxies_sort_mode", ProxiesSort.unsorted, mapFrom: ProxiesSort.values.byName, @@ -47,18 +48,18 @@ class ProxiesSortNotifier extends _$ProxiesSortNotifier { } @riverpod -class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { +class ProxiesOverviewNotifier extends _$ProxiesOverviewNotifier with AppLogger { @override - Stream> build() async* { + Stream> build() async* { ref.disposeDelay(const Duration(seconds: 15)); final serviceRunning = await ref.watch(serviceRunningProvider.future); if (!serviceRunning) { - throw const CoreServiceNotRunning(); + throw const ServiceNotRunning(); } final sortBy = ref.watch(proxiesSortNotifierProvider); yield* ref - .watch(coreFacadeProvider) - .watchOutbounds() + .watch(proxyRepositoryProvider) + .watchProxies() .throttleTime( const Duration(milliseconds: 100), leading: false, @@ -75,17 +76,17 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { .asyncMap((proxies) async => _sortOutbounds(proxies, sortBy)); } - Future> _sortOutbounds( - List outbounds, + Future> _sortOutbounds( + List proxies, ProxiesSort sortBy, ) async { return CombineWorker().execute( () { final groupWithSelected = { - for (final o in outbounds) o.tag: o.selected, + for (final o in proxies) o.tag: o.selected, }; - final sortedOutbounds = []; - for (final group in outbounds) { + final sortedProxies = []; + for (final group in proxies) { final sortedItems = switch (sortBy) { ProxiesSort.name => group.items.sortedBy((e) => e.tag), ProxiesSort.delay => group.items.sortedWith((a, b) { @@ -99,7 +100,7 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { }), ProxiesSort.unsorted => group.items, }; - final items = []; + final items = []; for (final item in sortedItems) { if (groupWithSelected.keys.contains(item.tag)) { items @@ -108,9 +109,9 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { items.add(item); } } - sortedOutbounds.add(group.copyWith(items: items)); + sortedProxies.add(group.copyWith(items: items)); } - return sortedOutbounds; + return sortedProxies; }, ); } @@ -121,8 +122,8 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { ); if (state case AsyncData(value: final outbounds)) { await ref - .read(coreFacadeProvider) - .selectOutbound(groupTag, outboundTag) + .read(proxyRepositoryProvider) + .selectProxy(groupTag, outboundTag) .getOrElse((err) { loggy.warning("error selecting outbound", err); throw err; @@ -140,7 +141,10 @@ class ProxiesNotifier extends _$ProxiesNotifier with AppLogger { Future urlTest(String groupTag) async { loggy.debug("testing group: [$groupTag]"); if (state case AsyncData()) { - await ref.read(coreFacadeProvider).urlTest(groupTag).getOrElse((err) { + await ref + .read(proxyRepositoryProvider) + .urlTest(groupTag) + .getOrElse((err) { loggy.error("error testing group", err); throw err; }).run(); diff --git a/lib/features/proxies/view/proxies_page.dart b/lib/features/proxy/overview/proxies_overview_page.dart similarity index 91% rename from lib/features/proxies/view/proxies_page.dart rename to lib/features/proxy/overview/proxies_overview_page.dart index c5952064..1720473a 100644 --- a/lib/features/proxies/view/proxies_page.dart +++ b/lib/features/proxy/overview/proxies_overview_page.dart @@ -1,21 +1,21 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/proxies/notifier/notifier.dart'; -import 'package:hiddify/features/proxies/widgets/widgets.dart'; +import 'package:hiddify/features/proxy/overview/proxies_overview_notifier.dart'; +import 'package:hiddify/features/proxy/widget/proxy_tile.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class ProxiesPage extends HookConsumerWidget with PresLogger { - const ProxiesPage({super.key}); +class ProxiesOverviewPage extends HookConsumerWidget with PresLogger { + const ProxiesOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final asyncProxies = ref.watch(proxiesNotifierProvider); - final notifier = ref.watch(proxiesNotifierProvider.notifier); + final asyncProxies = ref.watch(proxiesOverviewNotifierProvider); + final notifier = ref.watch(proxiesOverviewNotifierProvider.notifier); final sortBy = ref.watch(proxiesSortNotifierProvider); final selectActiveProxyMutation = useMutation( diff --git a/lib/features/proxies/widgets/proxy_tile.dart b/lib/features/proxy/widget/proxy_tile.dart similarity index 88% rename from lib/features/proxies/widgets/proxy_tile.dart rename to lib/features/proxy/widget/proxy_tile.dart index 180afc42..e23630e0 100644 --- a/lib/features/proxies/widgets/proxy_tile.dart +++ b/lib/features/proxy/widget/proxy_tile.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/features/proxy/model/proxy_entity.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -11,7 +11,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { required this.onSelect, }); - final OutboundGroupItem proxy; + final ProxyItemEntity proxy; final bool selected; final VoidCallback onSelect; @@ -22,7 +22,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { return ListTile( shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), title: Text( - sanitizedTag(proxy.tag), + proxy.name, overflow: TextOverflow.ellipsis, ), leading: Padding( @@ -40,9 +40,9 @@ class ProxyTile extends HookConsumerWidget with PresLogger { TextSpan( text: proxy.type.label, children: [ - if (proxy.selectedTag != null) + if (proxy.selectedName != null) TextSpan( - text: ' (${sanitizedTag(proxy.selectedTag!)})', + text: ' (${proxy.selectedName})', style: Theme.of(context).textTheme.bodySmall, ), ], @@ -61,7 +61,7 @@ class ProxyTile extends HookConsumerWidget with PresLogger { showDialog( context: context, builder: (context) => AlertDialog( - content: SelectionArea(child: Text(sanitizedTag(proxy.tag))), + content: SelectionArea(child: Text(proxy.name)), actions: [ TextButton( onPressed: Navigator.of(context).pop, diff --git a/lib/features/about/view/about_page.dart b/lib/features/settings/about/about_page.dart similarity index 91% rename from lib/features/about/view/about_page.dart rename to lib/features/settings/about/about_page.dart index bbec9e5f..fb7f1b81 100644 --- a/lib/features/about/view/about_page.dart +++ b/lib/features/settings/about/about_page.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/failures.dart'; -import 'package:hiddify/features/common/app_update_notifier.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; +import 'package:hiddify/core/model/failures.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_notifier.dart'; +import 'package:hiddify/features/app_update/notifier/app_update_state.dart'; +import 'package:hiddify/features/app_update/widget/new_version_dialog.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; -import 'package:hiddify/features/common/new_version_dialog.dart'; import 'package:hiddify/gen/assets.gen.dart'; import 'package:hiddify/services/service_providers.dart'; import 'package:hiddify/utils/utils.dart'; @@ -18,7 +20,7 @@ class AboutPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final appInfo = ref.watch(appInfoProvider); + final appInfo = ref.watch(appInfoProvider).requireValue; final appUpdate = ref.watch(appUpdateNotifierProvider); ref.listen( diff --git a/lib/features/settings/data/settings_data_providers.dart b/lib/features/settings/data/settings_data_providers.dart new file mode 100644 index 00000000..000f5b73 --- /dev/null +++ b/lib/features/settings/data/settings_data_providers.dart @@ -0,0 +1,9 @@ +import 'package:hiddify/features/settings/data/settings_repository.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'settings_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +SettingsRepository settingsRepository(SettingsRepositoryRef ref) { + return SettingsRepositoryImpl(); +} diff --git a/lib/features/settings/data/settings_repository.dart b/lib/features/settings/data/settings_repository.dart new file mode 100644 index 00000000..3aa949ee --- /dev/null +++ b/lib/features/settings/data/settings_repository.dart @@ -0,0 +1,44 @@ +import 'package:flutter/services.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/settings/model/settings_failure.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +abstract interface class SettingsRepository { + TaskEither isIgnoringBatteryOptimizations(); + TaskEither requestIgnoreBatteryOptimizations(); +} + +class SettingsRepositoryImpl + with ExceptionHandler, InfraLogger + implements SettingsRepository { + final _methodChannel = const MethodChannel("app.hiddify.com/platform"); + + @override + TaskEither isIgnoringBatteryOptimizations() { + return exceptionHandler( + () async { + loggy.debug("checking battery optimization status"); + final result = await _methodChannel + .invokeMethod("is_ignoring_battery_optimizations"); + loggy.debug("is ignoring battery optimizations? [$result]"); + return right(result!); + }, + SettingsUnexpectedFailure.new, + ); + } + + @override + TaskEither requestIgnoreBatteryOptimizations() { + return exceptionHandler( + () async { + loggy.debug("requesting ignore battery optimization"); + final result = await _methodChannel + .invokeMethod("request_ignore_battery_optimizations"); + loggy.debug("ignore battery optimization result: [$result]"); + return right(result!); + }, + SettingsUnexpectedFailure.new, + ); + } +} diff --git a/lib/features/settings/model/settings_failure.dart b/lib/features/settings/model/settings_failure.dart new file mode 100644 index 00000000..345c03a4 --- /dev/null +++ b/lib/features/settings/model/settings_failure.dart @@ -0,0 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; + +part 'settings_failure.freezed.dart'; + +@freezed +sealed class SettingsFailure with _$SettingsFailure, Failure { + const SettingsFailure._(); + + @With() + const factory SettingsFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = SettingsUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + SettingsUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/settings/notifier/platform_settings_notifier.dart b/lib/features/settings/notifier/platform_settings_notifier.dart new file mode 100644 index 00000000..2afb3af1 --- /dev/null +++ b/lib/features/settings/notifier/platform_settings_notifier.dart @@ -0,0 +1,25 @@ +import 'package:hiddify/features/settings/data/settings_data_providers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'platform_settings_notifier.g.dart'; + +@riverpod +class IgnoreBatteryOptimizations extends _$IgnoreBatteryOptimizations { + @override + Future build() async { + return ref + .watch(settingsRepositoryProvider) + .isIgnoringBatteryOptimizations() + .getOrElse((l) => false) + .run(); + } + + Future request() async { + await ref + .read(settingsRepositoryProvider) + .requestIgnoreBatteryOptimizations() + .run(); + await Future.delayed(const Duration(seconds: 1)); + ref.invalidateSelf(); + } +} diff --git a/lib/features/settings/view/settings_page.dart b/lib/features/settings/overview/settings_overview_page.dart similarity index 85% rename from lib/features/settings/view/settings_page.dart rename to lib/features/settings/overview/settings_overview_page.dart index 164641fe..3fee58b2 100644 --- a/lib/features/settings/view/settings_page.dart +++ b/lib/features/settings/overview/settings_overview_page.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/settings/widgets/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -class SettingsPage extends HookConsumerWidget { - const SettingsPage({super.key}); +class SettingsOverviewPage extends HookConsumerWidget { + const SettingsOverviewPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { diff --git a/lib/features/settings/view/config_options_page.dart b/lib/features/settings/view/config_options_page.dart deleted file mode 100644 index 50899b71..00000000 --- a/lib/features/settings/view/config_options_page.dart +++ /dev/null @@ -1,299 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/data/repository/config_options_store.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/log/model/log_level.dart'; -import 'package:hiddify/features/settings/widgets/widgets.dart'; -import 'package:hiddify/utils/utils.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:humanizer/humanizer.dart'; - -class ConfigOptionsPage extends HookConsumerWidget { - const ConfigOptionsPage({super.key}); - - static final _default = ConfigOptions.initial; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final t = ref.watch(translationsProvider); - - final options = ref.watch(configPreferencesProvider); - final serviceMode = ref.watch(serviceModeStoreProvider); - - return Scaffold( - appBar: AppBar( - title: Text(t.settings.config.pageTitle), - actions: [ - PopupMenuButton( - itemBuilder: (context) { - return [ - PopupMenuItem( - child: Text(t.general.addToClipboard), - onTap: () { - Clipboard.setData( - ClipboardData(text: options.format()), - ); - }, - ), - ]; - }, - ), - ], - ), - body: ListView( - children: [ - ListTile( - title: Text(t.settings.config.logLevel), - subtitle: Text(options.logLevel.name.toUpperCase()), - onTap: () async { - final logLevel = await SettingsPickerDialog( - title: t.settings.config.logLevel, - selected: options.logLevel, - options: LogLevel.choices, - getTitle: (e) => e.name.toUpperCase(), - resetValue: _default.logLevel, - ).show(context); - if (logLevel == null) return; - await ref.read(logLevelStore.notifier).update(logLevel); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.route), - // SwitchListTile( - // title: Text(t.settings.config.bypassLan), - // value: options.bypassLan, - // onChanged: ref.read(bypassLanStore.notifier).update, - // ), - SwitchListTile( - title: Text(t.settings.config.resolveDestination), - value: options.resolveDestination, - onChanged: ref.read(resolveDestinationStore.notifier).update, - ), - ListTile( - title: Text(t.settings.config.ipv6Mode), - subtitle: Text(options.ipv6Mode.present(t)), - onTap: () async { - final ipv6Mode = await SettingsPickerDialog( - title: t.settings.config.ipv6Mode, - selected: options.ipv6Mode, - options: IPv6Mode.values, - getTitle: (e) => e.present(t), - resetValue: _default.ipv6Mode, - ).show(context); - if (ipv6Mode == null) return; - await ref.read(ipv6ModeStore.notifier).update(ipv6Mode); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.dns), - ListTile( - title: Text(t.settings.config.remoteDnsAddress), - subtitle: Text(options.remoteDnsAddress), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.remoteDnsAddress, - initialValue: options.remoteDnsAddress, - resetValue: _default.remoteDnsAddress, - ).show(context); - if (url == null || url.isEmpty) return; - await ref.read(remoteDnsAddressStore.notifier).update(url); - }, - ), - ListTile( - title: Text(t.settings.config.remoteDnsDomainStrategy), - subtitle: Text(options.remoteDnsDomainStrategy.displayName), - onTap: () async { - final domainStrategy = await SettingsPickerDialog( - title: t.settings.config.remoteDnsDomainStrategy, - selected: options.remoteDnsDomainStrategy, - options: DomainStrategy.values, - getTitle: (e) => e.displayName, - resetValue: _default.remoteDnsDomainStrategy, - ).show(context); - if (domainStrategy == null) return; - await ref - .read(remoteDnsDomainStrategyStore.notifier) - .update(domainStrategy); - }, - ), - ListTile( - title: Text(t.settings.config.directDnsAddress), - subtitle: Text(options.directDnsAddress), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.directDnsAddress, - initialValue: options.directDnsAddress, - resetValue: _default.directDnsAddress, - ).show(context); - if (url == null || url.isEmpty) return; - await ref.read(directDnsAddressStore.notifier).update(url); - }, - ), - ListTile( - title: Text(t.settings.config.directDnsDomainStrategy), - subtitle: Text(options.directDnsDomainStrategy.displayName), - onTap: () async { - final domainStrategy = await SettingsPickerDialog( - title: t.settings.config.directDnsDomainStrategy, - selected: options.directDnsDomainStrategy, - options: DomainStrategy.values, - getTitle: (e) => e.displayName, - resetValue: _default.directDnsDomainStrategy, - ).show(context); - if (domainStrategy == null) return; - await ref - .read(directDnsDomainStrategyStore.notifier) - .update(domainStrategy); - }, - ), - // SwitchListTile( - // title: Text(t.settings.config.enableFakeDns), - // value: options.enableFakeDns, - // onChanged: ref.read(enableFakeDnsStore.notifier).update, - // ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.inbound), - // if (PlatformUtils.isDesktop) ...[ - // SwitchListTile( - // title: Text(t.settings.config.enableTun), - // value: options.enableTun, - // onChanged: ref.read(enableTunStore.notifier).update, - // ), - // SwitchListTile( - // title: Text(t.settings.config.setSystemProxy), - // value: options.setSystemProxy, - // onChanged: ref.read(setSystemProxyStore.notifier).update, - // ), - // ], - ListTile( - title: Text(t.settings.config.serviceMode), - subtitle: Text(serviceMode.present(t)), - onTap: () async { - final pickedMode = await SettingsPickerDialog( - title: t.settings.config.serviceMode, - selected: serviceMode, - options: ServiceMode.choices, - getTitle: (e) => e.present(t), - resetValue: ServiceMode.defaultMode, - ).show(context); - if (pickedMode == null) return; - await ref - .read(serviceModeStoreProvider.notifier) - .update(pickedMode); - }, - ), - SwitchListTile( - title: Text(t.settings.config.strictRoute), - value: options.strictRoute, - onChanged: ref.read(strictRouteStore.notifier).update, - ), - ListTile( - title: Text(t.settings.config.tunImplementation), - subtitle: Text(options.tunImplementation.name), - onTap: () async { - final tunImplementation = await SettingsPickerDialog( - title: t.settings.config.tunImplementation, - selected: options.tunImplementation, - options: TunImplementation.values, - getTitle: (e) => e.name, - resetValue: _default.tunImplementation, - ).show(context); - if (tunImplementation == null) return; - await ref - .read(tunImplementationStore.notifier) - .update(tunImplementation); - }, - ), - ListTile( - title: Text(t.settings.config.mixedPort), - subtitle: Text(options.mixedPort.toString()), - onTap: () async { - final mixedPort = await SettingsInputDialog( - title: t.settings.config.mixedPort, - initialValue: options.mixedPort, - resetValue: _default.mixedPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (mixedPort == null) return; - await ref.read(mixedPortStore.notifier).update(mixedPort); - }, - ), - ListTile( - title: Text(t.settings.config.localDnsPort), - subtitle: Text(options.localDnsPort.toString()), - onTap: () async { - final localDnsPort = await SettingsInputDialog( - title: t.settings.config.localDnsPort, - initialValue: options.localDnsPort, - resetValue: _default.localDnsPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (localDnsPort == null) return; - await ref.read(localDnsPortStore.notifier).update(localDnsPort); - }, - ), - const SettingsDivider(), - SettingsSection(t.settings.config.section.misc), - ListTile( - title: Text(t.settings.config.connectionTestUrl), - subtitle: Text(options.connectionTestUrl), - onTap: () async { - final url = await SettingsInputDialog( - title: t.settings.config.connectionTestUrl, - initialValue: options.connectionTestUrl, - resetValue: _default.connectionTestUrl, - ).show(context); - if (url == null || url.isEmpty || !isUrl(url)) return; - await ref.read(connectionTestUrlStore.notifier).update(url); - }, - ), - ListTile( - title: Text(t.settings.config.urlTestInterval), - subtitle: Text( - options.urlTestInterval.toApproximateTime(isRelativeToNow: false), - ), - onTap: () async { - final urlTestInterval = await SettingsSliderDialog( - title: t.settings.config.urlTestInterval, - initialValue: options.urlTestInterval.inMinutes.toDouble(), - resetValue: _default.urlTestInterval.inMinutes.toDouble(), - min: 1, - max: 60, - divisions: 60, - labelGen: (value) => Duration(minutes: value.toInt()) - .toApproximateTime(isRelativeToNow: false), - ).show(context); - if (urlTestInterval == null) return; - await ref - .read(urlTestIntervalStore.notifier) - .update(Duration(minutes: urlTestInterval.toInt())); - }, - ), - ListTile( - title: Text(t.settings.config.clashApiPort), - subtitle: Text(options.clashApiPort.toString()), - onTap: () async { - final clashApiPort = await SettingsInputDialog( - title: t.settings.config.clashApiPort, - initialValue: options.clashApiPort, - resetValue: _default.clashApiPort, - validator: isPort, - mapTo: int.tryParse, - digitsOnly: true, - ).show(context); - if (clashApiPort == null) return; - await ref.read(clashApiPortStore.notifier).update(clashApiPort); - }, - ), - const Gap(24), - ], - ), - ); - } -} diff --git a/lib/features/settings/view/view.dart b/lib/features/settings/view/view.dart deleted file mode 100644 index c94412af..00000000 --- a/lib/features/settings/view/view.dart +++ /dev/null @@ -1,3 +0,0 @@ -export 'config_options_page.dart'; -export 'per_app_proxy_page.dart'; -export 'settings_page.dart'; diff --git a/lib/features/settings/widgets/advanced_setting_tiles.dart b/lib/features/settings/widgets/advanced_setting_tiles.dart index 23f7a2d3..f82281f9 100644 --- a/lib/features/settings/widgets/advanced_setting_tiles.dart +++ b/lib/features/settings/widgets/advanced_setting_tiles.dart @@ -2,11 +2,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; +import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class AdvancedSettingTiles extends HookConsumerWidget { diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index d042c1d4..6c246c43 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -1,7 +1,9 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/core/prefs/prefs.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/theme/theme_preferences.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; import 'package:hiddify/services/auto_start_service.dart'; import 'package:hiddify/utils/utils.dart'; @@ -14,7 +16,7 @@ class GeneralSettingTiles extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final theme = ref.watch(themeProvider); + final themeMode = ref.watch(themePreferencesProvider); return Column( children: [ @@ -43,7 +45,7 @@ class GeneralSettingTiles extends HookConsumerWidget { ), ListTile( title: Text(t.settings.general.themeMode), - subtitle: Text(theme.mode.present(t)), + subtitle: Text(themeMode.present(t)), leading: const Icon(Icons.light_mode), onTap: () async { final selectedThemeMode = await showDialog( @@ -56,7 +58,7 @@ class GeneralSettingTiles extends HookConsumerWidget { (e) => RadioListTile( title: Text(e.present(t)), value: e, - groupValue: theme.mode, + groupValue: themeMode, onChanged: (e) => context.pop(e), ), ) @@ -66,8 +68,8 @@ class GeneralSettingTiles extends HookConsumerWidget { ); if (selectedThemeMode != null) { await ref - .read(themeModeNotifierProvider.notifier) - .update(selectedThemeMode); + .read(themePreferencesProvider.notifier) + .changeThemeMode(selectedThemeMode); } }, ), diff --git a/lib/features/settings/widgets/platform_settings_tiles.dart b/lib/features/settings/widgets/platform_settings_tiles.dart index 11f83834..cdeb18e4 100644 --- a/lib/features/settings/widgets/platform_settings_tiles.dart +++ b/lib/features/settings/widgets/platform_settings_tiles.dart @@ -1,22 +1,9 @@ import 'dart:io'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/services/service_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/settings/notifier/platform_settings_notifier.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:riverpod_annotation/riverpod_annotation.dart'; - -part 'platform_settings_tiles.g.dart'; - -@riverpod -Future isIgnoringBatteryOptimizations( - IsIgnoringBatteryOptimizationsRef ref, -) async => - ref - .watch(platformServicesProvider) - .isIgnoringBatteryOptimizations() - .getOrElse((l) => false) - .run(); class PlatformSettingsTiles extends HookConsumerWidget { const PlatformSettingsTiles({super.key}); @@ -26,7 +13,7 @@ class PlatformSettingsTiles extends HookConsumerWidget { final t = ref.watch(translationsProvider); final isIgnoringBatteryOptimizations = - ref.watch(isIgnoringBatteryOptimizationsProvider); + ref.watch(ignoreBatteryOptimizationsProvider); ListTile buildIgnoreTile(bool enabled) => ListTile( title: Text(t.settings.general.ignoreBatteryOptimizations), @@ -35,11 +22,8 @@ class PlatformSettingsTiles extends HookConsumerWidget { enabled: enabled, onTap: () async { await ref - .read(platformServicesProvider) - .requestIgnoreBatteryOptimizations() - .run(); - await Future.delayed(const Duration(seconds: 1)); - ref.invalidate(isIgnoringBatteryOptimizationsProvider); + .read(ignoreBatteryOptimizationsProvider.notifier) + .request(); }, ); diff --git a/lib/features/settings/widgets/settings_input_dialog.dart b/lib/features/settings/widgets/settings_input_dialog.dart index 394158da..84a552be 100644 --- a/lib/features/settings/widgets/settings_input_dialog.dart +++ b/lib/features/settings/widgets/settings_input_dialog.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/features/stats/data/stats_data_providers.dart b/lib/features/stats/data/stats_data_providers.dart new file mode 100644 index 00000000..e352369c --- /dev/null +++ b/lib/features/stats/data/stats_data_providers.dart @@ -0,0 +1,10 @@ +import 'package:hiddify/features/stats/data/stats_repository.dart'; +import 'package:hiddify/singbox/service/singbox_service_provider.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'stats_data_providers.g.dart'; + +@Riverpod(keepAlive: true) +StatsRepository statsRepository(StatsRepositoryRef ref) { + return StatsRepositoryImpl(singbox: ref.watch(singboxServiceProvider)); +} diff --git a/lib/features/stats/data/stats_repository.dart b/lib/features/stats/data/stats_repository.dart new file mode 100644 index 00000000..49025344 --- /dev/null +++ b/lib/features/stats/data/stats_repository.dart @@ -0,0 +1,33 @@ +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/utils/exception_handler.dart'; +import 'package:hiddify/features/stats/model/stats_entity.dart'; +import 'package:hiddify/features/stats/model/stats_failure.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; + +abstract interface class StatsRepository { + Stream> watchStats(); +} + +class StatsRepositoryImpl + with ExceptionHandler, InfraLogger + implements StatsRepository { + StatsRepositoryImpl({required this.singbox}); + + final SingboxService singbox; + + @override + Stream> watchStats() { + return singbox + .watchStats() + .map( + (event) => StatsEntity( + uplink: event.uplink, + downlink: event.downlink, + uplinkTotal: event.downlink, + downlinkTotal: event.downlinkTotal, + ), + ) + .handleExceptions(StatsUnexpectedFailure.new); + } +} diff --git a/lib/features/stats/model/stats_entity.dart b/lib/features/stats/model/stats_entity.dart new file mode 100644 index 00000000..13bc86f2 --- /dev/null +++ b/lib/features/stats/model/stats_entity.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'stats_entity.freezed.dart'; + +@freezed +class StatsEntity with _$StatsEntity { + const StatsEntity._(); + + const factory StatsEntity({ + required int uplink, + required int downlink, + required int uplinkTotal, + required int downlinkTotal, + }) = _StatsEntity; + + factory StatsEntity.empty() => const StatsEntity( + uplink: 0, + downlink: 0, + uplinkTotal: 0, + downlinkTotal: 0, + ); +} diff --git a/lib/features/stats/model/stats_failure.dart b/lib/features/stats/model/stats_failure.dart new file mode 100644 index 00000000..bce4ead3 --- /dev/null +++ b/lib/features/stats/model/stats_failure.dart @@ -0,0 +1,26 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/failures.dart'; + +part 'stats_failure.freezed.dart'; + +@freezed +sealed class StatsFailure with _$StatsFailure, Failure { + const StatsFailure._(); + + @With() + const factory StatsFailure.unexpected([ + Object? error, + StackTrace? stackTrace, + ]) = StatsUnexpectedFailure; + + @override + ({String type, String? message}) present(TranslationsEn t) { + return switch (this) { + StatsUnexpectedFailure() => ( + type: t.failure.unexpected, + message: null, + ), + }; + } +} diff --git a/lib/features/stats/notifier/stats_notifier.dart b/lib/features/stats/notifier/stats_notifier.dart new file mode 100644 index 00000000..d04adbf8 --- /dev/null +++ b/lib/features/stats/notifier/stats_notifier.dart @@ -0,0 +1,23 @@ +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/features/stats/data/stats_data_providers.dart'; +import 'package:hiddify/features/stats/model/stats_entity.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'stats_notifier.g.dart'; + +@riverpod +class StatsNotifier extends _$StatsNotifier with AppLogger { + @override + Stream build() async* { + final serviceRunning = await ref.watch(serviceRunningProvider.future); + if (serviceRunning) { + yield* ref + .watch(statsRepositoryProvider) + .watchStats() + .map((event) => event.getOrElse((_) => StatsEntity.empty())); + } else { + yield* Stream.value(StatsEntity.empty()); + } + } +} diff --git a/lib/features/common/side_bar_stats_overview.dart b/lib/features/stats/widget/side_bar_stats_overview.dart similarity index 89% rename from lib/features/common/side_bar_stats_overview.dart rename to lib/features/stats/widget/side_bar_stats_overview.dart index b3395cee..a1b6ff6b 100644 --- a/lib/features/common/side_bar_stats_overview.dart +++ b/lib/features/stats/widget/side_bar_stats_overview.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -import 'package:hiddify/core/core_providers.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/stats_provider.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/features/stats/model/stats_entity.dart'; +import 'package:hiddify/features/stats/notifier/stats_notifier.dart'; +import 'package:hiddify/utils/number_formatters.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; class SideBarStatsOverview extends HookConsumerWidget { @@ -13,7 +13,8 @@ class SideBarStatsOverview extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final stats = ref.watch(statsProvider).asData?.value ?? CoreStatus.empty(); + final stats = + ref.watch(statsNotifierProvider).asData?.value ?? StatsEntity.empty(); return Padding( padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), diff --git a/lib/features/system_tray/system_tray_controller.dart b/lib/features/system_tray/system_tray_controller.dart index 04108033..f4c0906f 100644 --- a/lib/features/system_tray/system_tray_controller.dart +++ b/lib/features/system_tray/system_tray_controller.dart @@ -1,14 +1,15 @@ import 'dart:io'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/model/constants.dart'; import 'package:hiddify/core/router/router.dart'; -import 'package:hiddify/data/repository/config_options_store.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/constants.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/features/common/connectivity/connectivity_controller.dart'; import 'package:hiddify/features/common/window/window_controller.dart'; +import 'package:hiddify/features/config_option/model/config_option_patch.dart'; +import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; +import 'package:hiddify/features/connection/model/connection_status.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/gen/assets.gen.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:tray_manager/tray_manager.dart'; @@ -31,11 +32,13 @@ class SystemTrayController extends _$SystemTrayController _initialized = true; } - final connection = switch (ref.watch(connectivityControllerProvider)) { + final connection = switch (ref.watch(connectionNotifierProvider)) { AsyncData(:final value) => value, _ => const Disconnected(), }; - final serviceMode = ref.watch(serviceModeStoreProvider); + final serviceMode = await ref + .watch(configOptionNotifierProvider.future) + .then((value) => value.serviceMode); final t = ref.watch(translationsProvider); final destinations = <(String label, String location)>[ @@ -79,8 +82,8 @@ class SystemTrayController extends _$SystemTrayController final newMode = ServiceMode.values.byName(menuItem.key!); loggy.debug("switching service mode: [$newMode]"); await ref - .read(serviceModeStoreProvider.notifier) - .update(newMode); + .read(configOptionNotifierProvider.notifier) + .updateOption(ConfigOptionPatch(serviceMode: newMode)); }, ), ), @@ -137,11 +140,11 @@ class SystemTrayController extends _$SystemTrayController } Future handleClickSetAsSystemProxy(MenuItem menuItem) async { - return ref.read(connectivityControllerProvider.notifier).toggleConnection(); + return ref.read(connectionNotifierProvider.notifier).toggleConnection(); } Future handleClickExitApp(MenuItem menuItem) async { - await ref.read(connectivityControllerProvider.notifier).abortConnection(); + await ref.read(connectionNotifierProvider.notifier).abortConnection(); await trayManager.destroy(); return ref.read(windowControllerProvider.notifier).quit(); } diff --git a/lib/main_dev.dart b/lib/main_dev.dart index 8dcdbd01..0e845d6b 100644 --- a/lib/main_dev.dart +++ b/lib/main_dev.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:hiddify/bootstrap.dart'; -import 'package:hiddify/domain/environment.dart'; +import 'package:hiddify/core/model/environment.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/main_prod.dart b/lib/main_prod.dart index 81b713ca..43110c09 100644 --- a/lib/main_prod.dart +++ b/lib/main_prod.dart @@ -1,6 +1,6 @@ import 'package:flutter/widgets.dart'; import 'package:hiddify/bootstrap.dart'; -import 'package:hiddify/domain/environment.dart'; +import 'package:hiddify/core/model/environment.dart'; void main() async { final widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); diff --git a/lib/services/auto_start_service.dart b/lib/services/auto_start_service.dart index bb0a31bf..7c60739e 100644 --- a/lib/services/auto_start_service.dart +++ b/lib/services/auto_start_service.dart @@ -1,6 +1,6 @@ import 'dart:io'; -import 'package:hiddify/core/core_providers.dart'; +import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:launch_at_startup/launch_at_startup.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -13,7 +13,7 @@ class AutoStartService extends _$AutoStartService with InfraLogger { Future build() async { loggy.debug("initializing"); if (!PlatformUtils.isDesktop) return false; - final appInfo = ref.watch(appInfoProvider); + final appInfo = ref.watch(appInfoProvider).requireValue; launchAtStartup.setup( appName: appInfo.name, appPath: Platform.resolvedExecutable, diff --git a/lib/services/platform_services.dart b/lib/services/platform_services.dart index 267f467c..71aadcaa 100644 --- a/lib/services/platform_services.dart +++ b/lib/services/platform_services.dart @@ -1,19 +1,10 @@ -import 'dart:convert'; -import 'dart:ffi'; import 'dart:io'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:hiddify/services/files_editor_service.dart'; -import 'package:hiddify/utils/ffi_utils.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:path_provider/path_provider.dart'; -import 'package:posix/posix.dart'; -import 'package:win32/win32.dart'; - -part 'platform_services.freezed.dart'; -part 'platform_services.g.dart'; class PlatformServices with InfraLogger { final _methodChannel = const MethodChannel("app.hiddify.com/platform"); @@ -47,130 +38,4 @@ class PlatformServices with InfraLogger { }, ); } - - Future hasPrivilege() async { - try { - if (Platform.isWindows) { - bool isElevated = false; - withMemory(sizeOf(), (phToken) { - withMemory(sizeOf(), (pReturnedSize) { - withMemory(sizeOf<_TokenElevation>(), - (pElevation) { - if (OpenProcessToken( - GetCurrentProcess(), - TOKEN_QUERY, - phToken.cast(), - ) == - 1) { - if (GetTokenInformation( - phToken.value, - TOKEN_INFORMATION_CLASS.TokenElevation, - pElevation, - sizeOf<_TokenElevation>(), - pReturnedSize, - ) == - 1) { - isElevated = pElevation.ref.tokenIsElevated != 0; - } - } - if (phToken.value != 0) { - CloseHandle(phToken.value); - } - }); - }); - }); - return isElevated; - } else if (Platform.isLinux || Platform.isMacOS) { - final euid = geteuid(); - return euid == 0; - } else { - return true; - } - } catch (e) { - loggy.warning("error checking privilege", e); - return true; // return true so core handles it - } - } - - TaskEither isIgnoringBatteryOptimizations() { - return TaskEither( - () async { - loggy.debug("checking battery optimization status"); - final result = await _methodChannel - .invokeMethod("is_ignoring_battery_optimizations"); - loggy.debug("is ignoring battery optimizations? [$result]"); - return right(result!); - }, - ); - } - - TaskEither requestIgnoreBatteryOptimizations() { - return TaskEither( - () async { - loggy.debug("requesting ignore battery optimization"); - final result = await _methodChannel - .invokeMethod("request_ignore_battery_optimizations"); - loggy.debug("ignore battery optimization result: [$result]"); - return right(result!); - }, - ); - } - - TaskEither> getInstalledPackages() { - return TaskEither( - () async { - loggy.debug("getting installed packages info"); - final result = - await _methodChannel.invokeMethod("get_installed_packages"); - if (result == null) return left("null response"); - return right( - (jsonDecode(result) as List).map((e) { - return InstalledPackageInfo.fromJson(e as Map); - }).toList(), - ); - }, - ); - } - - TaskEither getPackageIcon( - String packageName, - ) { - return TaskEither( - () async { - loggy.debug("getting package [$packageName] icon"); - final result = await _methodChannel.invokeMethod( - "get_package_icon", - {"packageName": packageName}, - ); - if (result == null) return left("null response"); - final Uint8List decoded; - try { - decoded = base64.decode(result); - } catch (e) { - return left("error parsing base64 response"); - } - return right(decoded); - }, - ); - } -} - -@freezed -class InstalledPackageInfo with _$InstalledPackageInfo { - @JsonSerializable(fieldRename: FieldRename.kebab) - const factory InstalledPackageInfo({ - required String packageName, - required String name, - required bool isSystemApp, - }) = _InstalledPackageInfo; - - factory InstalledPackageInfo.fromJson(Map json) => - _$InstalledPackageInfoFromJson(json); -} - -sealed class _TokenElevation extends Struct { - /// A nonzero value if the token has elevated privileges; - /// otherwise, a zero value. - @Int32() - external int tokenIsElevated; } diff --git a/lib/services/service_providers.dart b/lib/services/service_providers.dart index 6b92132a..c568c9c0 100644 --- a/lib/services/service_providers.dart +++ b/lib/services/service_providers.dart @@ -1,6 +1,5 @@ import 'package:hiddify/services/files_editor_service.dart'; import 'package:hiddify/services/platform_services.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'service_providers.g.dart'; @@ -9,9 +8,6 @@ part 'service_providers.g.dart'; FilesEditorService filesEditorService(FilesEditorServiceRef ref) => FilesEditorService(ref.watch(platformServicesProvider)); -@Riverpod(keepAlive: true) -SingboxService singboxService(SingboxServiceRef ref) => SingboxService(); - @Riverpod(keepAlive: true) PlatformServices platformServices(PlatformServicesRef ref) => PlatformServices(); diff --git a/lib/services/singbox/shared.dart b/lib/services/singbox/shared.dart deleted file mode 100644 index 6c34b189..00000000 --- a/lib/services/singbox/shared.dart +++ /dev/null @@ -1,46 +0,0 @@ -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/core_service_failure.dart'; - -mixin ServiceStatus { - ConnectionStatus mapEventToStatus(dynamic event) { - final status = event['status'] as String; - late ConnectionStatus connectionStatus; - switch (status) { - case "Stopped": - final failure = event["alert"] as String?; - final message = event["message"] as String?; - connectionStatus = ConnectionStatus.disconnected( - switch (failure) { - null => null, - "RequestVPNPermission" => MissingVpnPermission(message), - "RequestNotificationPermission" => - MissingNotificationPermission(message), - "EmptyConfiguration" || - "StartCommandServer" || - "CreateService" || - "StartService" => - CoreConnectionFailure(fromServiceAlert(failure, message)), - _ => const UnexpectedConnectionFailure(), - }, - ); - case "Starting": - connectionStatus = const Connecting(); - case "Started": - connectionStatus = const Connected(); - case "Stopping": - connectionStatus = const Disconnecting(); - } - return connectionStatus; - } - - CoreServiceFailure fromServiceAlert(String key, String? message) { - return switch (key) { - "EmptyConfiguration" => InvalidConfig(message), - "StartCommandServer" || - "CreateService" => - CoreServiceCreateFailure(message), - "StartService" => CoreServiceStartFailure(message), - _ => const CoreServiceOtherFailure(), - }; - } -} diff --git a/lib/services/singbox/singbox_service.dart b/lib/services/singbox/singbox_service.dart deleted file mode 100644 index ac1d4d90..00000000 --- a/lib/services/singbox/singbox_service.dart +++ /dev/null @@ -1,59 +0,0 @@ -import 'dart:io'; - -import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; -import 'package:hiddify/services/singbox/ffi_singbox_service.dart'; -import 'package:hiddify/services/singbox/mobile_singbox_service.dart'; - -abstract interface class SingboxService { - factory SingboxService() { - if (Platform.isAndroid || Platform.isIOS) { - return MobileSingboxService(); - } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { - return FFISingboxService(); - } - throw Exception("unsupported platform"); - } - - Future init(); - - TaskEither setup( - String baseDir, - String workingDir, - String tempDir, - bool debug, - ); - - TaskEither parseConfig( - String path, - String tempPath, - bool debug, - ); - - TaskEither changeConfigOptions(ConfigOptions options); - - TaskEither generateConfig( - String path, - ); - - TaskEither start(String configPath, bool disableMemoryLimit); - - TaskEither stop(); - - TaskEither restart(String configPath, bool disableMemoryLimit); - - Stream watchOutbounds(); - - TaskEither selectOutbound(String groupTag, String outboundTag); - - TaskEither urlTest(String groupTag); - - Stream watchConnectionStatus(); - - Stream watchStats(); - - Stream> watchLogs(String path); - - TaskEither clearLogs(); -} diff --git a/lib/singbox/model/singbox_config_enum.dart b/lib/singbox/model/singbox_config_enum.dart new file mode 100644 index 00000000..a7da0a56 --- /dev/null +++ b/lib/singbox/model/singbox_config_enum.dart @@ -0,0 +1,68 @@ +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/utils/platform_utils.dart'; +import 'package:json_annotation/json_annotation.dart'; + +enum ServiceMode { + proxy, + systemProxy, + tun; + + static ServiceMode get defaultMode => + PlatformUtils.isDesktop ? systemProxy : tun; + + static List get choices { + if (PlatformUtils.isDesktop) { + return values; + } + return [proxy, tun]; + } + + String present(TranslationsEn t) => switch (this) { + proxy => t.settings.config.serviceModes.proxy, + systemProxy => t.settings.config.serviceModes.systemProxy, + tun => t.settings.config.serviceModes.tun, + }; +} + +@JsonEnum(valueField: 'key') +enum IPv6Mode { + disable("ipv4_only"), + enable("prefer_ipv4"), + prefer("prefer_ipv6"), + only("ipv6_only"); + + const IPv6Mode(this.key); + + final String key; + + String present(TranslationsEn t) => switch (this) { + disable => t.settings.config.ipv6Modes.disable, + enable => t.settings.config.ipv6Modes.enable, + prefer => t.settings.config.ipv6Modes.prefer, + only => t.settings.config.ipv6Modes.only, + }; +} + +@JsonEnum(valueField: 'key') +enum DomainStrategy { + auto(""), + preferIpv6("prefer_ipv6"), + preferIpv4("prefer_ipv4"), + ipv4Only("ipv4_only"), + ipv6Only("ipv6_only"); + + const DomainStrategy(this.key); + + final String key; + + String get displayName => switch (this) { + auto => "auto", + _ => key, + }; +} + +enum TunImplementation { + mixed, + system, + gVisor; +} diff --git a/lib/singbox/model/singbox_config_option.dart b/lib/singbox/model/singbox_config_option.dart new file mode 100644 index 00000000..0a11102a --- /dev/null +++ b/lib/singbox/model/singbox_config_option.dart @@ -0,0 +1,62 @@ +import 'dart:convert'; + +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/features/log/model/log_level.dart'; +import 'package:hiddify/singbox/model/singbox_config_enum.dart'; +import 'package:hiddify/singbox/model/singbox_rule.dart'; + +part 'singbox_config_option.freezed.dart'; +part 'singbox_config_option.g.dart'; + +@freezed +class SingboxConfigOption with _$SingboxConfigOption { + const SingboxConfigOption._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxConfigOption({ + required bool executeConfigAsIs, + required LogLevel logLevel, + required bool resolveDestination, + required IPv6Mode ipv6Mode, + required String remoteDnsAddress, + required DomainStrategy remoteDnsDomainStrategy, + required String directDnsAddress, + required DomainStrategy directDnsDomainStrategy, + required int mixedPort, + required int localDnsPort, + required TunImplementation tunImplementation, + required int mtu, + required bool strictRoute, + required String connectionTestUrl, + @IntervalConverter() required Duration urlTestInterval, + required bool enableClashApi, + required int clashApiPort, + required bool enableTun, + required bool setSystemProxy, + required bool bypassLan, + required bool enableFakeDns, + required bool independentDnsCache, + required String geoipPath, + required String geositePath, + required List rules, + }) = _SingboxConfigOption; + + String format() { + const encoder = JsonEncoder.withIndent(' '); + return encoder.convert(toJson()); + } + + factory SingboxConfigOption.fromJson(Map json) => + _$SingboxConfigOptionFromJson(json); +} + +class IntervalConverter implements JsonConverter { + const IntervalConverter(); + + @override + Duration fromJson(String json) => + Duration(minutes: int.parse(json.replaceAll("m", ""))); + + @override + String toJson(Duration object) => "${object.inMinutes}m"; +} diff --git a/lib/singbox/model/singbox_outbound.dart b/lib/singbox/model/singbox_outbound.dart new file mode 100644 index 00000000..556c40d7 --- /dev/null +++ b/lib/singbox/model/singbox_outbound.dart @@ -0,0 +1,40 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; + +part 'singbox_outbound.freezed.dart'; +part 'singbox_outbound.g.dart'; + +@freezed +class SingboxOutboundGroup with _$SingboxOutboundGroup { + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxOutboundGroup({ + required String tag, + @JsonKey(fromJson: _typeFromJson) required ProxyType type, + required String selected, + @Default([]) List items, + }) = _SingboxOutboundGroup; + + factory SingboxOutboundGroup.fromJson(Map json) => + _$SingboxOutboundGroupFromJson(json); +} + +@freezed +class SingboxOutboundGroupItem with _$SingboxOutboundGroupItem { + const SingboxOutboundGroupItem._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxOutboundGroupItem({ + required String tag, + @JsonKey(fromJson: _typeFromJson) required ProxyType type, + required int urlTestDelay, + }) = _SingboxOutboundGroupItem; + + factory SingboxOutboundGroupItem.fromJson(Map json) => + _$SingboxOutboundGroupItemFromJson(json); +} + +ProxyType _typeFromJson(dynamic type) => + ProxyType.values + .firstOrNullWhere((e) => e.key == (type as String?)?.toLowerCase()) ?? + ProxyType.unknown; diff --git a/lib/domain/singbox/proxy_type.dart b/lib/singbox/model/singbox_proxy_type.dart similarity index 100% rename from lib/domain/singbox/proxy_type.dart rename to lib/singbox/model/singbox_proxy_type.dart diff --git a/lib/singbox/model/singbox_rule.dart b/lib/singbox/model/singbox_rule.dart new file mode 100644 index 00000000..b927ffee --- /dev/null +++ b/lib/singbox/model/singbox_rule.dart @@ -0,0 +1,35 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'singbox_rule.freezed.dart'; +part 'singbox_rule.g.dart'; + +@freezed +class SingboxRule with _$SingboxRule { + const SingboxRule._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxRule({ + String? domains, + String? ip, + String? port, + String? protocol, + @Default(RuleNetwork.tcpAndUdp) RuleNetwork network, + @Default(RuleOutbound.proxy) RuleOutbound outbound, + }) = _SingboxRule; + + factory SingboxRule.fromJson(Map json) => + _$SingboxRuleFromJson(json); +} + +enum RuleOutbound { proxy, bypass, block } + +@JsonEnum(valueField: 'key') +enum RuleNetwork { + tcpAndUdp(""), + tcp("tcp"), + udp("udp"); + + const RuleNetwork(this.key); + + final String? key; +} diff --git a/lib/singbox/model/singbox_stats.dart b/lib/singbox/model/singbox_stats.dart new file mode 100644 index 00000000..b0badbeb --- /dev/null +++ b/lib/singbox/model/singbox_stats.dart @@ -0,0 +1,22 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'singbox_stats.freezed.dart'; +part 'singbox_stats.g.dart'; + +@freezed +class SingboxStats with _$SingboxStats { + const SingboxStats._(); + + @JsonSerializable(fieldRename: FieldRename.kebab) + const factory SingboxStats({ + required int connectionsIn, + required int connectionsOut, + required int uplink, + required int downlink, + required int uplinkTotal, + required int downlinkTotal, + }) = _SingboxStats; + + factory SingboxStats.fromJson(Map json) => + _$SingboxStatsFromJson(json); +} diff --git a/lib/singbox/model/singbox_status.dart b/lib/singbox/model/singbox_status.dart new file mode 100644 index 00000000..88d9d1cc --- /dev/null +++ b/lib/singbox/model/singbox_status.dart @@ -0,0 +1,48 @@ +import 'package:dartx/dartx.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'singbox_status.freezed.dart'; + +@freezed +sealed class SingboxStatus with _$SingboxStatus { + const SingboxStatus._(); + + const factory SingboxStatus.stopped({ + SingboxAlert? alert, + String? message, + }) = SingboxStopped; + const factory SingboxStatus.starting() = SingboxStarting; + const factory SingboxStatus.started() = SingboxStarted; + const factory SingboxStatus.stopping() = SingboxStopping; + + factory SingboxStatus.fromEvent(dynamic event) { + switch (event) { + case { + "status": "Stopped", + "alert": final String? alertStr, + "message": final String? messageStr, + }: + final alert = SingboxAlert.values.firstOrNullWhere( + (e) => alertStr?.toLowerCase() == e.name.toLowerCase(), + ); + return SingboxStatus.stopped(alert: alert, message: messageStr); + case {"status": "Starting"}: + return const SingboxStarting(); + case {"status": "Started"}: + return const SingboxStarted(); + case {"status": "Stopping"}: + return const SingboxStopping(); + default: + throw Exception("unexpected status [$event]"); + } + } +} + +enum SingboxAlert { + requestVPNPermission, + requestNotificationPermission, + emptyConfiguration, + startCommandServer, + createService, + startService; +} diff --git a/lib/services/singbox/ffi_singbox_service.dart b/lib/singbox/service/ffi_singbox_service.dart similarity index 84% rename from lib/services/singbox/ffi_singbox_service.dart rename to lib/singbox/service/ffi_singbox_service.dart index 62bcf6b7..e7bbeede 100644 --- a/lib/services/singbox/ffi_singbox_service.dart +++ b/lib/singbox/service/ffi_singbox_service.dart @@ -7,11 +7,13 @@ import 'dart:isolate'; import 'package:combine/combine.dart'; import 'package:ffi/ffi.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connectivity.dart'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/core/model/directories.dart'; import 'package:hiddify/gen/singbox_generated_bindings.dart'; -import 'package:hiddify/services/singbox/shared.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_outbound.dart'; +import 'package:hiddify/singbox/model/singbox_stats.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; import 'package:hiddify/utils/utils.dart'; import 'package:loggy/loggy.dart'; import 'package:path/path.dart' as p; @@ -20,15 +22,13 @@ import 'package:watcher/watcher.dart'; final _logger = Loggy('FFISingboxService'); -class FFISingboxService - with ServiceStatus, InfraLogger - implements SingboxService { +class FFISingboxService with InfraLogger implements SingboxService { static final SingboxNativeLibrary _box = _gen(); - late final ValueStream _connectionStatus; - late final ReceivePort _connectionStatusReceiver; - Stream? _serviceStatsStream; - Stream? _outboundsStream; + late final ValueStream _status; + late final ReceivePort _statusReceiver; + Stream? _serviceStatsStream; + Stream>? _outboundsStream; static SingboxNativeLibrary _gen() { String fullPath = ""; @@ -50,34 +50,32 @@ class FFISingboxService @override Future init() async { loggy.debug("initializing"); - _connectionStatusReceiver = ReceivePort('service status receiver'); - final source = _connectionStatusReceiver + _statusReceiver = ReceivePort('service status receiver'); + final source = _statusReceiver .asBroadcastStream() - .map((event) => jsonDecode(event as String) as Map) - .map(mapEventToStatus); - _connectionStatus = ValueConnectableStream.seeded( + .map((event) => jsonDecode(event as String)) + .map(SingboxStatus.fromEvent); + _status = ValueConnectableStream.seeded( source, - const ConnectionStatus.disconnected(), + const SingboxStopped(), ).autoConnect(); } @override TaskEither setup( - String baseDir, - String workingDir, - String tempDir, + Directories directories, bool debug, ) { - final port = _connectionStatusReceiver.sendPort.nativePort; + final port = _statusReceiver.sendPort.nativePort; return TaskEither( () => CombineWorker().execute( () { _box.setupOnce(NativeApi.initializeApiDLData); final err = _box .setup( - baseDir.toNativeUtf8().cast(), - workingDir.toNativeUtf8().cast(), - tempDir.toNativeUtf8().cast(), + directories.baseDir.path.toNativeUtf8().cast(), + directories.workingDir.path.toNativeUtf8().cast(), + directories.tempDir.path.toNativeUtf8().cast(), port, debug ? 1 : 0, ) @@ -93,7 +91,7 @@ class FFISingboxService } @override - TaskEither parseConfig( + TaskEither validateConfigByPath( String path, String tempPath, bool debug, @@ -119,7 +117,7 @@ class FFISingboxService } @override - TaskEither changeConfigOptions(ConfigOptions options) { + TaskEither changeOptions(SingboxConfigOption options) { return TaskEither( () => CombineWorker().execute( () { @@ -138,7 +136,7 @@ class FFISingboxService } @override - TaskEither generateConfig( + TaskEither generateFullConfigByPath( String path, ) { return TaskEither( @@ -219,10 +217,10 @@ class FFISingboxService } @override - Stream watchConnectionStatus() => _connectionStatus; + Stream watchStatus() => _status; @override - Stream watchStats() { + Stream watchStats() { if (_serviceStatsStream != null) return _serviceStatsStream!; final receiver = ReceivePort('service stats receiver'); final statusStream = receiver.asBroadcastStream( @@ -242,7 +240,9 @@ class FFISingboxService loggy.error("[service stats client] error received: $event"); throw event.replaceFirst('error:', ""); } - return event; + return SingboxStats.fromJson( + jsonDecode(event) as Map, + ); } loggy.error("[service status client] unexpected type, msg: $event"); throw "invalid type"; @@ -262,7 +262,7 @@ class FFISingboxService } @override - Stream watchOutbounds() { + Stream> watchOutbounds() { if (_outboundsStream != null) return _outboundsStream!; final receiver = ReceivePort('outbounds receiver'); final outboundsStream = receiver.asBroadcastStream( @@ -282,7 +282,9 @@ class FFISingboxService loggy.error("[group client] error received: $event"); throw event.replaceFirst('error:', ""); } - return event; + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); } loggy.error("[group client] unexpected type, msg: $event"); throw "invalid type"; diff --git a/lib/services/singbox/mobile_singbox_service.dart b/lib/singbox/service/platform_singbox_service.dart similarity index 68% rename from lib/services/singbox/mobile_singbox_service.dart rename to lib/singbox/service/platform_singbox_service.dart index f4440772..52cb7f66 100644 --- a/lib/services/singbox/mobile_singbox_service.dart +++ b/lib/singbox/service/platform_singbox_service.dart @@ -2,48 +2,51 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:fpdart/fpdart.dart'; -import 'package:hiddify/domain/connectivity/connection_status.dart'; -import 'package:hiddify/domain/singbox/config_options.dart'; -import 'package:hiddify/services/singbox/shared.dart'; -import 'package:hiddify/services/singbox/singbox_service.dart'; -import 'package:hiddify/utils/utils.dart'; +import 'package:hiddify/core/model/directories.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_outbound.dart'; +import 'package:hiddify/singbox/model/singbox_stats.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:hiddify/utils/custom_loggers.dart'; import 'package:rxdart/rxdart.dart'; -class MobileSingboxService - with ServiceStatus, InfraLogger - implements SingboxService { +class PlatformSingboxService with InfraLogger implements SingboxService { late final _methodChannel = const MethodChannel("com.hiddify.app/method"); - late final _connectionStatusChannel = + late final _statusChannel = const EventChannel("com.hiddify.app/service.status"); late final _alertsChannel = const EventChannel("com.hiddify.app/service.alerts"); late final _logsChannel = const EventChannel("com.hiddify.app/service.logs"); - late final ValueStream _connectionStatus; + late final ValueStream _status; @override Future init() async { loggy.debug("initializing"); - final status = - _connectionStatusChannel.receiveBroadcastStream().map(mapEventToStatus); - final alerts = - _alertsChannel.receiveBroadcastStream().map(mapEventToStatus); - _connectionStatus = - ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); - await _connectionStatus.first; + final status = _statusChannel.receiveBroadcastStream().map( + (event) { + return SingboxStatus.fromEvent(event); + }, + ); + final alerts = _alertsChannel.receiveBroadcastStream().map( + (event) { + return SingboxStatus.fromEvent(event); + }, + ); + _status = ValueConnectableStream(Rx.merge([status, alerts])).autoConnect(); + await _status.first; } @override TaskEither setup( - String baseDir, - String workingDir, - String tempDir, + Directories directories, bool debug, ) => TaskEither.of(unit); @override - TaskEither parseConfig( + TaskEither validateConfigByPath( String path, String tempPath, bool debug, @@ -61,7 +64,7 @@ class MobileSingboxService } @override - TaskEither changeConfigOptions(ConfigOptions options) { + TaskEither changeOptions(SingboxConfigOption options) { return TaskEither( () async { await _methodChannel.invokeMethod( @@ -74,7 +77,7 @@ class MobileSingboxService } @override - TaskEither generateConfig( + TaskEither generateFullConfigByPath( String path, ) { return TaskEither( @@ -92,13 +95,13 @@ class MobileSingboxService } @override - TaskEither start(String configPath, bool disableMemoryLimit) { + TaskEither start(String path, bool disableMemoryLimit) { return TaskEither( () async { loggy.debug("starting"); await _methodChannel.invokeMethod( "start", - {"path": configPath}, + {"path": path}, ); return right(unit); }, @@ -117,13 +120,13 @@ class MobileSingboxService } @override - TaskEither restart(String configPath, bool disableMemoryLimit) { + TaskEither restart(String path, bool disableMemoryLimit) { return TaskEither( () async { loggy.debug("restarting"); await _methodChannel.invokeMethod( "restart", - {"path": configPath}, + {"path": path}, ); return right(unit); }, @@ -131,13 +134,15 @@ class MobileSingboxService } @override - Stream watchOutbounds() { + Stream> watchOutbounds() { const channel = EventChannel("com.hiddify.app/groups"); loggy.debug("watching outbounds"); return channel.receiveBroadcastStream().map( (event) { if (event case String _) { - return event; + return (jsonDecode(event) as List).map((e) { + return SingboxOutboundGroup.fromJson(e as Map); + }).toList(); } loggy.error("[group client] unexpected type, msg: $event"); throw "invalid type"; @@ -146,11 +151,11 @@ class MobileSingboxService } @override - Stream watchConnectionStatus() => _connectionStatus; + Stream watchStatus() => _status; @override - Stream watchStats() { - // TODO: implement watchStatus + Stream watchStats() { + // TODO: implement watchStats return const Stream.empty(); } diff --git a/lib/singbox/service/singbox_service.dart b/lib/singbox/service/singbox_service.dart new file mode 100644 index 00000000..3ad2d8c5 --- /dev/null +++ b/lib/singbox/service/singbox_service.dart @@ -0,0 +1,60 @@ +import 'dart:io'; + +import 'package:fpdart/fpdart.dart'; +import 'package:hiddify/core/model/directories.dart'; +import 'package:hiddify/singbox/model/singbox_config_option.dart'; +import 'package:hiddify/singbox/model/singbox_outbound.dart'; +import 'package:hiddify/singbox/model/singbox_stats.dart'; +import 'package:hiddify/singbox/model/singbox_status.dart'; +import 'package:hiddify/singbox/service/ffi_singbox_service.dart'; +import 'package:hiddify/singbox/service/platform_singbox_service.dart'; + +abstract interface class SingboxService { + factory SingboxService() { + if (Platform.isAndroid || Platform.isIOS) { + return PlatformSingboxService(); + } else if (Platform.isLinux || Platform.isWindows || Platform.isMacOS) { + return FFISingboxService(); + } + throw Exception("unsupported platform"); + } + + Future init(); + + TaskEither setup( + Directories directories, + bool debug, + ); + + TaskEither validateConfigByPath( + String path, + String tempPath, + bool debug, + ); + + TaskEither changeOptions(SingboxConfigOption options); + + TaskEither generateFullConfigByPath( + String path, + ); + + TaskEither start(String path, bool disableMemoryLimit); + + TaskEither stop(); + + TaskEither restart(String path, bool disableMemoryLimit); + + Stream> watchOutbounds(); + + TaskEither selectOutbound(String groupTag, String outboundTag); + + TaskEither urlTest(String groupTag); + + Stream watchStatus(); + + Stream watchStats(); + + Stream> watchLogs(String path); + + TaskEither clearLogs(); +} diff --git a/lib/singbox/service/singbox_service_provider.dart b/lib/singbox/service/singbox_service_provider.dart new file mode 100644 index 00000000..569d7ff1 --- /dev/null +++ b/lib/singbox/service/singbox_service_provider.dart @@ -0,0 +1,9 @@ +import 'package:hiddify/singbox/service/singbox_service.dart'; +import 'package:riverpod_annotation/riverpod_annotation.dart'; + +part 'singbox_service_provider.g.dart'; + +@Riverpod(keepAlive: true) +SingboxService singboxService(SingboxServiceRef ref) { + return SingboxService(); +} diff --git a/lib/utils/link_parsers.dart b/lib/utils/link_parsers.dart index d82b7aff..b481564b 100644 --- a/lib/utils/link_parsers.dart +++ b/lib/utils/link_parsers.dart @@ -1,6 +1,6 @@ import 'dart:convert'; -import 'package:hiddify/domain/singbox/singbox.dart'; +import 'package:hiddify/singbox/model/singbox_proxy_type.dart'; import 'package:hiddify/utils/validators.dart'; typedef ProfileLink = ({String url, String name}); diff --git a/lib/utils/mutation_state.dart b/lib/utils/mutation_state.dart index b83f9a4c..53aab501 100644 --- a/lib/utils/mutation_state.dart +++ b/lib/utils/mutation_state.dart @@ -1,5 +1,5 @@ import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; part 'mutation_state.freezed.dart'; diff --git a/lib/utils/pref_notifier.dart b/lib/utils/pref_notifier.dart index 90e6dfd3..6d05cc2d 100644 --- a/lib/utils/pref_notifier.dart +++ b/lib/utils/pref_notifier.dart @@ -1,4 +1,4 @@ -import 'package:hiddify/data/data_providers.dart'; +import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -75,7 +75,7 @@ class PrefNotifier extends AutoDisposeNotifier with InfraLogger { final P Function(T)? _mapTo; late final Pref _pref = Pref( - ref.watch(sharedPreferencesProvider), + ref.watch(sharedPreferencesProvider).requireValue, _key, _defaultValue, mapFrom: _mapFrom, diff --git a/lib/utils/sentry_utils.dart b/lib/utils/sentry_utils.dart index 44971fd6..a8976a50 100644 --- a/lib/utils/sentry_utils.dart +++ b/lib/utils/sentry_utils.dart @@ -1,7 +1,7 @@ import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:hiddify/domain/failures.dart'; +import 'package:hiddify/core/model/failures.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; diff --git a/test/data/local/generated_migrations/schema.dart b/test/core/database/generated_migrations/schema.dart similarity index 100% rename from test/data/local/generated_migrations/schema.dart rename to test/core/database/generated_migrations/schema.dart diff --git a/test/data/local/generated_migrations/schema_v1.dart b/test/core/database/generated_migrations/schema_v1.dart similarity index 100% rename from test/data/local/generated_migrations/schema_v1.dart rename to test/core/database/generated_migrations/schema_v1.dart diff --git a/test/data/local/generated_migrations/schema_v2.dart b/test/core/database/generated_migrations/schema_v2.dart similarity index 100% rename from test/data/local/generated_migrations/schema_v2.dart rename to test/core/database/generated_migrations/schema_v2.dart diff --git a/test/data/local/generated_migrations/schema_v3.dart b/test/core/database/generated_migrations/schema_v3.dart similarity index 100% rename from test/data/local/generated_migrations/schema_v3.dart rename to test/core/database/generated_migrations/schema_v3.dart diff --git a/test/data/local/migrations_test.dart b/test/core/database/migrations_test.dart similarity index 95% rename from test/data/local/migrations_test.dart rename to test/core/database/migrations_test.dart index e184117a..3739b818 100644 --- a/test/data/local/migrations_test.dart +++ b/test/core/database/migrations_test.dart @@ -1,6 +1,6 @@ import 'package:drift_dev/api/migrations.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:hiddify/data/local/database.dart'; +import 'package:hiddify/core/database/app_database.dart'; import 'generated_migrations/schema.dart';