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/authentication.dart b/lib/common/actions/authentication.dart new file mode 100644 index 00000000..2cde58c0 --- /dev/null +++ b/lib/common/actions/authentication.dart @@ -0,0 +1,53 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:local_auth/local_auth.dart'; +import 'package:restart_app/restart_app.dart'; + +import '../constants/constants.dart'; +import '../extensions/build_context_extension.dart'; +import '../preferences/preference_key.dart'; +import '../ui/snack_bar_utils.dart'; + +/// Asks the user to authenticate for the specified [reason] using the device lock method. +Future authenticate(BuildContext context, {required String reason}) async { + final localAuthentication = LocalAuthentication(); + + final canAuthenticate = await localAuthentication.isDeviceSupported(); + + // If the device has no authentication methods available, + // disable the app lock and restart it to remove the lock screen + if (!canAuthenticate) { + await PreferenceKey.lockApp.set(false); + + // The Restart package crashes the app if used in debug mode + if (kReleaseMode) { + await Restart.restartApp(); + } + + return false; + } + + bool authenticated; + try { + authenticated = await localAuthentication.authenticate(localizedReason: reason); + } on LocalAuthException catch (exception) { + if (exception.code != LocalAuthExceptionCode.userCanceled) { + logger.w("Authentication failed", exception); + } + + authenticated = false; + } + + // The authentication failed + if (!authenticated) { + if (!context.mounted) { + return false; + } + + SnackBarUtils().show(context, text: context.l.snack_bar_authentication_failed); + + return false; + } + + return true; +} 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/actions/labels/delete.dart b/lib/common/actions/labels/delete.dart index 6473bb59..ff65db37 100644 --- a/lib/common/actions/labels/delete.dart +++ b/lib/common/actions/labels/delete.dart @@ -5,6 +5,7 @@ import '../../../models/label/label.dart'; import '../../../providers/labels/labels/labels_provider.dart'; import '../../dialogs/confirmation_dialog.dart'; import '../../extensions/build_context_extension.dart'; +import '../authentication.dart'; import 'select.dart'; /// Deletes the [label]. @@ -23,6 +24,19 @@ Future deleteLabel(BuildContext context, WidgetRef ref, {required Label la return false; } + if (!context.mounted) { + return false; + } + + // If required, ask for authentication + if (label.requiresAuthentication) { + final authenticated = await authenticate(context, reason: context.l.lock_page_reason_action); + + if (!authenticated) { + return false; + } + } + return await ref.read(labelsProvider.notifier).delete([label]); } @@ -42,6 +56,20 @@ Future deleteLabels(BuildContext context, WidgetRef ref, {required List label.requiresAuthentication); + if (requiresAuthentication) { + final authenticated = await authenticate(context, reason: context.l.lock_page_reason_action); + + if (!authenticated) { + return false; + } + } + final succeeded = await ref.read(labelsProvider.notifier).delete(labels); if (context.mounted) { diff --git a/lib/common/actions/labels/lock.dart b/lib/common/actions/labels/lock.dart index 2d6705c7..fc4dfea1 100644 --- a/lib/common/actions/labels/lock.dart +++ b/lib/common/actions/labels/lock.dart @@ -1,23 +1,18 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:local_auth/local_auth.dart'; import '../../../models/label/label.dart'; import '../../../providers/labels/labels/labels_provider.dart'; import '../../extensions/build_context_extension.dart'; -import '../../preferences/preference_key.dart'; +import '../authentication.dart'; import 'select.dart'; /// Toggles whether the [labels] are locked. Future toggleLockLabels(BuildContext context, WidgetRef ref, {required List