Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions fastlane/metadata/android/en-US/changelogs/370.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
FIXED
- Performance drop with long notes
2 changes: 2 additions & 0 deletions fastlane/metadata/android/fr-FR/changelogs/370.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
CORRIGÉ
- Chutes de performances avec des notes longues
7 changes: 3 additions & 4 deletions lib/models/note/note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ sealed class Note implements Comparable<Note> {
return NoteStatus.available;
}

/// Note to JSON.
Map<String, dynamic> toJson();

/// The content as plain text.
@ignore
String get contentAsText;
Expand Down Expand Up @@ -189,10 +192,6 @@ sealed class Note implements Comparable<Note> {
@Index(type: IndexType.value, caseSensitive: false)
List<String> get titleIndexed => Isar.splitWords(title.toLowerCase());

/// The [contentAsText] indexed for full-text search.
@Index(type: IndexType.value, caseSensitive: false)
List<String> get contentIndexed => Isar.splitWords(contentAsText.toLowerCase());

/// Notes are sorted according to:
/// 1. Their pin state.
/// 2. The sort method chosen by the user.
Expand Down
1 change: 1 addition & 0 deletions lib/models/note/types/checklist_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class ChecklistNote extends Note {
}

/// Checklist note to JSON.
@override
Map<String, dynamic> toJson() => _$ChecklistNoteToJson(this);

@override
Expand Down
1 change: 1 addition & 0 deletions lib/models/note/types/markdown_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class MarkdownNote extends Note {
..content = EncryptionUtils().decrypt(password, json['content'] as String);

/// Plain text note to JSON.
@override
Map<String, dynamic> toJson() => _$MarkdownNoteToJson(this);

@override
Expand Down
1 change: 1 addition & 0 deletions lib/models/note/types/plain_text_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class PlainTextNote extends Note {
..content = EncryptionUtils().decrypt(password, json['content'] as String);

/// Plain text note to JSON.
@override
Map<String, dynamic> toJson() => _$PlainTextNoteToJson(this);

@override
Expand Down
1 change: 1 addition & 0 deletions lib/models/note/types/rich_text_note.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class RichTextNote extends Note {
..content = EncryptionUtils().decrypt(password, json['content'] as String);

/// Rich text note to JSON.
@override
Map<String, dynamic> toJson() => _$RichTextNoteToJson(this);

@override
Expand Down
61 changes: 49 additions & 12 deletions lib/pages/editor/widgets/editors/checklist_editor.dart
Original file line number Diff line number Diff line change
@@ -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});

Expand All @@ -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<ChecklistLine> checklistLines) {
ChecklistNote newNote = note
..checkboxes = checklistLines.map((checklistLine) => checklistLine.toggled).toList()
..texts = checklistLines.map((checklistLine) => checklistLine.text).toList();
@override
ConsumerState<ChecklistEditor> createState() => _ChecklistEditorState();
}

class _ChecklistEditorState extends ConsumerState<ChecklistEditor> {
Timer? saveDebounce;
List<ChecklistLine>? pendingChecklistLines;

void onChanged(List<ChecklistLine> 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),
),
),
],
Expand Down
28 changes: 24 additions & 4 deletions lib/pages/editor/widgets/editors/markdown_editor.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -33,6 +35,7 @@ class MarkdownEditor extends ConsumerStatefulWidget {

class _MarkdownEditorState extends ConsumerState<MarkdownEditor> {
late final TextEditingController contentTextController;
Timer? saveDebounce;

@override
void initState() {
Expand All @@ -41,10 +44,16 @@ class _MarkdownEditorState extends ConsumerState<MarkdownEditor> {
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) {
Expand All @@ -57,6 +66,17 @@ class _MarkdownEditorState extends ConsumerState<MarkdownEditor> {
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);
Expand Down Expand Up @@ -93,7 +113,7 @@ class _MarkdownEditorState extends ConsumerState<MarkdownEditor> {
expands: true,
decoration: InputDecoration.collapsed(hintText: context.l.hint_content),
spellCheckConfiguration: SpellCheckConfiguration(spellCheckService: DefaultSpellCheckService()),
onChanged: onChanged,
onChanged: (_) => onChanged(),
),
);
}
Expand Down
28 changes: 24 additions & 4 deletions lib/pages/editor/widgets/editors/plain_text_editor.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
Expand Down Expand Up @@ -30,6 +32,7 @@ class PlainTextEditor extends ConsumerStatefulWidget {

class _PlainTextEditorState extends ConsumerState<PlainTextEditor> {
late final TextEditingController contentTextController;
Timer? saveDebounce;

@override
void initState() {
Expand All @@ -38,10 +41,27 @@ class _PlainTextEditorState extends ConsumerState<PlainTextEditor> {
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
Expand All @@ -57,7 +77,7 @@ class _PlainTextEditorState extends ConsumerState<PlainTextEditor> {
expands: true,
decoration: InputDecoration.collapsed(hintText: context.l.hint_content),
spellCheckConfiguration: SpellCheckConfiguration(spellCheckService: DefaultSpellCheckService()),
onChanged: onChanged,
onChanged: (_) => onChanged(),
),
);
}
Expand Down
18 changes: 16 additions & 2 deletions lib/pages/editor/widgets/editors/rich_text_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class RichTextEditor extends ConsumerStatefulWidget {

class _RichTextEditorState extends ConsumerState<RichTextEditor> {
late StreamSubscription<ParchmentChange> changes;
Timer? saveDebounce;

@override
void initState() {
Expand Down Expand Up @@ -73,15 +74,28 @@ class _RichTextEditorState extends ConsumerState<RichTextEditor> {
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();
}

Expand Down
12 changes: 4 additions & 8 deletions lib/services/notes/notes_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,7 @@ class NotesService {
.optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!)))
.allOf<String, PlainTextNote>(
searchWords,
(q, word) =>
q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)),
(q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)),
)
.findAll()),
...await (_markdownNotes
Expand All @@ -167,8 +166,7 @@ class NotesService {
.optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!)))
.allOf<String, MarkdownNote>(
searchWords,
(q, word) =>
q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)),
(q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)),
)
.findAll()),
...await (_richTextNotes
Expand All @@ -178,8 +176,7 @@ class NotesService {
.optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!)))
.allOf<String, RichTextNote>(
searchWords,
(q, word) =>
q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)),
(q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)),
)
.findAll()),
...await (_checklistNotes
Expand All @@ -189,8 +186,7 @@ class NotesService {
.optional(label != null, (q) => q.labels((q2) => q2.nameEqualTo(label!)))
.allOf<String, ChecklistNote>(
searchWords,
(q, word) =>
q.group((q2) => q2.titleIndexedElementStartsWith(word).or().contentIndexedElementStartsWith(word)),
(q, word) => q.group((q2) => q2.titleIndexedElementStartsWith(word)),
)
.findAll()),
];
Expand Down
2 changes: 1 addition & 1 deletion pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading