diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a8ff20a..481275e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,13 @@ 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.0.0 - 2026-07-09 +## 2.2.1 - 2026-07-10 + +### Fixed + +- Performance drop with long notes + +## 2.2.0 - 2026-07-09 ### Fixed diff --git a/fastlane/metadata/android/en-US/changelogs/370.txt b/fastlane/metadata/android/en-US/changelogs/370.txt new file mode 100644 index 00000000..cab29701 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/370.txt @@ -0,0 +1,2 @@ +FIXED +- Performance drop with long notes \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/370.txt b/fastlane/metadata/android/fr-FR/changelogs/370.txt new file mode 100644 index 00000000..0ff8c7d5 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/370.txt @@ -0,0 +1,2 @@ +CORRIGÉ +- Chutes de performances avec des notes longues \ No newline at end of file diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index af7584bb..ce83537e 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -113,6 +113,9 @@ sealed class Note implements Comparable { return NoteStatus.available; } + /// Note to JSON. + Map toJson(); + /// The content as plain text. @ignore String get contentAsText; @@ -189,10 +192,6 @@ sealed class Note implements Comparable { @Index(type: IndexType.value, caseSensitive: false) List get titleIndexed => Isar.splitWords(title.toLowerCase()); - /// The [contentAsText] indexed for full-text search. - @Index(type: IndexType.value, caseSensitive: false) - List get contentIndexed => Isar.splitWords(contentAsText.toLowerCase()); - /// Notes are sorted according to: /// 1. Their pin state. /// 2. The sort method chosen by the user. diff --git a/lib/models/note/types/checklist_note.dart b/lib/models/note/types/checklist_note.dart index 246ef58b..161be227 100644 --- a/lib/models/note/types/checklist_note.dart +++ b/lib/models/note/types/checklist_note.dart @@ -68,6 +68,7 @@ class ChecklistNote extends Note { } /// Checklist note to JSON. + @override Map toJson() => _$ChecklistNoteToJson(this); @override diff --git a/lib/models/note/types/markdown_note.dart b/lib/models/note/types/markdown_note.dart index 371c518d..c92f1b50 100644 --- a/lib/models/note/types/markdown_note.dart +++ b/lib/models/note/types/markdown_note.dart @@ -53,6 +53,7 @@ class MarkdownNote extends Note { ..content = EncryptionUtils().decrypt(password, json['content'] as String); /// Plain text note to JSON. + @override Map toJson() => _$MarkdownNoteToJson(this); @override diff --git a/lib/models/note/types/plain_text_note.dart b/lib/models/note/types/plain_text_note.dart index ceb24af2..938590b8 100644 --- a/lib/models/note/types/plain_text_note.dart +++ b/lib/models/note/types/plain_text_note.dart @@ -53,6 +53,7 @@ class PlainTextNote extends Note { ..content = EncryptionUtils().decrypt(password, json['content'] as String); /// Plain text note to JSON. + @override Map toJson() => _$PlainTextNoteToJson(this); @override diff --git a/lib/models/note/types/rich_text_note.dart b/lib/models/note/types/rich_text_note.dart index a0e74086..f11eb5c0 100644 --- a/lib/models/note/types/rich_text_note.dart +++ b/lib/models/note/types/rich_text_note.dart @@ -56,6 +56,7 @@ class RichTextNote extends Note { ..content = EncryptionUtils().decrypt(password, json['content'] as String); /// Rich text note to JSON. + @override Map toJson() => _$RichTextNoteToJson(this); @override diff --git a/lib/pages/editor/widgets/editors/checklist_editor.dart b/lib/pages/editor/widgets/editors/checklist_editor.dart index 90498fe5..5eee085c 100644 --- a/lib/pages/editor/widgets/editors/checklist_editor.dart +++ b/lib/pages/editor/widgets/editors/checklist_editor.dart @@ -1,14 +1,17 @@ +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 ConsumerWidget { +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}); @@ -21,27 +24,61 @@ class ChecklistEditor extends ConsumerWidget { /// Whether the text fields are read only. final bool readOnly; - /// Called when an item of the checklist changes with the new [checklistLines]. - void onChecklistChanged(WidgetRef ref, List checklistLines) { - ChecklistNote newNote = note - ..checkboxes = checklistLines.map((checklistLine) => checklistLine.toggled).toList() - ..texts = checklistLines.map((checklistLine) => checklistLine.text).toList(); + @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 + ..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); + } - ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(newNote); + super.dispose(); } @override - Widget build(BuildContext context, WidgetRef ref) { + Widget build(BuildContext context) { return Column( children: [ Expanded( child: Checklist( - lines: note.checklistLines, - enabled: !readOnly, - autofocusFirstLine: isNewNote, + lines: widget.note.checklistLines, + enabled: !widget.readOnly, + autofocusFirstLine: widget.isNewNote, textInputAction: TextInputAction.newline, textCapitalization: TextCapitalization.sentences, - onChanged: (checklistLines) => onChecklistChanged(ref, checklistLines), + onChanged: (checklistLines) => onChanged(checklistLines), ), ), ], diff --git a/lib/pages/editor/widgets/editors/markdown_editor.dart b/lib/pages/editor/widgets/editors/markdown_editor.dart index 548336f2..9dd1d81d 100644 --- a/lib/pages/editor/widgets/editors/markdown_editor.dart +++ b/lib/pages/editor/widgets/editors/markdown_editor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; @@ -33,6 +35,7 @@ class MarkdownEditor extends ConsumerStatefulWidget { class _MarkdownEditorState extends ConsumerState { late final TextEditingController contentTextController; + Timer? saveDebounce; @override void initState() { @@ -41,10 +44,16 @@ class _MarkdownEditorState extends ConsumerState { contentTextController = TextEditingController(text: widget.note.content); } - void onChanged(String content) { - MarkdownNote note = widget.note..content = content; + 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; - ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); } void onOpenLink(String text, String? href, String title) { @@ -57,6 +66,17 @@ 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); @@ -93,7 +113,7 @@ class _MarkdownEditorState extends ConsumerState { expands: true, decoration: InputDecoration.collapsed(hintText: context.l.hint_content), spellCheckConfiguration: SpellCheckConfiguration(spellCheckService: DefaultSpellCheckService()), - onChanged: onChanged, + onChanged: (_) => onChanged(), ), ); } diff --git a/lib/pages/editor/widgets/editors/plain_text_editor.dart b/lib/pages/editor/widgets/editors/plain_text_editor.dart index 76824f3a..fb77cc3e 100644 --- a/lib/pages/editor/widgets/editors/plain_text_editor.dart +++ b/lib/pages/editor/widgets/editors/plain_text_editor.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -30,6 +32,7 @@ class PlainTextEditor extends ConsumerStatefulWidget { class _PlainTextEditorState extends ConsumerState { late final TextEditingController contentTextController; + Timer? saveDebounce; @override void initState() { @@ -38,10 +41,27 @@ class _PlainTextEditorState extends ConsumerState { contentTextController = TextEditingController(text: widget.note.content); } - void onChanged(String content) { - PlainTextNote note = widget.note..content = content; + 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); + } - ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + super.dispose(); } @override @@ -57,7 +77,7 @@ class _PlainTextEditorState extends ConsumerState { expands: true, decoration: InputDecoration.collapsed(hintText: context.l.hint_content), spellCheckConfiguration: SpellCheckConfiguration(spellCheckService: DefaultSpellCheckService()), - onChanged: onChanged, + onChanged: (_) => onChanged(), ), ); } diff --git a/lib/pages/editor/widgets/editors/rich_text_editor.dart b/lib/pages/editor/widgets/editors/rich_text_editor.dart index 40cc6b0e..7baacfff 100644 --- a/lib/pages/editor/widgets/editors/rich_text_editor.dart +++ b/lib/pages/editor/widgets/editors/rich_text_editor.dart @@ -46,6 +46,7 @@ class RichTextEditor extends ConsumerStatefulWidget { class _RichTextEditorState extends ConsumerState { late StreamSubscription changes; + Timer? saveDebounce; @override void initState() { @@ -73,15 +74,28 @@ class _RichTextEditorState extends ConsumerState { fleatherControllerCanUndoNotifier.value = widget.fleatherController.canUndo; fleatherControllerCanRedoNotifier.value = widget.fleatherController.canRedo; - RichTextNote note = widget.note..content = jsonEncode(widget.fleatherController.document.toJson()); + // 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; - ref.read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(note); + (widgetRef ?? ref).read(notesProvider(status: NoteStatus.available, label: currentLabelFilter).notifier).edit(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(); } diff --git a/lib/services/notes/notes_service.dart b/lib/services/notes/notes_service.dart index 8d7d2759..6c39f1c2 100644 --- a/lib/services/notes/notes_service.dart +++ b/lib/services/notes/notes_service.dart @@ -156,8 +156,7 @@ class NotesService { .optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!))) .allOf( searchWords, - (q, word) => - q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)), + (q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)), ) .findAll()), ...await (_markdownNotes @@ -167,8 +166,7 @@ class NotesService { .optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!))) .allOf( searchWords, - (q, word) => - q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)), + (q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)), ) .findAll()), ...await (_richTextNotes @@ -178,8 +176,7 @@ class NotesService { .optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!))) .allOf( searchWords, - (q, word) => - q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)), + (q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)), ) .findAll()), ...await (_checklistNotes @@ -189,8 +186,7 @@ class NotesService { .optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!))) .allOf( searchWords, - (q, word) => - q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)), + (q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)), ) .findAll()), ]; diff --git a/pubspec.yaml b/pubspec.yaml index f2395072..27032f9b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,7 +4,7 @@ repository: https://github.com/maelchiotti/LocalMaterialNotes publish_to: none -version: 2.2.0+36 +version: 2.2.1+37 environment: sdk: ^3.12.0