From 55230f6f0f316dcd3fe86a60e8e165644dc0b72c Mon Sep 17 00:00:00 2001 From: TheLastFlame Date: Thu, 3 Oct 2024 17:41:09 +1000 Subject: [PATCH 1/3] refactor: replace HookConsumerWidget with ConsumerWidget and add ThemeModePrefTile --- lib/features/common/general_pref_tiles.dart | 50 +++++++++++++++++-- .../widgets/general_setting_tiles.dart | 31 +----------- 2 files changed, 46 insertions(+), 35 deletions(-) diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index aad736f8e..0230602cf 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -5,12 +5,12 @@ import 'package:hiddify/core/localization/locale_extensions.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/region.dart'; -import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/core/theme/app_theme_mode.dart'; +import 'package:hiddify/core/theme/theme_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'; -class LocalePrefTile extends HookConsumerWidget { +class LocalePrefTile extends ConsumerWidget { const LocalePrefTile({super.key}); @override @@ -50,7 +50,7 @@ class LocalePrefTile extends HookConsumerWidget { } } -class RegionPrefTile extends HookConsumerWidget { +class RegionPrefTile extends ConsumerWidget { const RegionPrefTile({super.key}); @override @@ -102,7 +102,7 @@ class RegionPrefTile extends HookConsumerWidget { } } -class EnableAnalyticsPrefTile extends HookConsumerWidget { +class EnableAnalyticsPrefTile extends ConsumerWidget { const EnableAnalyticsPrefTile({ super.key, this.onChanged, @@ -137,3 +137,43 @@ class EnableAnalyticsPrefTile extends HookConsumerWidget { ); } } + +class ThemeModePrefTile extends ConsumerWidget { + const ThemeModePrefTile({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final themeMode = ref.watch(themePreferencesProvider); + + return ListTile( + title: Text(t.settings.general.themeMode), + subtitle: Text(themeMode.present(t)), + leading: const Icon(FluentIcons.weather_moon_20_regular), + onTap: () async { + final selectedThemeMode = await showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: Text(t.settings.general.themeMode), + children: AppThemeMode.values + .map( + (e) => RadioListTile( + title: Text(e.present(t)), + value: e, + groupValue: themeMode, + onChanged: Navigator.of(context).maybePop, + ), + ) + .toList(), + ); + }, + ); + if (selectedThemeMode != null) { + await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode); + } + }, + ); + } +} diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index 5d9cc0c9b..eec40da80 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -19,39 +19,10 @@ class GeneralSettingTiles extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final t = ref.watch(translationsProvider); - final themeMode = ref.watch(themePreferencesProvider); - return Column( children: [ const LocalePrefTile(), - ListTile( - title: Text(t.settings.general.themeMode), - subtitle: Text(themeMode.present(t)), - leading: const Icon(FluentIcons.weather_moon_20_regular), - onTap: () async { - final selectedThemeMode = await showDialog( - context: context, - builder: (context) { - return SimpleDialog( - title: Text(t.settings.general.themeMode), - children: AppThemeMode.values - .map( - (e) => RadioListTile( - title: Text(e.present(t)), - value: e, - groupValue: themeMode, - onChanged: Navigator.of(context).maybePop, - ), - ) - .toList(), - ); - }, - ); - if (selectedThemeMode != null) { - await ref.read(themePreferencesProvider.notifier).changeThemeMode(selectedThemeMode); - } - }, - ), + const ThemeModePrefTile(), const EnableAnalyticsPrefTile(), SwitchListTile( title: Text(t.settings.general.autoIpCheck), From eacdfd8d7c35fcad2258eed89efe8646ad4a3b33 Mon Sep 17 00:00:00 2001 From: TheLastFlame Date: Thu, 3 Oct 2024 17:43:42 +1000 Subject: [PATCH 2/3] chore: update .gitignore to exclude /data directory --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 758f1ce0d..ec505ec85 100644 --- a/.gitignore +++ b/.gitignore @@ -55,3 +55,6 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + + +/data \ No newline at end of file From e3cc9180e06abca6221ffb4e4f6cc0e692eab5b8 Mon Sep 17 00:00:00 2001 From: TheLastFlame Date: Thu, 3 Oct 2024 19:32:23 +1000 Subject: [PATCH 3/3] feat: add action options for closing the application --- assets/translations/strings_en.i18n.json | 13 +++- assets/translations/strings_ru.i18n.json | 13 +++- lib/core/preferences/actions_at_closing.dart | 13 ++++ lib/core/preferences/general_preferences.dart | 8 +++ lib/features/common/general_pref_tiles.dart | 42 +++++++++++ .../widgets/general_setting_tiles.dart | 3 +- .../window/widget/window_closing_dialog.dart | 69 +++++++++++++++++++ .../window/widget/window_wrapper.dart | 43 ++++++------ 8 files changed, 174 insertions(+), 30 deletions(-) create mode 100644 lib/core/preferences/actions_at_closing.dart create mode 100644 lib/features/window/widget/window_closing_dialog.dart diff --git a/assets/translations/strings_en.i18n.json b/assets/translations/strings_en.i18n.json index 01a02cbf7..dc48744c7 100644 --- a/assets/translations/strings_en.i18n.json +++ b/assets/translations/strings_en.i18n.json @@ -199,7 +199,13 @@ "ignoreBatteryOptimizationsMsg": "Remove Restrictions For Optimal VPN Performance", "dynamicNotification": "Display Speed in Notification", "hapticFeedback": "Haptic Feedback", - "autoIpCheck": "Automatically Check Connection IP" + "autoIpCheck": "Automatically Check Connection IP", + "actionAtClosing": "Action at closing", + "actionsAtClosing": { + "askEachTime": "Ask each time", + "hide": "Hide", + "exit": "Exit" + } }, "advanced": { "sectionTitle": "Advanced", @@ -425,6 +431,7 @@ "window": { "hide": "Hide", "close": "Exit", - "alertMessage": "Hide or Exit the application?" + "alertMessage": "Hide or Exit the application?", + "remember": "Remember my choice" } -} +} \ No newline at end of file diff --git a/assets/translations/strings_ru.i18n.json b/assets/translations/strings_ru.i18n.json index f66677874..63a5deb7e 100644 --- a/assets/translations/strings_ru.i18n.json +++ b/assets/translations/strings_ru.i18n.json @@ -199,7 +199,13 @@ "ignoreBatteryOptimizationsMsg": "Отключение ограничений для оптимальной производительности VPN", "dynamicNotification": "Отображение скорости в уведомлении", "hapticFeedback": "Тактильная обратная связь", - "autoIpCheck": "Автоматически проверять IP-адрес соединения" + "autoIpCheck": "Автоматически проверять IP-адрес соединения", + "actionAtClosing": "Действие при закрытии", + "actionsAtClosing": { + "askEachTime": "Каждый раз спрашивать", + "hide": "Скрыть", + "exit": "Выйти" + } }, "advanced": { "sectionTitle": "Расширенные", @@ -425,6 +431,7 @@ "window": { "hide": "Скрыть", "close": "Закрыть", - "alertMessage": "Скрыть или выйти из приложения?" + "alertMessage": "Скрыть приложение или выйти?", + "remember": "Запомнить выбор" } -} +} \ No newline at end of file diff --git a/lib/core/preferences/actions_at_closing.dart b/lib/core/preferences/actions_at_closing.dart new file mode 100644 index 000000000..e9c0247f1 --- /dev/null +++ b/lib/core/preferences/actions_at_closing.dart @@ -0,0 +1,13 @@ +import 'package:hiddify/gen/translations.g.dart'; + +enum ActionsAtClosing { + ask, + hide, + exit; + + String present(TranslationsEn t) => switch (this) { + ask => t.settings.general.actionsAtClosing.askEachTime, + hide => t.settings.general.actionsAtClosing.hide, + exit => t.settings.general.actionsAtClosing.exit, + }; +} diff --git a/lib/core/preferences/general_preferences.dart b/lib/core/preferences/general_preferences.dart index 0c92e31e9..cbc4b1008 100644 --- a/lib/core/preferences/general_preferences.dart +++ b/lib/core/preferences/general_preferences.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:hiddify/core/app_info/app_info_provider.dart'; import 'package:hiddify/core/model/environment.dart'; +import 'package:hiddify/core/preferences/actions_at_closing.dart'; // import 'package:hiddify/core/model/region.dart'; import 'package:hiddify/core/preferences/preferences_provider.dart'; import 'package:hiddify/core/utils/preferences_utils.dart'; @@ -61,6 +62,13 @@ abstract class Preferences { "store_reviewed_by_user", false, ); + + static final actionAtClose = PreferencesNotifier.create( + "action_at_close", + ActionsAtClosing.ask, + mapFrom: ActionsAtClosing.values.byName, + mapTo: (value) => value.name, + ); } @Riverpod(keepAlive: true) diff --git a/lib/features/common/general_pref_tiles.dart b/lib/features/common/general_pref_tiles.dart index 0230602cf..dfe379e8e 100644 --- a/lib/features/common/general_pref_tiles.dart +++ b/lib/features/common/general_pref_tiles.dart @@ -5,6 +5,8 @@ import 'package:hiddify/core/localization/locale_extensions.dart'; import 'package:hiddify/core/localization/locale_preferences.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/model/region.dart'; +import 'package:hiddify/core/preferences/actions_at_closing.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/core/theme/app_theme_mode.dart'; import 'package:hiddify/core/theme/theme_preferences.dart'; import 'package:hiddify/features/config_option/data/config_option_repository.dart'; @@ -177,3 +179,43 @@ class ThemeModePrefTile extends ConsumerWidget { ); } } + +class ClosingPrefTile extends ConsumerWidget { + const ClosingPrefTile({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final t = ref.watch(translationsProvider); + + final action = ref.watch(Preferences.actionAtClose); + + return ListTile( + title: Text(t.settings.general.actionAtClosing), + subtitle: Text(action.present(t)), + leading: const Icon(FluentIcons.arrow_exit_20_regular), + onTap: () async { + final selectedAction = await showDialog( + context: context, + builder: (context) { + return SimpleDialog( + title: Text(t.settings.general.actionAtClosing), + children: ActionsAtClosing.values + .map( + (e) => RadioListTile( + title: Text(e.present(t)), + value: e, + groupValue: action, + onChanged: Navigator.of(context).maybePop, + ), + ) + .toList(), + ); + }, + ); + if (selectedAction != null) { + await ref.read(Preferences.actionAtClose.notifier).update(selectedAction); + } + }, + ); + } +} diff --git a/lib/features/settings/widgets/general_setting_tiles.dart b/lib/features/settings/widgets/general_setting_tiles.dart index eec40da80..92047d80c 100644 --- a/lib/features/settings/widgets/general_setting_tiles.dart +++ b/lib/features/settings/widgets/general_setting_tiles.dart @@ -5,8 +5,6 @@ import 'package:flutter/material.dart'; import 'package:hiddify/core/haptic/haptic_service.dart'; import 'package:hiddify/core/localization/translations.dart'; import 'package:hiddify/core/preferences/general_preferences.dart'; -import 'package:hiddify/core/theme/app_theme_mode.dart'; -import 'package:hiddify/core/theme/theme_preferences.dart'; import 'package:hiddify/features/auto_start/notifier/auto_start_notifier.dart'; import 'package:hiddify/features/common/general_pref_tiles.dart'; import 'package:hiddify/utils/utils.dart'; @@ -47,6 +45,7 @@ class GeneralSettingTiles extends HookConsumerWidget { ), ], if (PlatformUtils.isDesktop) ...[ + const ClosingPrefTile(), SwitchListTile( title: Text(t.settings.general.autoStart), value: ref.watch(autoStartNotifierProvider).asData!.value, diff --git a/lib/features/window/widget/window_closing_dialog.dart b/lib/features/window/widget/window_closing_dialog.dart new file mode 100644 index 000000000..6d8933439 --- /dev/null +++ b/lib/features/window/widget/window_closing_dialog.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/actions_at_closing.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; +import 'package:hiddify/features/window/notifier/window_notifier.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class WindowClosingDialog extends ConsumerStatefulWidget { + const WindowClosingDialog({super.key}); + + @override + ConsumerState createState() => _WindowClosingDialogState(); +} + +class _WindowClosingDialogState extends ConsumerState { + bool remember = false; + + @override + Widget build(BuildContext context) { + final t = ref.watch(translationsProvider); + + return AlertDialog( + title: Text(t.window.alertMessage), + content: GestureDetector( + onTap: () => setState(() { + remember = !remember; + }), + behavior: HitTestBehavior.translucent, + child: Row( + children: [ + Checkbox( + value: remember, + onChanged: (v) { + remember = v ?? remember; + setState(() {}); + }, + ), + const SizedBox(width: 16), + Text( + t.window.remember, + style: const TextStyle(fontSize: 16), + ), + ], + ), + ), + actions: [ + TextButton( + onPressed: () { + if (remember) { + ref.read(Preferences.actionAtClose.notifier).update(ActionsAtClosing.exit); + } + ref.read(windowNotifierProvider.notifier).quit(); + }, + child: Text(t.window.close), + ), + FilledButton( + onPressed: () async { + if (remember) { + ref.read(Preferences.actionAtClose.notifier).update(ActionsAtClosing.hide); + } + Navigator.of(context).maybePop(false); + await ref.read(windowNotifierProvider.notifier).close(); + }, + child: Text(t.window.hide), + ), + ], + ); + } +} diff --git a/lib/features/window/widget/window_wrapper.dart b/lib/features/window/widget/window_wrapper.dart index 05a7151db..83694726e 100644 --- a/lib/features/window/widget/window_wrapper.dart +++ b/lib/features/window/widget/window_wrapper.dart @@ -1,10 +1,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:hiddify/core/localization/translations.dart'; +import 'package:hiddify/core/preferences/actions_at_closing.dart'; +import 'package:hiddify/core/preferences/general_preferences.dart'; import 'package:hiddify/features/common/adaptive_root_scaffold.dart'; -import 'package:hiddify/features/connection/notifier/connection_notifier.dart'; import 'package:hiddify/features/window/notifier/window_notifier.dart'; +import 'package:hiddify/features/window/widget/window_closing_dialog.dart'; import 'package:hiddify/utils/custom_loggers.dart'; import 'package:hiddify/utils/platform_utils.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -22,6 +23,8 @@ class WindowWrapper extends StatefulHookConsumerWidget { class _WindowWrapperState extends ConsumerState with WindowListener, AppLogger { late AlertDialog closeDialog; + bool isWindowClosingDialogOpened = false; + @override Widget build(BuildContext context) { ref.watch(windowNotifierProvider); @@ -52,27 +55,23 @@ class _WindowWrapperState extends ConsumerState with WindowListen await ref.read(windowNotifierProvider.notifier).close(); return; } - final t = ref.watch(translationsProvider); - await showDialog( - context: RootScaffold.stateKey.currentContext!, - builder: (BuildContext context) => AlertDialog( - title: Text(t.window.alertMessage), - actions: [ - TextButton( - onPressed: () async => await ref.read(windowNotifierProvider.notifier).quit(), - child: Text(t.window.close.toUpperCase()), - ), - TextButton( - onPressed: () async { - Navigator.of(context).maybePop(false); - await ref.read(windowNotifierProvider.notifier).close(); - }, - child: Text(t.window.hide.toUpperCase()), - ), - ], - ), - ); + switch (ref.read(Preferences.actionAtClose)) { + case ActionsAtClosing.ask: + if (isWindowClosingDialogOpened) return; + isWindowClosingDialogOpened = true; + await showDialog( + context: RootScaffold.stateKey.currentContext!, + builder: (BuildContext context) => const WindowClosingDialog(), + ); + isWindowClosingDialogOpened = false; + + case ActionsAtClosing.hide: + await ref.read(windowNotifierProvider.notifier).close(); + + case ActionsAtClosing.exit: + await ref.read(windowNotifierProvider.notifier).quit(); + } } @override