From 268fd9351cdaa78ce4b9f8585baf38545cc47c76 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 14:40:49 +0000 Subject: [PATCH 1/5] fix(settings): show the automatic export frequency of 3 days `settings_auto_export_frequency_value` had no `3` case although 3 days is one of the values of the slider (`automaticExportFrequenciesValues`), so the tile and the slider label showed "Every " with nothing after it. Adds `3{3 days}` to the English source. The other languages get it from Crowdin; until then they keep showing their `other` case. Refs #588 Co-Authored-By: Claude --- lib/l10n/translations/app_en.arb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/l10n/translations/app_en.arb b/lib/l10n/translations/app_en.arb index 5708e0c0..65e2885e 100644 --- a/lib/l10n/translations/app_en.arb +++ b/lib/l10n/translations/app_en.arb @@ -547,7 +547,7 @@ "@settings_auto_export_frequency": { "description": "Title of the settings tile to choose the frequency of the automatic exports." }, - "settings_auto_export_frequency_value": "Every {frequency, select, 1{day} 7{week} 14{2 weeks} 30{month} other{}}", + "settings_auto_export_frequency_value": "Every {frequency, select, 1{day} 3{3 days} 7{week} 14{2 weeks} 30{month} other{}}", "@settings_auto_export_frequency_value": { "description": "Frequency of the automatic exports.", "placeholders": { From cd8d8ab64c08488067077e9958c5f7b5cb011cf7 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 14:40:50 +0000 Subject: [PATCH 2/5] fix(database): run each migration in a single transaction The migrations to v2 and v3 removed the notes, then put them back, in separate transactions, and the `clear()` of v2 even ran outside of any transaction. A failure in the middle could lose the notes. Each migration now reads the notes and their labels first, then removes and puts back the notes and their label links in one `writeTxn`. The v3 migration changes the `id` of each note, and with it the `isarId` its label links are stored under, so the links were lost. It now reads the labels before and links them again after (`_putLabels`, which resets the links like `NotesService.putAllLabels`). Refs #588 Co-Authored-By: Claude --- lib/services/migration/migration_service.dart | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/lib/services/migration/migration_service.dart b/lib/services/migration/migration_service.dart index ac2e9859..b3730075 100644 --- a/lib/services/migration/migration_service.dart +++ b/lib/services/migration/migration_service.dart @@ -3,14 +3,13 @@ import 'package:isar_community/isar.dart'; import '../../common/constants/constants.dart'; import '../../common/preferences/preference_key.dart'; import '../../models/deprecated/note.dart'; +import '../../models/label/label.dart'; import '../../models/note/note.dart'; import '../database_service.dart'; -import '../notes/notes_service.dart'; /// Database migration service. class MigrationService { final _databaseService = DatabaseService(); - final _notesService = NotesService(); /// Migrates the database to the latest version if needed. Future migrateIfNeeded() async { @@ -30,13 +29,12 @@ class MigrationService { /// Moves all legacy notes from the legacy `notes` collection to the new `richTextNotes` collection /// to allow adding different types of notes. Future _migrateToV2() async { + final database = _databaseService.database; + // Get the notes and their labels - final notes = await _databaseService.database.notes.where().findAll(); + final notes = await database.notes.where().findAll(); final labels = notes.map((note) => note.labels.toList()).toList(); - // Remove the notes - await _databaseService.database.notes.clear(); - // Migrate the notes final migratedNotes = notes .map( @@ -53,9 +51,12 @@ class MigrationService { ) .toList(); - // Add the migrated notes to the new collection with their labels - await _notesService.putAll(migratedNotes); - await _notesService.putAllLabels(migratedNotes, labels); + // Replace the notes by the migrated ones with their labels, all at once so that a failure loses nothing + await database.writeTxn(() async { + await database.notes.clear(); + await database.richTextNotes.putAll(migratedNotes); + await _putLabels(migratedNotes, labels); + }); // Update the database version await PreferenceKey.databaseVersion.set(2); @@ -67,11 +68,12 @@ class MigrationService { /// /// Sets the new `id`, `archived` and `deletedTime` fields of each rich text note. Future _migrateToV3() async { - // Get the rich text notes - final richTextNotes = await _databaseService.database.richTextNotes.where().findAll(); + final database = _databaseService.database; - // Remove the notes with the old IDs - await _notesService.deleteAll(richTextNotes); + // Get the rich text notes, their labels and their old IDs + final richTextNotes = await database.richTextNotes.where().findAll(); + final labels = richTextNotes.map((note) => note.labels.toList()).toList(); + final oldIds = richTextNotes.map((note) => note.isarId).toList(); // Migrate the notes final migratedNotes = richTextNotes @@ -83,12 +85,26 @@ class MigrationService { ) .toList(); - // Put back the migrated notes with the right IDs - await _notesService.putAll(migratedNotes); + // Replace the notes with the old IDs by the migrated ones with their labels, which are linked to the IDs, + // all at once so that a failure loses nothing + await database.writeTxn(() async { + await database.richTextNotes.deleteAll(oldIds); + await database.richTextNotes.putAll(migratedNotes); + await _putLabels(migratedNotes, labels); + }); // Update the database version await PreferenceKey.databaseVersion.set(3); logger.i('Migrated the database from version 2 to version 3'); } + + /// Links each of the [notes] to its [labels], inside a write transaction. + Future _putLabels(List notes, List> labels) async { + for (var i = 0; i < notes.length; i++) { + await notes[i].labels.reset(); + notes[i].labels.addAll(labels[i]); + await notes[i].labels.save(); + } + } } From 2ac356ea1bfedf792eb3790b88698760afea9ffb Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 14:40:50 +0000 Subject: [PATCH 3/5] fix(notes): sort the notes by title case-insensitively `compareTo` compared the titles with `String.compareTo`, so "b" came after "Z". It now compares the lowercase titles, like the sort of the labels. The order of the notes sorted by title changes for the users with titles in mixed case. Refs #588 Co-Authored-By: Claude --- lib/models/note/note.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/models/note/note.dart b/lib/models/note/note.dart index 41013ca1..23334609 100644 --- a/lib/models/note/note.dart +++ b/lib/models/note/note.dart @@ -210,7 +210,9 @@ sealed class Note implements Comparable { case SortMethod.editedDate: return sortAscending ? editedTime.compareTo(other.editedTime) : other.editedTime.compareTo(editedTime); case SortMethod.title: - return sortAscending ? title.compareTo(other.title) : other.title.compareTo(title); + final titleComparison = title.toLowerCase().compareTo(other.title.toLowerCase()); + + return sortAscending ? titleComparison : -titleComparison; default: throw Exception('The sort method is not valid: $sortMethod'); } From 8e0fddedbd39004d80ec83e77fdfe7e0614f0219 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 14:40:50 +0000 Subject: [PATCH 4/5] refactor(settings): remove the unused preferenceSecure getter `PreferenceKey.preferenceSecure` called `PreferencesWrapper().get`, which throws an `ArgumentError` for a securely stored preference, and it was never used. The secure preferences are read with `preferenceOrDefaultSecure`. Refs #588 Co-Authored-By: Claude --- lib/common/preferences/preference_key.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/common/preferences/preference_key.dart b/lib/common/preferences/preference_key.dart index b6699507..8ed69bba 100644 --- a/lib/common/preferences/preference_key.dart +++ b/lib/common/preferences/preference_key.dart @@ -117,9 +117,6 @@ enum PreferenceKey { /// Returns the value of this preference if set, or its default value otherwise. T get preferenceOrDefault => PreferencesWrapper().get(this) ?? defaultValue; - /// Returns the value of this securely stored preference if set, or [null] otherwise. - T? get preferenceSecure => PreferencesWrapper().get(this); - /// Returns the value of this securely stored preference if set, or its default value otherwise. Future get preferenceOrDefaultSecure async => await PreferencesWrapper().getSecure(this) ?? defaultValue as String; From 8bc86feef4f18710b09810431980f08e4acb6ed5 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 16:30:24 +0000 Subject: [PATCH 5/5] refactor(database): remove the unused putAllLabels Since #585, the backup import links the labels inside `NotesService.import`, and the migrations now do it in their own transaction (`_putLabels`), so `NotesService.putAllLabels` is no longer used anywhere. Refs #588 Co-Authored-By: Claude --- lib/services/notes/notes_service.dart | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/lib/services/notes/notes_service.dart b/lib/services/notes/notes_service.dart index 215652f6..4d1cac93 100644 --- a/lib/services/notes/notes_service.dart +++ b/lib/services/notes/notes_service.dart @@ -242,25 +242,6 @@ class NotesService { }); } - /// Updates the [notes] with their corresponding [notesLabels] in the database. - Future putAllLabels(List notes, List> notesLabels) async { - assert( - notes.length == notesLabels.length, - 'The list of labels (${notesLabels.length} items) should be the same length as the list of notes (${notes.length} notes)', - ); - - await _database.writeTxn(() async { - for (var i = 0; i < notes.length; i++) { - final note = notes[i]; - final labels = notesLabels[i]; - - await note.labels.reset(); - note.labels.addAll(labels); - await note.labels.save(); - } - }); - } - /// Imports the [labels] and the [notes] in a single transaction, so that a failure leaves the database untouched. /// /// Only the labels whose name is not in the database yet are added. Each note is linked to the labels