Add reconnect alert for config options
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<SingboxConfigOption>(
|
||||
(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,
|
||||
),
|
||||
],
|
||||
_ => <SingboxRule>[],
|
||||
};
|
||||
|
||||
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**
|
||||
|
||||
@@ -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<void> build() async {}
|
||||
Future<bool> 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<void> exportJsonToClipboard() async {
|
||||
final map = {
|
||||
|
||||
@@ -16,6 +16,8 @@ import 'package:hiddify/utils/utils.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
abstract interface class ConnectionRepository {
|
||||
SingboxConfigOption? get configOptionsSnapshot;
|
||||
|
||||
TaskEither<ConnectionFailure, Unit> setup();
|
||||
Stream<ConnectionStatus> watchConnectionStatus();
|
||||
TaskEither<ConnectionFailure, Unit> 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)
|
||||
|
||||
@@ -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<ConnectionWrapper>
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user