add basic routing options, auto update routing assets,use ruleset, remove geo assets

This commit is contained in:
hiddify-com
2024-07-04 21:04:44 +02:00
parent 93f4bbca32
commit c6d34e7455
27 changed files with 773 additions and 835 deletions

View File

@@ -190,8 +190,9 @@ ios-libs: #not tested
curl -L $(CORE_URL)/$(CORE_NAME)-ios.tar.gz | tar xz -C "$(IOS_OUT)" curl -L $(CORE_URL)/$(CORE_NAME)-ios.tar.gz | tar xz -C "$(IOS_OUT)"
get-geo-assets: get-geo-assets:
curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db echo ""
curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db # curl -L https://github.com/SagerNet/sing-geoip/releases/latest/download/geoip.db -o $(GEO_ASSETS_DIR)/geoip.db
# curl -L https://github.com/SagerNet/sing-geosite/releases/latest/download/geosite.db -o $(GEO_ASSETS_DIR)/geosite.db
build-headers: build-headers:
make -C libcore -f Makefile headers && mv $(BINDIR)/$(CORE_NAME)-headers.h $(BINDIR)/libcore.h make -C libcore -f Makefile headers && mv $(BINDIR)/$(CORE_NAME)-headers.h $(BINDIR)/libcore.h

View File

@@ -361,6 +361,7 @@
"warpConfigGenerated": "WARP Config Generated", "warpConfigGenerated": "WARP Config Generated",
"pageTitle": "Config Options", "pageTitle": "Config Options",
"logLevel": "Log Level", "logLevel": "Log Level",
"blockAds": "Block Advertisements",
"resolveDestination": "Resolve Destination", "resolveDestination": "Resolve Destination",
"ipv6Mode": "IPv6 Route", "ipv6Mode": "IPv6 Route",
"ipv6Modes": { "ipv6Modes": {

View File

@@ -361,6 +361,7 @@
"warpConfigGenerated": "پیکربندی WARP ایجاد شد", "warpConfigGenerated": "پیکربندی WARP ایجاد شد",
"pageTitle": "تنظیمات پیکربندی", "pageTitle": "تنظیمات پیکربندی",
"logLevel": "سطح گزارش", "logLevel": "سطح گزارش",
"blockAds": "مسدود سازی تبلیغات",
"resolveDestination": "جایگذاری مقصد", "resolveDestination": "جایگذاری مقصد",
"ipv6Mode": "مسیریابی IPv6", "ipv6Mode": "مسیریابی IPv6",
"ipv6Modes": { "ipv6Modes": {

View File

@@ -17,7 +17,7 @@ import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/features/app/widget/app.dart'; import 'package:hiddify/features/app/widget/app.dart';
import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart'; import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart';
import 'package:hiddify/features/deep_link/notifier/deep_link_notifier.dart'; import 'package:hiddify/features/deep_link/notifier/deep_link_notifier.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
import 'package:hiddify/features/log/data/log_data_providers.dart'; import 'package:hiddify/features/log/data/log_data_providers.dart';
import 'package:hiddify/features/profile/data/profile_data_providers.dart'; import 'package:hiddify/features/profile/data/profile_data_providers.dart';
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
@@ -36,8 +36,7 @@ Future<void> lazyBootstrap(
LoggerController.preInit(); LoggerController.preInit();
FlutterError.onError = Logger.logFlutterError; FlutterError.onError = Logger.logFlutterError;
WidgetsBinding.instance.platformDispatcher.onError = WidgetsBinding.instance.platformDispatcher.onError = Logger.logPlatformDispatcherError;
Logger.logPlatformDispatcherError;
final stopWatch = Stopwatch()..start(); final stopWatch = Stopwatch()..start();
@@ -62,14 +61,11 @@ Future<void> lazyBootstrap(
() => container.read(sharedPreferencesProvider.future), () => container.read(sharedPreferencesProvider.future),
); );
final enableAnalytics = final enableAnalytics = await container.read(analyticsControllerProvider.future);
await container.read(analyticsControllerProvider.future);
if (enableAnalytics) { if (enableAnalytics) {
await _init( await _init(
"analytics", "analytics",
() => container () => container.read(analyticsControllerProvider.notifier).enableAnalytics(),
.read(analyticsControllerProvider.notifier)
.enableAnalytics(),
); );
} }
@@ -78,8 +74,7 @@ Future<void> lazyBootstrap(
() async { () async {
try { try {
await PreferencesMigration( await PreferencesMigration(
sharedPreferences: sharedPreferences: container.read(sharedPreferencesProvider).requireValue,
container.read(sharedPreferencesProvider).requireValue,
).migrate(); ).migrate();
} catch (e, stackTrace) { } catch (e, stackTrace) {
Logger.bootstrap.error("preferences migration failed", e, stackTrace); Logger.bootstrap.error("preferences migration failed", e, stackTrace);
@@ -99,8 +94,7 @@ Future<void> lazyBootstrap(
); );
final silentStart = container.read(Preferences.silentStart); final silentStart = container.read(Preferences.silentStart);
Logger.bootstrap Logger.bootstrap.debug("silent start [${silentStart ? "Enabled" : "Disabled"}]");
.debug("silent start [${silentStart ? "Enabled" : "Disabled"}]");
if (!silentStart) { if (!silentStart) {
await container.read(windowNotifierProvider.notifier).open(focus: false); await container.read(windowNotifierProvider.notifier).open(focus: false);
} else { } else {
@@ -118,10 +112,7 @@ Future<void> lazyBootstrap(
await _init("logger controller", () => LoggerController.postInit(debug)); await _init("logger controller", () => LoggerController.postInit(debug));
Logger.bootstrap.info(appInfo.format()); Logger.bootstrap.info(appInfo.format());
await _init(
"geo assets repository",
() => container.read(geoAssetRepositoryProvider.future),
);
await _init( await _init(
"profile repository", "profile repository",
() => container.read(profileRepositoryProvider.future), () => container.read(profileRepositoryProvider.future),
@@ -180,13 +171,10 @@ Future<T> _init<T>(
}) async { }) async {
final stopWatch = Stopwatch()..start(); final stopWatch = Stopwatch()..start();
Logger.bootstrap.info("initializing [$name]"); Logger.bootstrap.info("initializing [$name]");
Future<T> func() => timeout != null Future<T> func() => timeout != null ? initializer().timeout(Duration(milliseconds: timeout)) : initializer();
? initializer().timeout(Duration(milliseconds: timeout))
: initializer();
try { try {
final result = await func(); final result = await func();
Logger.bootstrap Logger.bootstrap.debug("[$name] initialized in ${stopWatch.elapsedMilliseconds}ms");
.debug("[$name] initialized in ${stopWatch.elapsedMilliseconds}ms");
return result; return result;
} catch (e, stackTrace) { } catch (e, stackTrace) {
Logger.bootstrap.error("[$name] error initializing", e, stackTrace); Logger.bootstrap.error("[$name] error initializing", e, stackTrace);

View File

@@ -6,8 +6,8 @@ import 'package:hiddify/core/database/connection/database_connection.dart';
import 'package:hiddify/core/database/converters/duration_converter.dart'; import 'package:hiddify/core/database/converters/duration_converter.dart';
import 'package:hiddify/core/database/schema_versions.dart'; import 'package:hiddify/core/database/schema_versions.dart';
import 'package:hiddify/core/database/tables/database_tables.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/data/geo_asset_data_mapper.dart';
import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; // import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart';
import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hiddify/utils/custom_loggers.dart';
@@ -60,11 +60,11 @@ class AppDatabase extends _$AppDatabase with InfraLogger {
Future<void> _prePopulateGeoAssets() async { Future<void> _prePopulateGeoAssets() async {
loggy.debug("populating default geo assets"); loggy.debug("populating default geo assets");
await transaction(() async { await transaction(() async {
final geoAssets = defaultGeoAssets.map((e) => e.toEntry()); // final geoAssets = defaultGeoAssets.map((e) => e.toEntry());
for (final geoAsset in geoAssets) { // for (final geoAsset in geoAssets) {
await into(geoAssetEntries) // await into(geoAssetEntries)
.insert(geoAsset, mode: InsertMode.insertOrIgnore); // .insert(geoAsset, mode: InsertMode.insertOrIgnore);
} // }
}); });
} }
} }

View File

@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart';
import 'package:hiddify/core/model/environment.dart'; import 'package:hiddify/core/model/environment.dart';
import 'package:hiddify/core/model/region.dart'; // import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/core/utils/preferences_utils.dart';
import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart'; import 'package:hiddify/features/per_app_proxy/model/per_app_proxy_mode.dart';
@@ -19,13 +19,6 @@ abstract class Preferences {
overrideValue: _debugIntroPage && kDebugMode ? false : null, overrideValue: _debugIntroPage && kDebugMode ? false : null,
); );
static final region = PreferencesNotifier.create<Region, String>(
"region",
Region.other,
mapFrom: Region.values.byName,
mapTo: (value) => value.name,
);
static final silentStart = PreferencesNotifier.create<bool, bool>( static final silentStart = PreferencesNotifier.create<bool, bool>(
"silent_start", "silent_start",
false, false,
@@ -37,8 +30,7 @@ abstract class Preferences {
PlatformUtils.isDesktop, PlatformUtils.isDesktop,
); );
static final perAppProxyMode = static final perAppProxyMode = PreferencesNotifier.create<PerAppProxyMode, String>(
PreferencesNotifier.create<PerAppProxyMode, String>(
"per_app_proxy_mode", "per_app_proxy_mode",
PerAppProxyMode.off, PerAppProxyMode.off,
mapFrom: PerAppProxyMode.values.byName, mapFrom: PerAppProxyMode.values.byName,
@@ -103,10 +95,7 @@ class PerAppProxyList extends _$PerAppProxyList {
); );
@override @override
List<String> build() => List<String> build() => ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include ? _include.read() : _exclude.read();
ref.watch(Preferences.perAppProxyMode) == PerAppProxyMode.include
? _include.read()
: _exclude.read();
Future<void> update(List<String> value) { Future<void> update(List<String> value) {
state = value; state = value;

View File

@@ -4,7 +4,7 @@ import 'package:hiddify/core/router/app_router.dart';
import 'package:hiddify/features/common/adaptive_root_scaffold.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/config_option/overview/config_options_page.dart';
import 'package:hiddify/features/config_option/widget/quick_settings_modal.dart'; import 'package:hiddify/features/config_option/widget/quick_settings_modal.dart';
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_page.dart';
import 'package:hiddify/features/home/widget/home_page.dart'; import 'package:hiddify/features/home/widget/home_page.dart';
import 'package:hiddify/features/intro/widget/intro_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/log/overview/logs_overview_page.dart';
@@ -19,8 +19,7 @@ import 'package:hiddify/utils/utils.dart';
part 'routes.g.dart'; part 'routes.g.dart';
GlobalKey<NavigatorState>? _dynamicRootKey = GlobalKey<NavigatorState>? _dynamicRootKey = useMobileRouter ? rootNavigatorKey : null;
useMobileRouter ? rootNavigatorKey : null;
@TypedShellRoute<MobileWrapperRoute>( @TypedShellRoute<MobileWrapperRoute>(
routes: [ routes: [
@@ -60,10 +59,6 @@ GlobalKey<NavigatorState>? _dynamicRootKey =
path: "per-app-proxy", path: "per-app-proxy",
name: PerAppProxyRoute.name, name: PerAppProxyRoute.name,
), ),
TypedGoRoute<GeoAssetsRoute>(
path: "routing-assets",
name: GeoAssetsRoute.name,
),
], ],
), ),
TypedGoRoute<LogsOverviewRoute>( TypedGoRoute<LogsOverviewRoute>(
@@ -130,12 +125,7 @@ class MobileWrapperRoute extends ShellRouteData {
TypedGoRoute<SettingsRoute>( TypedGoRoute<SettingsRoute>(
path: "/settings", path: "/settings",
name: SettingsRoute.name, name: SettingsRoute.name,
routes: [ routes: [],
TypedGoRoute<GeoAssetsRoute>(
path: "routing-assets",
name: GeoAssetsRoute.name,
),
],
), ),
TypedGoRoute<LogsOverviewRoute>( TypedGoRoute<LogsOverviewRoute>(
path: "/logs", path: "/logs",
@@ -229,8 +219,7 @@ class ProfilesOverviewRoute extends GoRouteData {
Page<void> buildPage(BuildContext context, GoRouterState state) { Page<void> buildPage(BuildContext context, GoRouterState state) {
return BottomSheetPage( return BottomSheetPage(
name: name, name: name,
builder: (controller) => builder: (controller) => ProfilesOverviewModal(scrollController: controller),
ProfilesOverviewModal(scrollController: controller),
); );
} }
} }
@@ -358,28 +347,6 @@ class PerAppProxyRoute extends GoRouteData {
} }
} }
class GeoAssetsRoute extends GoRouteData {
const GeoAssetsRoute();
static const name = "Routing Assets";
static final GlobalKey<NavigatorState>? $parentNavigatorKey = _dynamicRootKey;
@override
Page<void> buildPage(BuildContext context, GoRouterState state) {
if (useMobileRouter) {
return const MaterialPage(
name: name,
child: GeoAssetsOverviewPage(),
);
}
return const MaterialPage(
fullscreenDialog: true,
name: name,
child: GeoAssetsOverviewPage(),
);
}
}
class AboutRoute extends GoRouteData { class AboutRoute extends GoRouteData {
const AboutRoute(); const AboutRoute();
static const name = "About"; static const name = "About";

View File

@@ -39,36 +39,37 @@ class App extends HookConsumerWidget with PresLogger {
return WindowWrapper( return WindowWrapper(
TrayWrapper( TrayWrapper(
ShortcutWrapper( ShortcutWrapper(
ConnectionWrapper(DynamicColorBuilder( ConnectionWrapper(
builder: DynamicColorBuilder(
(ColorScheme? lightColorScheme, ColorScheme? darkColorScheme) { builder: (ColorScheme? lightColorScheme, ColorScheme? darkColorScheme) {
return MaterialApp.router( return MaterialApp.router(
routerConfig: router, routerConfig: router,
locale: locale.flutterLocale, locale: locale.flutterLocale,
supportedLocales: AppLocaleUtils.supportedLocales, supportedLocales: AppLocaleUtils.supportedLocales,
localizationsDelegates: GlobalMaterialLocalizations.delegates, localizationsDelegates: GlobalMaterialLocalizations.delegates,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
themeMode: themeMode.flutterThemeMode, themeMode: themeMode.flutterThemeMode,
theme: theme.lightTheme(lightColorScheme), theme: theme.lightTheme(lightColorScheme),
darkTheme: theme.darkTheme(darkColorScheme), darkTheme: theme.darkTheme(darkColorScheme),
title: Constants.appName, title: Constants.appName,
builder: (context, child) { builder: (context, child) {
child = UpgradeAlert( child = UpgradeAlert(
upgrader: upgrader, upgrader: upgrader,
navigatorKey: router.routerDelegate.navigatorKey, navigatorKey: router.routerDelegate.navigatorKey,
child: child ?? const SizedBox(), child: child ?? const SizedBox(),
);
if (kDebugMode && _debugAccessibility) {
return AccessibilityTools(
checkFontOverflows: true,
child: child,
); );
} if (kDebugMode && _debugAccessibility) {
return child; return AccessibilityTools(
}, checkFontOverflows: true,
); child: child,
}, );
)), }
return child;
},
);
},
),
),
), ),
), ),
); );

View File

@@ -6,6 +6,8 @@ import 'package:hiddify/core/localization/locale_preferences.dart';
import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.dart';
import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
class LocalePrefTile extends HookConsumerWidget { class LocalePrefTile extends HookConsumerWidget {
@@ -41,9 +43,7 @@ class LocalePrefTile extends HookConsumerWidget {
}, },
); );
if (selectedLocale != null) { if (selectedLocale != null) {
await ref await ref.read(localePreferencesProvider.notifier).changeLocale(selectedLocale);
.read(localePreferencesProvider.notifier)
.changeLocale(selectedLocale);
} }
}, },
); );
@@ -57,7 +57,7 @@ class RegionPrefTile extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); final t = ref.watch(translationsProvider);
final region = ref.watch(Preferences.region); final region = ref.watch(ConfigOptions.region);
return ListTile( return ListTile(
title: Text(t.settings.general.region), title: Text(t.settings.general.region),
@@ -83,7 +83,16 @@ class RegionPrefTile extends HookConsumerWidget {
}, },
); );
if (selectedRegion != null) { if (selectedRegion != null) {
await ref.read(Preferences.region.notifier).update(selectedRegion); // await ref.read(Preferences.region.notifier).update(selectedRegion);
await ref.watch(ConfigOptions.region.notifier).update(selectedRegion);
// await ref.read(configOptionNotifierProvider.notifier).build();
// await ref.watch(ConfigOptions.resolveDestination.notifier).update(!ref.watch(ConfigOptions.resolveDestination.notifier).raw());
//for reload config
// final tmp = ref.watch(ConfigOptions.resolveDestination.notifier).raw();
// await ref.watch(ConfigOptions.resolveDestination.notifier).update(!tmp);
// await ref.watch(ConfigOptions.resolveDestination.notifier).update(tmp);
//TODO: fix it
} }
}, },
); );
@@ -117,13 +126,9 @@ class EnableAnalyticsPrefTile extends HookConsumerWidget {
return onChanged!(value); return onChanged!(value);
} }
if (enabled) { if (enabled) {
await ref await ref.read(analyticsControllerProvider.notifier).disableAnalytics();
.read(analyticsControllerProvider.notifier)
.disableAnalytics();
} else { } else {
await ref await ref.read(analyticsControllerProvider.notifier).enableAnalytics();
.read(analyticsControllerProvider.notifier)
.enableAnalytics();
} }
}, },
); );

View File

@@ -1,6 +1,6 @@
import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart';
import 'package:hiddify/features/config_option/data/config_option_repository.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'; import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'config_option_data_providers.g.dart'; part 'config_option_data_providers.g.dart';
@@ -12,7 +12,5 @@ ConfigOptionRepository configOptionRepository(
return ConfigOptionRepository( return ConfigOptionRepository(
preferences: ref.watch(sharedPreferencesProvider).requireValue, preferences: ref.watch(sharedPreferencesProvider).requireValue,
getConfigOptions: () => ref.read(ConfigOptions.singboxConfigOptions.future), getConfigOptions: () => ref.read(ConfigOptions.singboxConfigOptions.future),
geoAssetRepository: ref.watch(geoAssetRepositoryProvider).requireValue,
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
); );
} }

View File

@@ -3,13 +3,12 @@ import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/model/optional_range.dart';
import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/core/utils/exception_handler.dart';
import 'package:hiddify/core/utils/json_converters.dart'; import 'package:hiddify/core/utils/json_converters.dart';
import 'package:hiddify/core/utils/preferences_utils.dart'; import 'package:hiddify/core/utils/preferences_utils.dart';
import 'package:hiddify/features/config_option/model/config_option_failure.dart'; import 'package:hiddify/features/config_option/model/config_option_failure.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.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/features/log/model/log_level.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_config_enum.dart';
import 'package:hiddify/singbox/model/singbox_config_option.dart'; import 'package:hiddify/singbox/model/singbox_config_option.dart';
@@ -26,6 +25,17 @@ abstract class ConfigOptions {
mapTo: (value) => value.key, mapTo: (value) => value.key,
); );
static final region = PreferencesNotifier.create<Region, String>(
"region",
Region.other,
mapFrom: Region.values.byName,
mapTo: (value) => value.name,
);
static final blockAds = PreferencesNotifier.create<bool, bool>(
"block-ads",
false,
);
static final logLevel = PreferencesNotifier.create<LogLevel, String>( static final logLevel = PreferencesNotifier.create<LogLevel, String>(
"log-level", "log-level",
LogLevel.warn, LogLevel.warn,
@@ -305,6 +315,8 @@ abstract class ConfigOptions {
}; };
static final Map<String, StateNotifierProvider<PreferencesNotifier, dynamic>> preferences = { static final Map<String, StateNotifierProvider<PreferencesNotifier, dynamic>> preferences = {
"region": region,
"block-ads": blockAds,
"service-mode": serviceMode, "service-mode": serviceMode,
"log-level": logLevel, "log-level": logLevel,
"resolve-destination": resolveDestination, "resolve-destination": resolveDestination,
@@ -359,44 +371,46 @@ abstract class ConfigOptions {
static final singboxConfigOptions = FutureProvider<SingboxConfigOption>( static final singboxConfigOptions = FutureProvider<SingboxConfigOption>(
(ref) async { (ref) async {
final region = ref.watch(Preferences.region); // final region = ref.watch(Preferences.region);
final rules = switch (region) { final rules = <SingboxRule>[];
Region.ir => [ // final rules = switch (region) {
const SingboxRule( // Region.ir => [
domains: "domain:.ir,geosite:ir", // const SingboxRule(
ip: "geoip:ir", // domains: "domain:.ir,geosite:ir",
outbound: RuleOutbound.bypass, // ip: "geoip:ir",
), // outbound: RuleOutbound.bypass,
], // ),
Region.cn => [ // ],
const SingboxRule( // Region.cn => [
domains: "domain:.cn,geosite:cn", // const SingboxRule(
ip: "geoip:cn", // domains: "domain:.cn,geosite:cn",
outbound: RuleOutbound.bypass, // ip: "geoip:cn",
), // outbound: RuleOutbound.bypass,
], // ),
Region.ru => [ // ],
const SingboxRule( // Region.ru => [
domains: "domain:.ru", // const SingboxRule(
ip: "geoip:ru", // domains: "domain:.ru",
outbound: RuleOutbound.bypass, // ip: "geoip:ru",
), // outbound: RuleOutbound.bypass,
], // ),
Region.af => [ // ],
const SingboxRule( // Region.af => [
domains: "domain:.af,geosite:af", // const SingboxRule(
ip: "geoip:af", // domains: "domain:.af,geosite:af",
outbound: RuleOutbound.bypass, // ip: "geoip:af",
), // outbound: RuleOutbound.bypass,
], // ),
_ => <SingboxRule>[], // ],
}; // _ => <SingboxRule>[],
// };
final geoAssetsRepo = await ref.watch(geoAssetRepositoryProvider.future);
final geoAssets = await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run();
final mode = ref.watch(serviceMode); final mode = ref.watch(serviceMode);
// final reg = ref.watch(Preferences.region.notifier).raw();
return SingboxConfigOption( return SingboxConfigOption(
region: ref.watch(region).name,
blockAds: ref.watch(blockAds),
executeConfigAsIs: false, executeConfigAsIs: false,
logLevel: ref.watch(logLevel), logLevel: ref.watch(logLevel),
resolveDestination: ref.watch(resolveDestination), resolveDestination: ref.watch(resolveDestination),
@@ -461,14 +475,14 @@ abstract class ConfigOptions {
noise: ref.watch(warpNoise), noise: ref.watch(warpNoise),
noiseDelay: ref.watch(warpNoiseDelay), noiseDelay: ref.watch(warpNoiseDelay),
), ),
geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath( // geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath(
geoAssets.geoip.providerName, // geoAssets.geoip.providerName,
geoAssets.geoip.fileName, // geoAssets.geoip.fileName,
), // ),
geositePath: ref.watch(geoAssetPathResolverProvider).relativePath( // geositePath: ref.watch(geoAssetPathResolverProvider).relativePath(
geoAssets.geosite.providerName, // geoAssets.geosite.providerName,
geoAssets.geosite.fileName, // geoAssets.geosite.fileName,
), // ),
rules: rules, rules: rules,
); );
}, },
@@ -479,14 +493,10 @@ class ConfigOptionRepository with ExceptionHandler, InfraLogger {
ConfigOptionRepository({ ConfigOptionRepository({
required this.preferences, required this.preferences,
required this.getConfigOptions, required this.getConfigOptions,
required this.geoAssetRepository,
required this.geoAssetPathResolver,
}); });
final SharedPreferences preferences; final SharedPreferences preferences;
final Future<SingboxConfigOption> Function() getConfigOptions; final Future<SingboxConfigOption> Function() getConfigOptions;
final GeoAssetRepository geoAssetRepository;
final GeoAssetPathResolver geoAssetPathResolver;
TaskEither<ConfigOptionFailure, SingboxConfigOption> getFullSingboxConfigOption() { TaskEither<ConfigOptionFailure, SingboxConfigOption> getFullSingboxConfigOption() {
return exceptionHandler( return exceptionHandler(

View File

@@ -15,16 +15,13 @@ class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
@override @override
Future<bool> build() async { Future<bool> build() async {
final serviceRunning = await ref.watch(serviceRunningProvider.future); final serviceRunning = await ref.watch(serviceRunningProvider.future);
final serviceSingboxOptions = final serviceSingboxOptions = ref.read(connectionRepositoryProvider).configOptionsSnapshot;
ref.read(connectionRepositoryProvider).configOptionsSnapshot;
ref.listen( ref.listen(
ConfigOptions.singboxConfigOptions, ConfigOptions.singboxConfigOptions,
(previous, next) async { (previous, next) async {
if (!serviceRunning || serviceSingboxOptions == null) return; if (!serviceRunning || serviceSingboxOptions == null) return;
if (next case AsyncData(:final value) when next != previous) { if (next case AsyncData(:final value) when next != previous) {
if (_lastUpdate == null || if (_lastUpdate == null || DateTime.now().difference(_lastUpdate!) > const Duration(milliseconds: 100)) {
DateTime.now().difference(_lastUpdate!) >
const Duration(milliseconds: 100)) {
_lastUpdate = DateTime.now(); _lastUpdate = DateTime.now();
state = AsyncData(value != serviceSingboxOptions); state = AsyncData(value != serviceSingboxOptions);
} }
@@ -63,8 +60,7 @@ class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
Future<bool> importFromClipboard() async { Future<bool> importFromClipboard() async {
try { try {
final input = final input = await Clipboard.getData("text/plain").then((value) => value?.text);
await Clipboard.getData("text/plain").then((value) => value?.text);
if (input == null) return false; if (input == null) return false;
if (jsonDecode(input) case final Map<String, dynamic> map) { if (jsonDecode(input) case final Map<String, dynamic> map) {
for (final option in ConfigOptions.preferences.entries) { for (final option in ConfigOptions.preferences.entries) {

View File

@@ -4,8 +4,8 @@ import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart'; import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/model/optional_range.dart';
import 'package:hiddify/core/model/region.dart';
import 'package:hiddify/core/notification/in_app_notification_controller.dart'; import 'package:hiddify/core/notification/in_app_notification_controller.dart';
import 'package:hiddify/core/preferences/general_preferences.dart';
import 'package:hiddify/core/widget/adaptive_icon.dart'; import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/core/widget/tip_card.dart'; import 'package:hiddify/core/widget/tip_card.dart';
import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/confirmation_dialogs.dart';
@@ -140,6 +140,18 @@ class ConfigOptionsPage extends HookConsumerWidget {
), ),
const SettingsDivider(), const SettingsDivider(),
SettingsSection(t.config.section.route), SettingsSection(t.config.section.route),
ChoicePreferenceWidget(
selected: ref.watch(ConfigOptions.region),
preferences: ref.watch(ConfigOptions.region.notifier),
choices: Region.values,
title: t.settings.general.region,
presentChoice: (value) => value.present(t),
),
SwitchListTile(
title: Text(experimental(t.config.blockAds)),
value: ref.watch(ConfigOptions.blockAds),
onChanged: ref.watch(ConfigOptions.blockAds.notifier).update,
),
SwitchListTile( SwitchListTile(
title: Text(experimental(t.config.bypassLan)), title: Text(experimental(t.config.bypassLan)),
value: ref.watch(ConfigOptions.bypassLan), value: ref.watch(ConfigOptions.bypassLan),

View File

@@ -2,7 +2,7 @@ import 'package:hiddify/core/directories/directories_provider.dart';
import 'package:hiddify/features/config_option/data/config_option_data_providers.dart'; 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_platform_source.dart';
import 'package:hiddify/features/connection/data/connection_repository.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/features/profile/data/profile_data_providers.dart';
import 'package:hiddify/singbox/service/singbox_service_provider.dart'; import 'package:hiddify/singbox/service/singbox_service_provider.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@@ -19,6 +19,5 @@ ConnectionRepository connectionRepository(
singbox: ref.watch(singboxServiceProvider), singbox: ref.watch(singboxServiceProvider),
platformSource: ConnectionPlatformSourceImpl(), platformSource: ConnectionPlatformSourceImpl(),
profilePathResolver: ref.watch(profilePathResolverProvider), profilePathResolver: ref.watch(profilePathResolverProvider),
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
); );
} }

View File

@@ -1,5 +1,3 @@
import 'dart:io';
import 'package:fpdart/fpdart.dart'; import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/model/directories.dart'; import 'package:hiddify/core/model/directories.dart';
import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/core/utils/exception_handler.dart';
@@ -7,7 +5,7 @@ import 'package:hiddify/features/config_option/data/config_option_repository.dar
import 'package:hiddify/features/connection/data/connection_platform_source.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_failure.dart';
import 'package:hiddify/features/connection/model/connection_status.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/features/profile/data/profile_path_resolver.dart';
import 'package:hiddify/singbox/model/singbox_config_option.dart'; import 'package:hiddify/singbox/model/singbox_config_option.dart';
import 'package:hiddify/singbox/model/singbox_status.dart'; import 'package:hiddify/singbox/model/singbox_status.dart';
@@ -33,16 +31,13 @@ abstract interface class ConnectionRepository {
); );
} }
class ConnectionRepositoryImpl class ConnectionRepositoryImpl with ExceptionHandler, InfraLogger implements ConnectionRepository {
with ExceptionHandler, InfraLogger
implements ConnectionRepository {
ConnectionRepositoryImpl({ ConnectionRepositoryImpl({
required this.directories, required this.directories,
required this.singbox, required this.singbox,
required this.platformSource, required this.platformSource,
required this.configOptionRepository, required this.configOptionRepository,
required this.profilePathResolver, required this.profilePathResolver,
required this.geoAssetPathResolver,
}); });
final Directories directories; final Directories directories;
@@ -50,7 +45,6 @@ class ConnectionRepositoryImpl
final ConnectionPlatformSource platformSource; final ConnectionPlatformSource platformSource;
final ConfigOptionRepository configOptionRepository; final ConfigOptionRepository configOptionRepository;
final ProfilePathResolver profilePathResolver; final ProfilePathResolver profilePathResolver;
final GeoAssetPathResolver geoAssetPathResolver;
SingboxConfigOption? _configOptionsSnapshot; SingboxConfigOption? _configOptionsSnapshot;
@override @override
@@ -64,16 +58,10 @@ class ConnectionRepositoryImpl
(event) => switch (event) { (event) => switch (event) {
SingboxStopped(:final alert?, :final message) => Disconnected( SingboxStopped(:final alert?, :final message) => Disconnected(
switch (alert) { switch (alert) {
SingboxAlert.emptyConfiguration => SingboxAlert.emptyConfiguration => ConnectionFailure.invalidConfig(message),
ConnectionFailure.invalidConfig(message), SingboxAlert.requestNotificationPermission => ConnectionFailure.missingNotificationPermission(message),
SingboxAlert.requestNotificationPermission => SingboxAlert.requestVPNPermission => ConnectionFailure.missingVpnPermission(message),
ConnectionFailure.missingNotificationPermission(message), SingboxAlert.startCommandServer || SingboxAlert.createService || SingboxAlert.startService => ConnectionFailure.unexpected(message),
SingboxAlert.requestVPNPermission =>
ConnectionFailure.missingVpnPermission(message),
SingboxAlert.startCommandServer ||
SingboxAlert.createService ||
SingboxAlert.startService =>
ConnectionFailure.unexpected(message),
}, },
), ),
SingboxStopped() => const Disconnected(), SingboxStopped() => const Disconnected(),
@@ -89,21 +77,19 @@ class ConnectionRepositoryImpl
return TaskEither<ConnectionFailure, SingboxConfigOption>.Do( return TaskEither<ConnectionFailure, SingboxConfigOption>.Do(
($) async { ($) async {
final options = await $( final options = await $(
configOptionRepository configOptionRepository.getFullSingboxConfigOption().mapLeft((l) => const InvalidConfigOption()),
.getFullSingboxConfigOption()
.mapLeft((l) => const InvalidConfigOption()),
); );
return $( return $(
TaskEither( TaskEither(
() async { () async {
final geoip = geoAssetPathResolver.resolvePath(options.geoipPath); // final geoip = geoAssetPathResolver.resolvePath(options.geoipPath);
final geosite = // final geosite =
geoAssetPathResolver.resolvePath(options.geositePath); // geoAssetPathResolver.resolvePath(options.geositePath);
if (!await File(geoip).exists() || // if (!await File(geoip).exists() ||
!await File(geosite).exists()) { // !await File(geosite).exists()) {
return left(const ConnectionFailure.missingGeoAssets()); // return left(const ConnectionFailure.missingGeoAssets());
} // }
return right(options); return right(options);
}, },
), ),
@@ -119,10 +105,7 @@ class ConnectionRepositoryImpl
return exceptionHandler( return exceptionHandler(
() { () {
_configOptionsSnapshot = options; _configOptionsSnapshot = options;
return singbox return singbox.changeOptions(options).mapLeft(InvalidConfigOption.new).run();
.changeOptions(options)
.mapLeft(InvalidConfigOption.new)
.run();
}, },
UnexpectedConnectionFailure.new, UnexpectedConnectionFailure.new,
); );

View File

@@ -8,10 +8,10 @@ import 'package:hiddify/features/connection/model/connection_status.dart';
import 'package:hiddify/features/profile/model/profile_entity.dart'; import 'package:hiddify/features/profile/model/profile_entity.dart';
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
import 'package:hiddify/utils/utils.dart'; import 'package:hiddify/utils/utils.dart';
import 'package:in_app_review/in_app_review.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:in_app_review/in_app_review.dart';
part 'connection_notifier.g.dart'; part 'connection_notifier.g.dart';
@@ -32,8 +32,7 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
if (next case AsyncData(value: final Connected _)) { if (next case AsyncData(value: final Connected _)) {
await ref.read(hapticServiceProvider.notifier).heavyImpact(); await ref.read(hapticServiceProvider.notifier).heavyImpact();
if (Platform.isAndroid && if (Platform.isAndroid && !ref.read(Preferences.storeReviewedByUser)) {
!ref.read(Preferences.storeReviewedByUser)) {
if (await InAppReview.instance.isAvailable()) { if (await InAppReview.instance.isAvailable()) {
InAppReview.instance.requestReview(); InAppReview.instance.requestReview();
ref.read(Preferences.storeReviewedByUser.notifier).update(true); ref.read(Preferences.storeReviewedByUser.notifier).update(true);
@@ -55,16 +54,14 @@ class ConnectionNotifier extends _$ConnectionNotifier with AppLogger {
}, },
); );
yield* _connectionRepo.watchConnectionStatus().doOnData((event) { yield* _connectionRepo.watchConnectionStatus().doOnData((event) {
if (event case Disconnected(connectionFailure: final _?) if (event case Disconnected(connectionFailure: final _?) when PlatformUtils.isDesktop) {
when PlatformUtils.isDesktop) {
ref.read(Preferences.startedByUser.notifier).update(false); ref.read(Preferences.startedByUser.notifier).update(false);
} }
loggy.info("connection status: ${event.format()}"); loggy.info("connection status: ${event.format()}");
}); });
} }
ConnectionRepository get _connectionRepo => ConnectionRepository get _connectionRepo => ref.read(connectionRepositoryProvider);
ref.read(connectionRepositoryProvider);
Future<void> mayConnect() async { Future<void> mayConnect() async {
if (state case AsyncData(:final value)) { if (state case AsyncData(:final value)) {

View File

@@ -1,32 +1,32 @@
import 'package:hiddify/core/database/database_provider.dart'; // import 'package:hiddify/core/database/database_provider.dart';
import 'package:hiddify/core/directories/directories_provider.dart'; // import 'package:hiddify/core/directories/directories_provider.dart';
import 'package:hiddify/core/http_client/http_client_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_data_source.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_path_resolver.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/features/geo_asset/data/geo_asset_repository.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; // import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'geo_asset_data_providers.g.dart'; // part 'geo_asset_data_providers.g.dart';
@Riverpod(keepAlive: true) // @Riverpod(keepAlive: true)
Future<GeoAssetRepository> geoAssetRepository(GeoAssetRepositoryRef ref) async { // Future<GeoAssetRepository> geoAssetRepository(GeoAssetRepositoryRef ref) async {
final repo = GeoAssetRepositoryImpl( // final repo = GeoAssetRepositoryImpl(
geoAssetDataSource: ref.watch(geoAssetDataSourceProvider), // geoAssetDataSource: ref.watch(geoAssetDataSourceProvider),
geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider), // geoAssetPathResolver: ref.watch(geoAssetPathResolverProvider),
httpClient: ref.watch(httpClientProvider), // httpClient: ref.watch(httpClientProvider),
); // );
await repo.init().getOrElse((l) => throw l).run(); // await repo.init().getOrElse((l) => throw l).run();
return repo; // return repo;
} // }
@Riverpod(keepAlive: true) // @Riverpod(keepAlive: true)
GeoAssetDataSource geoAssetDataSource(GeoAssetDataSourceRef ref) { // GeoAssetDataSource geoAssetDataSource(GeoAssetDataSourceRef ref) {
return GeoAssetsDao(ref.watch(appDatabaseProvider)); // return GeoAssetsDao(ref.watch(appDatabaseProvider));
} // }
@Riverpod(keepAlive: true) // @Riverpod(keepAlive: true)
GeoAssetPathResolver geoAssetPathResolver(GeoAssetPathResolverRef ref) { // GeoAssetPathResolver geoAssetPathResolver(GeoAssetPathResolverRef ref) {
return GeoAssetPathResolver( // return GeoAssetPathResolver(
ref.watch(appDirectoriesProvider).requireValue.workingDir, // ref.watch(appDirectoriesProvider).requireValue.workingDir,
); // );
} // }

View File

@@ -1,232 +1,232 @@
import 'dart:io'; // import 'dart:io';
import 'package:dartx/dartx_io.dart'; // import 'package:dartx/dartx_io.dart';
import 'package:drift/drift.dart'; // import 'package:drift/drift.dart';
import 'package:flutter/services.dart'; // import 'package:flutter/services.dart';
import 'package:fpdart/fpdart.dart'; // import 'package:fpdart/fpdart.dart';
import 'package:hiddify/core/database/app_database.dart'; // import 'package:hiddify/core/database/app_database.dart';
import 'package:hiddify/core/http_client/dio_http_client.dart'; // import 'package:hiddify/core/http_client/dio_http_client.dart';
import 'package:hiddify/core/utils/exception_handler.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_mapper.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_source.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_path_resolver.dart';
import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart'; // import 'package:hiddify/features/geo_asset/model/default_geo_assets.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; // import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart'; // import 'package:hiddify/features/geo_asset/model/geo_asset_failure.dart';
import 'package:hiddify/gen/assets.gen.dart'; // import 'package:hiddify/gen/assets.gen.dart';
import 'package:hiddify/utils/custom_loggers.dart'; // import 'package:hiddify/utils/custom_loggers.dart';
import 'package:rxdart/rxdart.dart'; // import 'package:rxdart/rxdart.dart';
import 'package:watcher/watcher.dart'; // import 'package:watcher/watcher.dart';
abstract interface class GeoAssetRepository { // abstract interface class GeoAssetRepository {
/// populate bundled geo assets directory with bundled files if needed // /// populate bundled geo assets directory with bundled files if needed
TaskEither<GeoAssetFailure, Unit> init(); // TaskEither<GeoAssetFailure, Unit> init();
TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})> // TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})>
getActivePair(); // getActivePair();
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll(); // Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll();
TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset); // TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset);
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset); // TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset);
TaskEither<GeoAssetFailure, Unit> addRecommended(); // TaskEither<GeoAssetFailure, Unit> addRecommended();
} // }
class GeoAssetRepositoryImpl // class GeoAssetRepositoryImpl
with ExceptionHandler, InfraLogger // with ExceptionHandler, InfraLogger
implements GeoAssetRepository { // implements GeoAssetRepository {
GeoAssetRepositoryImpl({ // GeoAssetRepositoryImpl({
required this.geoAssetDataSource, // required this.geoAssetDataSource,
required this.geoAssetPathResolver, // required this.geoAssetPathResolver,
required this.httpClient, // required this.httpClient,
}); // });
final GeoAssetDataSource geoAssetDataSource; // final GeoAssetDataSource geoAssetDataSource;
final GeoAssetPathResolver geoAssetPathResolver; // final GeoAssetPathResolver geoAssetPathResolver;
final DioHttpClient httpClient; // final DioHttpClient httpClient;
@override // @override
TaskEither<GeoAssetFailure, Unit> init() { // TaskEither<GeoAssetFailure, Unit> init() {
return exceptionHandler( // return exceptionHandler(
() async { // () async {
loggy.debug("initializing"); // loggy.debug("initializing");
final geoipFile = geoAssetPathResolver.file( // final geoipFile = geoAssetPathResolver.file(
defaultGeoip.providerName, // defaultGeoip.providerName,
defaultGeoip.fileName, // defaultGeoip.fileName,
); // );
final geositeFile = geoAssetPathResolver.file( // final geositeFile = geoAssetPathResolver.file(
defaultGeosite.providerName, // defaultGeosite.providerName,
defaultGeosite.fileName, // defaultGeosite.fileName,
); // );
final dirExists = await geoAssetPathResolver.directory.exists(); // final dirExists = await geoAssetPathResolver.directory.exists();
if (!dirExists) { // if (!dirExists) {
await geoAssetPathResolver.directory.create(recursive: true); // await geoAssetPathResolver.directory.create(recursive: true);
} // }
if (!dirExists || !await geoipFile.exists()) { // if (!dirExists || !await geoipFile.exists()) {
final bundledGeoip = await rootBundle.load(Assets.core.geoip); // final bundledGeoip = await rootBundle.load(Assets.core.geoip);
await geoipFile.writeAsBytes(bundledGeoip.buffer.asInt8List()); // await geoipFile.writeAsBytes(bundledGeoip.buffer.asInt8List());
} // }
if (!dirExists || !await geositeFile.exists()) { // if (!dirExists || !await geositeFile.exists()) {
final bundledGeosite = await rootBundle.load(Assets.core.geosite); // final bundledGeosite = await rootBundle.load(Assets.core.geosite);
await geositeFile.writeAsBytes(bundledGeosite.buffer.asInt8List()); // await geositeFile.writeAsBytes(bundledGeosite.buffer.asInt8List());
} // }
return right(unit); // return right(unit);
}, // },
GeoAssetUnexpectedFailure.new, // GeoAssetUnexpectedFailure.new,
); // );
} // }
@override // @override
TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})> // TaskEither<GeoAssetFailure, ({GeoAssetEntity geoip, GeoAssetEntity geosite})>
getActivePair() { // getActivePair() {
return exceptionHandler( // return exceptionHandler(
() async { // () async {
final geoip = // final geoip =
await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geoip); // await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geoip);
final geosite = // final geosite =
await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geosite); // await geoAssetDataSource.getActiveAssetByType(GeoAssetType.geosite);
if (geoip == null || geosite == null) { // if (geoip == null || geosite == null) {
return left(const GeoAssetFailure.activeAssetNotFound()); // return left(const GeoAssetFailure.activeAssetNotFound());
} // }
return right((geoip: geoip.toEntity(), geosite: geosite.toEntity())); // return right((geoip: geoip.toEntity(), geosite: geosite.toEntity()));
}, // },
GeoAssetUnexpectedFailure.new, // GeoAssetUnexpectedFailure.new,
); // );
} // }
@override // @override
Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll() { // Stream<Either<GeoAssetFailure, List<GeoAssetWithFileSize>>> watchAll() {
final persistedStream = geoAssetDataSource // final persistedStream = geoAssetDataSource
.watchAll() // .watchAll()
.map((event) => event.map((e) => e.toEntity())); // .map((event) => event.map((e) => e.toEntity()));
final filesStream = _watchGeoFiles(); // final filesStream = _watchGeoFiles();
return Rx.combineLatest2( // return Rx.combineLatest2(
persistedStream, // persistedStream,
filesStream, // filesStream,
(assets, files) => assets.map( // (assets, files) => assets.map(
(e) { // (e) {
final path = // final path =
geoAssetPathResolver.file(e.providerName, e.fileName).path; // geoAssetPathResolver.file(e.providerName, e.fileName).path;
final file = files.firstOrNullWhere((e) => e.path == path); // final file = files.firstOrNullWhere((e) => e.path == path);
final stat = file?.statSync(); // final stat = file?.statSync();
return (e, stat?.size); // return (e, stat?.size);
}, // },
).toList(), // ).toList(),
).handleExceptions(GeoAssetUnexpectedFailure.new); // ).handleExceptions(GeoAssetUnexpectedFailure.new);
} // }
Iterable<File> _geoFiles = []; // Iterable<File> _geoFiles = [];
Stream<Iterable<File>> _watchGeoFiles() async* { // Stream<Iterable<File>> _watchGeoFiles() async* {
yield await _readGeoFiles(); // yield await _readGeoFiles();
yield* Watcher( // yield* Watcher(
geoAssetPathResolver.directory.path, // geoAssetPathResolver.directory.path,
pollingDelay: const Duration(seconds: 1), // pollingDelay: const Duration(seconds: 1),
).events.asyncMap((event) async { // ).events.asyncMap((event) async {
await _readGeoFiles(); // await _readGeoFiles();
return _geoFiles; // return _geoFiles;
}); // });
} // }
Future<Iterable<File>> _readGeoFiles() async { // Future<Iterable<File>> _readGeoFiles() async {
return _geoFiles = Directory(geoAssetPathResolver.directory.path) // return _geoFiles = Directory(geoAssetPathResolver.directory.path)
.listSync() // .listSync()
.whereType<File>() // .whereType<File>()
.where((e) => e.extension == '.db'); // .where((e) => e.extension == '.db');
} // }
@override // @override
TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset) { // TaskEither<GeoAssetFailure, Unit> update(GeoAssetEntity geoAsset) {
return exceptionHandler( // return exceptionHandler(
() async { // () async {
loggy.debug( // loggy.debug(
"checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", // "checking latest release of [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
); // );
final response = await httpClient.get<Map>(geoAsset.repositoryUrl); // final response = await httpClient.get<Map>(geoAsset.repositoryUrl);
if (response.statusCode != 200 || response.data == null) { // if (response.statusCode != 200 || response.data == null) {
return left( // return left(
GeoAssetUnexpectedFailure.new( // GeoAssetUnexpectedFailure.new(
"invalid response", // "invalid response",
StackTrace.current, // StackTrace.current,
), // ),
); // );
} // }
final file = // final file =
geoAssetPathResolver.file(geoAsset.providerName, geoAsset.name); // geoAssetPathResolver.file(geoAsset.providerName, geoAsset.name);
final tagName = response.data!['tag_name'] as String; // final tagName = response.data!['tag_name'] as String;
loggy.debug("latest release of [${geoAsset.name}]: [$tagName]"); // loggy.debug("latest release of [${geoAsset.name}]: [$tagName]");
if (tagName == geoAsset.version && await file.exists()) { // if (tagName == geoAsset.version && await file.exists()) {
await geoAssetDataSource.patch( // await geoAssetDataSource.patch(
geoAsset.id, // geoAsset.id,
GeoAssetEntriesCompanion(lastCheck: Value(DateTime.now())), // GeoAssetEntriesCompanion(lastCheck: Value(DateTime.now())),
); // );
return left(const GeoAssetFailure.noUpdateAvailable()); // return left(const GeoAssetFailure.noUpdateAvailable());
} // }
final assets = (response.data!['assets'] as List) // final assets = (response.data!['assets'] as List)
.whereType<Map<String, dynamic>>(); // .whereType<Map<String, dynamic>>();
final asset = // final asset =
assets.firstOrNullWhere((e) => e["name"] == geoAsset.name); // assets.firstOrNullWhere((e) => e["name"] == geoAsset.name);
if (asset == null) { // if (asset == null) {
return left( // return left(
GeoAssetUnexpectedFailure.new( // GeoAssetUnexpectedFailure.new(
"couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]", // "couldn't find [${geoAsset.name}] on [${geoAsset.repositoryUrl}]",
StackTrace.current, // StackTrace.current,
), // ),
); // );
} // }
final downloadUrl = asset["browser_download_url"] as String; // final downloadUrl = asset["browser_download_url"] as String;
loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]"); // loggy.debug("[${geoAsset.name}] download url: [$downloadUrl]");
final tempPath = "${file.path}.tmp"; // final tempPath = "${file.path}.tmp";
await file.parent.create(recursive: true); // await file.parent.create(recursive: true);
await httpClient.download(downloadUrl, tempPath); // await httpClient.download(downloadUrl, tempPath);
await File(tempPath).rename(file.path); // await File(tempPath).rename(file.path);
await geoAssetDataSource.patch( // await geoAssetDataSource.patch(
geoAsset.id, // geoAsset.id,
GeoAssetEntriesCompanion( // GeoAssetEntriesCompanion(
version: Value(tagName), // version: Value(tagName),
lastCheck: Value(DateTime.now()), // lastCheck: Value(DateTime.now()),
), // ),
); // );
return right(unit); // return right(unit);
}, // },
GeoAssetUnexpectedFailure.new, // GeoAssetUnexpectedFailure.new,
); // );
} // }
@override // @override
TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset) { // TaskEither<GeoAssetFailure, Unit> markAsActive(GeoAssetEntity geoAsset) {
return exceptionHandler( // return exceptionHandler(
() async { // () async {
await geoAssetDataSource.patch( // await geoAssetDataSource.patch(
geoAsset.id, // geoAsset.id,
const GeoAssetEntriesCompanion(active: Value(true)), // const GeoAssetEntriesCompanion(active: Value(true)),
); // );
return right(unit); // return right(unit);
}, // },
GeoAssetUnexpectedFailure.new, // GeoAssetUnexpectedFailure.new,
); // );
} // }
@override // @override
TaskEither<GeoAssetFailure, Unit> addRecommended() { // TaskEither<GeoAssetFailure, Unit> addRecommended() {
return exceptionHandler( // return exceptionHandler(
() async { // () async {
final persistedIds = await geoAssetDataSource // final persistedIds = await geoAssetDataSource
.watchAll() // .watchAll()
.first // .first
.then((value) => value.map((e) => e.id)); // .then((value) => value.map((e) => e.id));
final missing = // final missing =
recommendedGeoAssets.where((e) => !persistedIds.contains(e.id)); // recommendedGeoAssets.where((e) => !persistedIds.contains(e.id));
for (final geoAsset in missing) { // for (final geoAsset in missing) {
await geoAssetDataSource.insert(geoAsset.toEntry()); // await geoAssetDataSource.insert(geoAsset.toEntry());
} // }
return right(unit); // return right(unit);
}, // },
GeoAssetUnexpectedFailure.new, // GeoAssetUnexpectedFailure.new,
); // );
} // }
} // }

View File

@@ -1,53 +1,53 @@
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; // import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
/// default geoip asset bundled with the app // /// default geoip asset bundled with the app
const defaultGeoip = GeoAssetEntity( // const defaultGeoip = GeoAssetEntity(
id: "sing-box-geoip", // id: "sing-box-geoip",
name: "geoip.db", // name: "geoip.db",
type: GeoAssetType.geoip, // type: GeoAssetType.geoip,
active: true, // active: true,
providerName: "SagerNet/sing-geoip", // providerName: "SagerNet/sing-geoip",
); // );
/// default geosite asset bundled with the app // /// default geosite asset bundled with the app
const defaultGeosite = GeoAssetEntity( // const defaultGeosite = GeoAssetEntity(
id: "sing-box-geosite", // id: "sing-box-geosite",
name: "geosite.db", // name: "geosite.db",
type: GeoAssetType.geosite, // type: GeoAssetType.geosite,
active: true, // active: true,
providerName: "SagerNet/sing-geosite", // providerName: "SagerNet/sing-geosite",
); // );
const defaultGeoAssets = [defaultGeoip, defaultGeosite]; // const defaultGeoAssets = [defaultGeoip, defaultGeosite];
const recommendedGeoAssets = [ // const recommendedGeoAssets = [
...defaultGeoAssets, // ...defaultGeoAssets,
GeoAssetEntity( // GeoAssetEntity(
id: "chocolate4U-geoip", // id: "chocolate4U-geoip",
name: "geoip.db", // name: "geoip.db",
type: GeoAssetType.geoip, // type: GeoAssetType.geoip,
active: false, // active: false,
providerName: "Chocolate4U/Iran-sing-box-rules", // providerName: "Chocolate4U/Iran-sing-box-rules",
), // ),
GeoAssetEntity( // GeoAssetEntity(
id: "chocolate4U-geosite", // id: "chocolate4U-geosite",
name: "geosite.db", // name: "geosite.db",
type: GeoAssetType.geosite, // type: GeoAssetType.geosite,
active: false, // active: false,
providerName: "Chocolate4U/Iran-sing-box-rules", // providerName: "Chocolate4U/Iran-sing-box-rules",
), // ),
GeoAssetEntity( // GeoAssetEntity(
id: "soffchen-geoip", // id: "soffchen-geoip",
name: "geoip.db", // name: "geoip.db",
type: GeoAssetType.geoip, // type: GeoAssetType.geoip,
active: false, // active: false,
providerName: "soffchen/sing-geoip", // providerName: "soffchen/sing-geoip",
), // ),
GeoAssetEntity( // GeoAssetEntity(
id: "soffchen-geosite", // id: "soffchen-geosite",
name: "geosite.db", // name: "geosite.db",
type: GeoAssetType.geosite, // type: GeoAssetType.geosite,
active: false, // active: false,
providerName: "soffchen/sing-geosite", // providerName: "soffchen/sing-geosite",
), // ),
]; // ];

View File

@@ -1,33 +1,33 @@
import 'package:fpdart/fpdart.dart'; // import 'package:fpdart/fpdart.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; // import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; // import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/utils/custom_loggers.dart'; // import 'package:hiddify/utils/custom_loggers.dart';
import 'package:hiddify/utils/riverpod_utils.dart'; // import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; // import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'geo_asset_notifier.g.dart'; // part 'geo_asset_notifier.g.dart';
@riverpod // @riverpod
class FetchGeoAsset extends _$FetchGeoAsset with AppLogger { // class FetchGeoAsset extends _$FetchGeoAsset with AppLogger {
@override // @override
Future<Unit?> build(String id) async { // Future<Unit?> build(String id) async {
ref.disposeDelay(const Duration(seconds: 10)); // ref.disposeDelay(const Duration(seconds: 10));
return null; // return null;
} // }
Future<void> fetch(GeoAssetEntity geoAsset) async { // Future<void> fetch(GeoAssetEntity geoAsset) async {
state = const AsyncLoading(); // state = const AsyncLoading();
state = await AsyncValue.guard( // state = await AsyncValue.guard(
() => ref // () => ref
.read(geoAssetRepositoryProvider) // .read(geoAssetRepositoryProvider)
.requireValue // .requireValue
.update(geoAsset) // .update(geoAsset)
.getOrElse( // .getOrElse(
(failure) { // (failure) {
loggy.warning("error updating geo asset $failure", failure); // loggy.warning("error updating geo asset $failure", failure);
throw failure; // throw failure;
}, // },
).run(), // ).run(),
); // );
} // }
} // }

View File

@@ -1,55 +1,55 @@
import 'package:dartx/dartx.dart'; // import 'package:dartx/dartx.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart'; // import 'package:hiddify/features/geo_asset/data/geo_asset_data_providers.dart';
import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart'; // import 'package:hiddify/features/geo_asset/data/geo_asset_repository.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart'; // import 'package:hiddify/features/geo_asset/model/geo_asset_entity.dart';
import 'package:hiddify/utils/custom_loggers.dart'; // import 'package:hiddify/utils/custom_loggers.dart';
import 'package:hiddify/utils/riverpod_utils.dart'; // import 'package:hiddify/utils/riverpod_utils.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; // import 'package:riverpod_annotation/riverpod_annotation.dart';
part 'geo_assets_overview_notifier.g.dart'; // part 'geo_assets_overview_notifier.g.dart';
typedef GroupedRoutingAssets = ({ // typedef GroupedRoutingAssets = ({
List<GeoAssetWithFileSize> geoip, // List<GeoAssetWithFileSize> geoip,
List<GeoAssetWithFileSize> geosite, // List<GeoAssetWithFileSize> geosite,
}); // });
@riverpod // @riverpod
class GeoAssetsOverviewNotifier extends _$GeoAssetsOverviewNotifier // class GeoAssetsOverviewNotifier extends _$GeoAssetsOverviewNotifier
with AppLogger { // with AppLogger {
@override // @override
Stream<GroupedRoutingAssets> build() { // Stream<GroupedRoutingAssets> build() {
ref.disposeDelay(const Duration(seconds: 5)); // ref.disposeDelay(const Duration(seconds: 5));
return ref.watch(geoAssetRepositoryProvider).requireValue.watchAll().map( // return ref.watch(geoAssetRepositoryProvider).requireValue.watchAll().map(
(event) { // (event) {
final grouped = event // final grouped = event
.getOrElse((l) => throw l) // .getOrElse((l) => throw l)
.groupBy((element) => element.$1.type); // .groupBy((element) => element.$1.type);
return ( // return (
geoip: grouped.getOrElse(GeoAssetType.geoip, () => []), // geoip: grouped.getOrElse(GeoAssetType.geoip, () => []),
geosite: grouped.getOrElse(GeoAssetType.geosite, () => []), // geosite: grouped.getOrElse(GeoAssetType.geosite, () => []),
); // );
}, // },
); // );
} // }
GeoAssetRepository get _geoAssetRepo => // GeoAssetRepository get _geoAssetRepo =>
ref.read(geoAssetRepositoryProvider).requireValue; // ref.read(geoAssetRepositoryProvider).requireValue;
Future<void> markAsActive(GeoAssetEntity geoAsset) async { // Future<void> markAsActive(GeoAssetEntity geoAsset) async {
await _geoAssetRepo.markAsActive(geoAsset).getOrElse( // await _geoAssetRepo.markAsActive(geoAsset).getOrElse(
(f) { // (f) {
loggy.warning("error marking geo asset as active", f); // loggy.warning("error marking geo asset as active", f);
throw f; // throw f;
}, // },
).run(); // ).run();
} // }
Future<void> addRecommended() async { // Future<void> addRecommended() async {
await _geoAssetRepo.addRecommended().getOrElse( // await _geoAssetRepo.addRecommended().getOrElse(
(f) { // (f) {
loggy.warning("error adding recommended geo assets", f); // loggy.warning("error adding recommended geo assets", f);
throw f; // throw f;
}, // },
).run(); // ).run();
} // }
} // }

View File

@@ -1,102 +1,102 @@
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:gap/gap.dart'; // import 'package:gap/gap.dart';
import 'package:hiddify/core/localization/translations.dart'; // import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/widget/adaptive_icon.dart'; // import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/core/widget/animated_visibility.dart'; // import 'package:hiddify/core/widget/animated_visibility.dart';
import 'package:hiddify/core/widget/tip_card.dart'; // import 'package:hiddify/core/widget/tip_card.dart';
import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart'; // import 'package:hiddify/features/geo_asset/overview/geo_assets_overview_notifier.dart';
import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart'; // import 'package:hiddify/features/geo_asset/widget/geo_asset_tile.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; // import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:sliver_tools/sliver_tools.dart'; // import 'package:sliver_tools/sliver_tools.dart';
class GeoAssetsOverviewPage extends HookConsumerWidget { // class GeoAssetsOverviewPage extends HookConsumerWidget {
const GeoAssetsOverviewPage({super.key}); // const GeoAssetsOverviewPage({super.key});
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); // final t = ref.watch(translationsProvider);
final state = ref.watch(geoAssetsOverviewNotifierProvider); // final state = ref.watch(geoAssetsOverviewNotifierProvider);
return Scaffold( // return Scaffold(
body: CustomScrollView( // body: CustomScrollView(
slivers: [ // slivers: [
SliverAppBar( // SliverAppBar(
title: Text(t.settings.geoAssets.pageTitle), // title: Text(t.settings.geoAssets.pageTitle),
pinned: true, // pinned: true,
actions: [ // actions: [
PopupMenuButton( // PopupMenuButton(
icon: Icon(AdaptiveIcon(context).more), // icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) { // itemBuilder: (context) {
return [ // return [
PopupMenuItem( // PopupMenuItem(
child: Text(t.settings.geoAssets.addRecommended), // child: Text(t.settings.geoAssets.addRecommended),
onTap: () { // onTap: () {
ref // ref
.read(geoAssetsOverviewNotifierProvider.notifier) // .read(geoAssetsOverviewNotifierProvider.notifier)
.addRecommended(); // .addRecommended();
}, // },
), // ),
]; // ];
}, // },
), // ),
], // ],
), // ),
if (state case AsyncData(value: (:final geoip, :final geosite))) // if (state case AsyncData(value: (:final geoip, :final geosite)))
SliverPinnedHeader( // SliverPinnedHeader(
child: AnimatedVisibility( // child: AnimatedVisibility(
visible: (geoip + geosite) // visible: (geoip + geosite)
.where((e) => e.$1.active && e.$2 == null) // .where((e) => e.$1.active && e.$2 == null)
.isNotEmpty, // .isNotEmpty,
axis: Axis.vertical, // axis: Axis.vertical,
child: // child:
TipCard(message: t.settings.geoAssets.missingGeoAssetsMsg), // TipCard(message: t.settings.geoAssets.missingGeoAssetsMsg),
), // ),
), // ),
switch (state) { // switch (state) {
AsyncData(value: (:final geoip, :final geosite)) => MultiSliver( // AsyncData(value: (:final geoip, :final geosite)) => MultiSliver(
children: [ // children: [
ListTile( // ListTile(
title: Text("${t.settings.geoAssets.geoip} "), // title: Text("${t.settings.geoAssets.geoip} "),
titleTextStyle: Theme.of(context).textTheme.headlineSmall, // titleTextStyle: Theme.of(context).textTheme.headlineSmall,
dense: true, // dense: true,
), // ),
SliverList.builder( // SliverList.builder(
itemBuilder: (context, index) { // itemBuilder: (context, index) {
final geoAsset = geoip[index]; // final geoAsset = geoip[index];
return GeoAssetTile( // return GeoAssetTile(
geoAsset, // geoAsset,
onMarkAsActive: () => ref // onMarkAsActive: () => ref
.read(geoAssetsOverviewNotifierProvider.notifier) // .read(geoAssetsOverviewNotifierProvider.notifier)
.markAsActive(geoAsset.$1), // .markAsActive(geoAsset.$1),
); // );
}, // },
itemCount: geoip.length, // itemCount: geoip.length,
), // ),
const Divider(indent: 16, endIndent: 16), // const Divider(indent: 16, endIndent: 16),
ListTile( // ListTile(
title: Text("${t.settings.geoAssets.geosite} "), // title: Text("${t.settings.geoAssets.geosite} "),
titleTextStyle: Theme.of(context).textTheme.headlineSmall, // titleTextStyle: Theme.of(context).textTheme.headlineSmall,
dense: true, // dense: true,
), // ),
SliverList.builder( // SliverList.builder(
itemBuilder: (context, index) { // itemBuilder: (context, index) {
final geoAsset = geosite[index]; // final geoAsset = geosite[index];
return GeoAssetTile( // return GeoAssetTile(
geoAsset, // geoAsset,
onMarkAsActive: () => ref // onMarkAsActive: () => ref
.read(geoAssetsOverviewNotifierProvider.notifier) // .read(geoAssetsOverviewNotifierProvider.notifier)
.markAsActive(geoAsset.$1), // .markAsActive(geoAsset.$1),
); // );
}, // },
itemCount: geosite.length, // itemCount: geosite.length,
), // ),
const Gap(16), // const Gap(16),
], // ],
), // ),
_ => const SliverToBoxAdapter(), // _ => const SliverToBoxAdapter(),
}, // },
], // ],
), // ),
); // );
} // }
} // }

View File

@@ -1,116 +1,116 @@
import 'package:dartx/dartx.dart'; // import 'package:dartx/dartx.dart';
import 'package:flutter/material.dart'; // import 'package:flutter/material.dart';
import 'package:hiddify/core/localization/translations.dart'; // import 'package:hiddify/core/localization/translations.dart';
import 'package:hiddify/core/model/failures.dart'; // import 'package:hiddify/core/model/failures.dart';
import 'package:hiddify/core/widget/adaptive_icon.dart'; // import 'package:hiddify/core/widget/adaptive_icon.dart';
import 'package:hiddify/features/geo_asset/model/geo_asset_entity.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/model/geo_asset_failure.dart';
import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart'; // import 'package:hiddify/features/geo_asset/notifier/geo_asset_notifier.dart';
import 'package:hiddify/utils/utils.dart'; // import 'package:hiddify/utils/utils.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; // import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:humanizer/humanizer.dart'; // import 'package:humanizer/humanizer.dart';
class GeoAssetTile extends HookConsumerWidget { // class GeoAssetTile extends HookConsumerWidget {
GeoAssetTile( // GeoAssetTile(
GeoAssetWithFileSize geoAssetWithFileSize, { // GeoAssetWithFileSize geoAssetWithFileSize, {
super.key, // super.key,
required this.onMarkAsActive, // required this.onMarkAsActive,
}) : geoAsset = geoAssetWithFileSize.$1, // }) : geoAsset = geoAssetWithFileSize.$1,
size = geoAssetWithFileSize.$2; // size = geoAssetWithFileSize.$2;
final GeoAssetEntity geoAsset; // final GeoAssetEntity geoAsset;
final int? size; // final int? size;
final VoidCallback onMarkAsActive; // final VoidCallback onMarkAsActive;
@override // @override
Widget build(BuildContext context, WidgetRef ref) { // Widget build(BuildContext context, WidgetRef ref) {
final t = ref.watch(translationsProvider); // final t = ref.watch(translationsProvider);
final fetchState = ref.watch(fetchGeoAssetProvider(geoAsset.id)); // final fetchState = ref.watch(fetchGeoAssetProvider(geoAsset.id));
final fileMissing = size == null; // final fileMissing = size == null;
ref.listen( // ref.listen(
fetchGeoAssetProvider(geoAsset.id), // fetchGeoAssetProvider(geoAsset.id),
(_, next) { // (_, next) {
switch (next) { // switch (next) {
case AsyncError(:final error): // case AsyncError(:final error):
if (error case GeoAssetNoUpdateAvailable()) { // if (error case GeoAssetNoUpdateAvailable()) {
return CustomToast(t.failure.geoAssets.notUpdate).show(context); // return CustomToast(t.failure.geoAssets.notUpdate).show(context);
} // }
CustomAlertDialog.fromErr(t.presentError(error)).show(context); // CustomAlertDialog.fromErr(t.presentError(error)).show(context);
case AsyncData(value: final _?): // case AsyncData(value: final _?):
CustomToast.success(t.settings.geoAssets.successMsg).show(context); // CustomToast.success(t.settings.geoAssets.successMsg).show(context);
} // }
}, // },
); // );
return ListTile( // return ListTile(
title: Text.rich( // title: Text.rich(
TextSpan( // TextSpan(
children: [ // children: [
TextSpan(text: geoAsset.name), // TextSpan(text: geoAsset.name),
if (geoAsset.providerName.isNotBlank) // if (geoAsset.providerName.isNotBlank)
TextSpan(text: " (${geoAsset.providerName})"), // TextSpan(text: " (${geoAsset.providerName})"),
], // ],
), // ),
), // ),
isThreeLine: true, // isThreeLine: true,
subtitle: fetchState.isLoading // subtitle: fetchState.isLoading
? const LinearProgressIndicator() // ? const LinearProgressIndicator()
: Row( // : Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, // mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ // children: [
if (geoAsset.version.isNotNullOrBlank) // if (geoAsset.version.isNotNullOrBlank)
Padding( // Padding(
padding: const EdgeInsetsDirectional.only(end: 8), // padding: const EdgeInsetsDirectional.only(end: 8),
child: Text( // child: Text(
t.settings.geoAssets.version(version: geoAsset.version!), // t.settings.geoAssets.version(version: geoAsset.version!),
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
) // )
else // else
const SizedBox(), // const SizedBox(),
Flexible( // Flexible(
child: Text.rich( // child: Text.rich(
TextSpan( // TextSpan(
children: [ // children: [
if (fileMissing) // if (fileMissing)
TextSpan( // TextSpan(
text: t.settings.geoAssets.fileMissing, // text: t.settings.geoAssets.fileMissing,
style: TextStyle( // style: TextStyle(
color: Theme.of(context).colorScheme.error, // color: Theme.of(context).colorScheme.error,
), // ),
) // )
else // else
TextSpan(text: size?.bytes().toString()), // TextSpan(text: size?.bytes().toString()),
if (geoAsset.lastCheck != null) ...[ // if (geoAsset.lastCheck != null) ...[
const TextSpan(text: ""), // const TextSpan(text: " • "),
TextSpan(text: geoAsset.lastCheck!.format()), // TextSpan(text: geoAsset.lastCheck!.format()),
], // ],
], // ],
), // ),
overflow: TextOverflow.ellipsis, // overflow: TextOverflow.ellipsis,
), // ),
), // ),
], // ],
), // ),
selected: geoAsset.active, // selected: geoAsset.active,
onTap: onMarkAsActive, // onTap: onMarkAsActive,
trailing: PopupMenuButton( // trailing: PopupMenuButton(
icon: Icon(AdaptiveIcon(context).more), // icon: Icon(AdaptiveIcon(context).more),
itemBuilder: (context) { // itemBuilder: (context) {
return [ // return [
PopupMenuItem( // PopupMenuItem(
enabled: !fetchState.isLoading, // enabled: !fetchState.isLoading,
onTap: () => ref // onTap: () => ref
.read(FetchGeoAssetProvider(geoAsset.id).notifier) // .read(FetchGeoAssetProvider(geoAsset.id).notifier)
.fetch(geoAsset), // .fetch(geoAsset),
child: fileMissing // child: fileMissing
? Text(t.settings.geoAssets.download) // ? Text(t.settings.geoAssets.download)
: Text(t.settings.geoAssets.update), // : Text(t.settings.geoAssets.update),
), // ),
]; // ];
}, // },
), // ),
); // );
} // }
} // }

View File

@@ -23,16 +23,16 @@ class AdvancedSettingTiles extends HookConsumerWidget {
return Column( return Column(
children: [ children: [
const RegionPrefTile(), // const RegionPrefTile(),
ListTile( // ListTile(
title: Text(t.settings.geoAssets.pageTitle), // title: Text(t.settings.geoAssets.pageTitle),
leading: const Icon( // leading: const Icon(
FluentIcons.arrow_routing_rectangle_multiple_24_regular, // FluentIcons.arrow_routing_rectangle_multiple_24_regular,
), // ),
onTap: () async { // onTap: () async {
await const GeoAssetsRoute().push(context); // // await const GeoAssetsRoute().push(context);
}, // },
), // ),
if (Platform.isAndroid) ...[ if (Platform.isAndroid) ...[
ListTile( ListTile(
title: Text(t.settings.network.perAppProxyPageTitle), title: Text(t.settings.network.perAppProxyPageTitle),
@@ -40,11 +40,8 @@ class AdvancedSettingTiles extends HookConsumerWidget {
trailing: Switch( trailing: Switch(
value: perAppProxy, value: perAppProxy,
onChanged: (value) async { onChanged: (value) async {
final newMode = final newMode = perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude;
perAppProxy ? PerAppProxyMode.off : PerAppProxyMode.exclude; await ref.read(Preferences.perAppProxyMode.notifier).update(newMode);
await ref
.read(Preferences.perAppProxyMode.notifier)
.update(newMode);
if (!perAppProxy && context.mounted) { if (!perAppProxy && context.mounted) {
await const PerAppProxyRoute().push(context); await const PerAppProxyRoute().push(context);
} }
@@ -52,9 +49,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
), ),
onTap: () async { onTap: () async {
if (!perAppProxy) { if (!perAppProxy) {
await ref await ref.read(Preferences.perAppProxyMode.notifier).update(PerAppProxyMode.exclude);
.read(Preferences.perAppProxyMode.notifier)
.update(PerAppProxyMode.exclude);
} }
if (context.mounted) await const PerAppProxyRoute().push(context); if (context.mounted) await const PerAppProxyRoute().push(context);
}, },
@@ -66,9 +61,7 @@ class AdvancedSettingTiles extends HookConsumerWidget {
value: !disableMemoryLimit, value: !disableMemoryLimit,
secondary: const Icon(FluentIcons.developer_board_24_regular), secondary: const Icon(FluentIcons.developer_board_24_regular),
onChanged: (value) async { onChanged: (value) async {
await ref await ref.read(Preferences.disableMemoryLimit.notifier).update(!value);
.read(Preferences.disableMemoryLimit.notifier)
.update(!value);
}, },
), ),
if (Platform.isIOS) if (Platform.isIOS)

View File

@@ -23,7 +23,7 @@ class GeneralSettingTiles extends HookConsumerWidget {
return Column( return Column(
children: [ children: [
const LocalePrefTile(), // const LocalePrefTile(),
ListTile( ListTile(
title: Text(t.settings.general.themeMode), title: Text(t.settings.general.themeMode),
subtitle: Text(themeMode.present(t)), subtitle: Text(themeMode.present(t)),
@@ -48,9 +48,7 @@ class GeneralSettingTiles extends HookConsumerWidget {
}, },
); );
if (selectedThemeMode != null) { if (selectedThemeMode != null) {
await ref await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode);
.read(themePreferencesProvider.notifier)
.changeThemeMode(selectedThemeMode);
} }
}, },
), ),
@@ -67,17 +65,14 @@ class GeneralSettingTiles extends HookConsumerWidget {
secondary: const Icon(FluentIcons.top_speed_24_regular), secondary: const Icon(FluentIcons.top_speed_24_regular),
value: ref.watch(Preferences.dynamicNotification), value: ref.watch(Preferences.dynamicNotification),
onChanged: (value) async { onChanged: (value) async {
await ref await ref.read(Preferences.dynamicNotification.notifier).update(value);
.read(Preferences.dynamicNotification.notifier)
.update(value);
}, },
), ),
SwitchListTile( SwitchListTile(
title: Text(t.settings.general.hapticFeedback), title: Text(t.settings.general.hapticFeedback),
secondary: const Icon(FluentIcons.phone_vibrate_24_regular), secondary: const Icon(FluentIcons.phone_vibrate_24_regular),
value: ref.watch(hapticServiceProvider), value: ref.watch(hapticServiceProvider),
onChanged: onChanged: ref.read(hapticServiceProvider.notifier).updatePreference,
ref.read(hapticServiceProvider.notifier).updatePreference,
), ),
], ],
if (PlatformUtils.isDesktop) ...[ if (PlatformUtils.isDesktop) ...[

View File

@@ -16,6 +16,8 @@ class SingboxConfigOption with _$SingboxConfigOption {
@JsonSerializable(fieldRename: FieldRename.kebab) @JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxConfigOption({ const factory SingboxConfigOption({
required String region,
required bool blockAds,
required bool executeConfigAsIs, required bool executeConfigAsIs,
required LogLevel logLevel, required LogLevel logLevel,
required bool resolveDestination, required bool resolveDestination,
@@ -42,8 +44,8 @@ class SingboxConfigOption with _$SingboxConfigOption {
required bool enableFakeDns, required bool enableFakeDns,
required bool enableDnsRouting, required bool enableDnsRouting,
required bool independentDnsCache, required bool independentDnsCache,
required String geoipPath, // required String geoipPath,
required String geositePath, // required String geositePath,
required List<SingboxRule> rules, required List<SingboxRule> rules,
required SingboxMuxOption mux, required SingboxMuxOption mux,
required SingboxTlsTricks tlsTricks, required SingboxTlsTricks tlsTricks,

View File

@@ -9,6 +9,7 @@ class SingboxRule with _$SingboxRule {
@JsonSerializable(fieldRename: FieldRename.kebab) @JsonSerializable(fieldRename: FieldRename.kebab)
const factory SingboxRule({ const factory SingboxRule({
String? ruleSetUrl,
String? domains, String? domains,
String? ip, String? ip,
String? port, String? port,
@@ -17,8 +18,7 @@ class SingboxRule with _$SingboxRule {
@Default(RuleOutbound.proxy) RuleOutbound outbound, @Default(RuleOutbound.proxy) RuleOutbound outbound,
}) = _SingboxRule; }) = _SingboxRule;
factory SingboxRule.fromJson(Map<String, dynamic> json) => factory SingboxRule.fromJson(Map<String, dynamic> json) => _$SingboxRuleFromJson(json);
_$SingboxRuleFromJson(json);
} }
enum RuleOutbound { proxy, bypass, block } enum RuleOutbound { proxy, bypass, block }