feat: Implement permanent deletion and restoration of notes with updated UI

This commit is contained in:
2026-05-19 11:40:01 +02:00
parent 2a898111fa
commit f550476177
7 changed files with 236 additions and 132 deletions
+69 -29
View File
@@ -30,6 +30,10 @@ class NoteRepository {
return _loadNotesFromDatabase();
}
Future<List<Note>> loadDeletedNotes() async {
return _loadDeletedNotesFromDatabase();
}
Future<Note> createNote(Note note) async {
final int id = await _database.insertNoteAtTop(
NotesCompanion.insert(
@@ -63,12 +67,12 @@ class NoteRepository {
updatedAt: note.updatedAt,
sortIndex: note.index,
serverVersion: note.serverVersion,
isDeleted: note.isDeleted,
isDeleted: false,
categoryId: note.categoryId,
),
);
return note;
return note.copyWith(isDeleted: false, isPermanentlyDeleted: false);
}
Future<void> deleteNote(Note note) async {
@@ -76,7 +80,11 @@ class NoteRepository {
note.id ??
(throw ArgumentError('Note id is required to delete a note.'));
await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index);
if (note.isDeleted || note.isPermanentlyDeleted) {
await _database.permanentlyDeleteNote(noteId);
} else {
await _database.deleteNoteAndShift(id: noteId, removedIndex: note.index);
}
}
Future<void> moveNote(Note note, int newIndex) async {
@@ -131,7 +139,7 @@ class NoteRepository {
final int totalNotesToEncrypt = unsyncedNotes.length;
// Build sync request (note: we send encrypted data, but locally we have plaintext)
// Encrypt all notes before sending.
// Encrypt notes that still contain content before sending.
if (totalNotesToEncrypt == 0) {
onProgress?.call(
SyncStatus.encrypting,
@@ -254,6 +262,7 @@ class NoteRepository {
final String decryptedTitle = decryptedNote['title']! as String;
final String decryptedBody = decryptedNote['body']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index];
final bool isPermanentlyDeleted = noteResponse.isPermanentlyDeleted;
if (existingNote != null) {
// Update existing note
@@ -261,14 +270,14 @@ class NoteRepository {
DbNote(
id: existingNote.id,
uuid: noteId,
title: decryptedTitle,
body: decryptedBody,
title: isPermanentlyDeleted ? '' : decryptedTitle,
body: isPermanentlyDeleted ? '' : decryptedBody,
createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position,
serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted,
categoryId: noteResponse.categoryId,
categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
),
);
} else {
@@ -278,14 +287,14 @@ class NoteRepository {
.insert(
NotesCompanion(
uuid: Value(noteResponse.id),
title: Value(decryptedTitle),
body: Value(decryptedBody),
title: Value(isPermanentlyDeleted ? '' : decryptedTitle),
body: Value(isPermanentlyDeleted ? '' : decryptedBody),
createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position),
serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted),
categoryId: Value(noteResponse.categoryId),
categoryId: Value(isPermanentlyDeleted ? null : noteResponse.categoryId),
),
);
}
@@ -297,6 +306,11 @@ class NoteRepository {
return rows.map(_fromDbNote).toList();
}
Future<List<Note>> _loadDeletedNotesFromDatabase() async {
final List<DbNote> rows = await _database.getDeletedNotes();
return rows.map(_fromDbNote).toList();
}
Note _fromDbNote(DbNote row) {
return Note(
id: row.id,
@@ -308,6 +322,7 @@ class NoteRepository {
index: row.sortIndex,
serverVersion: row.serverVersion,
isDeleted: row.isDeleted,
isPermanentlyDeleted: _isPermanentlyDeleted(row),
categoryId: row.categoryId,
);
}
@@ -443,10 +458,13 @@ Map<String, Object?> _syncNoteToDecryptionInput(
'id': row.id,
'encryptedTitle': row.encryptedTitle,
'encryptedBody': row.encryptedBody,
'isPermanentlyDeleted': row.isPermanentlyDeleted,
};
}
Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
final bool isPermanentlyDeleted = _isPermanentlyDeleted(row);
return <String, Object?>{
'index': index,
'uuid': row.uuid,
@@ -458,6 +476,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
'serverVersion': row.serverVersion,
'position': row.sortIndex,
'isDeleted': row.isDeleted,
'isPermanentlyDeleted': isPermanentlyDeleted,
};
}
@@ -470,6 +489,7 @@ SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
serverVersion: row['serverVersion']! as int,
position: row['position']! as int,
isDeleted: row['isDeleted']! as bool,
isPermanentlyDeleted: row['isPermanentlyDeleted']! as bool,
updatedAt: DateTime.parse(row['updatedAt']! as String),
);
}
@@ -483,17 +503,25 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
final List<Map<String, Object?>> encryptedNotes = [];
for (final Map<String, Object?> note in notes) {
final bool isPermanentlyDeleted = note['isPermanentlyDeleted']! as bool;
final String title = note['title']! as String;
final String body = note['body']! as String;
final String encryptedTitle = await NoteEncryption.encryptNote(
title,
masterKey,
);
final String encryptedBody = await NoteEncryption.encryptNote(
body,
masterKey,
);
final String encryptedTitle;
final String encryptedBody;
if (isPermanentlyDeleted) {
encryptedTitle = '';
encryptedBody = '';
} else {
encryptedTitle = await NoteEncryption.encryptNote(
title,
masterKey,
);
encryptedBody = await NoteEncryption.encryptNote(
body,
masterKey,
);
}
encryptedNotes.add(<String, Object?>{
'index': note['index'] as int,
@@ -504,6 +532,7 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
'serverVersion': note['serverVersion']! as int,
'position': note['position']! as int,
'isDeleted': note['isDeleted']! as bool,
'isPermanentlyDeleted': isPermanentlyDeleted,
'updatedAt': note['updatedAt']! as String,
});
}
@@ -520,19 +549,25 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
final List<Map<String, Object?>> decryptedNotes = [];
for (final Map<String, Object?> note in notes) {
final bool isPermanentlyDeleted = note['isPermanentlyDeleted']! as bool;
String decryptedTitle = 'Encrypted';
String decryptedBody = 'Encrypted';
try {
decryptedTitle = await NoteEncryption.decryptNote(
note['encryptedTitle']! as String,
masterKey,
);
decryptedBody = await NoteEncryption.decryptNote(
note['encryptedBody']! as String,
masterKey,
);
} catch (e) {
print('Failed to decrypt note ${note['id']}: $e');
if (!isPermanentlyDeleted) {
try {
decryptedTitle = await NoteEncryption.decryptNote(
note['encryptedTitle']! as String,
masterKey,
);
decryptedBody = await NoteEncryption.decryptNote(
note['encryptedBody']! as String,
masterKey,
);
} catch (e) {
print('Failed to decrypt note ${note['id']}: $e');
}
} else {
decryptedTitle = '';
decryptedBody = '';
}
decryptedNotes.add(<String, Object?>{
@@ -540,12 +575,17 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
'id': note['id'] as String,
'title': decryptedTitle,
'body': decryptedBody,
'isPermanentlyDeleted': isPermanentlyDeleted,
});
}
return decryptedNotes;
}
bool _isPermanentlyDeleted(DbNote row) {
return row.isDeleted && row.title.isEmpty && row.body.isEmpty;
}
int _parallelWorkerCount(int itemCount) {
final int cappedByCpu = math.max(
1,