From eb37203d0b9c263a538aaa96d7245eadce46d0d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:22:02 +0200 Subject: [PATCH 1/3] fix: factorize editor save and fix rich text note not saving when switching to read-only mode (#562) --- lib/pages/editor/editor_page.dart | 87 ++++++++++++++++--- .../widgets/editors/checklist_editor.dart | 53 +++-------- .../widgets/editors/markdown_editor.dart | 36 +++----- .../widgets/editors/plain_text_editor.dart | 36 +++----- .../widgets/editors/rich_text_editor.dart | 20 ++--- 5 files changed, 114 insertions(+), 118 deletions(-) diff --git a/lib/pages/editor/editor_page.dart b/lib/pages/editor/editor_page.dart index fca4a0e5..bdb88a3f 100644 --- a/lib/pages/editor/editor_page.dart +++ b/lib/pages/editor/editor_page.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:fleather/fleather.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -35,6 +37,12 @@ class EditorPage extends ConsumerStatefulWidget { } class _EditorState extends ConsumerState { + Timer? saveDebounce; + Note? pendingNote; + + FleatherController? _fleatherController; + RichTextNote? _fleatherControllerNote; + @override void initState() { super.initState(); @@ -47,6 +55,15 @@ class _EditorState extends ConsumerState { @override void dispose() { + // If a save was waiting to happen, save immediately + if (saveDebounce?.isActive ?? false) { + saveDebounce!.cancel(); + + if (pendingNote != null) { + save(pendingNote!, globalRef); + } + } + // ignore: avoid_ref_inside_state_dispose globalRef.read(notesProvider(status: NoteStatus.available).notifier).removeEmpty(); // ignore: avoid_ref_inside_state_dispose @@ -59,6 +76,42 @@ class _EditorState extends ConsumerState { editorFocusNode.requestFocus(); } + void scheduleSave(Note note) { + pendingNote = note; + + saveDebounce?.cancel(); + saveDebounce = Timer(const Duration(milliseconds: 1000), () => save(note)); + } + + void save(Note note, [WidgetRef? widgetRef]) { + (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + + pendingNote = null; + } + + FleatherController getFleatherController(RichTextNote note) { + // Do not reset the controller if the note hasn't changed (happens when the editing mode switches) + if (_fleatherController != null && _fleatherControllerNote == note) { + return _fleatherController!; + } + + _fleatherController?.dispose(); + _fleatherController = FleatherController( + document: note.document, + autoFormats: AutoFormats( + autoFormats: [ + const AutoFormatLinks(), + const MarkdownInlineShortcuts(), + const MarkdownLineShortcuts(), + const AutoTextDirection(), + ], + ), + ); + _fleatherControllerNote = note; + + return _fleatherController!; + } + @override Widget build(BuildContext context) { return ValueListenableBuilder( @@ -85,31 +138,37 @@ class _EditorState extends ConsumerState { Widget? toolbar; switch (currentNote) { case PlainTextNote note: - contentEditor = PlainTextEditor(note: note, readOnly: readOnly, autofocus: autofocus); - case RichTextNote note: - final fleatherController = FleatherController( - document: note.document, - autoFormats: AutoFormats( - autoFormats: [ - const AutoFormatLinks(), - const MarkdownInlineShortcuts(), - const MarkdownLineShortcuts(), - const AutoTextDirection(), - ], - ), + contentEditor = PlainTextEditor( + note: note, + readOnly: readOnly, + autofocus: autofocus, + onChanged: scheduleSave, ); + case RichTextNote note: + final fleatherController = getFleatherController(note); fleatherControllerNotifier.value = fleatherController; contentEditor = RichTextEditor( note: note, fleatherController: fleatherController, readOnly: readOnly, autofocus: autofocus, + onChanged: scheduleSave, ); toolbar = Toolbar(fleatherController: fleatherController); case MarkdownNote note: - contentEditor = MarkdownEditor(note: note, readOnly: readOnly, autofocus: autofocus); + contentEditor = MarkdownEditor( + note: note, + readOnly: readOnly, + autofocus: autofocus, + onChanged: scheduleSave, + ); case ChecklistNote note: - contentEditor = ChecklistEditor(note: note, isNewNote: widget.isNewNote, readOnly: readOnly); + contentEditor = ChecklistEditor( + note: note, + isNewNote: widget.isNewNote, + readOnly: readOnly, + onChanged: scheduleSave, + ); } final editor = Scaffold( diff --git a/lib/pages/editor/widgets/editors/checklist_editor.dart b/lib/pages/editor/widgets/editors/checklist_editor.dart index 5eee085c..200b8dab 100644 --- a/lib/pages/editor/widgets/editors/checklist_editor.dart +++ b/lib/pages/editor/widgets/editors/checklist_editor.dart @@ -1,19 +1,19 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter_checklist/checklist.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import '../../../../common/constants/constants.dart'; import '../../../../models/note/note.dart'; -import '../../../../models/note/note_status.dart'; -import '../../../../providers/notes/notes_provider.dart'; -import '../../../../providers/notifiers/notifiers.dart'; /// Checklist editor. class ChecklistEditor extends ConsumerStatefulWidget { /// Editor allowing to edit the checklist content of a [ChecklistNote]. - const ChecklistEditor({super.key, required this.note, required this.isNewNote, required this.readOnly}); + const ChecklistEditor({ + super.key, + required this.note, + required this.isNewNote, + required this.readOnly, + required this.onChanged, + }); /// The note to display. final ChecklistNote note; @@ -24,47 +24,22 @@ class ChecklistEditor extends ConsumerStatefulWidget { /// Whether the text fields are read only. final bool readOnly; + /// Called when the note has changed. + final ValueChanged onChanged; + @override ConsumerState createState() => _ChecklistEditorState(); } class _ChecklistEditorState extends ConsumerState { - Timer? saveDebounce; - List? pendingChecklistLines; - void onChanged(List checklistLines) { - pendingChecklistLines = checklistLines; - - // Reset the saving debounce timer on each change - saveDebounce?.cancel(); - saveDebounce = Timer(const Duration(milliseconds: 1000), save); - } - - void save([WidgetRef? widgetRef]) { - if (pendingChecklistLines == null) { - return; - } - - final checkboxes = pendingChecklistLines!.map((checklistLine) => checklistLine.toggled).toList(); - final texts = pendingChecklistLines!.map((checklistLine) => checklistLine.text).toList(); - final newNote = widget.note + final checkboxes = checklistLines.map((checklistLine) => checklistLine.toggled).toList(); + final texts = checklistLines.map((checklistLine) => checklistLine.text).toList(); + final note = widget.note ..checkboxes = checkboxes ..texts = texts; - (widgetRef ?? ref) - .read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier) - .edit(newNote); - } - - @override - void dispose() { - // If a save was waiting to happen, save immediately - if (saveDebounce?.isActive ?? false) { - saveDebounce!.cancel(); - save(globalRef); - } - - super.dispose(); + widget.onChanged(note); } @override diff --git a/lib/pages/editor/widgets/editors/markdown_editor.dart b/lib/pages/editor/widgets/editors/markdown_editor.dart index 9dd1d81d..d287b42e 100644 --- a/lib/pages/editor/widgets/editors/markdown_editor.dart +++ b/lib/pages/editor/widgets/editors/markdown_editor.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; @@ -11,14 +9,17 @@ import '../../../../common/constants/constants.dart'; import '../../../../common/constants/paddings.dart'; import '../../../../common/extensions/build_context_extension.dart'; import '../../../../models/note/note.dart'; -import '../../../../models/note/note_status.dart'; -import '../../../../providers/notes/notes_provider.dart'; -import '../../../../providers/notifiers/notifiers.dart'; /// Markdown editor. class MarkdownEditor extends ConsumerStatefulWidget { /// Markdown allowing to edit the markdown text content of a [MarkdownNote]. - const MarkdownEditor({super.key, required this.note, required this.readOnly, required this.autofocus}); + const MarkdownEditor({ + super.key, + required this.note, + required this.readOnly, + required this.autofocus, + required this.onChanged, + }); /// The note to display. final MarkdownNote note; @@ -29,13 +30,15 @@ class MarkdownEditor extends ConsumerStatefulWidget { /// Whether the text field should request focus. final bool autofocus; + /// Called when the note has changed. + final ValueChanged onChanged; + @override ConsumerState createState() => _MarkdownEditorState(); } class _MarkdownEditorState extends ConsumerState { late final TextEditingController contentTextController; - Timer? saveDebounce; @override void initState() { @@ -45,15 +48,9 @@ class _MarkdownEditorState extends ConsumerState { } void onChanged() { - // Reset the saving debounce timer on each change - saveDebounce?.cancel(); - saveDebounce = Timer(const Duration(milliseconds: 1000), save); - } - - void save([WidgetRef? widgetRef]) { final note = widget.note..content = contentTextController.text; - (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + widget.onChanged(note); } void onOpenLink(String text, String? href, String title) { @@ -66,17 +63,6 @@ class _MarkdownEditorState extends ConsumerState { launchUrl(uri); } - @override - void dispose() { - // If a save was waiting to happen, save immediately - if (saveDebounce?.isActive ?? false) { - saveDebounce!.cancel(); - save(globalRef); - } - - super.dispose(); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); diff --git a/lib/pages/editor/widgets/editors/plain_text_editor.dart b/lib/pages/editor/widgets/editors/plain_text_editor.dart index fb77cc3e..e62af973 100644 --- a/lib/pages/editor/widgets/editors/plain_text_editor.dart +++ b/lib/pages/editor/widgets/editors/plain_text_editor.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -8,14 +6,17 @@ import '../../../../common/constants/constants.dart'; import '../../../../common/constants/paddings.dart'; import '../../../../common/extensions/build_context_extension.dart'; import '../../../../models/note/note.dart'; -import '../../../../models/note/note_status.dart'; -import '../../../../providers/notes/notes_provider.dart'; -import '../../../../providers/notifiers/notifiers.dart'; /// Plain text editor. class PlainTextEditor extends ConsumerStatefulWidget { /// Text editor allowing to edit the plain text content of a [PlainTextNote]. - const PlainTextEditor({super.key, required this.note, required this.readOnly, required this.autofocus}); + const PlainTextEditor({ + super.key, + required this.note, + required this.readOnly, + required this.autofocus, + required this.onChanged, + }); /// The note to display. final PlainTextNote note; @@ -26,13 +27,15 @@ class PlainTextEditor extends ConsumerStatefulWidget { /// Whether the text field should request focus. final bool autofocus; + /// Called when the note has changed. + final ValueChanged onChanged; + @override ConsumerState createState() => _PlainTextEditorState(); } class _PlainTextEditorState extends ConsumerState { late final TextEditingController contentTextController; - Timer? saveDebounce; @override void initState() { @@ -42,26 +45,9 @@ class _PlainTextEditorState extends ConsumerState { } void onChanged() { - // Reset the saving debounce timer on each change - saveDebounce?.cancel(); - saveDebounce = Timer(const Duration(milliseconds: 1000), save); - } - - void save([WidgetRef? widgetRef]) { final note = widget.note..content = contentTextController.text; - (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); - } - - @override - void dispose() { - // If a save was waiting to happen, save immediately - if (saveDebounce?.isActive ?? false) { - saveDebounce!.cancel(); - save(globalRef); - } - - super.dispose(); + widget.onChanged(note); } @override diff --git a/lib/pages/editor/widgets/editors/rich_text_editor.dart b/lib/pages/editor/widgets/editors/rich_text_editor.dart index 7baacfff..1e32b90c 100644 --- a/lib/pages/editor/widgets/editors/rich_text_editor.dart +++ b/lib/pages/editor/widgets/editors/rich_text_editor.dart @@ -13,8 +13,6 @@ import '../../../../common/extensions/build_context_extension.dart'; import '../../../../common/preferences/enums/font.dart'; import '../../../../common/preferences/preference_key.dart'; import '../../../../models/note/note.dart'; -import '../../../../models/note/note_status.dart'; -import '../../../../providers/notes/notes_provider.dart'; import '../../../../providers/notifiers/notifiers.dart'; /// Rich text editor. @@ -26,6 +24,7 @@ class RichTextEditor extends ConsumerStatefulWidget { required this.note, required this.readOnly, required this.autofocus, + required this.onChanged, }); /// The note to display. @@ -40,6 +39,9 @@ class RichTextEditor extends ConsumerStatefulWidget { /// Whether the text field should request focus. final bool autofocus; + /// Called when the note has changed. + final ValueChanged onChanged; + @override ConsumerState createState() => _RichTextEditorState(); } @@ -74,28 +76,16 @@ class _RichTextEditorState extends ConsumerState { fleatherControllerCanUndoNotifier.value = widget.fleatherController.canUndo; fleatherControllerCanRedoNotifier.value = widget.fleatherController.canRedo; - // Reset the saving debounce timer on each change - saveDebounce?.cancel(); - saveDebounce = Timer(const Duration(milliseconds: 1000), save); - } - - void save([WidgetRef? widgetRef]) { final content = jsonEncode(widget.fleatherController.document.toJson()); final note = widget.note..content = content; - (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + widget.onChanged(note); } @override void dispose() { changes.cancel(); - // If a save was waiting to happen, save immediately - if (saveDebounce?.isActive ?? false) { - saveDebounce!.cancel(); - save(globalRef); - } - super.dispose(); } From 5a2bc3bfafad36e2f83f150ed1cb86766a66a9a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:25:31 +0200 Subject: [PATCH 2/3] chore: v2.2.2 --- CHANGELOG.md | 6 ++++++ fastlane/metadata/android/en-US/changelogs/380.txt | 2 ++ fastlane/metadata/android/fr-FR/changelogs/380.txt | 2 ++ pubspec.yaml | 2 +- 4 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/380.txt create mode 100644 fastlane/metadata/android/fr-FR/changelogs/380.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 481275e9..8bc000ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 2.2.2 - 2026-07-14 + +### Fixed + +- Rich text note not saving when switching to read-only mode + ## 2.2.1 - 2026-07-10 ### Fixed diff --git a/fastlane/metadata/android/en-US/changelogs/380.txt b/fastlane/metadata/android/en-US/changelogs/380.txt new file mode 100644 index 00000000..28249a46 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/380.txt @@ -0,0 +1,2 @@ +FIXED +- Rich text note not saving when switching to read-only mode \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/380.txt b/fastlane/metadata/android/fr-FR/changelogs/380.txt new file mode 100644 index 00000000..5fdc8fd0 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/380.txt @@ -0,0 +1,2 @@ +CORRIGÉ +- Note de texte riche ne s'enregistrait pas lors du passage en mode lecture seule \ No newline at end of file diff --git a/pubspec.yaml b/pubspec.yaml index 27032f9b..60f81d96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/maelchiotti/LocalMaterialNotes publish_to: none -version: 2.2.1+37 +version: 2.2.2+38 environment: sdk: ^3.12.0 From 11e4ba1cba7dc1d20fd63baa064d205350421f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ma=C3=ABl=20Chiotti?= <44336112+maelchiotti@users.noreply.github.com> Date: Tue, 14 Jul 2026 18:41:02 +0200 Subject: [PATCH 3/3] chore: v2.2.2 --- fastlane/metadata/android/fr-FR/changelogs/380.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fastlane/metadata/android/fr-FR/changelogs/380.txt b/fastlane/metadata/android/fr-FR/changelogs/380.txt index 5fdc8fd0..e8c67bf3 100644 --- a/fastlane/metadata/android/fr-FR/changelogs/380.txt +++ b/fastlane/metadata/android/fr-FR/changelogs/380.txt @@ -1,2 +1,2 @@ CORRIGÉ -- Note de texte riche ne s'enregistrait pas lors du passage en mode lecture seule \ No newline at end of file +- Note de texte riche ne s'enregistre pas lors du passage en mode lecture seule \ No newline at end of file