From d87e2077710afb02551497771cd036ecb3bf24d4 Mon Sep 17 00:00:00 2001 From: problematicconsumer Date: Mon, 4 Mar 2024 15:58:56 +0330 Subject: [PATCH] Add Config options import --- assets/translations/strings_en.i18n.json | 7 +- build.yaml | 3 + lib/core/utils/preferences_utils.dart | 16 +++ lib/features/common/confirmation_dialogs.dart | 3 +- .../data/config_option_repository.dart | 101 ++++++++++-------- .../notifier/config_option_notifier.dart | 58 ++++++++-- .../overview/config_options_page.dart | 49 ++++++++- .../profile/details/profile_details_page.dart | 1 + lib/features/profile/widget/profile_tile.dart | 1 + lib/singbox/model/singbox_config_option.dart | 5 +- libcore | 2 +- pubspec.lock | 32 ++++++ pubspec.yaml | 1 + 13 files changed, 216 insertions(+), 63 deletions(-) diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 1bce00fe..3aee9c91 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -17,7 +17,8 @@ "decline": "Decline", "unknown": "Unknown", "hidden": "Hidden", - "timeout": "timeout" + "timeout": "timeout", + "clipboardExportSuccessMsg": "Added to Clipboard" }, "intro": { "termsAndPolicyCaution(rich)": "by continuing you agree with ${tap(@:about.termsAndConditions)}", @@ -166,6 +167,10 @@ "requiresRestartMsg": "For this to take effect restart the app", "experimental": "Experimental", "experimentalMsg": "Features with Experimental flag are still in development and might cause issues.", + "exportOptions": "Export Options to Clipboard", + "exportAllOptions": "Export Options to Clipboard (debug)", + "importOptions": "Import Options from Clipboard", + "importOptionsMsg": "This will rewrite all config options with provided values. Are you sure?", "general": { "sectionTitle": "General", "locale": "Language", diff --git a/build.yaml b/build.yaml index ea4b8fac..8aed9949 100644 --- a/build.yaml +++ b/build.yaml @@ -1,6 +1,9 @@ targets: $default: builders: + json_serializable: + options: + explicit_to_json: true drift_dev: options: store_date_time_values_as_text: true diff --git a/lib/core/utils/preferences_utils.dart b/lib/core/utils/preferences_utils.dart index b79b5d8d..2cbd32dd 100644 --- a/lib/core/utils/preferences_utils.dart +++ b/lib/core/utils/preferences_utils.dart @@ -71,6 +71,17 @@ class PreferencesEntry with InfraLogger { } } + Future writeRaw(P input) async { + final T value; + if (mapFrom != null) { + value = mapFrom!(input); + } else { + value = input as T; + } + if (await write(value)) return value; + return null; + } + Future remove() async { try { await preferences.remove(key); @@ -145,6 +156,11 @@ class PreferencesNotifier extends StateNotifier { return value as P; } + Future updateRaw(P input) async { + final value = await entry.writeRaw(input); + if (value != null) state = value; + } + Future update(T value) async { if (await entry.write(value)) state = value; } diff --git a/lib/features/common/confirmation_dialogs.dart b/lib/features/common/confirmation_dialogs.dart index 51085309..9a136196 100644 --- a/lib/features/common/confirmation_dialogs.dart +++ b/lib/features/common/confirmation_dialogs.dart @@ -1,4 +1,3 @@ -import 'package:fluentui_system_icons/fluentui_system_icons.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -13,7 +12,7 @@ Future showConfirmationDialog( builder: (context) { final localizations = MaterialLocalizations.of(context); return AlertDialog( - icon: const Icon(FluentIcons.delete_24_regular), + icon: icon != null ? Icon(icon) : null, title: Text(title), content: Text(message), actions: [ diff --git a/lib/features/config_option/data/config_option_repository.dart b/lib/features/config_option/data/config_option_repository.dart index e607a553..db77793d 100644 --- a/lib/features/config_option/data/config_option_repository.dart +++ b/lib/features/config_option/data/config_option_repository.dart @@ -284,47 +284,58 @@ abstract class ConfigOptions { }, ); - /// list of all config option preferences - static final preferences = [ - serviceMode, - logLevel, - resolveDestination, - ipv6Mode, - remoteDnsAddress, - remoteDnsDomainStrategy, - directDnsAddress, - directDnsDomainStrategy, - mixedPort, - localDnsPort, - tunImplementation, - mtu, - strictRoute, - connectionTestUrl, - urlTestInterval, - clashApiPort, - bypassLan, - allowConnectionFromLan, - enableDnsRouting, - enableTlsFragment, - tlsFragmentSize, - tlsFragmentSleep, - enableTlsMixedSniCase, - enableTlsPadding, - tlsPaddingSize, - enableMux, - muxPadding, - muxMaxStreams, - muxProtocol, - enableWarp, - warpDetourMode, - warpLicenseKey, - warpAccountId, - warpAccessToken, - warpCleanIp, - warpPort, - warpNoise, - warpWireguardConfig, - ]; + /// preferences to exclude from share and export + static final privatePreferencesKeys = { + "warp.license-key", + "warp.access-token", + "warp.account-id", + "warp.wireguard-config", + }; + + static final Map> + preferences = { + "service-mode": serviceMode, + "log-level": logLevel, + "resolve-destination": resolveDestination, + "ipv6-mode": ipv6Mode, + "remote-dns-address": remoteDnsAddress, + "remote-dns-domain-strategy": remoteDnsDomainStrategy, + "direct-dns-address": directDnsAddress, + "direct-dns-domain-strategy": directDnsDomainStrategy, + "mixed-port": mixedPort, + "local-dns-port": localDnsPort, + "tun-implementation": tunImplementation, + "mtu": mtu, + "strict-route": strictRoute, + "connection-test-url": connectionTestUrl, + "url-test-interval": urlTestInterval, + "clash-api-port": clashApiPort, + "bypass-lan": bypassLan, + "allow-connection-from-lan": allowConnectionFromLan, + "enable-dns-routing": enableDnsRouting, + "enable-tls-fragment": enableTlsFragment, + "tls-fragment-size": tlsFragmentSize, + "tls-fragment-sleep": tlsFragmentSleep, + "enable-tls-mixed-sni-case": enableTlsMixedSniCase, + "enable-tls-padding": enableTlsPadding, + "tls-padding-size": tlsPaddingSize, + "enable-mux": enableMux, + "mux-padding": muxPadding, + "mux-max-streams": muxMaxStreams, + "mux-protocol": muxProtocol, + + // warp + "warp.enable": enableWarp, + "warp.mode": warpDetourMode, + "warp.license-key": warpLicenseKey, + "warp.account-id": warpAccountId, + "warp.access-token": warpAccessToken, + "warp.clean-ip": warpCleanIp, + "warp.clean-port": warpPort, + "warp.noise": warpNoise, + "warp.noise-delay": warpNoiseDelay, + "warp.wireguard-config": warpWireguardConfig, + }; static final singboxConfigOptions = FutureProvider( (ref) async { @@ -411,8 +422,8 @@ abstract class ConfigOptions { accessToken: ref.watch(warpAccessToken), cleanIp: ref.watch(warpCleanIp), cleanPort: ref.watch(warpPort), - warpNoise: ref.watch(warpNoise), - warpNoiseDelay: ref.watch(warpNoiseDelay), + noise: ref.watch(warpNoise), + noiseDelay: ref.watch(warpNoiseDelay), ), geoipPath: ref.watch(geoAssetPathResolverProvider).relativePath( geoAssets.geoip.providerName, @@ -477,8 +488,8 @@ abstract class ConfigOptions { accessToken: ref.read(warpAccessToken), cleanIp: ref.read(warpCleanIp), cleanPort: ref.read(warpPort), - warpNoise: ref.read(warpNoise), - warpNoiseDelay: ref.read(warpNoiseDelay), + noise: ref.read(warpNoise), + noiseDelay: ref.read(warpNoiseDelay), ), geoipPath: "", geositePath: "", diff --git a/lib/features/config_option/notifier/config_option_notifier.dart b/lib/features/config_option/notifier/config_option_notifier.dart index 0d41031b..4c04e621 100644 --- a/lib/features/config_option/notifier/config_option_notifier.dart +++ b/lib/features/config_option/notifier/config_option_notifier.dart @@ -5,6 +5,7 @@ import 'package:hiddify/features/config_option/data/config_option_repository.dar 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:json_path/json_path.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'config_option_notifier.g.dart'; @@ -36,18 +37,57 @@ class ConfigOptionNotifier extends _$ConfigOptionNotifier with AppLogger { DateTime? _lastUpdate; - Future exportJsonToClipboard() async { - final map = { - for (final option in ConfigOptions.preferences) - ref.read(option.notifier).entry.key: ref.read(option.notifier).raw(), - }; - const encoder = JsonEncoder.withIndent(' '); - final json = encoder.convert(map); - await Clipboard.setData(ClipboardData(text: json)); + Future exportJsonToClipboard({bool excludePrivate = true}) async { + try { + final options = await ref.read(ConfigOptions.singboxConfigOptions.future); + Map map = options.toJson(); + if (excludePrivate) { + for (final key in ConfigOptions.privatePreferencesKeys) { + final query = key.split('.').map((e) => '["$e"]').join(); + final res = JsonPath('\$$query').read(map).firstOrNull; + if (res != null) { + map = res.pointer.remove(map)! as Map; + } + } + } + + const encoder = JsonEncoder.withIndent(' '); + final json = encoder.convert(map); + await Clipboard.setData(ClipboardData(text: json)); + return true; + } catch (e, st) { + loggy.warning("error exporting config options to clipboard", e, st); + return false; + } + } + + Future importFromClipboard() async { + try { + final input = + await Clipboard.getData("text/plain").then((value) => value?.text); + if (input == null) return false; + if (jsonDecode(input) case final Map map) { + for (final option in ConfigOptions.preferences.entries) { + final query = option.key.split('.').map((e) => '["$e"]').join(); + final res = JsonPath('\$$query').read(map).firstOrNull; + if (res?.value case final value?) { + try { + await ref.read(option.value.notifier).updateRaw(value); + } catch (e) { + loggy.debug("error updating [${option.key}]: $e", e); + } + } + } + } + return true; + } catch (e, st) { + loggy.warning("error importing config options to clipboard", e, st); + return false; + } } Future resetOption() async { - for (final option in ConfigOptions.preferences) { + for (final option in ConfigOptions.preferences.values) { await ref.read(option.notifier).reset(); } ref.invalidateSelf(); diff --git a/lib/features/config_option/overview/config_options_page.dart b/lib/features/config_option/overview/config_options_page.dart index 571ae985..a916cad1 100644 --- a/lib/features/config_option/overview/config_options_page.dart +++ b/lib/features/config_option/overview/config_options_page.dart @@ -3,8 +3,11 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/optional_range.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/tip_card.dart'; +import 'package:hiddify/features/common/confirmation_dialogs.dart'; import 'package:hiddify/features/common/nested_app_bar.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; import 'package:hiddify/features/config_option/notifier/config_option_notifier.dart'; @@ -40,10 +43,50 @@ class ConfigOptionsPage extends HookConsumerWidget { itemBuilder: (context) { return [ PopupMenuItem( - onTap: ref + onTap: () async => ref .read(configOptionNotifierProvider.notifier) - .exportJsonToClipboard, - child: Text(t.general.addToClipboard), + .exportJsonToClipboard() + .then((success) { + if (success) { + ref + .read(inAppNotificationControllerProvider) + .showSuccessToast( + t.general.clipboardExportSuccessMsg, + ); + } + }), + child: Text(t.settings.exportOptions), + ), + if (ref.watch(debugModeNotifierProvider)) + PopupMenuItem( + onTap: () async => ref + .read(configOptionNotifierProvider.notifier) + .exportJsonToClipboard(excludePrivate: false) + .then((success) { + if (success) { + ref + .read(inAppNotificationControllerProvider) + .showSuccessToast( + t.general.clipboardExportSuccessMsg, + ); + } + }), + child: Text(t.settings.exportAllOptions), + ), + PopupMenuItem( + onTap: () async { + final shouldImport = await showConfirmationDialog( + context, + title: t.settings.importOptions, + message: t.settings.importOptionsMsg, + ); + if (shouldImport) { + await ref + .read(configOptionNotifierProvider.notifier) + .importFromClipboard(); + } + }, + child: Text(t.settings.importOptions), ), PopupMenuItem( child: Text(t.settings.config.resetBtn), diff --git a/lib/features/profile/details/profile_details_page.dart b/lib/features/profile/details/profile_details_page.dart index e1ae13b9..472d1746 100644 --- a/lib/features/profile/details/profile_details_page.dart +++ b/lib/features/profile/details/profile_details_page.dart @@ -116,6 +116,7 @@ class ProfileDetailsPage extends HookConsumerWidget with PresLogger { context, title: t.profile.delete.buttonTxt, message: t.profile.delete.confirmationMsg, + icon: FluentIcons.delete_24_regular, ); if (deleteConfirmed) { await notifier.delete(); diff --git a/lib/features/profile/widget/profile_tile.dart b/lib/features/profile/widget/profile_tile.dart index e2cb7288..93b5a9cc 100644 --- a/lib/features/profile/widget/profile_tile.dart +++ b/lib/features/profile/widget/profile_tile.dart @@ -327,6 +327,7 @@ class ProfileActionsMenu extends HookConsumerWidget { context, title: t.profile.delete.buttonTxt, message: t.profile.delete.confirmationMsg, + icon: FluentIcons.delete_24_regular, ); if (deleteConfirmed) { deleteProfileMutation.setFuture( diff --git a/lib/singbox/model/singbox_config_option.dart b/lib/singbox/model/singbox_config_option.dart index 6b107aba..10397446 100644 --- a/lib/singbox/model/singbox_config_option.dart +++ b/lib/singbox/model/singbox_config_option.dart @@ -68,6 +68,7 @@ class SingboxConfigOption with _$SingboxConfigOption { @freezed class SingboxWarpOption with _$SingboxWarpOption { + @JsonSerializable(fieldRename: FieldRename.kebab, createFieldMap: true) const factory SingboxWarpOption({ required bool enable, required WarpDetourMode mode, @@ -77,8 +78,8 @@ class SingboxWarpOption with _$SingboxWarpOption { required String accessToken, required String cleanIp, required int cleanPort, - @OptionalRangeJsonConverter() required OptionalRange warpNoise, - @OptionalRangeJsonConverter() required OptionalRange warpNoiseDelay, + @OptionalRangeJsonConverter() required OptionalRange noise, + @OptionalRangeJsonConverter() required OptionalRange noiseDelay, }) = _SingboxWarpOption; factory SingboxWarpOption.fromJson(Map json) => diff --git a/libcore b/libcore index 3793b614..f9e6f022 160000 --- a/libcore +++ b/libcore @@ -1 +1 @@ -Subproject commit 3793b614dbcb80468100836c01a2eaf94fa093a5 +Subproject commit f9e6f022c89604c2dac87d0ddd0831f8337fcdc3 diff --git a/pubspec.lock b/pubspec.lock index e2a142e9..71c3cdda 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -813,6 +813,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.4" + iregexp: + dependency: transitive + description: + name: iregexp + sha256: "143859dcaeecf6f683102786762d70a47ef8441a0d2287a158172d32d38799cf" + url: "https://pub.dev" + source: hosted + version: "0.1.2" js: dependency: transitive description: @@ -837,6 +845,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" + json_path: + dependency: "direct main" + description: + name: json_path + sha256: "149d32ceb7dc22422ea6d09e401fd688f54e1343bc9ff8c3cb1900ca3b1ad8b1" + url: "https://pub.dev" + source: hosted + version: "0.7.1" json_serializable: dependency: "direct dev" description: @@ -893,6 +909,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.0" + maybe_just_nothing: + dependency: transitive + description: + name: maybe_just_nothing + sha256: "0c06326e26d08f6ed43247404376366dc4d756cef23a4f1db765f546224c35e0" + url: "https://pub.dev" + source: hosted + version: "0.5.3" menu_base: dependency: transitive description: @@ -1229,6 +1253,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.2" + rfc_6901: + dependency: transitive + description: + name: rfc_6901 + sha256: df1bbfa3d023009598f19636d6114c6ac1e0b7bb7bf6a260f0e6e6ce91416820 + url: "https://pub.dev" + source: hosted + version: "0.2.0" riverpod: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c19d9c19..ab48500a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,6 +76,7 @@ dependencies: circle_flags: ^4.0.2 http: ^1.2.0 timezone_to_country: ^2.1.0 + json_path: ^0.7.1 dev_dependencies: flutter_test: