refactor: Update Note and Category models to use 'id' instead of 'uuid', and adjust related database operations

- Changed 'uuid' to 'id' in Note and Category models for consistency.
- Updated database operations in NoteRepository to reflect the new 'id' field.
- Modified sync models to accommodate changes in Note and Category structures.
- Adjusted the handling of notes and categories during synchronization.
- Refactored the note editor and home screen to use the new 'id' field.
- Ensured that the 'isDirty' flag is properly set and utilized across models.
This commit is contained in:
2026-05-20 11:05:30 +02:00
parent 34f45a912f
commit def755e1c5
10 changed files with 520 additions and 323 deletions
+144 -82
View File
@@ -35,9 +35,9 @@ class NoteRepository {
}
Future<Note> createNote(Note note) async {
final int id = await _database.insertNoteAtTop(
await _database.insertNoteAtTop(
NotesCompanion.insert(
uuid: note.uuid,
id: note.id,
title: note.title,
body: note.body,
createdAt: note.createdAt,
@@ -45,56 +45,77 @@ class NoteRepository {
sortIndex: 0,
serverVersion: const Value(0),
isDeleted: const Value(false),
categoryId: const Value(null),
categoryId: Value(note.categoryId),
isDirty: const Value(true),
),
);
return note.copyWith(id: id, index: 0);
return note.copyWith(position: 0, isDirty: true);
}
Future<Note> updateNote(Note note) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to update a note.'));
final DbNote? existingNote = await (_database.select(
_database.notes,
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
final DbNote row =
existingNote ??
(throw ArgumentError('Note not found for id ${note.id}.'));
await _database.updateNoteRow(
DbNote(
id: noteId,
uuid: note.uuid,
id: row.id,
title: note.title,
body: note.body,
createdAt: note.createdAt,
createdAt: row.createdAt,
updatedAt: note.updatedAt,
sortIndex: note.index,
sortIndex: row.sortIndex,
serverVersion: note.serverVersion,
isDeleted: false,
categoryId: note.categoryId,
isDirty: true,
),
);
return note.copyWith(isDeleted: false, isPermanentlyDeleted: false);
return note.copyWith(
isDeleted: false,
isPermanentlyDeleted: false,
isDirty: true,
position: row.sortIndex.toDouble(),
);
}
Future<void> deleteNote(Note note) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to delete a note.'));
final DbNote? existingNote = await (_database.select(
_database.notes,
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
if (note.isDeleted || note.isPermanentlyDeleted) {
await _database.permanentlyDeleteNote(noteId);
final DbNote row =
existingNote ??
(throw ArgumentError('Note not found for id ${note.id}.'));
if (row.isDeleted || note.isDeleted || note.isPermanentlyDeleted) {
await _database.permanentlyDeleteNote(row.id);
} else {
await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index);
await _database.deleteNoteAndShift(
id: row.id,
removedIndex: row.sortIndex,
);
}
}
Future<void> moveNote(Note note, int newIndex) async {
final int noteId =
note.id ??
(throw ArgumentError('Note id is required to reorder a note.'));
final DbNote? existingNote = await (_database.select(
_database.notes,
)..where((n) => n.id.equals(note.id))).getSingleOrNull();
final DbNote row =
existingNote ??
(throw ArgumentError('Note not found for id ${note.id}.'));
await _database.moveNote(
id: noteId,
oldIndex: note.index,
id: row.id,
oldIndex: row.sortIndex,
newIndex: newIndex,
);
}
@@ -120,26 +141,17 @@ class NoteRepository {
? DateTime.utc(1970, 1, 1)
: lastSync;
// Collect pending local changes.
// If we already synced at least once, use updatedAt to avoid re-sending
// old notes that were already uploaded.
final List<DbNote> unsyncedNotes;
final List<DbCategory> unsyncedCategories;
if (forceFull || lastSync == null) {
unsyncedNotes = await _database.getUnsyncedNotes();
unsyncedCategories = await _database.getUnsyncedCategories();
} else {
unsyncedNotes = await _database.getNotesChangedSince(lastSync);
unsyncedCategories = await _database.getCategoriesChangedSince(
lastSync,
);
}
// Collect pending local changes. Dirty flags are the source of truth for
// outbound sync; `lastSyncAt` is only used for asking the server what it
// changed since our previous successful sync.
final List<DbNote> unsyncedNotes = await _database.getUnsyncedNotes();
final List<DbCategory> unsyncedCategories = await _database
.getUnsyncedCategories();
final int totalNotesToEncrypt = unsyncedNotes.length;
// Build sync request (note: we send encrypted data, but locally we have plaintext)
// Encrypt notes that still contain content before sending.
// Build sync request: local data is plaintext, encryption happens only
// for the outbound payload.
if (totalNotesToEncrypt == 0) {
onProgress?.call(
SyncStatus.encrypting,
@@ -164,9 +176,8 @@ class NoteRepository {
},
);
final List<SyncCategoryPayload> categoriesPayload = unsyncedCategories
.map((cat) => SyncCategoryPayload.fromCategory(_fromDbCategory(cat)))
.toList();
final List<SyncCategoryPayload> categoriesPayload =
await _encryptCategories(unsyncedCategories, masterKey: _masterKey);
final SyncRequest syncRequest = SyncRequest(
lastSyncAt: lastSyncForRequest,
@@ -184,7 +195,27 @@ class NoteRepository {
final Map<String, dynamic> syncResult = await _authApi.sync(syncRequest);
if (syncResult['error'] == true) {
return {'error': true, 'message': syncResult['body']};
final List<String> details = [];
final Object? message = syncResult['message'];
final Object? exception = syncResult['exception'];
final Object? stackTrace = syncResult['stackTrace'];
final Object? body = syncResult['body'];
if (message != null) details.add(message.toString());
if (exception != null && exception.toString() != details.firstOrNull) {
details.add('Exception: ${exception.toString()}');
}
if (body != null) {
details.add('Body: ${body.toString()}');
}
if (stackTrace != null) {
details.add('StackTrace: ${stackTrace.toString()}');
}
return {
'error': true,
'message': details.join('\n\n'),
};
}
final SyncResponse response = syncResult['data'] as SyncResponse;
@@ -217,8 +248,11 @@ class NoteRepository {
'notesCount': response.changes.notes.length,
'categoriesCount': response.changes.categories.length,
};
} catch (e) {
return {'error': true, 'message': e.toString()};
} catch (e, st) {
return {
'error': true,
'message': '$e\n\nStackTrace: $st',
};
}
}
@@ -229,13 +263,23 @@ class NoteRepository {
// Apply categories from server
for (final SyncCategoryResponse catResponse
in response.changes.categories) {
final String categoryName =
catResponse.isDeleted || catResponse.encryptedName.isEmpty
? ''
: await NoteEncryption.decryptNote(
catResponse.encryptedName,
_masterKey,
);
await _database.upsertCategory(
CategoriesCompanion(
uuid: Value(catResponse.id),
encryptedName: Value(catResponse.encryptedName),
id: Value(catResponse.id),
encryptedName: Value(categoryName),
serverVersion: Value(catResponse.serverVersion),
isDeleted: Value(catResponse.isDeleted),
updatedAt: Value(catResponse.updatedAt),
isDirty: const Value(false),
),
);
}
@@ -254,14 +298,14 @@ class NoteRepository {
for (var index = 0; index < decryptedNotes.length; index += 1) {
final Map<String, Object?> decryptedNote = decryptedNotes[index];
final String noteId = decryptedNote['id']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index];
final String noteId = (decryptedNote['id'] as String?) ?? noteResponse.id;
final existingNote = await (_database.select(
_database.notes,
)..where((n) => n.uuid.equals(noteId))).getSingleOrNull();
)..where((n) => n.id.equals(noteId))).getSingleOrNull();
final String decryptedTitle = decryptedNote['title']! as String;
final String decryptedBody = decryptedNote['body']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index];
final String decryptedTitle = (decryptedNote['title'] as String?) ?? '';
final String decryptedBody = (decryptedNote['body'] as String?) ?? '';
final bool isPermanentlyDeleted = noteResponse.isPermanentlyDeleted;
if (existingNote != null) {
@@ -269,15 +313,15 @@ class NoteRepository {
await _database.updateNoteRow(
DbNote(
id: existingNote.id,
uuid: noteId,
title: isPermanentlyDeleted ? '' : decryptedTitle,
body: isPermanentlyDeleted ? '' : decryptedBody,
createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position,
sortIndex: noteResponse.position.round(),
serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted,
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
isDirty: false,
),
);
} else {
@@ -286,15 +330,18 @@ class NoteRepository {
.into(_database.notes)
.insert(
NotesCompanion(
uuid: Value(noteResponse.id),
id: Value(noteResponse.id),
title: Value(isPermanentlyDeleted ? '' : decryptedTitle),
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position),
sortIndex: Value(noteResponse.position.round()),
serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted),
categoryId: Value(isPermanentlyDeleted ? null : noteResponse.categoryId),
categoryId: Value(
isPermanentlyDeleted ? null : noteResponse.categoryId,
),
isDirty: const Value(false),
),
);
}
@@ -314,26 +361,16 @@ class NoteRepository {
Note _fromDbNote(DbNote row) {
return Note(
id: row.id,
uuid: row.uuid,
title: row.title,
body: row.body,
createdAt: row.createdAt,
updatedAt: row.updatedAt,
index: row.sortIndex,
position: row.sortIndex.toDouble(),
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
isPermanentlyDeleted: _isPermanentlyDeleted(row),
categoryId: row.categoryId,
);
}
Category _fromDbCategory(DbCategory row) {
return Category(
uuid: row.uuid,
encryptedName: row.encryptedName,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
updatedAt: row.updatedAt,
isDirty: row.isDirty,
);
}
}
@@ -395,6 +432,35 @@ Future<List<SyncNotePayload>> _encryptNotesInParallel(
return orderedPayloads.cast<SyncNotePayload>();
}
Future<List<SyncCategoryPayload>> _encryptCategories(
List<DbCategory> categories, {
required String masterKey,
}) async {
final List<SyncCategoryPayload> payloads = [];
for (final DbCategory row in categories) {
final String encryptedName = row.encryptedName.isEmpty
? ''
: await NoteEncryption.encryptNote(row.encryptedName, masterKey);
payloads.add(
SyncCategoryPayload.fromCategory(
Category(
id: row.id,
name: row.encryptedName,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
updatedAt: row.updatedAt,
isDirty: row.isDirty,
),
encryptedName: encryptedName,
),
);
}
return payloads;
}
List<Future<List<Map<String, Object?>>>> _decryptNoteBatches(
List<SyncNoteResponse> notes, {
required String masterKey,
@@ -467,7 +533,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
return <String, Object?>{
'index': index,
'uuid': row.uuid,
'id': row.id,
'title': row.title,
'body': row.body,
'createdAt': row.createdAt.toIso8601String(),
@@ -487,7 +553,7 @@ SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
encryptedTitle: row['encryptedTitle']! as String,
encryptedBody: row['encryptedBody']! as String,
serverVersion: row['serverVersion']! as int,
position: row['position']! as int,
position: (row['position']! as num).toDouble(),
isDeleted: row['isDeleted']! as bool,
isPermanentlyDeleted: row['isPermanentlyDeleted']! as bool,
updatedAt: DateTime.parse(row['updatedAt']! as String),
@@ -513,19 +579,13 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
encryptedTitle = '';
encryptedBody = '';
} else {
encryptedTitle = await NoteEncryption.encryptNote(
title,
masterKey,
);
encryptedBody = await NoteEncryption.encryptNote(
body,
masterKey,
);
encryptedTitle = await NoteEncryption.encryptNote(title, masterKey);
encryptedBody = await NoteEncryption.encryptNote(body, masterKey);
}
encryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
'id': note['uuid'] as String,
'id': note['id'] as String,
'categoryId': note['categoryId'] as String?,
'encryptedTitle': encryptedTitle,
'encryptedBody': encryptedBody,
@@ -570,9 +630,11 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
decryptedBody = '';
}
final String noteId = note['id']! as String;
decryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
'id': note['id'] as String,
'id': noteId,
'title': decryptedTitle,
'body': decryptedBody,
'isPermanentlyDeleted': isPermanentlyDeleted,