Skip to content

[411] Label lock #423

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Mar 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 50 additions & 51 deletions lib/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import 'package:back_button_interceptor/back_button_interceptor.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:flutter_app_lock/flutter_app_lock.dart';
import 'package:flutter_checklist/checklist.dart';
import 'package:flutter_fgbg/flutter_fgbg.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'common/actions/labels/select.dart';
Expand All @@ -16,11 +16,9 @@ import 'common/extensions/locale_extension.dart';
import 'common/preferences/preference_key.dart';
import 'common/system_utils.dart';
import 'common/ui/theme_utils.dart';
import 'common/widgets/placeholders/error_placeholder.dart';
import 'l10n/app_localizations/app_localizations.g.dart';
import 'models/note/note_status.dart';
import 'pages/lock/lock_page.dart';
import 'pages/notes/notes_page.dart';
import 'navigation/router.dart';
import 'providers/labels/labels_list/labels_list_provider.dart';
import 'providers/labels/labels_navigation/labels_navigation_provider.dart';
import 'providers/notifiers/notifiers.dart';
Expand Down Expand Up @@ -88,6 +86,34 @@ class _AppState extends ConsumerState<App> {
return intercept;
}

/// Called when the application goes to the background or the foreground.
void onFGBGEvent(FGBGType value) {
switch (value) {
case FGBGType.background:
lastForegroundTimestamp = DateTime.timestamp();
case FGBGType.foreground:
final now = DateTime.timestamp();

final lockApp = PreferenceKey.lockApp.preferenceOrDefault;
if (lockApp) {
final lockAppDelay = Duration(seconds: PreferenceKey.lockAppDelay.preferenceOrDefault);
final timeSinceBackground = now.difference(lastForegroundTimestamp);
if (timeSinceBackground > lockAppDelay) {
lockAppNotifier.lock();
}
}

final lockNote = PreferenceKey.lockNote.preferenceOrDefault;
if (lockNote) {
final lockNoteDelay = Duration(seconds: PreferenceKey.lockNoteDelay.preferenceOrDefault);
final timeSinceBackground = now.difference(lastForegroundTimestamp);
if (timeSinceBackground > lockNoteDelay) {
lockNoteNotifier.lock();
}
}
}
}

@override
Widget build(BuildContext context) {
final themeMode = ref.watch(preferencesProvider.select((preferences) => preferences.themeMode));
Expand All @@ -98,8 +124,6 @@ class _AppState extends ConsumerState<App> {
final useWhiteTextDarkMode = ref.watch(
preferencesProvider.select((preferences) => preferences.useWhiteTextDarkMode),
);
final lock = PreferenceKey.lockApp.preferenceOrDefault;
final lockDelay = PreferenceKey.lockAppDelay.preferenceOrDefault;

return DynamicColorBuilder(
builder: (lightDynamicColorScheme, darkDynamicColorScheme) {
Expand All @@ -120,51 +144,26 @@ class _AppState extends ConsumerState<App> {
data: MediaQuery.of(context).copyWith(
textScaler: TextScaler.linear(textScaling),
),
child: MaterialApp(
title: 'Material Notes',
home: NotesPage(label: null),
navigatorKey: rootNavigatorKey,
builder: (context, child) {
// Change the widget shown when a widget building fails
ErrorWidget.builder = (errorDetails) => ErrorPlaceholder.errorDetails(errorDetails);

if (child == null) {
throw Exception('MaterialApp child is null');
}

return Directionality(
textDirection: SystemUtils().deviceLocale.textDirection,
child: AppLock(
initiallyEnabled: lock,
initialBackgroundLockLatency: Duration(seconds: lockDelay),
builder: (BuildContext context, Object? launchArg) {
SystemUtils().setQuickActions(context, ref);

return child;
},
lockScreenBuilder: (BuildContext context) {
final l = AppLocalizations.of(context);

return LockPage(
back: false,
description: l.lock_page_description_app,
reason: l.lock_page_reason_app,
);
},
),
);
},
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
localizationsDelegates: [
...AppLocalizations.localizationsDelegates,
ChecklistLocalizations.delegate,
FleatherLocalizations.delegate,
],
supportedLocales: SupportedLanguage.locales,
locale: SystemUtils().appLocale,
debugShowCheckedModeBanner: false,
child: Directionality(
textDirection: SystemUtils().deviceLocale.textDirection,
child: FGBGNotifier(
onEvent: onFGBGEvent,
child: MaterialApp.router(
title: 'Material Notes',
routerConfig: router,
theme: lightTheme,
darkTheme: darkTheme,
themeMode: themeMode,
localizationsDelegates: [
...AppLocalizations.localizationsDelegates,
ChecklistLocalizations.delegate,
FleatherLocalizations.delegate,
],
supportedLocales: SupportedLanguage.locales,
locale: SystemUtils().appLocale,
debugShowCheckedModeBanner: false,
),
),
),
);
},
Expand Down
34 changes: 34 additions & 0 deletions lib/common/actions/labels/lock.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
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 '../../preferences/preference_key.dart';
import 'select.dart';

/// Toggles whether the [labels] are locked.
Future<bool> toggleLockLabels(
WidgetRef ref, {
required List<Label> labels,
bool requireAuthentication = false,
}) async {
if (requireAuthentication) {
final lockLabelPreference = PreferenceKey.lockLabel.preferenceOrDefault;
final anyLocked = labels.any((label) => label.locked);

// If the lock note setting is enabled and the note was locked, then ask to authenticate before unlocking the note
if (lockLabelPreference && anyLocked) {
final bool authenticated = await LocalAuthentication().authenticate(localizedReason: 'toggle');

if (!authenticated) {
return false;
}
}
}

final toggled = await ref.read(labelsProvider.notifier).toggleLock(labels);

exitLabelsSelectionMode(ref);

return toggled;
}
12 changes: 1 addition & 11 deletions lib/common/actions/labels/pin.dart
Original file line number Diff line number Diff line change
@@ -1,20 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../models/label/label.dart';
import '../../../providers/labels/labels/labels_provider.dart';
import 'select.dart';

/// Toggles the pined status of the [label].
///
/// Returns `true` if the pined status of the [label] was toggled, `false` otherwise.
Future<bool> togglePinLabel(BuildContext context, WidgetRef ref, {required Label label}) async {
await ref.read(labelsProvider.notifier).togglePin([label]);

return false;
}

/// Toggles the pined status of the [labels].
/// Toggles whether the [labels] are pinned.
Future<void> togglePinLabels(WidgetRef ref, {required List<Label> labels}) async {
await ref.read(labelsProvider.notifier).togglePin(labels);

Expand Down
11 changes: 1 addition & 10 deletions lib/common/actions/labels/visible.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,7 @@ import '../../../models/label/label.dart';
import '../../../providers/labels/labels/labels_provider.dart';
import 'select.dart';

/// Toggles the visible status of the [label].
///
/// Returns `true` if the visible status of the [label] was toggled, `false` otherwise.
Future<bool> toggleVisibleLabel(WidgetRef ref, {required Label label}) async {
await ref.read(labelsProvider.notifier).toggleVisible([label]);

return false;
}

/// Toggles the visible status of the [labels].
/// Toggles whether the [labels] are visible.
Future<void> toggleVisibleLabels(WidgetRef ref, {required List<Label> labels}) async {
await ref.read(labelsProvider.notifier).toggleVisible(labels);

Expand Down
8 changes: 6 additions & 2 deletions lib/common/actions/notes/add.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
import 'dart:async';
import 'dart:convert';

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:parchment_delta/parchment_delta.dart';

import '../../../models/note/note.dart';
import '../../../models/note/note_status.dart';
import '../../../models/note/types/note_type.dart';
import '../../../navigation/navigator_utils.dart';
import '../../../navigation/navigation_routes.dart';
import '../../../providers/notes/notes_provider.dart';
import '../../../providers/notifiers/notifiers.dart';
import '../../types.dart';
import 'select.dart';

/// Adds a note.
Expand Down Expand Up @@ -53,5 +56,6 @@ Future<void> addNote(BuildContext context, WidgetRef ref, {required NoteType not
return;
}

NavigatorUtils.pushNotesEditor(context, false, true);
final EditorPageExtra extra = (readOnly: false, isNewNote: true);
unawaited(context.pushNamed(NavigationRoute.editor.name, extra: extra));
}
6 changes: 3 additions & 3 deletions lib/common/actions/notes/lock.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import '../../../providers/notifiers/notifiers.dart';
import '../../preferences/preference_key.dart';
import 'select.dart';

/// Toggles the locked status of the [notes].
/// Toggles whether the [notes] are locked.
Future<bool> toggleLockNotes(
BuildContext context,
WidgetRef ref, {
Expand All @@ -18,10 +18,10 @@ Future<bool> toggleLockNotes(
}) async {
if (requireAuthentication) {
final lockNotePreference = PreferenceKey.lockNote.preferenceOrDefault;
final wasLocked = notes.first.locked;
final anyLocked = notes.any((note) => note.locked);

// If the lock note setting is enabled and the note was locked, then ask to authenticate before unlocking the note
if (lockNotePreference && wasLocked) {
if (lockNotePreference && anyLocked) {
final bool authenticated = await LocalAuthentication().authenticate(localizedReason: 'toggle');

if (!authenticated) {
Expand Down
2 changes: 1 addition & 1 deletion lib/common/actions/notes/pin.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import '../../../providers/notes/notes_provider.dart';
import '../../../providers/notifiers/notifiers.dart';
import 'select.dart';

/// Toggles the pined status of the [notes].
/// Toggles whether the [notes] are pinned.
Future<bool> togglePinNotes(BuildContext context, WidgetRef ref, {required List<Note> notes}) async {
final toggled =
await ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).togglePin(notes);
Expand Down
2 changes: 1 addition & 1 deletion lib/common/actions/notes/select.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../../models/note/note.dart';
Expand Down
3 changes: 3 additions & 0 deletions lib/common/constants/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,6 @@ const automaticExportFrequenciesValues = [1.0, 3.0, 7.0, 14.0, 30.0];

/// Delays for the lock feature.
const lockDelayValues = [0.0, 3.0, 5.0, 10.0, 30.0, 60.0, 120.0, 300.0, 99999.0];

/// The last time the application was in the foreground.
var lastForegroundTimestamp = DateTime.timestamp();
3 changes: 0 additions & 3 deletions lib/common/constants/paddings.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,6 @@ class Paddings {
/// Padding for the separator of the app bar.
static EdgeInsetsDirectional get appBarSeparator => const EdgeInsetsDirectional.symmetric(horizontal: 8);

/// Padding for the end of the app bar.
static EdgeInsetsDirectional get appBarActionsEnd => const EdgeInsetsDirectional.only(end: 8);

/// Padding for the notes list when the notes tiles have a background.
static EdgeInsetsDirectional get notesWithBackground => fab + const EdgeInsetsDirectional.symmetric(horizontal: 8);

Expand Down
8 changes: 7 additions & 1 deletion lib/common/constants/sizes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@ enum Sizes {
/// The size of the icon in a settings page when displaying the value of a setting.
settingValueIconSize(16),

/// The size of the pin icon in a note tile.
/// The size of small icons.
iconSmall(16),

/// The size of extra small icons.
iconExtraSmall(12),

/// The size of the empty placeholder icon.
placeholderIcon(64),

Expand All @@ -40,6 +43,9 @@ enum Sizes {

/// Size of the color indicators.
colorIndicator(40),

/// Padding at the end of the app bars.
appBarEnd(8),
;

/// The size to apply.
Expand Down
14 changes: 7 additions & 7 deletions lib/common/navigation/app_bars/notes/editor_app_bar.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:fleather/fleather.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:gap/gap.dart';

import '../../../../models/note/note_status.dart';
import '../../../../models/note/types/note_type.dart';
Expand All @@ -16,7 +17,7 @@ import '../../../actions/notes/restore.dart';
import '../../../actions/notes/share.dart';
import '../../../actions/notes/unarchive.dart';
import '../../../constants/constants.dart';
import '../../../constants/paddings.dart';
import '../../../constants/sizes.dart';
import '../../../preferences/preference_key.dart';
import '../../../system_utils.dart';
import '../../../widgets/placeholders/empty_placeholder.dart';
Expand Down Expand Up @@ -150,6 +151,7 @@ class _BackAppBarState extends ConsumerState<EditorAppBar> {
final editorController = fleatherControllerNotifier.value;
final showEditorModeButton = PreferenceKey.editorModeButton.preferenceOrDefault;
final enableLabels = PreferenceKey.enableLabels.preferenceOrDefault;
final lockNote = PreferenceKey.lockNote.preferenceOrDefault;

return ValueListenableBuilder(
valueListenable: editorHasFocusNotifier,
Expand All @@ -158,9 +160,7 @@ class _BackAppBarState extends ConsumerState<EditorAppBar> {
valueListenable: isEditorInEditModeNotifier,
builder: (context, isEditMode, child) {
return AppBar(
leading: BackButton(
onPressed: () => Navigator.of(rootNavigatorKey.currentContext!).pop(),
),
leading: BackButton(),
actions: [
if (note.status == NoteStatus.available) ...[
if (note.type == NoteType.richText) ...[
Expand Down Expand Up @@ -210,8 +210,8 @@ class _BackAppBarState extends ConsumerState<EditorAppBar> {
const PopupMenuDivider(),
if (note.pinned) EditorAvailableMenuOption.unpin.popupMenuItem(context),
if (!note.pinned) EditorAvailableMenuOption.pin.popupMenuItem(context),
if (note.locked) EditorAvailableMenuOption.unlock.popupMenuItem(context),
if (!note.locked) EditorAvailableMenuOption.lock.popupMenuItem(context),
if (lockNote && note.locked) EditorAvailableMenuOption.unlock.popupMenuItem(context),
if (lockNote && !note.locked) EditorAvailableMenuOption.lock.popupMenuItem(context),
if (enableLabels) EditorAvailableMenuOption.selectLabels.popupMenuItem(context),
const PopupMenuDivider(),
EditorAvailableMenuOption.archive.popupMenuItem(context),
Expand Down Expand Up @@ -244,7 +244,7 @@ class _BackAppBarState extends ConsumerState<EditorAppBar> {
]),
onSelected: onDeletedMenuOptionSelected,
),
Padding(padding: Paddings.appBarActionsEnd),
Gap(Sizes.appBarEnd.size),
],
);
},
Expand Down
Loading