From 6251c9e1d2b40e88f1ea57c312a5f4792d9f9202 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Mon, 20 Jan 2025 13:59:49 +0100 Subject: [PATCH 01/15] [368] Add chinese traditional localization (#369) * [368] feat: add chinese traditional localization and improve how localizations work * [368] docs: update pre-releases link in README.md --- lib/l10n/translations/app_zh-TW.arb | 1 + 1 file changed, 1 insertion(+) create mode 100644 lib/l10n/translations/app_zh-TW.arb diff --git a/lib/l10n/translations/app_zh-TW.arb b/lib/l10n/translations/app_zh-TW.arb new file mode 100644 index 00000000..9e26dfee --- /dev/null +++ b/lib/l10n/translations/app_zh-TW.arb @@ -0,0 +1 @@ +{} \ No newline at end of file From 04e0425a1c13c37bd14dc05ae695908369cc0cc8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Sun, 5 Jan 2025 17:48:35 +0100 Subject: [PATCH 02/15] Add rich text notes class and migration (#354) * [303] feat: add rich text notes class and migration * [303] style: fix comment --- lib/services/migration/migration_service.dart | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/lib/services/migration/migration_service.dart b/lib/services/migration/migration_service.dart index b0863c7f..0bdcfd14 100644 --- a/lib/services/migration/migration_service.dart +++ b/lib/services/migration/migration_service.dart @@ -37,14 +37,14 @@ class MigrationService { final richTextNotes = oldNotes .map( (oldNote) => RichTextNote( - deleted: oldNote.deleted, - pinned: oldNote.pinned, - createdTime: oldNote.createdTime, - editedTime: oldNote.editedTime, - title: oldNote.title, - content: oldNote.content, - ), - ) + deleted: oldNote.deleted, + pinned: oldNote.pinned, + createdTime: oldNote.createdTime, + editedTime: oldNote.editedTime, + title: oldNote.title, + content: oldNote.content, + ), + ) .toList(); // Add the new notes to the new collection with their labels @@ -55,8 +55,8 @@ class MigrationService { final oldNotesCount = await _databaseService.database.notes.count(); final addedRichTextNotesCount = richTextNotes.length; assert( - oldNotesCount == addedRichTextNotesCount, - 'The count of old notes ($oldNotesCount) is different from the count of rich text notes ($addedRichTextNotesCount) after the migration to v2', + oldNotesCount == addedRichTextNotesCount, + 'The count of old notes ($oldNotesCount) is different from the count of rich text notes ($addedRichTextNotesCount) after the migration to v2', ); // Update the database version From 48349af8cecaf7d1a179760f46af1d7d8392eefb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Sun, 5 Jan 2025 23:53:35 +0100 Subject: [PATCH 03/15] [303] feat: add plain text note class and refactor editor widgets --- .../navigation/app_bars/editor_app_bar.dart | 25 ++- lib/models/note/note.dart | 6 +- .../note/plain_text/plain_text_note.dart | 73 +++++++ lib/models/note/rich_text/rich_text_note.dart | 5 - lib/navigation/navigator_utils.dart | 5 +- ...dart => rich_text_editor_link_dialog.dart} | 8 +- lib/pages/editor/editor_page.dart | 198 +++++++----------- lib/pages/editor/widgets/editor_field.dart | 82 -------- .../widgets/rich_text/rich_text_editor.dart | 117 +++++++++++ .../rich_text_editor_button.dart} | 8 +- .../rich_text_editor_link_button.dart} | 18 +- .../rich_text_editor_toolbar.dart} | 120 +++++------ lib/pages/editor/widgets/title_editor.dart | 71 +++++++ lib/providers/notifiers/notifiers.dart | 4 +- lib/services/database_service.dart | 7 +- lib/services/notes/notes_service.dart | 24 ++- 16 files changed, 458 insertions(+), 313 deletions(-) create mode 100644 lib/models/note/plain_text/plain_text_note.dart rename lib/pages/editor/dialogs/{link_dialog.dart => rich_text_editor_link_dialog.dart} (84%) delete mode 100644 lib/pages/editor/widgets/editor_field.dart create mode 100644 lib/pages/editor/widgets/rich_text/rich_text_editor.dart rename lib/pages/editor/widgets/{editor_button.dart => rich_text/rich_text_editor_button.dart} (84%) rename lib/pages/editor/widgets/{link_button.dart => rich_text/rich_text_editor_link_button.dart} (75%) rename lib/pages/editor/widgets/{editor_toolbar.dart => rich_text/rich_text_editor_toolbar.dart} (52%) create mode 100644 lib/pages/editor/widgets/title_editor.dart diff --git a/lib/common/navigation/app_bars/editor_app_bar.dart b/lib/common/navigation/app_bars/editor_app_bar.dart index e5971b8d..b20c4fe3 100644 --- a/lib/common/navigation/app_bars/editor_app_bar.dart +++ b/lib/common/navigation/app_bars/editor_app_bar.dart @@ -37,7 +37,7 @@ class EditorAppBar extends ConsumerStatefulWidget { class _BackAppBarState extends ConsumerState { /// Switches the editor mode between editing and viewing. void switchMode() { - isFleatherEditorEditMode.value = !isFleatherEditorEditMode.value; + isEditorInEditModeNotifier.value = !isEditorInEditModeNotifier.value; } /// Performs the action associated with the selected [menuOption] on the not deleted note. @@ -149,9 +149,9 @@ class _BackAppBarState extends ConsumerState { final enableLabels = PreferenceKey.enableLabels.getPreferenceOrDefault(); return ValueListenableBuilder( - valueListenable: fleatherFieldHasFocusNotifier, - builder: (context, hasFocus, child) => ValueListenableBuilder( - valueListenable: isFleatherEditorEditMode, + valueListenable: editorHasFocusNotifier, + builder: (context, editorHasFocus, child) => ValueListenableBuilder( + valueListenable: isEditorInEditModeNotifier, builder: (context, isEditMode, child) => AppBar( leading: BackButton(), actions: note == null @@ -164,10 +164,13 @@ class _BackAppBarState extends ConsumerState { builder: (context, canUndo, child) => IconButton( icon: const Icon(Icons.undo), tooltip: l.tooltip_undo, - onPressed: - hasFocus && canUndo && editorController != null && editorController.canUndo && isEditMode - ? undo - : null, + onPressed: editorHasFocus && + canUndo && + editorController != null && + editorController.canUndo && + isEditMode + ? undo + : null, ), ), if (showUndoRedoButtons) @@ -176,18 +179,18 @@ class _BackAppBarState extends ConsumerState { builder: (context, canRedo, child) => IconButton( icon: const Icon(Icons.redo), tooltip: l.tooltip_redo, - onPressed: hasFocus && canRedo && isEditMode ? redo : null, + onPressed: editorHasFocus && canRedo && isEditMode ? redo : null, ), ), if (showChecklistButton) IconButton( icon: const Icon(Icons.checklist), tooltip: l.tooltip_toggle_checkbox, - onPressed: hasFocus && isEditMode ? toggleChecklist : null, + onPressed: editorHasFocus && isEditMode ? toggleChecklist : null, ), if (showEditorModeButton) ValueListenableBuilder( - valueListenable: isFleatherEditorEditMode, + valueListenable: isEditorInEditModeNotifier, builder: (context, isEditMode, child) => IconButton( icon: Icon(isEditMode ? Icons.visibility : Icons.edit), tooltip: isEditMode diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index c2247224..5b728eef 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -13,6 +13,8 @@ import '../label/label.dart'; part 'note.g.dart'; +part 'plain_text/plain_text_note.dart'; + part 'rich_text/rich_text_note.dart'; /// Converts the [labels] to a JSON-compatible list of strings. @@ -63,7 +65,7 @@ sealed class Note implements Comparable { required this.title, }); - /// Returns this note with title and the content encrypted using the [password]. + /// Returns this note with the [title] and the content encrypted using the [password]. Note encrypted(String password); /// Note content as plain text. @@ -80,7 +82,7 @@ sealed class Note implements Comparable { /// Note title and content to be shared as a single text. @ignore - String get shareText; + String get shareText => '$title\n\n$contentPreview'; /// Whether the title is empty. @ignore diff --git a/lib/models/note/plain_text/plain_text_note.dart b/lib/models/note/plain_text/plain_text_note.dart new file mode 100644 index 00000000..8aa8abd1 --- /dev/null +++ b/lib/models/note/plain_text/plain_text_note.dart @@ -0,0 +1,73 @@ +part of '../note.dart'; + +/// Plain text note. +@JsonSerializable() +@Collection() +class PlainTextNote extends Note { + /// The content of the note. + String content; + + /// Note with plain text content. + PlainTextNote({ + required super.deleted, + required super.pinned, + required super.createdTime, + required super.editedTime, + required super.title, + required this.content, + }); + + /// Plain text note with empty title and content. + factory PlainTextNote.empty() => PlainTextNote( + deleted: false, + pinned: false, + createdTime: DateTime.now(), + editedTime: DateTime.now(), + title: '', + content: '', + ); + + /// Plain text note with the provided [content]. + factory PlainTextNote.content(String content) => PlainTextNote( + deleted: false, + pinned: false, + createdTime: DateTime.now(), + editedTime: DateTime.now(), + title: '', + content: content, + ); + + /// Plain text note from [json] data. + factory PlainTextNote.fromJson(Map json) => _$PlainTextNoteFromJson(json); + + /// Plain text note from [json] data, encrypted with [password]. + factory PlainTextNote.fromJsonEncrypted(Map json, String password) => _$PlainTextNoteFromJson(json) + ..title = (json['title'] as String).isEmpty ? '' : EncryptionUtils().decrypt(password, json['title'] as String) + ..content = EncryptionUtils().decrypt(password, json['content'] as String); + + /// Plain text note to JSON. + Map toJson() => _$PlainTextNoteToJson(this); + + @override + Note encrypted(String password) => this + ..title = isTitleEmpty ? '' : EncryptionUtils().encrypt(password, title) + ..content = EncryptionUtils().encrypt(password, content); + + @ignore + @override + String get plainText => content; + + @ignore + @override + String get markdown => content; + + @ignore + @override + String get contentPreview { + return content.trim(); + } + + @ignore + @override + bool get isContentEmpty => content.isEmpty; +} diff --git a/lib/models/note/rich_text/rich_text_note.dart b/lib/models/note/rich_text/rich_text_note.dart index f2b21c8c..a5f6eaff 100644 --- a/lib/models/note/rich_text/rich_text_note.dart +++ b/lib/models/note/rich_text/rich_text_note.dart @@ -51,7 +51,6 @@ class RichTextNote extends Note { /// Rich text note to JSON. Map toJson() => _$RichTextNoteToJson(this); - /// Returns this note with the [title] and the [content] encrypted with the [password]. @override Note encrypted(String password) => this ..title = isTitleEmpty ? '' : EncryptionUtils().encrypt(password, title) @@ -106,10 +105,6 @@ class RichTextNote extends Note { return content.trim(); } - @ignore - @override - String get shareText => '$title\n\n$contentPreview'; - @ignore @override bool get isContentEmpty => content == _emptyContent; diff --git a/lib/navigation/navigator_utils.dart b/lib/navigation/navigator_utils.dart index 2e40d4b4..2f2098c2 100644 --- a/lib/navigation/navigator_utils.dart +++ b/lib/navigation/navigator_utils.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; + import '../common/preferences/preference_key.dart'; -import 'navigation_routes.dart'; import '../pages/editor/editor_page.dart'; import '../providers/notifiers/notifiers.dart'; +import 'navigation_routes.dart'; /// Utilities for the navigation. class NavigatorUtils { @@ -28,7 +29,7 @@ class NavigatorUtils { /// Pushes the notes editor route with its parameters [readOnly] and a [isNewNote]. static void pushNotesEditor(BuildContext context, bool readOnly, bool isNewNote) { - isFleatherEditorEditMode.value = !PreferenceKey.openEditorReadingMode.getPreferenceOrDefault(); + isEditorInEditModeNotifier.value = !PreferenceKey.openEditorReadingMode.getPreferenceOrDefault(); push( context, diff --git a/lib/pages/editor/dialogs/link_dialog.dart b/lib/pages/editor/dialogs/rich_text_editor_link_dialog.dart similarity index 84% rename from lib/pages/editor/dialogs/link_dialog.dart rename to lib/pages/editor/dialogs/rich_text_editor_link_dialog.dart index e35f9f6e..0a4b83b5 100644 --- a/lib/pages/editor/dialogs/link_dialog.dart +++ b/lib/pages/editor/dialogs/rich_text_editor_link_dialog.dart @@ -4,15 +4,15 @@ import 'package:string_validator/string_validator.dart'; import '../../../common/constants/constants.dart'; /// Dialog to add a link in the editor. -class LinkDialog extends StatefulWidget { +class RichTextEditorLinkDialog extends StatefulWidget { /// Default constructor. - const LinkDialog({super.key}); + const RichTextEditorLinkDialog({super.key}); @override - State createState() => _LinkDialogState(); + State createState() => _RichTextEditorLinkDialogState(); } -class _LinkDialogState extends State { +class _RichTextEditorLinkDialogState extends State { /// Controller for the link text field. final _linkController = TextEditingController(); diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart index 82e7123f..4e7786d4 100644 --- a/lib/pages/editor/editor_page.dart +++ b/lib/pages/editor/editor_page.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -13,17 +11,16 @@ import '../../common/preferences/preference_key.dart'; import '../../common/widgets/keys.dart'; import '../../common/widgets/placeholders/loading_placeholder.dart'; import '../../models/note/note.dart'; -import '../../providers/notes/notes_provider.dart'; import '../../providers/notifiers/notifiers.dart'; import 'widgets/editor_field.dart'; +import '../../utils/keys.dart'; import 'widgets/editor_labels_list.dart'; -import 'widgets/editor_toolbar.dart'; +import 'widgets/plain_text/plain_text_editor.dart'; +import 'widgets/rich_text/rich_text_editor.dart'; +import 'widgets/rich_text/rich_text_editor_toolbar.dart'; +import 'widgets/title_editor.dart'; /// Page displaying the note editor. -/// -/// Contains: -/// - The text field for the title of the note. -/// - The text field for the content of the note. class NotesEditorPage extends ConsumerStatefulWidget { /// Default constructor. const NotesEditorPage({ @@ -44,138 +41,97 @@ class NotesEditorPage extends ConsumerStatefulWidget { } class _EditorState extends ConsumerState { - /// Controller of the title of the note. - late TextEditingController titleController; - - /// Controller of the content of the note. - late FleatherController editorController; - - @override - void initState() { - super.initState(); - - // If this is a new note, force the editing mode - if (widget.isNewNote) { - isFleatherEditorEditMode.value = true; - } - } - - /// Saves the [newTitle] of the [note] in the database. - void _synchronizeTitle(Note note, String? newTitle) { - if (newTitle == null) { - return; - } - - note.title = newTitle; - - ref.read(notesProvider(label: currentLabelFilter).notifier).edit(note); - } - - /// Saves the new content of the [note] in the database. - /// - /// Also updates whether the undo/redo actions can be used. - void _synchronizeContent(Note note) { - fleatherControllerCanUndoNotifier.value = editorController.canUndo; - fleatherControllerCanRedoNotifier.value = editorController.canRedo; - - switch (note) { - case RichTextNote note: - note.content = jsonEncode(editorController.document.toDelta().toJson()); - } - - ref.read(notesProvider(label: currentLabelFilter).notifier).edit(note); - } - /// Request focus on the content editor. - void _requestEditorFocus(_) { + void requestEditorFocus() { editorFocusNode.requestFocus(); } @override Widget build(BuildContext context) { final showEditorModeButton = PreferenceKey.editorModeButton.getPreferenceOrDefault(); - final showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault(); final focusTitleOnNewNote = PreferenceKey.focusTitleOnNewNote.getPreferenceOrDefault(); + final showToolbar = PreferenceKey.showToolbar.getPreferenceOrDefault(); final enableLabels = PreferenceKey.enableLabels.getPreferenceOrDefault(); final showLabelsListInEditorPage = PreferenceKey.showLabelsListInEditorPage.getPreferenceOrDefault(); return ValueListenableBuilder( valueListenable: currentNoteNotifier, builder: (context, currentNote, child) { - if (currentNote == null) { - return const LoadingPlaceholder(); - } - - titleController = TextEditingController(text: currentNote.title); - - switch (currentNote) { - case final RichTextNote note: - editorController = FleatherController(document: note.document); - } - - editorController.addListener(() => _synchronizeContent(currentNote)); - - fleatherControllerNotifier.value = editorController; - - final showLabelsList = enableLabels && showLabelsListInEditorPage && currentNote.labelsVisibleSorted.isNotEmpty; - - return Scaffold( - appBar: const TopNavigation( - appbar: EditorAppBar( - key: Keys.appBarEditor, - ), - ), - body: ValueListenableBuilder( - valueListenable: isFleatherEditorEditMode, - builder: (context, isEditMode, child) => Column( - children: [ - Expanded( - child: Padding( - padding: Paddings.pageButBottom, - child: Column( - children: [ - TextField( - key: Keys.editorTitleTextField, - readOnly: widget.readOnly, - autofocus: widget.isNewNote && focusTitleOnNewNote, - textCapitalization: TextCapitalization.sentences, - textInputAction: TextInputAction.next, - style: Theme.of(context).textTheme.titleLarge, - decoration: InputDecoration.collapsed( - hintText: l.hint_title, + return ValueListenableBuilder( + valueListenable: isEditorInEditModeNotifier, + builder: (context, isEditorInEditMode, child) { + if (currentNote == null) { + return const LoadingPlaceholder(); + } + + final readOnly = widget.readOnly || (showEditorModeButton && !isEditorInEditMode); + final autofocus = widget.isNewNote && !focusTitleOnNewNote; + final showLabelsList = + enableLabels && showLabelsListInEditorPage && currentNote.labelsVisibleSorted.isNotEmpty; + + Widget contentEditor; + Widget? toolbar; + switch (currentNote) { + case PlainTextNote note: + contentEditor = PlainTextEditor( + note: note, + isNewNote: widget.isNewNote, + readOnly: readOnly, + autofocus: autofocus, + ); + case RichTextNote note: + final fleatherController = FleatherController(document: note.document); + fleatherControllerNotifier.value = fleatherController; + contentEditor = RichTextEditor( + note: note, + fleatherController: fleatherController, + isNewNote: widget.isNewNote, + readOnly: readOnly, + autofocus: autofocus, + ); + toolbar = RichTextEditorToolbar( + fleatherController: fleatherController, + ); + } + + return Scaffold( + appBar: const TopNavigation( + appbar: EditorAppBar( + key: Keys.appBarEditor, + ), + ), + body: Column( + children: [ + Expanded( + child: Padding( + padding: Paddings.pageButBottom, + child: Column( + children: [ + TitleEditor( + readOnly: widget.readOnly, + isNewNote: widget.isNewNote, + onSubmitted: requestEditorFocus, ), - controller: titleController, - onChanged: (text) => _synchronizeTitle(currentNote, text), - onSubmitted: _requestEditorFocus, - ), - Gap(8.0), - Expanded( - child: Focus( - onFocusChange: (hasFocus) => fleatherFieldHasFocusNotifier.value = hasFocus, - child: EditorField( - key: Keys.editorContentTextField, - fleatherController: editorController, - readOnly: widget.readOnly || (showEditorModeButton && !isEditMode), - autofocus: widget.isNewNote && !focusTitleOnNewNote, - ), + Gap(8.0), + Expanded( + child: contentEditor, ), - ), - ], + ], + ), ), ), - ), - SafeArea( - child: Column( - children: [ - if (showLabelsList) EditorLabelsList(readOnly: widget.readOnly), - if (showToolbar && isEditMode && !currentNote.deleted) - EditorToolbar(editorController: editorController), - ], + SafeArea( + child: Column( + children: [ + if (showLabelsList) EditorLabelsList(readOnly: widget.readOnly), + if (toolbar != null && showToolbar && isEditorInEditMode && !currentNote.deleted) toolbar, + ], + ), ), - ), - ], - ), - ), + ], + ), + ); + }, ); }, ); diff --git a/lib/pages/editor/widgets/editor_field.dart b/lib/pages/editor/widgets/editor_field.dart deleted file mode 100644 index 6a7a5d54..00000000 --- a/lib/pages/editor/widgets/editor_field.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:fleather/fleather.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:url_launcher/url_launcher.dart'; - -import '../../../common/constants/constants.dart'; -import '../../../common/constants/paddings.dart'; -import '../../../common/preferences/enums/font.dart'; -import '../../../common/preferences/preference_key.dart'; - -/// Text field to edit the content of a note. -class EditorField extends StatelessWidget { - /// Default constructor. - const EditorField({ - super.key, - required this.fleatherController, - required this.readOnly, - required this.autofocus, - }); - - /// Controller of the content text field. - final FleatherController fleatherController; - - /// Whether the text fields are read only. - final bool readOnly; - - /// Whether to automatically focus the content text field. - final bool autofocus; - - /// Opens the [url]. - void _launchUrl(String? url) { - if (url == null) { - return; - } - - launchUrl(Uri.parse(url)); - } - - @override - Widget build(BuildContext context) { - final useParagraphsSpacing = PreferenceKey.useParagraphsSpacing.getPreferenceOrDefault(); - final editorFont = Font.editorFromPreference(); - - return DefaultTextStyle.merge( - style: TextStyle( - fontFamily: editorFont.familyName, - ), - child: Builder( - builder: (context) { - final fleatherThemeFallback = FleatherThemeData.fallback(context); - final FleatherThemeData fleatherTheme; - if (useParagraphsSpacing) { - fleatherTheme = fleatherThemeFallback; - } else { - fleatherTheme = fleatherThemeFallback.copyWith( - paragraph: TextBlockTheme( - style: fleatherThemeFallback.paragraph.style, - spacing: const VerticalSpacing.zero(), - ), - ); - } - - return FleatherTheme( - data: fleatherTheme, - child: FleatherEditor( - controller: fleatherController, - focusNode: editorFocusNode, - autofocus: autofocus, - readOnly: readOnly, - expands: true, - onLaunchUrl: _launchUrl, - spellCheckConfiguration: SpellCheckConfiguration( - spellCheckService: DefaultSpellCheckService(), - ), - padding: Paddings.bottomSystemUi, - ), - ); - }, - ), - ); - } -} diff --git a/lib/pages/editor/widgets/rich_text/rich_text_editor.dart b/lib/pages/editor/widgets/rich_text/rich_text_editor.dart new file mode 100644 index 00000000..3189e916 --- /dev/null +++ b/lib/pages/editor/widgets/rich_text/rich_text_editor.dart @@ -0,0 +1,117 @@ +import 'dart:convert'; + +import 'package:fleather/fleather.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../../common/constants/constants.dart'; +import '../../../../common/constants/paddings.dart'; +import '../../../../common/preferences/enums/font.dart'; +import '../../../../common/preferences/preference_key.dart'; +import '../../../../models/note/note.dart'; +import '../../../../providers/notes/notes_provider.dart'; +import '../../../../providers/notifiers/notifiers.dart'; + +/// Text field to edit the content of a note. +class RichTextEditor extends ConsumerStatefulWidget { + /// Default constructor. + const RichTextEditor({ + super.key, + required this.fleatherController, + required this.note, + required this.isNewNote, + required this.readOnly, + required this.autofocus, + }); + + final RichTextNote note; + + final FleatherController fleatherController; + + final bool isNewNote; + + /// Whether the text fields are read only. + final bool readOnly; + + /// Whether this is a new note. + final bool autofocus; + + @override + ConsumerState createState() => _RichTextEditorState(); +} + +class _RichTextEditorState extends ConsumerState { + @override + void initState() { + super.initState(); + + // If this is a new note, force the editing mode + if (widget.isNewNote) { + isEditorInEditModeNotifier.value = true; + } + } + + void onFocusChange(bool hasFocus) { + editorHasFocusNotifier.value = hasFocus; + } + + /// Opens the [url]. + void launchUrl(String? url) { + if (url == null) { + return; + } + + launchUrl(url); + } + + /// Saves the new content of the [note] in the database. + /// + /// Also updates whether the undo/redo actions can be used. + void onChanged(RichTextNote note) { + fleatherControllerCanUndoNotifier.value = widget.fleatherController.canUndo; + fleatherControllerCanRedoNotifier.value = widget.fleatherController.canRedo; + + note.content = jsonEncode(widget.fleatherController.document.toDelta().toJson()); + + ref.read(notesProvider.notifier).edit(note); + } + + @override + Widget build(BuildContext context) { + final useParagraphsSpacing = PreferenceKey.useParagraphsSpacing.getPreferenceOrDefault(); + final editorFont = Font.editorFromPreference(); + + final fleatherThemeFallback = FleatherThemeData.fallback(context); + final fleatherThemeParagraph = TextBlockTheme( + style: fleatherThemeFallback.paragraph.style.copyWith(fontFamily: editorFont.familyName), + spacing: useParagraphsSpacing ? fleatherThemeFallback.paragraph.spacing : const VerticalSpacing.zero(), + ); + + widget.fleatherController.addListener(() => onChanged(widget.note)); + + return Focus( + onFocusChange: onFocusChange, + child: FleatherTheme( + data: fleatherThemeFallback.copyWith( + paragraph: fleatherThemeParagraph, + ), + child: FleatherField( + controller: widget.fleatherController, + focusNode: editorFocusNode, + readOnly: widget.readOnly, + autofocus: widget.autofocus, + expands: true, + decoration: InputDecoration.collapsed( + hintText: l.hint_note, + ), + onLaunchUrl: launchUrl, + spellCheckConfiguration: SpellCheckConfiguration( + spellCheckService: DefaultSpellCheckService(), + ), + padding: Paddings.bottomSystemUi, + ), + ), + ); + } +} diff --git a/lib/pages/editor/widgets/editor_button.dart b/lib/pages/editor/widgets/rich_text/rich_text_editor_button.dart similarity index 84% rename from lib/pages/editor/widgets/editor_button.dart rename to lib/pages/editor/widgets/rich_text/rich_text_editor_button.dart index cc93b67f..cbbb9100 100644 --- a/lib/pages/editor/widgets/editor_button.dart +++ b/lib/pages/editor/widgets/rich_text/rich_text_editor_button.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; -import '../../../common/constants/paddings.dart'; -import '../../../common/constants/sizes.dart'; +import '../../../../common/constants/paddings.dart'; +import '../../../../common/constants/sizes.dart'; /// Custom toolbar button. -class EditorButton extends StatelessWidget { +class RichTextEditorButton extends StatelessWidget { /// Default constructor. - const EditorButton({ + const RichTextEditorButton({ super.key, required this.icon, required this.onPressed, diff --git a/lib/pages/editor/widgets/link_button.dart b/lib/pages/editor/widgets/rich_text/rich_text_editor_link_button.dart similarity index 75% rename from lib/pages/editor/widgets/link_button.dart rename to lib/pages/editor/widgets/rich_text/rich_text_editor_link_button.dart index 36c3d411..6f827a9a 100644 --- a/lib/pages/editor/widgets/link_button.dart +++ b/lib/pages/editor/widgets/rich_text/rich_text_editor_link_button.dart @@ -1,13 +1,13 @@ import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; -import '../dialogs/link_dialog.dart'; -import 'editor_button.dart'; +import '../../dialogs/rich_text_editor_link_dialog.dart'; +import 'rich_text_editor_button.dart'; /// Custom toolbar button to add a link. -class LinkButton extends StatefulWidget { +class RichTextEditorLinkButton extends StatefulWidget { /// Default constructor. - const LinkButton({ + const RichTextEditorLinkButton({ super.key, required this.controller, }); @@ -16,10 +16,10 @@ class LinkButton extends StatefulWidget { final FleatherController controller; @override - State createState() => _LinkButtonState(); + State createState() => _RichTextEditorLinkButtonState(); } -class _LinkButtonState extends State { +class _RichTextEditorLinkButtonState extends State { /// Whether the button is enabled. var _enabled = false; @@ -31,7 +31,7 @@ class _LinkButtonState extends State { } @override - void didUpdateWidget(covariant LinkButton oldWidget) { + void didUpdateWidget(covariant RichTextEditorLinkButton oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.controller != widget.controller) { @@ -59,7 +59,7 @@ class _LinkButtonState extends State { final link = await showAdaptiveDialog( context: context, useRootNavigator: false, - builder: (context) => const LinkDialog(), + builder: (context) => const RichTextEditorLinkDialog(), ); if (link == null || link.isEmpty) { @@ -71,7 +71,7 @@ class _LinkButtonState extends State { @override Widget build(BuildContext context) { - return EditorButton( + return RichTextEditorButton( icon: Icons.link, iconColor: _enabled ? Theme.of(context).iconTheme.color : Theme.of(context).disabledColor, onPressed: _enabled ? _enterLink : null, diff --git a/lib/pages/editor/widgets/editor_toolbar.dart b/lib/pages/editor/widgets/rich_text/rich_text_editor_toolbar.dart similarity index 52% rename from lib/pages/editor/widgets/editor_toolbar.dart rename to lib/pages/editor/widgets/rich_text/rich_text_editor_toolbar.dart index 11afa814..ca20a277 100644 --- a/lib/pages/editor/widgets/editor_toolbar.dart +++ b/lib/pages/editor/widgets/rich_text/rich_text_editor_toolbar.dart @@ -1,68 +1,64 @@ import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; -import '../../../common/constants/paddings.dart'; -import '../../../common/constants/sizes.dart'; -import '../../../common/preferences/preference_key.dart'; -import 'editor_button.dart'; -import 'link_button.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; +import '../../../../common/constants/paddings.dart'; +import '../../../../common/constants/sizes.dart'; +import '../../../../common/preferences/preference_key.dart'; +import 'rich_text_editor_button.dart'; +import 'rich_text_editor_link_button.dart'; + /// Toolbar for the content text field that enables advanced formatting options. -class EditorToolbar extends StatefulWidget { +class RichTextEditorToolbar extends StatelessWidget { /// Default constructor. - const EditorToolbar({ + const RichTextEditorToolbar({ super.key, - required this.editorController, + required this.fleatherController, }); - /// Controller of the content text field. - final FleatherController editorController; - - @override - State createState() => _EditorToolbarState(); -} + final FleatherController fleatherController; -class _EditorToolbarState extends State { /// Builds a button of the toolbar. /// /// Overrides the default button style of fleather. - Widget _buttonBuilder( + Widget buttonBuilder( BuildContext context, ParchmentAttribute attribute, IconData icon, bool isToggled, VoidCallback? onPressed, - ) => - Padding( - padding: Paddings.vertical(2), - child: ConstrainedBox( - constraints: BoxConstraints.tightFor( - width: Sizes.editorToolbarButtonHeight.size, - height: Sizes.editorToolbarButtonWidth.size, - ), - child: RawMaterialButton( - shape: const CircleBorder(), - visualDensity: VisualDensity.compact, - fillColor: isToggled ? Theme.of(context).colorScheme.secondary : null, - elevation: 0, - onPressed: onPressed, - child: Icon(icon), - ), + ) { + return Padding( + padding: Paddings.vertical(2), + child: ConstrainedBox( + constraints: BoxConstraints.tightFor( + width: Sizes.editorToolbarButtonHeight.size, + height: Sizes.editorToolbarButtonWidth.size, ), - ); + child: RawMaterialButton( + shape: const CircleBorder(), + visualDensity: VisualDensity.compact, + fillColor: isToggled ? Theme.of(context).colorScheme.secondary : null, + elevation: 0, + onPressed: onPressed, + child: Icon(icon), + ), + ), + ); + } /// Inserts a rule in the content. /// /// Copied from the fleather source code to allow using a custom button. - void _insertRule() { - final index = widget.editorController.selection.baseOffset; - final length = widget.editorController.selection.extentOffset - index; - final newSelection = widget.editorController.selection.copyWith( + void insertRule(FleatherController fleatherController) { + final index = fleatherController.selection.baseOffset; + final length = fleatherController.selection.extentOffset - index; + final newSelection = fleatherController.selection.copyWith( baseOffset: index + 2, extentOffset: index + 2, ); - widget.editorController.replaceText(index, length, BlockEmbed.horizontalRule, selection: newSelection); + fleatherController.replaceText(index, length, BlockEmbed.horizontalRule, selection: newSelection); } @override @@ -80,70 +76,70 @@ class _EditorToolbarState extends State { ToggleStyleButton( attribute: ParchmentAttribute.bold, icon: Icons.format_bold, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.italic, icon: Icons.format_italic, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.underline, icon: Icons.format_underline, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.strikethrough, icon: Icons.format_strikethrough, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), if (!showChecklistButton) ToggleStyleButton( attribute: ParchmentAttribute.block.checkList, icon: Icons.checklist, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.block.bulletList, icon: Icons.format_list_bulleted, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.block.numberList, icon: Icons.format_list_numbered, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.inlineCode, icon: Icons.code, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.block.code, icon: Symbols.code_blocks, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), ToggleStyleButton( attribute: ParchmentAttribute.block.quote, icon: Icons.format_quote, - controller: widget.editorController, - childBuilder: _buttonBuilder, + controller: fleatherController, + childBuilder: buttonBuilder, ), - LinkButton( - controller: widget.editorController, + RichTextEditorLinkButton( + controller: fleatherController, ), - EditorButton( + RichTextEditorButton( icon: Icons.horizontal_rule, - onPressed: _insertRule, + onPressed: () => insertRule(fleatherController), ), Padding(padding: Paddings.vertical(2)), ], diff --git a/lib/pages/editor/widgets/title_editor.dart b/lib/pages/editor/widgets/title_editor.dart new file mode 100644 index 00000000..f6df3dd2 --- /dev/null +++ b/lib/pages/editor/widgets/title_editor.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +import '../../../common/constants/constants.dart'; +import '../../../common/preferences/preference_key.dart'; +import '../../../models/note/note.dart'; +import '../../../providers/notes/notes_provider.dart'; +import '../../../providers/notifiers/notifiers.dart'; + +class TitleEditor extends ConsumerStatefulWidget { + const TitleEditor({ + super.key, + required this.readOnly, + required this.isNewNote, + required this.onSubmitted, + }); + + /// Whether the page is read only. + final bool readOnly; + + /// Whether this is a new note, so the title or content field should be auto focused + /// and the editor mode should be forced to editing. + final bool isNewNote; + + final VoidCallback onSubmitted; + + @override + ConsumerState createState() => _TitleEditorState(); +} + +class _TitleEditorState extends ConsumerState { + /// Controller of the title of the note. + late TextEditingController titleController; + + /// Saves the [newTitle] of the [note] in the database. + void onChanged(Note? note, String? newTitle) { + if (note == null || newTitle == null) { + return; + } + + note.title = newTitle; + + ref.read(notesProvider.notifier).edit(note); + } + + @override + Widget build(BuildContext context) { + final focusTitleOnNewNote = PreferenceKey.focusTitleOnNewNote.getPreferenceOrDefault(); + + return ValueListenableBuilder( + valueListenable: currentNoteNotifier, + builder: (context, currentNote, child) { + titleController = TextEditingController(text: currentNote?.title); + + return TextField( + readOnly: widget.readOnly, + autofocus: widget.isNewNote && focusTitleOnNewNote, + textCapitalization: TextCapitalization.sentences, + textInputAction: TextInputAction.next, + style: Theme.of(context).textTheme.titleLarge, + decoration: InputDecoration.collapsed( + hintText: l.hint_title, + ), + controller: titleController, + onChanged: (text) => onChanged(currentNote, text), + onSubmitted: (_) => widget.onSubmitted, + ); + }, + ); + } +} diff --git a/lib/providers/notifiers/notifiers.dart b/lib/providers/notifiers/notifiers.dart index 1f0683e1..12c11682 100644 --- a/lib/providers/notifiers/notifiers.dart +++ b/lib/providers/notifiers/notifiers.dart @@ -27,7 +27,7 @@ final fleatherControllerCanUndoNotifier = ValueNotifier(false); final fleatherControllerCanRedoNotifier = ValueNotifier(false); /// Notifier for whether the fleather editor has focus. -final fleatherFieldHasFocusNotifier = ValueNotifier(false); +final editorHasFocusNotifier = ValueNotifier(false); /// Notifier for whether the fleather editor is in edit mode. -final isFleatherEditorEditMode = ValueNotifier(!PreferenceKey.openEditorReadingMode.getPreferenceOrDefault()); +final isEditorInEditModeNotifier = ValueNotifier(!PreferenceKey.openEditorReadingMode.getPreferenceOrDefault()); diff --git a/lib/services/database_service.dart b/lib/services/database_service.dart index 4db33dbd..be33cf2d 100644 --- a/lib/services/database_service.dart +++ b/lib/services/database_service.dart @@ -33,7 +33,12 @@ class DatabaseService { // Initialize the database database = await Isar.open( - [NoteSchema, RichTextNoteSchema, LabelSchema], + [ + NoteSchema, + PlainTextNoteSchema, + RichTextNoteSchema, + LabelSchema, + ], name: databaseName, directory: databaseDirectory, ); diff --git a/lib/services/notes/notes_service.dart b/lib/services/notes/notes_service.dart index 0d8f1d94..4d29917c 100644 --- a/lib/services/notes/notes_service.dart +++ b/lib/services/notes/notes_service.dart @@ -22,13 +22,20 @@ class NotesService { NotesService._internal(); - final Isar _database = DatabaseService().database; - final _richTextNotes = DatabaseService().database.richTextNotes; + late final Isar _database; + + late final IsarCollection _plainTextNotes; + late final IsarCollection _richTextNotes; late final MimirIndex _index; /// Ensures the notes service is initialized. Future ensureInitialized() async { + _database = DatabaseService().database; + + _plainTextNotes = DatabaseService().database.plainTextNotes; + _richTextNotes = DatabaseService().database.richTextNotes; + _index = await DatabaseService().mimir.openIndex( 'notes', primaryKey: 'id', @@ -135,6 +142,8 @@ class NotesService { Future put(Note note) async { await _database.writeTxn(() async { switch (note) { + case final PlainTextNote note: + await _plainTextNotes.put(note); case final RichTextNote note: await _richTextNotes.put(note); } @@ -145,13 +154,12 @@ class NotesService { /// Puts the [notes] in the database. Future putAll(List notes) async { + final plainTextNotes = notes.whereType().toList(); + final richTextNotes = notes.whereType().toList(); + await _database.writeTxn(() async { - for (final note in notes) { - switch (note) { - case final RichTextNote note: - await _richTextNotes.put(note); - } - } + await _plainTextNotes.putAll(plainTextNotes); + await _richTextNotes.putAll(richTextNotes); }); await _updateAllIndexes(notes); From 8586fb4c9fcc7ac18a0c862dbfbbf8c8336860e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Mon, 6 Jan 2025 01:16:45 +0100 Subject: [PATCH 04/15] [303] feat: add plain text note editor --- lib/common/constants/constants.dart | 9 +- .../navigation/app_bars/editor_app_bar.dart | 24 ++--- .../app_bars/notes_selection_app_bar.dart | 44 +++++----- lib/common/widgets/notes/notes_list.dart | 6 +- lib/models/note/index/note_index.dart | 3 +- lib/models/note/note.dart | 52 +++++++++-- .../note/plain_text/plain_text_note.dart | 8 +- lib/models/note/rich_text/rich_text_note.dart | 4 + lib/pages/editor/editor_page.dart | 8 +- lib/pages/editor/sheets/about_sheet.dart | 87 +++++++++---------- .../editor/widgets/editor_labels_list.dart | 4 +- .../widgets/plain_text/plain_text_editor.dart | 78 +++++++++++++++++ .../widgets/rich_text/rich_text_editor.dart | 19 ++-- .../rich_text/rich_text_editor_button.dart | 4 +- .../rich_text_editor_link_button.dart | 31 ++++--- .../rich_text/rich_text_editor_toolbar.dart | 11 +-- lib/pages/editor/widgets/title_editor.dart | 6 +- lib/services/notes/notes_service.dart | 70 ++++++++++----- pubspec.lock | 2 +- pubspec.yaml | 1 + 20 files changed, 300 insertions(+), 171 deletions(-) create mode 100644 lib/pages/editor/widgets/plain_text/plain_text_editor.dart diff --git a/lib/common/constants/constants.dart b/lib/common/constants/constants.dart index 34b79c74..f824a347 100644 --- a/lib/common/constants/constants.dart +++ b/lib/common/constants/constants.dart @@ -1,9 +1,14 @@ import 'package:flutter/material.dart'; -import '../logs/app_logger.dart'; -import '../../l10n/app_localizations/app_localizations.g.dart'; import 'package:parchment/codecs.dart'; import 'package:saf_stream/saf_stream.dart'; import 'package:saf_util/saf_util.dart'; +import 'package:uuid/uuid.dart'; + +import '../../l10n/app_localizations/app_localizations.g.dart'; +import '../logs/app_logger.dart'; + +/// An UUID generator. +final uuid = Uuid(); /// Contact email address. const contactEmail = 'contact@maelchiotti.dev'; diff --git a/lib/common/navigation/app_bars/editor_app_bar.dart b/lib/common/navigation/app_bars/editor_app_bar.dart index b20c4fe3..26f6bcb0 100644 --- a/lib/common/navigation/app_bars/editor_app_bar.dart +++ b/lib/common/navigation/app_bars/editor_app_bar.dart @@ -2,6 +2,7 @@ import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import '../../../models/note/note.dart'; import '../../../pages/editor/sheets/about_sheet.dart'; import '../../../providers/notifiers/notifiers.dart'; import '../../actions/notes/copy.dart'; @@ -16,16 +17,9 @@ import '../../preferences/preference_key.dart'; import '../enums/bin_menu_option.dart'; import '../enums/note_menu_option.dart'; -/// Editor's app bar. -/// -/// Contains: -/// - A back button. -/// - The title of the editor route. -/// - The undo/redo buttons if enabled by the user. -/// - The checklist button if enabled by the user. -/// - The menu with further actions. +/// Editor app bar. class EditorAppBar extends ConsumerStatefulWidget { - /// Default constructor. + /// App bar of the editor page allowing to perform actions on the note. const EditorAppBar({ super.key, }); @@ -35,12 +29,10 @@ class EditorAppBar extends ConsumerStatefulWidget { } class _BackAppBarState extends ConsumerState { - /// Switches the editor mode between editing and viewing. void switchMode() { isEditorInEditModeNotifier.value = !isEditorInEditModeNotifier.value; } - /// Performs the action associated with the selected [menuOption] on the not deleted note. Future onNoteMenuOptionSelected(NoteMenuOption menuOption) async { // Manually close the keyboard FocusManager.instance.primaryFocus?.unfocus(); @@ -74,7 +66,6 @@ class _BackAppBarState extends ConsumerState { } } - /// Performs the action associated with the selected [menuOption] on the deleted note. Future onBinMenuOptionSelected(BinMenuOption menuOption) async { // Manually close the keyboard FocusManager.instance.primaryFocus?.unfocus(); @@ -102,7 +93,6 @@ class _BackAppBarState extends ConsumerState { } } - /// Undoes the latest change in the editor. void undo() { final editorController = fleatherControllerNotifier.value; @@ -113,7 +103,6 @@ class _BackAppBarState extends ConsumerState { editorController.undo(); } - /// Redoes the latest change in the editor. void redo() { final editorController = fleatherControllerNotifier.value; @@ -124,7 +113,6 @@ class _BackAppBarState extends ConsumerState { editorController.redo(); } - /// Toggles the presence of the checklist in the currently active line of the editor. void toggleChecklist() { final editorController = fleatherControllerNotifier.value; @@ -158,7 +146,7 @@ class _BackAppBarState extends ConsumerState { ? null : [ if (!note.deleted) ...[ - if (showUndoRedoButtons) + if (note is RichTextNote && showUndoRedoButtons) ValueListenableBuilder( valueListenable: fleatherControllerCanUndoNotifier, builder: (context, canUndo, child) => IconButton( @@ -173,7 +161,7 @@ class _BackAppBarState extends ConsumerState { : null, ), ), - if (showUndoRedoButtons) + if (note is RichTextNote && showUndoRedoButtons) ValueListenableBuilder( valueListenable: fleatherControllerCanRedoNotifier, builder: (context, canRedo, child) => IconButton( @@ -182,7 +170,7 @@ class _BackAppBarState extends ConsumerState { onPressed: editorHasFocus && canRedo && isEditMode ? redo : null, ), ), - if (showChecklistButton) + if (note is RichTextNote && showChecklistButton) IconButton( icon: const Icon(Icons.checklist), tooltip: l.tooltip_toggle_checkbox, diff --git a/lib/common/navigation/app_bars/notes_selection_app_bar.dart b/lib/common/navigation/app_bars/notes_selection_app_bar.dart index 1dea8abe..5fbedbdd 100644 --- a/lib/common/navigation/app_bars/notes_selection_app_bar.dart +++ b/lib/common/navigation/app_bars/notes_selection_app_bar.dart @@ -102,25 +102,27 @@ class NotesSelectionAppBar extends ConsumerWidget { } @override - Widget build(BuildContext context, WidgetRef ref) => notesPage - ? ref.watch(notesProvider(label: currentLabelFilter)).when( - data: (notes) => buildAppBar( - context, - ref, - notes.where((note) => note.selected).toList(), - notes.length, - ), - error: (exception, stackTrace) => ErrorPlaceholder(exception: exception, stackTrace: stackTrace), - loading: () => const LoadingPlaceholder(), - ) - : ref.watch(binProvider).when( - data: (notes) => buildAppBar( - context, - ref, - notes.where((note) => note.selected).toList(), - notes.length, - ), - error: (exception, stackTrace) => ErrorPlaceholder(exception: exception, stackTrace: stackTrace), - loading: () => const LoadingPlaceholder(), - ); + Widget build(BuildContext context, WidgetRef ref) { + return notesPage + ? ref.watch(notesProvider(label: currentLabelFilter)).when( + data: (notes) => buildAppBar( + context, + ref, + notes.where((note) => note.selected).toList(), + notes.length, + ), + error: (exception, stackTrace) => ErrorPlaceholder(exception: exception, stackTrace: stackTrace), + loading: () => const LoadingPlaceholder(), + ) + : ref.watch(binProvider).when( + data: (notes) => buildAppBar( + context, + ref, + notes.where((note) => note.selected).toList(), + notes.length, + ), + error: (exception, stackTrace) => ErrorPlaceholder(exception: exception, stackTrace: stackTrace), + loading: () => const LoadingPlaceholder(), + ); + } } diff --git a/lib/common/widgets/notes/notes_list.dart b/lib/common/widgets/notes/notes_list.dart index 9a51b480..e6f8aa9a 100644 --- a/lib/common/widgets/notes/notes_list.dart +++ b/lib/common/widgets/notes/notes_list.dart @@ -2,11 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart'; -import '../../../models/label/label.dart'; import '../../../models/note/note.dart'; import '../../../providers/bin/bin_provider.dart'; import '../../../providers/notes/notes_provider.dart'; -import '../../../providers/notifiers/notifiers.dart'; import '../../../providers/preferences/preferences_provider.dart'; import '../../constants/paddings.dart'; import '../../constants/separators.dart'; @@ -83,9 +81,7 @@ class NotesList extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return notesPage ? ref.watch(notesProvider(label: label)).when( - data: (notes) { - return child(context, ref, notes); - }, + data: (notes) => child(context, ref, notes), error: (exception, stackTrace) => ErrorPlaceholder(exception: exception, stackTrace: stackTrace), loading: () => const LoadingPlaceholder(), ) diff --git a/lib/models/note/index/note_index.dart b/lib/models/note/index/note_index.dart index abb6b778..523a1857 100644 --- a/lib/models/note/index/note_index.dart +++ b/lib/models/note/index/note_index.dart @@ -1,4 +1,5 @@ import 'package:json_annotation/json_annotation.dart'; + import '../note.dart'; part 'note_index.g.dart'; @@ -7,7 +8,7 @@ part 'note_index.g.dart'; @JsonSerializable() class NoteIndex { /// The ID of the note. - final int id; + final String id; /// Whether the note is deleted. final bool deleted; diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index 5b728eef..8e33993c 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -22,11 +22,15 @@ List labelToJson(IsarLinks