feat: Implement permanent deletion and restoration of notes with updated UI
This commit is contained in:
@@ -111,6 +111,18 @@ class AppDatabase extends _$AppDatabase {
|
||||
return deleteNote(id, removedIndex);
|
||||
}
|
||||
|
||||
Future<void> permanentlyDeleteNote(int id) async {
|
||||
await (update(notes)..where((n) => n.id.equals(id))).write(
|
||||
NotesCompanion(
|
||||
title: const Value(''),
|
||||
body: const Value(''),
|
||||
categoryId: const Value(null),
|
||||
isDeleted: const Value(true),
|
||||
updatedAt: Value(DateTime.now()),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> moveNote({
|
||||
required int id,
|
||||
required int oldIndex,
|
||||
@@ -146,6 +158,12 @@ class AppDatabase extends _$AppDatabase {
|
||||
)..where((n) => n.updatedAt.isBiggerThanValue(since))).get();
|
||||
}
|
||||
|
||||
Future<List<DbNote>> getDeletedNotes() {
|
||||
return (select(notes)
|
||||
..where((n) => n.isDeleted.equals(true) & n.title.isNotValue('') & n.body.isNotValue('')))
|
||||
.get();
|
||||
}
|
||||
|
||||
Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) {
|
||||
return (select(
|
||||
categories,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -69,6 +69,13 @@ String _readStringValue(dynamic value) {
|
||||
return jsonEncode(value);
|
||||
}
|
||||
|
||||
String _readOptionalStringValue(dynamic value) {
|
||||
if (value == null) {
|
||||
return '';
|
||||
}
|
||||
return _readStringValue(value);
|
||||
}
|
||||
|
||||
int _readIntValue(dynamic value) {
|
||||
if (value is int) {
|
||||
return value;
|
||||
@@ -127,6 +134,7 @@ class SyncNotePayload {
|
||||
required this.serverVersion,
|
||||
this.position = 0,
|
||||
this.isDeleted = false,
|
||||
this.isPermanentlyDeleted = false,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@@ -137,12 +145,14 @@ class SyncNotePayload {
|
||||
final int serverVersion;
|
||||
final int position;
|
||||
final bool isDeleted;
|
||||
final bool isPermanentlyDeleted;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory SyncNotePayload.fromNote(
|
||||
Note note, {
|
||||
required String encryptedTitle,
|
||||
required String encryptedBody,
|
||||
bool isPermanentlyDeleted = false,
|
||||
}) {
|
||||
return SyncNotePayload(
|
||||
id: note.uuid,
|
||||
@@ -152,6 +162,7 @@ class SyncNotePayload {
|
||||
serverVersion: note.serverVersion,
|
||||
position: note.index,
|
||||
isDeleted: note.isDeleted,
|
||||
isPermanentlyDeleted: isPermanentlyDeleted,
|
||||
updatedAt: note.updatedAt,
|
||||
);
|
||||
}
|
||||
@@ -165,6 +176,7 @@ class SyncNotePayload {
|
||||
'serverVersion': serverVersion,
|
||||
if (position != 0) 'position': position,
|
||||
if (isDeleted) 'isDeleted': isDeleted,
|
||||
if (isPermanentlyDeleted) 'isPermanentlyDeleted': isPermanentlyDeleted,
|
||||
'updatedAt': updatedAt.toIso8601String(),
|
||||
};
|
||||
}
|
||||
@@ -237,6 +249,7 @@ class SyncNoteResponse {
|
||||
required this.serverVersion,
|
||||
this.position = 0,
|
||||
this.isDeleted = false,
|
||||
this.isPermanentlyDeleted = false,
|
||||
required this.updatedAt,
|
||||
});
|
||||
|
||||
@@ -247,6 +260,7 @@ class SyncNoteResponse {
|
||||
final int serverVersion;
|
||||
final int position;
|
||||
final bool isDeleted;
|
||||
final bool isPermanentlyDeleted;
|
||||
final DateTime updatedAt;
|
||||
|
||||
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
|
||||
@@ -255,11 +269,12 @@ class SyncNoteResponse {
|
||||
categoryId: json['categoryId'] == null
|
||||
? null
|
||||
: _readStringValue(json['categoryId']),
|
||||
encryptedTitle: _readStringValue(json['encrypted_title']),
|
||||
encryptedBody: _readStringValue(json['encrypted_body']),
|
||||
encryptedTitle: _readOptionalStringValue(json['encrypted_title']),
|
||||
encryptedBody: _readOptionalStringValue(json['encrypted_body']),
|
||||
serverVersion: _readIntValue(json['serverVersion']),
|
||||
position: json['position'] as int? ?? 0,
|
||||
isDeleted: json['isDeleted'] as bool? ?? false,
|
||||
isPermanentlyDeleted: json['isPermanentlyDeleted'] as bool? ?? false,
|
||||
updatedAt: DateTime.parse(json['updatedAt'] as String),
|
||||
);
|
||||
}
|
||||
@@ -267,8 +282,8 @@ class SyncNoteResponse {
|
||||
Note toNote() {
|
||||
return Note(
|
||||
uuid: id,
|
||||
title: 'Encrypted', // placeholder, será descifrado por la app
|
||||
body: 'Encrypted', // placeholder, será descifrado por la app
|
||||
title: isPermanentlyDeleted ? '' : 'Encrypted',
|
||||
body: isPermanentlyDeleted ? '' : 'Encrypted',
|
||||
createdAt: updatedAt,
|
||||
updatedAt: updatedAt,
|
||||
index: position,
|
||||
|
||||
Reference in New Issue
Block a user