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
+18
View File
@@ -111,6 +111,18 @@ class AppDatabase extends _$AppDatabase {
return deleteNote(id, removedIndex); 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({ Future<void> moveNote({
required int id, required int id,
required int oldIndex, required int oldIndex,
@@ -146,6 +158,12 @@ class AppDatabase extends _$AppDatabase {
)..where((n) => n.updatedAt.isBiggerThanValue(since))).get(); )..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) { Future<List<DbCategory>> getCategoriesChangedSince(DateTime since) {
return (select( return (select(
categories, categories,
+69 -29
View File
@@ -30,6 +30,10 @@ class NoteRepository {
return _loadNotesFromDatabase(); return _loadNotesFromDatabase();
} }
Future<List<Note>> loadDeletedNotes() async {
return _loadDeletedNotesFromDatabase();
}
Future<Note> createNote(Note note) async { Future<Note> createNote(Note note) async {
final int id = await _database.insertNoteAtTop( final int id = await _database.insertNoteAtTop(
NotesCompanion.insert( NotesCompanion.insert(
@@ -63,12 +67,12 @@ class NoteRepository {
updatedAt: note.updatedAt, updatedAt: note.updatedAt,
sortIndex: note.index, sortIndex: note.index,
serverVersion: note.serverVersion, serverVersion: note.serverVersion,
isDeleted: note.isDeleted, isDeleted: false,
categoryId: note.categoryId, categoryId: note.categoryId,
), ),
); );
return note; return note.copyWith(isDeleted: false, isPermanentlyDeleted: false);
} }
Future<void> deleteNote(Note note) async { Future<void> deleteNote(Note note) async {
@@ -76,7 +80,11 @@ class NoteRepository {
note.id ?? note.id ??
(throw ArgumentError('Note id is required to delete a note.')); (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 { Future<void> moveNote(Note note, int newIndex) async {
@@ -131,7 +139,7 @@ class NoteRepository {
final int totalNotesToEncrypt = unsyncedNotes.length; final int totalNotesToEncrypt = unsyncedNotes.length;
// Build sync request (note: we send encrypted data, but locally we have plaintext) // 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) { if (totalNotesToEncrypt == 0) {
onProgress?.call( onProgress?.call(
SyncStatus.encrypting, SyncStatus.encrypting,
@@ -254,6 +262,7 @@ class NoteRepository {
final String decryptedTitle = decryptedNote['title']! as String; final String decryptedTitle = decryptedNote['title']! as String;
final String decryptedBody = decryptedNote['body']! as String; final String decryptedBody = decryptedNote['body']! as String;
final SyncNoteResponse noteResponse = response.changes.notes[index]; final SyncNoteResponse noteResponse = response.changes.notes[index];
final bool isPermanentlyDeleted = noteResponse.isPermanentlyDeleted;
if (existingNote != null) { if (existingNote != null) {
// Update existing note // Update existing note
@@ -261,14 +270,14 @@ class NoteRepository {
DbNote( DbNote(
id: existingNote.id, id: existingNote.id,
uuid: noteId, uuid: noteId,
title: decryptedTitle, title: isPermanentlyDeleted ? '' : decryptedTitle,
body: decryptedBody, body: isPermanentlyDeleted ? '' : decryptedBody,
createdAt: existingNote.createdAt, createdAt: existingNote.createdAt,
updatedAt: noteResponse.updatedAt, updatedAt: noteResponse.updatedAt,
sortIndex: noteResponse.position, sortIndex: noteResponse.position,
serverVersion: noteResponse.serverVersion, serverVersion: noteResponse.serverVersion,
isDeleted: noteResponse.isDeleted, isDeleted: noteResponse.isDeleted,
categoryId: noteResponse.categoryId, categoryId: isPermanentlyDeleted ? null : noteResponse.categoryId,
), ),
); );
} else { } else {
@@ -278,14 +287,14 @@ class NoteRepository {
.insert( .insert(
NotesCompanion( NotesCompanion(
uuid: Value(noteResponse.id), uuid: Value(noteResponse.id),
title: Value(decryptedTitle), title: Value(isPermanentlyDeleted ? '' : decryptedTitle),
body: Value(decryptedBody), body: Value(isPermanentlyDeleted ? '' : decryptedBody),
createdAt: Value(noteResponse.updatedAt), createdAt: Value(noteResponse.updatedAt),
updatedAt: Value(noteResponse.updatedAt), updatedAt: Value(noteResponse.updatedAt),
sortIndex: Value(noteResponse.position), sortIndex: Value(noteResponse.position),
serverVersion: Value(noteResponse.serverVersion), serverVersion: Value(noteResponse.serverVersion),
isDeleted: Value(noteResponse.isDeleted), 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(); 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) { Note _fromDbNote(DbNote row) {
return Note( return Note(
id: row.id, id: row.id,
@@ -308,6 +322,7 @@ class NoteRepository {
index: row.sortIndex, index: row.sortIndex,
serverVersion: row.serverVersion, serverVersion: row.serverVersion,
isDeleted: row.isDeleted, isDeleted: row.isDeleted,
isPermanentlyDeleted: _isPermanentlyDeleted(row),
categoryId: row.categoryId, categoryId: row.categoryId,
); );
} }
@@ -443,10 +458,13 @@ Map<String, Object?> _syncNoteToDecryptionInput(
'id': row.id, 'id': row.id,
'encryptedTitle': row.encryptedTitle, 'encryptedTitle': row.encryptedTitle,
'encryptedBody': row.encryptedBody, 'encryptedBody': row.encryptedBody,
'isPermanentlyDeleted': row.isPermanentlyDeleted,
}; };
} }
Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) { Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
final bool isPermanentlyDeleted = _isPermanentlyDeleted(row);
return <String, Object?>{ return <String, Object?>{
'index': index, 'index': index,
'uuid': row.uuid, 'uuid': row.uuid,
@@ -458,6 +476,7 @@ Map<String, Object?> _dbNoteToEncryptionInput(DbNote row, int index) {
'serverVersion': row.serverVersion, 'serverVersion': row.serverVersion,
'position': row.sortIndex, 'position': row.sortIndex,
'isDeleted': row.isDeleted, 'isDeleted': row.isDeleted,
'isPermanentlyDeleted': isPermanentlyDeleted,
}; };
} }
@@ -470,6 +489,7 @@ SyncNotePayload _syncNotePayloadFromEncryptionResult(Map<String, Object?> row) {
serverVersion: row['serverVersion']! as int, serverVersion: row['serverVersion']! as int,
position: row['position']! as int, position: row['position']! as int,
isDeleted: row['isDeleted']! as bool, isDeleted: row['isDeleted']! as bool,
isPermanentlyDeleted: row['isPermanentlyDeleted']! as bool,
updatedAt: DateTime.parse(row['updatedAt']! as String), updatedAt: DateTime.parse(row['updatedAt']! as String),
); );
} }
@@ -483,17 +503,25 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
final List<Map<String, Object?>> encryptedNotes = []; final List<Map<String, Object?>> encryptedNotes = [];
for (final Map<String, Object?> note in notes) { for (final Map<String, Object?> note in notes) {
final bool isPermanentlyDeleted = note['isPermanentlyDeleted']! as bool;
final String title = note['title']! as String; final String title = note['title']! as String;
final String body = note['body']! as String; final String body = note['body']! as String;
final String encryptedTitle = await NoteEncryption.encryptNote( final String encryptedTitle;
title, final String encryptedBody;
masterKey, if (isPermanentlyDeleted) {
); encryptedTitle = '';
final String encryptedBody = await NoteEncryption.encryptNote( encryptedBody = '';
body, } else {
masterKey, encryptedTitle = await NoteEncryption.encryptNote(
); title,
masterKey,
);
encryptedBody = await NoteEncryption.encryptNote(
body,
masterKey,
);
}
encryptedNotes.add(<String, Object?>{ encryptedNotes.add(<String, Object?>{
'index': note['index'] as int, 'index': note['index'] as int,
@@ -504,6 +532,7 @@ Future<List<Map<String, Object?>>> _encryptNoteBatch(
'serverVersion': note['serverVersion']! as int, 'serverVersion': note['serverVersion']! as int,
'position': note['position']! as int, 'position': note['position']! as int,
'isDeleted': note['isDeleted']! as bool, 'isDeleted': note['isDeleted']! as bool,
'isPermanentlyDeleted': isPermanentlyDeleted,
'updatedAt': note['updatedAt']! as String, 'updatedAt': note['updatedAt']! as String,
}); });
} }
@@ -520,19 +549,25 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
final List<Map<String, Object?>> decryptedNotes = []; final List<Map<String, Object?>> decryptedNotes = [];
for (final Map<String, Object?> note in notes) { for (final Map<String, Object?> note in notes) {
final bool isPermanentlyDeleted = note['isPermanentlyDeleted']! as bool;
String decryptedTitle = 'Encrypted'; String decryptedTitle = 'Encrypted';
String decryptedBody = 'Encrypted'; String decryptedBody = 'Encrypted';
try { if (!isPermanentlyDeleted) {
decryptedTitle = await NoteEncryption.decryptNote( try {
note['encryptedTitle']! as String, decryptedTitle = await NoteEncryption.decryptNote(
masterKey, note['encryptedTitle']! as String,
); masterKey,
decryptedBody = await NoteEncryption.decryptNote( );
note['encryptedBody']! as String, decryptedBody = await NoteEncryption.decryptNote(
masterKey, note['encryptedBody']! as String,
); masterKey,
} catch (e) { );
print('Failed to decrypt note ${note['id']}: $e'); } catch (e) {
print('Failed to decrypt note ${note['id']}: $e');
}
} else {
decryptedTitle = '';
decryptedBody = '';
} }
decryptedNotes.add(<String, Object?>{ decryptedNotes.add(<String, Object?>{
@@ -540,12 +575,17 @@ Future<List<Map<String, Object?>>> _decryptNoteBatch(
'id': note['id'] as String, 'id': note['id'] as String,
'title': decryptedTitle, 'title': decryptedTitle,
'body': decryptedBody, 'body': decryptedBody,
'isPermanentlyDeleted': isPermanentlyDeleted,
}); });
} }
return decryptedNotes; return decryptedNotes;
} }
bool _isPermanentlyDeleted(DbNote row) {
return row.isDeleted && row.title.isEmpty && row.body.isEmpty;
}
int _parallelWorkerCount(int itemCount) { int _parallelWorkerCount(int itemCount) {
final int cappedByCpu = math.max( final int cappedByCpu = math.max(
1, 1,
+19 -4
View File
@@ -69,6 +69,13 @@ String _readStringValue(dynamic value) {
return jsonEncode(value); return jsonEncode(value);
} }
String _readOptionalStringValue(dynamic value) {
if (value == null) {
return '';
}
return _readStringValue(value);
}
int _readIntValue(dynamic value) { int _readIntValue(dynamic value) {
if (value is int) { if (value is int) {
return value; return value;
@@ -127,6 +134,7 @@ class SyncNotePayload {
required this.serverVersion, required this.serverVersion,
this.position = 0, this.position = 0,
this.isDeleted = false, this.isDeleted = false,
this.isPermanentlyDeleted = false,
required this.updatedAt, required this.updatedAt,
}); });
@@ -137,12 +145,14 @@ class SyncNotePayload {
final int serverVersion; final int serverVersion;
final int position; final int position;
final bool isDeleted; final bool isDeleted;
final bool isPermanentlyDeleted;
final DateTime updatedAt; final DateTime updatedAt;
factory SyncNotePayload.fromNote( factory SyncNotePayload.fromNote(
Note note, { Note note, {
required String encryptedTitle, required String encryptedTitle,
required String encryptedBody, required String encryptedBody,
bool isPermanentlyDeleted = false,
}) { }) {
return SyncNotePayload( return SyncNotePayload(
id: note.uuid, id: note.uuid,
@@ -152,6 +162,7 @@ class SyncNotePayload {
serverVersion: note.serverVersion, serverVersion: note.serverVersion,
position: note.index, position: note.index,
isDeleted: note.isDeleted, isDeleted: note.isDeleted,
isPermanentlyDeleted: isPermanentlyDeleted,
updatedAt: note.updatedAt, updatedAt: note.updatedAt,
); );
} }
@@ -165,6 +176,7 @@ class SyncNotePayload {
'serverVersion': serverVersion, 'serverVersion': serverVersion,
if (position != 0) 'position': position, if (position != 0) 'position': position,
if (isDeleted) 'isDeleted': isDeleted, if (isDeleted) 'isDeleted': isDeleted,
if (isPermanentlyDeleted) 'isPermanentlyDeleted': isPermanentlyDeleted,
'updatedAt': updatedAt.toIso8601String(), 'updatedAt': updatedAt.toIso8601String(),
}; };
} }
@@ -237,6 +249,7 @@ class SyncNoteResponse {
required this.serverVersion, required this.serverVersion,
this.position = 0, this.position = 0,
this.isDeleted = false, this.isDeleted = false,
this.isPermanentlyDeleted = false,
required this.updatedAt, required this.updatedAt,
}); });
@@ -247,6 +260,7 @@ class SyncNoteResponse {
final int serverVersion; final int serverVersion;
final int position; final int position;
final bool isDeleted; final bool isDeleted;
final bool isPermanentlyDeleted;
final DateTime updatedAt; final DateTime updatedAt;
factory SyncNoteResponse.fromJson(Map<String, dynamic> json) { factory SyncNoteResponse.fromJson(Map<String, dynamic> json) {
@@ -255,11 +269,12 @@ class SyncNoteResponse {
categoryId: json['categoryId'] == null categoryId: json['categoryId'] == null
? null ? null
: _readStringValue(json['categoryId']), : _readStringValue(json['categoryId']),
encryptedTitle: _readStringValue(json['encrypted_title']), encryptedTitle: _readOptionalStringValue(json['encrypted_title']),
encryptedBody: _readStringValue(json['encrypted_body']), encryptedBody: _readOptionalStringValue(json['encrypted_body']),
serverVersion: _readIntValue(json['serverVersion']), serverVersion: _readIntValue(json['serverVersion']),
position: json['position'] as int? ?? 0, position: json['position'] as int? ?? 0,
isDeleted: json['isDeleted'] as bool? ?? false, isDeleted: json['isDeleted'] as bool? ?? false,
isPermanentlyDeleted: json['isPermanentlyDeleted'] as bool? ?? false,
updatedAt: DateTime.parse(json['updatedAt'] as String), updatedAt: DateTime.parse(json['updatedAt'] as String),
); );
} }
@@ -267,8 +282,8 @@ class SyncNoteResponse {
Note toNote() { Note toNote() {
return Note( return Note(
uuid: id, uuid: id,
title: 'Encrypted', // placeholder, será descifrado por la app title: isPermanentlyDeleted ? '' : 'Encrypted',
body: 'Encrypted', // placeholder, será descifrado por la app body: isPermanentlyDeleted ? '' : 'Encrypted',
createdAt: updatedAt, createdAt: updatedAt,
updatedAt: updatedAt, updatedAt: updatedAt,
index: position, index: position,
+4
View File
@@ -18,6 +18,7 @@ class Note {
required this.index, required this.index,
this.serverVersion = 0, this.serverVersion = 0,
this.isDeleted = false, this.isDeleted = false,
this.isPermanentlyDeleted = false,
this.categoryId, this.categoryId,
}) : uuid = uuid ?? Uuid().v4(); }) : uuid = uuid ?? Uuid().v4();
@@ -30,6 +31,7 @@ class Note {
final int index; final int index;
final int serverVersion; final int serverVersion;
final bool isDeleted; final bool isDeleted;
final bool isPermanentlyDeleted;
final String? categoryId; final String? categoryId;
Note copyWith({ Note copyWith({
@@ -42,6 +44,7 @@ class Note {
int? index, int? index,
int? serverVersion, int? serverVersion,
bool? isDeleted, bool? isDeleted,
bool? isPermanentlyDeleted,
String? categoryId, String? categoryId,
}) { }) {
return Note( return Note(
@@ -54,6 +57,7 @@ class Note {
index: index ?? this.index, index: index ?? this.index,
serverVersion: serverVersion ?? this.serverVersion, serverVersion: serverVersion ?? this.serverVersion,
isDeleted: isDeleted ?? this.isDeleted, isDeleted: isDeleted ?? this.isDeleted,
isPermanentlyDeleted: isPermanentlyDeleted ?? this.isPermanentlyDeleted,
categoryId: categoryId ?? this.categoryId, categoryId: categoryId ?? this.categoryId,
); );
} }
+70 -83
View File
@@ -47,6 +47,7 @@ class _HomeScreenState extends State<HomeScreen> {
bool _isLoading = true; bool _isLoading = true;
bool _isDragging = false; bool _isDragging = false;
bool _isMenuOpen = false; bool _isMenuOpen = false;
bool _showDeletedNotes = false;
PointerDeviceKind _lastPointerKind = PointerDeviceKind.mouse; PointerDeviceKind _lastPointerKind = PointerDeviceKind.mouse;
bool _requiresLongPressToDrag(PointerDeviceKind kind) { bool _requiresLongPressToDrag(PointerDeviceKind kind) {
@@ -71,7 +72,9 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _loadNotes() async { Future<void> _loadNotes() async {
try { try {
final List<Note> storedNotes = await widget.repository.loadNotes(); final List<Note> storedNotes = _showDeletedNotes
? await widget.repository.loadDeletedNotes()
: await widget.repository.loadNotes();
if (!mounted) return; if (!mounted) return;
@@ -95,19 +98,8 @@ class _HomeScreenState extends State<HomeScreen> {
} }
if (result is Note) { if (result is Note) {
final Note createdNote = await widget.repository.createNote(result); await widget.repository.createNote(result);
final List<Note> updatedNotes = _normalizeNotes(<Note>[ await _loadNotes();
createdNote,
..._notes,
]);
if (!mounted) {
return;
}
setState(() {
_notes = updatedNotes;
});
// Trigger sync after creating a note. // Trigger sync after creating a note.
try { try {
@@ -118,18 +110,7 @@ class _HomeScreenState extends State<HomeScreen> {
Future<void> _deleteNote(Note note) async { Future<void> _deleteNote(Note note) async {
await widget.repository.deleteNote(note); await widget.repository.deleteNote(note);
await _loadNotes();
final List<Note> updatedNotes = _normalizeNotes(
_notes.where((Note item) => item.id != note.id).toList(),
);
if (!mounted) {
return;
}
setState(() {
_notes = updatedNotes;
});
// Trigger sync after deleting a note. // Trigger sync after deleting a note.
try { try {
@@ -142,19 +123,10 @@ class _HomeScreenState extends State<HomeScreen> {
return; return;
} }
final List<Note> updatedNotes = [..._notes]; final Note movedNote = _notes[oldIndex];
final Note movedNote = updatedNotes.removeAt(oldIndex);
updatedNotes.insert(newIndex, movedNote);
await widget.repository.moveNote(movedNote, newIndex); await widget.repository.moveNote(movedNote, newIndex);
await _loadNotes();
if (!mounted) {
return;
}
setState(() {
_notes = _normalizeNotes(updatedNotes);
});
} }
Future<void> _openNoteEditor(Note note) async { Future<void> _openNoteEditor(Note note) async {
@@ -173,19 +145,9 @@ class _HomeScreenState extends State<HomeScreen> {
} }
if (result is Note) { if (result is Note) {
final int noteIndex = _notes.indexWhere((Note item) => item == note); if (_notes.any((Note item) => item == note)) {
if (noteIndex != -1) { await widget.repository.updateNote(result);
final Note savedNote = await widget.repository.updateNote(result); await _loadNotes();
final List<Note> updatedNotes = [..._notes];
updatedNotes[noteIndex] = savedNote;
if (!mounted) {
return;
}
setState(() {
_notes = _normalizeNotes(updatedNotes);
});
// Trigger sync after editing a note. // Trigger sync after editing a note.
try { try {
await widget.onRequestSync(); await widget.onRequestSync();
@@ -194,12 +156,6 @@ class _HomeScreenState extends State<HomeScreen> {
} }
} }
List<Note> _normalizeNotes(List<Note> notes) {
return notes.asMap().entries.map((MapEntry<int, Note> entry) {
return entry.value.copyWith(index: entry.key);
}).toList();
}
List<Note> _getFilteredNotes() { List<Note> _getFilteredNotes() {
if (_searchQuery.isEmpty) { if (_searchQuery.isEmpty) {
return _notes; return _notes;
@@ -215,6 +171,36 @@ class _HomeScreenState extends State<HomeScreen> {
.toList(); .toList();
} }
Future<void> _handleMenuItemTapped(String item) async {
setState(() {
_isMenuOpen = false;
});
if (item == 'settings') {
widget.onOpenSettings();
return;
}
if (item == 'deleted_notes') {
setState(() {
_showDeletedNotes = true;
_searchQuery = '';
_isLoading = true;
});
await _loadNotes();
return;
}
if (item == 'all_notes') {
setState(() {
_showDeletedNotes = false;
_searchQuery = '';
_isLoading = true;
});
await _loadNotes();
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final double width = MediaQuery.of(context).size.width; final double width = MediaQuery.of(context).size.width;
@@ -223,8 +209,8 @@ class _HomeScreenState extends State<HomeScreen> {
final Widget body = _isLoading final Widget body = _isLoading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: _notes.isEmpty : _notes.isEmpty
? const _EmptyState() ? _EmptyState(showDeletedNotes: _showDeletedNotes)
: RefreshIndicator( : RefreshIndicator(
onRefresh: () async { onRefresh: () async {
await widget.onRequestSync(); await widget.onRequestSync();
}, },
@@ -599,15 +585,10 @@ class _HomeScreenState extends State<HomeScreen> {
color: const Color.fromRGBO(24, 25, 26, 1), color: const Color.fromRGBO(24, 25, 26, 1),
elevation: 8, elevation: 8,
child: MenuDrawer( child: MenuDrawer(
onMenuItemTapped: (String item) { onMenuItemTapped: _handleMenuItemTapped,
setState(() { selectedItem: _showDeletedNotes
_isMenuOpen = false; ? 'deleted_notes'
}); : 'all_notes',
if (item == 'settings') {
widget.onOpenSettings();
}
},
), ),
), ),
), ),
@@ -618,41 +599,47 @@ class _HomeScreenState extends State<HomeScreen> {
), ),
), ),
), ),
floatingActionButton: FloatingActionButton( floatingActionButton: _showDeletedNotes
onPressed: _openNoteComposer, ? null
child: const MouseRegion( : FloatingActionButton(
cursor: SystemMouseCursors.click, onPressed: _openNoteComposer,
child: Icon(Icons.add), child: const MouseRegion(
), cursor: SystemMouseCursors.click,
), child: Icon(Icons.add),
),
),
); );
} }
} }
class _EmptyState extends StatelessWidget { class _EmptyState extends StatelessWidget {
const _EmptyState(); const _EmptyState({required this.showDeletedNotes});
final bool showDeletedNotes;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Center( return Center(
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: const [ children: [
Icon(Icons.note_add_outlined, color: Colors.white54, size: 48), const Icon(Icons.note_add_outlined, color: Colors.white54, size: 48),
SizedBox(height: 12), const SizedBox(height: 12),
Text( Text(
'Aún no hay notas', showDeletedNotes ? 'No hay notas borradas' : 'Aún no hay notas',
style: TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
), ),
SizedBox(height: 8), const SizedBox(height: 8),
Text( Text(
'Pulsa el botón + para crear la primera.', showDeletedNotes
? 'Las notas borradas aparecerán aquí para poder restaurarlas.'
: 'Pulsa el botón + para crear la primera.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle(color: Colors.white70), style: const TextStyle(color: Colors.white70),
), ),
], ],
), ),
+12 -9
View File
@@ -93,18 +93,21 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
} }
void _deleteNote() { void _deleteNote() {
final bool isDeletedNote = _currentNote.isDeleted;
showDialog( showDialog(
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertDialog( return AlertDialog(
backgroundColor: const Color(0xFF303134), backgroundColor: const Color(0xFF303134),
title: const Text( title: Text(
'Eliminar nota', isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar nota',
style: TextStyle(color: Colors.white), style: const TextStyle(color: Colors.white),
), ),
content: const Text( content: Text(
'¿Estás seguro de que deseas eliminar esta nota?', isDeletedNote
style: TextStyle(color: Colors.white70), ? 'Esta nota ya está borrada. Si la eliminas ahora, se borrará permanentemente.'
: '¿Estás seguro de que deseas eliminar esta nota?',
style: const TextStyle(color: Colors.white70),
), ),
actions: [ actions: [
TextButton( TextButton(
@@ -119,9 +122,9 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
Navigator.of(context).pop(); Navigator.of(context).pop();
Navigator.of(context).pop('delete'); Navigator.of(context).pop('delete');
}, },
child: const Text( child: Text(
'Eliminar', isDeletedNote ? 'Eliminar permanentemente' : 'Eliminar',
style: TextStyle(color: Colors.red), style: const TextStyle(color: Colors.red),
), ),
), ),
], ],
+44 -7
View File
@@ -4,9 +4,11 @@ class MenuDrawer extends StatelessWidget {
const MenuDrawer({ const MenuDrawer({
super.key, super.key,
this.onMenuItemTapped, this.onMenuItemTapped,
this.selectedItem,
}); });
final ValueChanged<String>? onMenuItemTapped; final ValueChanged<String>? onMenuItemTapped;
final String? selectedItem;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -19,9 +21,11 @@ class MenuDrawer extends StatelessWidget {
), ),
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 8),
_MenuItemTile( _MenuItemTile(
icon: Icons.note, icon: Icons.note,
label: 'Todas mis notas', label: 'Todas mis notas',
selected: selectedItem == 'all_notes',
onTap: () => onMenuItemTapped?.call('all_notes'), onTap: () => onMenuItemTapped?.call('all_notes'),
), ),
Expanded( Expanded(
@@ -33,12 +37,19 @@ class MenuDrawer extends StatelessWidget {
), ),
), ),
), ),
_MenuItemTile(
icon: Icons.delete_outline,
label: 'Mis notas borradas',
selected: selectedItem == 'deleted_notes',
onTap: () => onMenuItemTapped?.call('deleted_notes'),
),
const Divider(color: Colors.white12, height: 16), const Divider(color: Colors.white12, height: 16),
_MenuItemTile( _MenuItemTile(
icon: Icons.settings, icon: Icons.settings,
label: 'Configuración', label: 'Configuración',
onTap: () => onMenuItemTapped?.call('settings'), onTap: () => onMenuItemTapped?.call('settings'),
), ),
const SizedBox(height: 6),
], ],
), ),
); );
@@ -49,23 +60,49 @@ class _MenuItemTile extends StatelessWidget {
const _MenuItemTile({ const _MenuItemTile({
required this.icon, required this.icon,
required this.label, required this.label,
this.selected = false,
this.onTap, this.onTap,
}); });
final IconData icon; final IconData icon;
final String label; final String label;
final bool selected;
final VoidCallback? onTap; final VoidCallback? onTap;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return ListTile( final Color backgroundColor = selected
leading: Icon(icon, color: Colors.white70), ? Colors.white.withValues(alpha: 0.10)
title: Text( : Colors.transparent;
label, final Color foregroundColor = selected ? Colors.white : Colors.white70;
style: const TextStyle(color: Colors.white70, fontSize: 14),
return AnimatedContainer(
duration: const Duration(milliseconds: 180),
curve: Curves.easeOutCubic,
margin: EdgeInsets.only(
left: selected ? 0 : 8,
right: 8,
top: 2,
bottom: 2,
),
decoration: BoxDecoration(
color: backgroundColor,
borderRadius: selected
? const BorderRadius.only(
topRight: Radius.circular(999),
bottomRight: Radius.circular(999),
)
: BorderRadius.circular(12),
),
child: ListTile(
leading: Icon(icon, color: foregroundColor),
title: Text(
label,
style: TextStyle(color: foregroundColor, fontSize: 14),
),
onTap: onTap,
hoverColor: Colors.white.withValues(alpha: 0.1),
), ),
onTap: onTap,
hoverColor: Colors.white.withValues(alpha: 0.1),
); );
} }
} }