From 60db9a223992e4d1ccde0b82099f71c89b6d5846 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Sun, 3 Mar 2024 14:03:36 +0330 Subject: [PATCH] Add reconnect alert for config options --- assets/translations/strings_en.i18n.json | 2 + .../in_app_notification_controller.dart | 46 ++++++++ .../data/config_option_repository.dart | 103 ++++++++++++++++++ .../notifier/config_option_notifier.dart | 26 ++++- .../data/connection_repository.dart | 7 ++ .../connection/widget/connection_wrapper.dart | 19 ++++ 6 files changed, 202 insertions(+), 1 deletion(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 464fe4f0..1bce00fe 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -220,6 +220,8 @@ }, "config": { "resetBtn": "Reset options", + "reconnectMsg": "Reconnect for changes to take effect", + "reconnectBtn": "Reconnect", "serviceMode": "Service Mode", "serviceModes": { "proxy": "Proxy Service Only", diff --git a/lib/core/notification/in_app_notification_controller.dart b/lib/core/notification/in_app_notification_controller.dart index 976dbefc..29764965 100644 --- a/lib/core/notification/in_app_notification_controller.dart +++ b/lib/core/notification/in_app_notification_controller.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; import 'package:hiddify/core/model/failures.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; import 'package:hiddify/utils/utils.dart'; @@ -86,6 +87,51 @@ class InAppNotificationController with AppLogger { } CustomAlertDialog.fromErr(error).show(context); } + + void showActionToast( + String message, { + required String actionText, + required VoidCallback callback, + Duration duration = const Duration(seconds: 5), + }) { + final context = RootScaffold.stateKey.currentContext; + if (context == null) return; + toastification.dismissAll(); + + toastification.showCustom( + context: context, + autoCloseDuration: duration, + alignment: Alignment.bottomLeft, + builder: (context, holder) { + return GestureDetector( + onTap: () => toastification.dismiss(holder), + child: Card( + margin: const EdgeInsets.all(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + child: Row( + children: [ + Expanded(child: Text(message)), + const Gap(8), + Row( + children: [ + TextButton( + onPressed: () { + toastification.dismiss(holder); + callback(); + }, + child: Text(actionText), + ), + ], + ), + ], + ), + ), + ), + ); + }, + ); + } } extension NotificationTypeX on NotificationType { diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index bb851969..e607a553 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -2,10 +2,12 @@ import 'package:dartx/dartx.dart'; import 'package:fpdart/fpdart.dart'; import 'package:hiddify/core/model/optional_range.dart'; import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/utils/exception_handler.dart'; import 'package:hiddify/core/utils/json_converters.dart'; import 'package:hiddify/core/utils/preferences_utils.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'; @@ -324,6 +326,107 @@ abstract class ConfigOptions { warpWireguardConfig, ]; + static final singboxConfigOptions = FutureProvider( + (ref) async { + final region = ref.watch(Preferences.region); + 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, + ), + ], + Region.af => [ + const SingboxRule( + domains: "domain:.af,geosite:af", + ip: "geoip:af", + outbound: RuleOutbound.bypass, + ), + ], + _ => [], + }; + + final geoAssetsRepo = await ref.watch(geoAssetRepositoryProvider.future); + final geoAssets = + await geoAssetsRepo.getActivePair().getOrElse((l) => throw l).run(); + + final mode = ref.watch(serviceMode); + return SingboxConfigOption( + executeConfigAsIs: false, + logLevel: ref.watch(logLevel), + resolveDestination: ref.watch(resolveDestination), + ipv6Mode: ref.watch(ipv6Mode), + remoteDnsAddress: ref.watch(remoteDnsAddress), + remoteDnsDomainStrategy: ref.watch(remoteDnsDomainStrategy), + directDnsAddress: ref.watch(directDnsAddress), + directDnsDomainStrategy: ref.watch(directDnsDomainStrategy), + mixedPort: ref.watch(mixedPort), + localDnsPort: ref.watch(localDnsPort), + tunImplementation: ref.watch(tunImplementation), + mtu: ref.watch(mtu), + strictRoute: ref.watch(strictRoute), + connectionTestUrl: ref.watch(connectionTestUrl), + urlTestInterval: ref.watch(urlTestInterval), + enableClashApi: ref.watch(enableClashApi), + clashApiPort: ref.watch(clashApiPort), + enableTun: mode == ServiceMode.tun, + enableTunService: mode == ServiceMode.tunService, + setSystemProxy: mode == ServiceMode.systemProxy, + bypassLan: ref.watch(bypassLan), + allowConnectionFromLan: ref.watch(allowConnectionFromLan), + enableFakeDns: ref.watch(enableFakeDns), + enableDnsRouting: ref.watch(enableDnsRouting), + independentDnsCache: ref.watch(independentDnsCache), + enableTlsFragment: ref.watch(enableTlsFragment), + tlsFragmentSize: ref.watch(tlsFragmentSize), + tlsFragmentSleep: ref.watch(tlsFragmentSleep), + enableTlsMixedSniCase: ref.watch(enableTlsMixedSniCase), + enableTlsPadding: ref.watch(enableTlsPadding), + tlsPaddingSize: ref.watch(tlsPaddingSize), + enableMux: ref.watch(enableMux), + muxPadding: ref.watch(muxPadding), + muxMaxStreams: ref.watch(muxMaxStreams), + muxProtocol: ref.watch(muxProtocol), + warp: SingboxWarpOption( + enable: ref.watch(enableWarp), + mode: ref.watch(warpDetourMode), + wireguardConfig: ref.watch(warpWireguardConfig), + licenseKey: ref.watch(warpLicenseKey), + accountId: ref.watch(warpAccountId), + accessToken: ref.watch(warpAccessToken), + cleanIp: ref.watch(warpCleanIp), + cleanPort: ref.watch(warpPort), + warpNoise: ref.watch(warpNoise), + warpNoiseDelay: ref.watch(warpNoiseDelay), + ), + geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath( + geoAssets.geoip.providerName, + geoAssets.geoip.fileName, + ), + geositePath: ref.watch(geoAssetPathResolverProvider).relativePath( + geoAssets.geosite.providerName, + geoAssets.geosite.fileName, + ), + rules: rules, + ); + }, + ); + /// singbox options /// /// **this is partial, don't use it directly** diff --git a/lib/features/config_option/notifier/config_option_notifier.dart b/lib/features/config_option/notifier/config_option_notifier.dart index 3efda727..0d41031b 100644 --- a/lib/features/config_option/notifier/config_option_notifier.dart +++ b/lib/features/config_option/notifier/config_option_notifier.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'package:flutter/services.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; +import 'package:hiddify/features/connection/data/connection_data_providers.dart'; +import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; @@ -10,7 +12,29 @@ part 'config_option_notifier.g.dart'; @Riverpod(keepAlive: true) class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { @override - Future build() async {} + Future build() async { + final serviceRunning = await ref.watch(serviceRunningProvider.future); + final serviceSingboxOptions = + ref.read(connectionRepositoryProvider).configOptionsSnapshot; + ref.listen( + ConfigOptions.singboxConfigOptions, + (previous, next) async { + if (!serviceRunning || serviceSingboxOptions == null) return; + if (next case AsyncData(:final value) when next != previous) { + if (_lastUpdate == null || + DateTime.now().difference(_lastUpdate!) > + const Duration(seconds: 3)) { + _lastUpdate = DateTime.now(); + state = AsyncData(value != serviceSingboxOptions); + } + } + }, + fireImmediately: true, + ); + return false; + } + + DateTime? _lastUpdate; Future exportJsonToClipboard() async { final map = { diff --git a/lib/features/connection/data/connection_repository.dart b/lib/features/connection/data/connection_repository.dart index 67917541..27c35625 100644 --- a/lib/features/connection/data/connection_repository.dart +++ b/lib/features/connection/data/connection_repository.dart @@ -16,6 +16,8 @@ import 'package:hiddify/utils/utils.dart'; import 'package:meta/meta.dart'; abstract interface class ConnectionRepository { + SingboxConfigOption? get configOptionsSnapshot; + TaskEither setup(); Stream watchConnectionStatus(); TaskEither connect( @@ -50,6 +52,10 @@ class ConnectionRepositoryImpl final ProfilePathResolver profilePathResolver; final GeoAssetPathResolver geoAssetPathResolver; + SingboxConfigOption? _configOptionsSnapshot; + @override + SingboxConfigOption? get configOptionsSnapshot => _configOptionsSnapshot; + bool _initialized = false; @override @@ -112,6 +118,7 @@ class ConnectionRepositoryImpl ) { return exceptionHandler( () { + _configOptionsSnapshot = options; return singbox .changeOptions(options) .mapLeft(InvalidConfigOption.new) diff --git a/lib/features/connection/widget/connection_wrapper.dart b/lib/features/connection/widget/connection_wrapper.dart index e5c2aa9e..04bc78e8 100644 --- a/lib/features/connection/widget/connection_wrapper.dart +++ b/lib/features/connection/widget/connection_wrapper.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/notification/in_app_notification_controller.dart'; +import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; +import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -19,6 +23,21 @@ class _ConnectionWrapperState extends ConsumerState Widget build(BuildContext context) { ref.listen(connectionNotifierProvider, (_, __) {}); + ref.listen(configOptionNotifierProvider, (previous, next) { + if (next case AsyncData(value: true)) { + final t = ref.read(translationsProvider); + ref.watch(inAppNotificationControllerProvider).showActionToast( + t.settings.config.reconnectMsg, + actionText: t.settings.config.reconnectBtn, + callback: () async { + await ref + .read(connectionNotifierProvider.notifier) + .reconnect(await ref.read(activeProfileProvider.future)); + }, + ); + } + }); + return widget.child; }