From a69eaf01873a7c44cc87f9790c5996dd5cee2082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Sat, 30 May 2026 01:08:20 +0200 Subject: [PATCH] feat: suggest to select a backup location --- lib/app.dart | 9 +++- lib/common/actions/backup.dart | 33 ++++++++++++++ lib/common/dialogs/require_backup_dialog.dart | 28 ++++++++++++ lib/l10n/translations/app_en.arb | 16 +++++++ .../settings/pages/settings_backup_page.dart | 43 ++++++++----------- lib/services/backup/auto_backup_service.dart | 7 ++- 6 files changed, 109 insertions(+), 27 deletions(-) create mode 100644 lib/common/actions/backup.dart create mode 100644 lib/common/dialogs/require_backup_dialog.dart diff --git a/lib/app.dart b/lib/app.dart index 9af2a2a5..6972a28f 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -8,6 +8,7 @@ import 'package:flutter_checklist/checklist.dart'; import 'package:flutter_fgbg/flutter_fgbg.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'common/actions/backup.dart'; import 'common/constants/constants.dart'; import 'common/enums/supported_language.dart'; import 'common/extensions/locale_extension.dart'; @@ -21,6 +22,7 @@ import 'providers/labels/labels_list/labels_list_provider.dart'; import 'providers/labels/labels_navigation/labels_navigation_provider.dart'; import 'providers/notifiers/notifiers.dart'; import 'providers/preferences/preferences_provider.dart'; +import 'services/backup/auto_backup_service.dart'; /// MaterialNotes application. class App extends ConsumerStatefulWidget { @@ -53,9 +55,14 @@ class _AppState extends ConsumerState with AfterLayoutMixin { } @override - FutureOr afterFirstLayout(BuildContext context) { + Future afterFirstLayout(BuildContext context) async { // Using the context provided by afterFirstLayout doesn't work SystemUtils().setQuickActions(rootNavigatorKey.currentContext!); + + // If the backup directory is still the default, ask to select it + if (await AutoExportUtils().isAutoExportDirectoryDefault) { + await requireBackupDirectory(rootNavigatorKey.currentContext!); + } } @override diff --git a/lib/common/actions/backup.dart b/lib/common/actions/backup.dart new file mode 100644 index 00000000..43e5ef28 --- /dev/null +++ b/lib/common/actions/backup.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; + +import '../../services/backup/auto_backup_service.dart'; +import '../dialogs/require_backup_dialog.dart'; +import '../files/files_utils.dart'; +import '../preferences/preference_key.dart'; + +/// Requires the user to select a backup directory. +Future requireBackupDirectory(BuildContext context) async { + final select = await showAdaptiveDialog( + context: context, + useRootNavigator: false, + builder: (context) => RequireBackupDialog(), + ); + + if (select == null || !select) { + return; + } + + await selectBackupDirectory(); +} + +/// Asks the user to select a backup directory. +Future selectBackupDirectory() async { + final autoExportDirectory = await selectDirectory(); + + if (autoExportDirectory == null) { + return; + } + + await PreferenceKey.autoExportDirectory.set(autoExportDirectory); + await AutoExportUtils().setAutoExportDirectory(); +} diff --git a/lib/common/dialogs/require_backup_dialog.dart b/lib/common/dialogs/require_backup_dialog.dart new file mode 100644 index 00000000..75166bbc --- /dev/null +++ b/lib/common/dialogs/require_backup_dialog.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; + +import '../extensions/build_context_extension.dart'; + +/// Require backup dialog. +class RequireBackupDialog extends StatelessWidget { + /// Dialog to require the user to select a backup location. + const RequireBackupDialog({super.key}); + + @override + Widget build(BuildContext context) { + return AlertDialog.adaptive( + title: Text(context.l.dialog_require_backup_title), + content: SingleChildScrollView(child: Column(children: [Text(context.l.dialog_require_backup_description)])), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + style: TextButton.styleFrom(foregroundColor: Theme.of(context).colorScheme.error), + child: Text(context.l.dialog_require_backup_ignore), + ), + FilledButton( + onPressed: () => Navigator.pop(context, true), + child: Text(context.l.dialog_require_backup_select), + ), + ], + ); + } +} diff --git a/lib/l10n/translations/app_en.arb b/lib/l10n/translations/app_en.arb index 9d80f487..5708e0c0 100644 --- a/lib/l10n/translations/app_en.arb +++ b/lib/l10n/translations/app_en.arb @@ -835,6 +835,22 @@ "@hint_label_name": { "description": "Hint for the text field of the name of a tag." }, + "dialog_require_backup_title": "Select a backup location", + "@dialog_require_backup_title": { + "description": "Title of the dialog to select a backup location." + }, + "dialog_require_backup_description": "As your notes are only stored locally on your device, any issue could lead to losing them all. Uninstalling the application would also delete all your notes.\n\nSelecting a backup location will ensure you can always retrieve your notes. For example, you could create a folder named \"Material Notes backups\" under your existing \"Documents\" folder.", + "@dialog_require_backup_description": { + "description": "Description of the dialog to select a backup location." + }, + "dialog_require_backup_ignore": "Ignore", + "@dialog_require_backup_ignore": { + "description": "Button to ignore the dialog to select a backup location." + }, + "dialog_require_backup_select": "Select", + "@dialog_require_backup_select": { + "description": "Button to select a backup location in the related dialog." + }, "dialog_export_encryption_password": "Password", "@dialog_export_encryption_password": { "description": "Hint for the password text field in the dialog to configure the encryption of an automatic or manual export." diff --git a/lib/pages/settings/pages/settings_backup_page.dart b/lib/pages/settings/pages/settings_backup_page.dart index 1c59e0ba..82d093cb 100644 --- a/lib/pages/settings/pages/settings_backup_page.dart +++ b/lib/pages/settings/pages/settings_backup_page.dart @@ -6,11 +6,11 @@ import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:settings_tiles/settings_tiles.dart'; import 'package:simple_icons/simple_icons.dart'; +import '../../../common/actions/backup.dart'; import '../../../common/constants/constants.dart'; import '../../../common/constants/paddings.dart'; import '../../../common/extensions/build_context_extension.dart'; import '../../../common/extensions/string_extension.dart'; -import '../../../common/files/files_utils.dart'; import '../../../common/navigation/app_bars/basic_app_bar.dart'; import '../../../common/navigation/top_navigation.dart'; import '../../../common/preferences/preference_key.dart'; @@ -40,7 +40,7 @@ class _SettingsBackupPageState extends ConsumerState { /// Asks the user to choose a JSON file to import. /// /// If the file is encrypted, asks for the password used to encrypt it. - Future _import() async { + Future import() async { try { final imported = await ManualBackupService().import(context); @@ -70,7 +70,7 @@ class _SettingsBackupPageState extends ConsumerState { /// Asks the user to configure the immediate export as JSON. /// /// Asks whether to encrypt and where to store the export file. - Future _exportAsJson() async { + Future exportAsJson() async { await showAdaptiveDialog<(bool, String?)?>( context: context, useRootNavigator: false, @@ -102,7 +102,7 @@ class _SettingsBackupPageState extends ConsumerState { /// Asks the user to configure the immediate export as Markdown. /// /// Asks where to store the export file. - Future _exportAsMarkdown() async { + Future exportAsMarkdown() async { try { final exported = await ManualBackupService().exportAsMarkdown(); @@ -119,7 +119,7 @@ class _SettingsBackupPageState extends ConsumerState { } /// Toggles the setting to enable the automatic export. - Future _toggleEnableAutoExport(bool toggled) async { + Future toggleEnableAutoExport(bool toggled) async { await PreferenceKey.enableAutoExport.set(toggled); setState(() {}); @@ -139,7 +139,7 @@ class _SettingsBackupPageState extends ConsumerState { /// Toggles the setting to enable the automatic export encryption. /// /// If enabled, asks the user for the password used for the encryption. - Future _toggleAutoExportEncryption(bool toggled) async { + Future toggleAutoExportEncryption(bool toggled) async { if (!toggled) { await PreferenceKey.autoExportPassword.remove(); await PreferenceKey.autoExportEncryption.set(false); @@ -170,28 +170,21 @@ class _SettingsBackupPageState extends ConsumerState { } /// Sets automatic export frequency to [frequency]. - Future _submittedAutoExportFrequency(double frequency) async { + Future submittedAutoExportFrequency(double frequency) async { setState(() { PreferenceKey.autoExportFrequency.set(frequency.toInt()); }); } /// Asks the user to choose a directory for the automatic export. - Future _setAutoExportDirectory() async { - final autoExportDirectory = await selectDirectory(); - - if (autoExportDirectory == null) { - return; - } - - await PreferenceKey.autoExportDirectory.set(autoExportDirectory); - await AutoExportUtils().setAutoExportDirectory(); + Future setAutoExportDirectory() async { + await selectBackupDirectory(); setState(() {}); } /// Resets the directory of the automatic export to its default value. - Future _resetAutoExportDirectory() async { + Future resetAutoExportDirectory() async { await PreferenceKey.autoExportDirectory.remove(); await AutoExportUtils().setAutoExportDirectory(); @@ -220,7 +213,7 @@ class _SettingsBackupPageState extends ConsumerState { icon: SettingTileIcon(Icons.file_upload), title: Text(context.l.settings_import), description: Text(context.l.settings_import_description), - onTap: _import, + onTap: import, ), ], ), @@ -231,13 +224,13 @@ class _SettingsBackupPageState extends ConsumerState { icon: SettingTileIcon(SimpleIcons.json), title: Text(context.l.settings_export_json), description: Text(context.l.settings_export_json_description), - onTap: _exportAsJson, + onTap: exportAsJson, ), SettingActionTile( icon: SettingTileIcon(SimpleIcons.markdown), title: Text(context.l.settings_export_markdown), description: Text(context.l.settings_export_markdown_description), - onTap: _exportAsMarkdown, + onTap: exportAsMarkdown, ), ], ), @@ -249,7 +242,7 @@ class _SettingsBackupPageState extends ConsumerState { title: Text(context.l.settings_auto_export), description: Text(context.l.settings_auto_export_description), toggled: enableAutoExport, - onChanged: _toggleEnableAutoExport, + onChanged: toggleEnableAutoExport, ), SettingSwitchTile( enabled: enableAutoExport, @@ -257,7 +250,7 @@ class _SettingsBackupPageState extends ConsumerState { title: Text(context.l.settings_auto_export_encryption), description: Text(context.l.settings_auto_export_encryption_description), toggled: autoExportEncryption, - onChanged: _toggleAutoExportEncryption, + onChanged: toggleAutoExportEncryption, ), SettingCustomSliderTile( enabled: enableAutoExport, @@ -272,7 +265,7 @@ class _SettingsBackupPageState extends ConsumerState { label: (frequency) => context.l.settings_auto_export_frequency_value(frequency.toInt().toString()), values: automaticExportFrequenciesValues, initialValue: autoExportFrequency.toDouble(), - onSubmitted: _submittedAutoExportFrequency, + onSubmitted: submittedAutoExportFrequency, ), SettingActionTile( enabled: enableAutoExport, @@ -283,9 +276,9 @@ class _SettingsBackupPageState extends ConsumerState { trailing: IconButton( icon: const Icon(Symbols.reset_settings), tooltip: context.l.tooltip_reset, - onPressed: enableAutoExport ? _resetAutoExportDirectory : null, + onPressed: enableAutoExport ? resetAutoExportDirectory : null, ), - onTap: _setAutoExportDirectory, + onTap: setAutoExportDirectory, ), ], ), diff --git a/lib/services/backup/auto_backup_service.dart b/lib/services/backup/auto_backup_service.dart index 76e80261..e842755f 100644 --- a/lib/services/backup/auto_backup_service.dart +++ b/lib/services/backup/auto_backup_service.dart @@ -72,13 +72,18 @@ class AutoExportUtils { await createDirectory(autoExportDirectory); } - /// Returns the default automatic export directory. + /// The default automatic export directory. Future get autoExportDirectoryDefault async { final baseDirectory = (await getApplicationDocumentsDirectory()).path; return join(baseDirectory, 'backups'); } + /// Whether the auto export directory is the default one. + Future get isAutoExportDirectoryDefault async { + return PreferenceKey.autoExportDirectory.preferenceOrDefault.isEmpty; + } + /// Checks if an automatic export should be performed. /// /// An automatic export should be performed if it is enabled and either if no automatic export has been performed yet,