Add reconnect alert for config options
This commit is contained in:
@@ -220,6 +220,8 @@
|
|||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"resetBtn": "Reset options",
|
"resetBtn": "Reset options",
|
||||||
|
"reconnectMsg": "Reconnect for changes to take effect",
|
||||||
|
"reconnectBtn": "Reconnect",
|
||||||
"serviceMode": "Service Mode",
|
"serviceMode": "Service Mode",
|
||||||
"serviceModes": {
|
"serviceModes": {
|
||||||
"proxy": "Proxy Service Only",
|
"proxy": "Proxy Service Only",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hiddify/core/model/failures.dart';
|
import 'package:hiddify/core/model/failures.dart';
|
||||||
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
import 'package:hiddify/features/common/adaptive_root_scaffold.dart';
|
||||||
import 'package:hiddify/utils/utils.dart';
|
import 'package:hiddify/utils/utils.dart';
|
||||||
@@ -86,6 +87,51 @@ class InAppNotificationController with AppLogger {
|
|||||||
}
|
}
|
||||||
CustomAlertDialog.fromErr(error).show(context);
|
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 {
|
extension NotificationTypeX on NotificationType {
|
||||||
|
|||||||
@@ -2,10 +2,12 @@ import 'package:dartx/dartx.dart';
|
|||||||
import 'package:fpdart/fpdart.dart';
|
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/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_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:hiddify/features/log/model/log_level.dart';
|
import 'package:hiddify/features/log/model/log_level.dart';
|
||||||
@@ -324,6 +326,107 @@ abstract class ConfigOptions {
|
|||||||
warpWireguardConfig,
|
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
|
/// singbox options
|
||||||
///
|
///
|
||||||
/// **this is partial, don't use it directly**
|
/// **this is partial, don't use it directly**
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.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/connection/data/connection_data_providers.dart';
|
||||||
|
import 'package:hiddify/features/connection/notifier/connection_notifier.dart';
|
||||||
import 'package:hiddify/utils/custom_loggers.dart';
|
import 'package:hiddify/utils/custom_loggers.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
@@ -10,7 +12,29 @@ part 'config_option_notifier.g.dart';
|
|||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
|
class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger {
|
||||||
@override
|
@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 {
|
Future<void> exportJsonToClipboard() async {
|
||||||
final map = {
|
final map = {
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ import 'package:hiddify/utils/utils.dart';
|
|||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
abstract interface class ConnectionRepository {
|
abstract interface class ConnectionRepository {
|
||||||
|
SingboxConfigOption? get configOptionsSnapshot;
|
||||||
|
|
||||||
TaskEither<ConnectionFailure, Unit> setup();
|
TaskEither<ConnectionFailure, Unit> setup();
|
||||||
Stream<ConnectionStatus> watchConnectionStatus();
|
Stream<ConnectionStatus> watchConnectionStatus();
|
||||||
TaskEither<ConnectionFailure, Unit> connect(
|
TaskEither<ConnectionFailure, Unit> connect(
|
||||||
@@ -50,6 +52,10 @@ class ConnectionRepositoryImpl
|
|||||||
final ProfilePathResolver profilePathResolver;
|
final ProfilePathResolver profilePathResolver;
|
||||||
final GeoAssetPathResolver geoAssetPathResolver;
|
final GeoAssetPathResolver geoAssetPathResolver;
|
||||||
|
|
||||||
|
SingboxConfigOption? _configOptionsSnapshot;
|
||||||
|
@override
|
||||||
|
SingboxConfigOption? get configOptionsSnapshot => _configOptionsSnapshot;
|
||||||
|
|
||||||
bool _initialized = false;
|
bool _initialized = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -112,6 +118,7 @@ class ConnectionRepositoryImpl
|
|||||||
) {
|
) {
|
||||||
return exceptionHandler(
|
return exceptionHandler(
|
||||||
() {
|
() {
|
||||||
|
_configOptionsSnapshot = options;
|
||||||
return singbox
|
return singbox
|
||||||
.changeOptions(options)
|
.changeOptions(options)
|
||||||
.mapLeft(InvalidConfigOption.new)
|
.mapLeft(InvalidConfigOption.new)
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/connection/notifier/connection_notifier.dart';
|
||||||
|
import 'package:hiddify/features/profile/notifier/active_profile_notifier.dart';
|
||||||
import 'package:hiddify/utils/custom_loggers.dart';
|
import 'package:hiddify/utils/custom_loggers.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
|
||||||
@@ -19,6 +23,21 @@ class _ConnectionWrapperState extends ConsumerState<ConnectionWrapper>
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
ref.listen(connectionNotifierProvider, (_, __) {});
|
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;
|
return widget.child;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user