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(); }