feat: add trash view

This commit is contained in:
2026-05-15 23:22:58 +03:00
parent ddf0c365a1
commit eb4d8d3c50
16 changed files with 1075 additions and 75 deletions

View File

@@ -717,6 +717,82 @@ class ChecklistsMessages {
/// ```
String get undo => """Undo""";
/// ```dart
/// "View trash"
/// ```
String get viewTrash => """View trash""";
/// ```dart
/// "Exit trash"
/// ```
String get exitTrash => """Exit trash""";
/// ```dart
/// "Trash"
/// ```
String get trashTitle => """Trash""";
/// ```dart
/// "Trash is empty."
/// ```
String get noTrashedItems => """Trash is empty.""";
/// ```dart
/// "Empty trash"
/// ```
String get emptyTrash => """Empty trash""";
/// ```dart
/// "Empty the trash?"
/// ```
String get emptyTrashConfirm => """Empty the trash?""";
/// ```dart
/// "All items in the trash will be permanently deleted. This cannot be undone."
/// ```
String get emptyTrashConfirmBody =>
"""All items in the trash will be permanently deleted. This cannot be undone.""";
/// ```dart
/// "Failed to empty trash."
/// ```
String get emptyTrashFailed => """Failed to empty trash.""";
/// ```dart
/// "Restore"
/// ```
String get restoreItem => """Restore""";
/// ```dart
/// "Delete permanently"
/// ```
String get permanentlyDeleteItem => """Delete permanently""";
/// ```dart
/// "Permanently delete this item?"
/// ```
String get permanentlyDeleteConfirm => """Permanently delete this item?""";
/// ```dart
/// "This cannot be undone."
/// ```
String get permanentlyDeleteConfirmBody => """This cannot be undone.""";
/// ```dart
/// "Failed to restore item."
/// ```
String get restoreFailed => """Failed to restore item.""";
/// ```dart
/// "Failed to delete item."
/// ```
String get permanentlyDeleteFailed => """Failed to delete item.""";
/// ```dart
/// "Item restored"
/// ```
String get itemRestored => """Item restored""";
/// ```dart
/// "New list"
/// ```
@@ -1586,6 +1662,23 @@ Please complete login in your browser.""",
"""checklists.moveFailed""": """Failed to move item.""",
"""checklists.itemMarkedDone""": """Item marked as done""",
"""checklists.undo""": """Undo""",
"""checklists.viewTrash""": """View trash""",
"""checklists.exitTrash""": """Exit trash""",
"""checklists.trashTitle""": """Trash""",
"""checklists.noTrashedItems""": """Trash is empty.""",
"""checklists.emptyTrash""": """Empty trash""",
"""checklists.emptyTrashConfirm""": """Empty the trash?""",
"""checklists.emptyTrashConfirmBody""":
"""All items in the trash will be permanently deleted. This cannot be undone.""",
"""checklists.emptyTrashFailed""": """Failed to empty trash.""",
"""checklists.restoreItem""": """Restore""",
"""checklists.permanentlyDeleteItem""": """Delete permanently""",
"""checklists.permanentlyDeleteConfirm""":
"""Permanently delete this item?""",
"""checklists.permanentlyDeleteConfirmBody""": """This cannot be undone.""",
"""checklists.restoreFailed""": """Failed to restore item.""",
"""checklists.permanentlyDeleteFailed""": """Failed to delete item.""",
"""checklists.itemRestored""": """Item restored""",
"""checklists.createList""": """New list""",
"""checklists.listName""": """List name""",
"""checklists.listDescription""": """Description (optional)""",

View File

@@ -131,6 +131,21 @@ checklists:
moveFailed: Failed to move item.
itemMarkedDone: Item marked as done
undo: Undo
viewTrash: View trash
exitTrash: Exit trash
trashTitle: Trash
noTrashedItems: Trash is empty.
emptyTrash: Empty trash
emptyTrashConfirm: Empty the trash?
emptyTrashConfirmBody: All items in the trash will be permanently deleted. This cannot be undone.
emptyTrashFailed: Failed to empty trash.
restoreItem: Restore
permanentlyDeleteItem: Delete permanently
permanentlyDeleteConfirm: Permanently delete this item?
permanentlyDeleteConfirmBody: This cannot be undone.
restoreFailed: Failed to restore item.
permanentlyDeleteFailed: Failed to delete item.
itemRestored: Item restored
createList: New list
listName: List name
listDescription: Description (optional)

View File

@@ -723,6 +723,86 @@ class ChecklistsMessagesDe extends ChecklistsMessages {
/// ```
String get undo => """Rückgängig""";
/// ```dart
/// "Papierkorb anzeigen"
/// ```
String get viewTrash => """Papierkorb anzeigen""";
/// ```dart
/// "Papierkorb verlassen"
/// ```
String get exitTrash => """Papierkorb verlassen""";
/// ```dart
/// "Papierkorb"
/// ```
String get trashTitle => """Papierkorb""";
/// ```dart
/// "Der Papierkorb ist leer."
/// ```
String get noTrashedItems => """Der Papierkorb ist leer.""";
/// ```dart
/// "Papierkorb leeren"
/// ```
String get emptyTrash => """Papierkorb leeren""";
/// ```dart
/// "Papierkorb leeren?"
/// ```
String get emptyTrashConfirm => """Papierkorb leeren?""";
/// ```dart
/// "Alle Einträge im Papierkorb werden endgültig gelöscht. Dies kann nicht rückgängig gemacht werden."
/// ```
String get emptyTrashConfirmBody =>
"""Alle Einträge im Papierkorb werden endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.""";
/// ```dart
/// "Papierkorb konnte nicht geleert werden."
/// ```
String get emptyTrashFailed => """Papierkorb konnte nicht geleert werden.""";
/// ```dart
/// "Wiederherstellen"
/// ```
String get restoreItem => """Wiederherstellen""";
/// ```dart
/// "Endgültig löschen"
/// ```
String get permanentlyDeleteItem => """Endgültig löschen""";
/// ```dart
/// "Diesen Eintrag endgültig löschen?"
/// ```
String get permanentlyDeleteConfirm =>
"""Diesen Eintrag endgültig löschen?""";
/// ```dart
/// "Dies kann nicht rückgängig gemacht werden."
/// ```
String get permanentlyDeleteConfirmBody =>
"""Dies kann nicht rückgängig gemacht werden.""";
/// ```dart
/// "Eintrag konnte nicht wiederhergestellt werden."
/// ```
String get restoreFailed =>
"""Eintrag konnte nicht wiederhergestellt werden.""";
/// ```dart
/// "Eintrag konnte nicht gelöscht werden."
/// ```
String get permanentlyDeleteFailed =>
"""Eintrag konnte nicht gelöscht werden.""";
/// ```dart
/// "Eintrag wiederhergestellt"
/// ```
String get itemRestored => """Eintrag wiederhergestellt""";
/// ```dart
/// "Neue Liste"
/// ```
@@ -1607,6 +1687,27 @@ Bitte melde dich in deinem Browser an.""",
"""checklists.moveFailed""": """Eintrag konnte nicht verschoben werden.""",
"""checklists.itemMarkedDone""": """Eintrag als erledigt markiert""",
"""checklists.undo""": """Rückgängig""",
"""checklists.viewTrash""": """Papierkorb anzeigen""",
"""checklists.exitTrash""": """Papierkorb verlassen""",
"""checklists.trashTitle""": """Papierkorb""",
"""checklists.noTrashedItems""": """Der Papierkorb ist leer.""",
"""checklists.emptyTrash""": """Papierkorb leeren""",
"""checklists.emptyTrashConfirm""": """Papierkorb leeren?""",
"""checklists.emptyTrashConfirmBody""":
"""Alle Einträge im Papierkorb werden endgültig gelöscht. Dies kann nicht rückgängig gemacht werden.""",
"""checklists.emptyTrashFailed""":
"""Papierkorb konnte nicht geleert werden.""",
"""checklists.restoreItem""": """Wiederherstellen""",
"""checklists.permanentlyDeleteItem""": """Endgültig löschen""",
"""checklists.permanentlyDeleteConfirm""":
"""Diesen Eintrag endgültig löschen?""",
"""checklists.permanentlyDeleteConfirmBody""":
"""Dies kann nicht rückgängig gemacht werden.""",
"""checklists.restoreFailed""":
"""Eintrag konnte nicht wiederhergestellt werden.""",
"""checklists.permanentlyDeleteFailed""":
"""Eintrag konnte nicht gelöscht werden.""",
"""checklists.itemRestored""": """Eintrag wiederhergestellt""",
"""checklists.createList""": """Neue Liste""",
"""checklists.listName""": """Listenname""",
"""checklists.listDescription""": """Beschreibung (optional)""",

View File

@@ -131,6 +131,21 @@ checklists:
moveFailed: Eintrag konnte nicht verschoben werden.
itemMarkedDone: Eintrag als erledigt markiert
undo: "Rückgängig"
viewTrash: Papierkorb anzeigen
exitTrash: Papierkorb verlassen
trashTitle: Papierkorb
noTrashedItems: Der Papierkorb ist leer.
emptyTrash: Papierkorb leeren
emptyTrashConfirm: Papierkorb leeren?
emptyTrashConfirmBody: "Alle Einträge im Papierkorb werden endgültig gelöscht. Dies kann nicht rückgängig gemacht werden."
emptyTrashFailed: Papierkorb konnte nicht geleert werden.
restoreItem: Wiederherstellen
permanentlyDeleteItem: Endgültig löschen
permanentlyDeleteConfirm: Diesen Eintrag endgültig löschen?
permanentlyDeleteConfirmBody: "Dies kann nicht rückgängig gemacht werden."
restoreFailed: Eintrag konnte nicht wiederhergestellt werden.
permanentlyDeleteFailed: Eintrag konnte nicht gelöscht werden.
itemRestored: Eintrag wiederhergestellt
createList: Neue Liste
listName: Listenname
listDescription: Beschreibung (optional)

View File

@@ -721,6 +721,84 @@ class ChecklistsMessagesEs extends ChecklistsMessages {
/// ```
String get undo => """Deshacer""";
/// ```dart
/// "Ver papelera"
/// ```
String get viewTrash => """Ver papelera""";
/// ```dart
/// "Salir de la papelera"
/// ```
String get exitTrash => """Salir de la papelera""";
/// ```dart
/// "Papelera"
/// ```
String get trashTitle => """Papelera""";
/// ```dart
/// "La papelera está vacía."
/// ```
String get noTrashedItems => """La papelera está vacía.""";
/// ```dart
/// "Vaciar papelera"
/// ```
String get emptyTrash => """Vaciar papelera""";
/// ```dart
/// "¿Vaciar la papelera?"
/// ```
String get emptyTrashConfirm => """¿Vaciar la papelera?""";
/// ```dart
/// "Todos los artículos de la papelera se eliminarán permanentemente. Esta acción no se puede deshacer."
/// ```
String get emptyTrashConfirmBody =>
"""Todos los artículos de la papelera se eliminarán permanentemente. Esta acción no se puede deshacer.""";
/// ```dart
/// "No se pudo vaciar la papelera."
/// ```
String get emptyTrashFailed => """No se pudo vaciar la papelera.""";
/// ```dart
/// "Restaurar"
/// ```
String get restoreItem => """Restaurar""";
/// ```dart
/// "Eliminar permanentemente"
/// ```
String get permanentlyDeleteItem => """Eliminar permanentemente""";
/// ```dart
/// "¿Eliminar este artículo permanentemente?"
/// ```
String get permanentlyDeleteConfirm =>
"""¿Eliminar este artículo permanentemente?""";
/// ```dart
/// "Esta acción no se puede deshacer."
/// ```
String get permanentlyDeleteConfirmBody =>
"""Esta acción no se puede deshacer.""";
/// ```dart
/// "No se pudo restaurar el artículo."
/// ```
String get restoreFailed => """No se pudo restaurar el artículo.""";
/// ```dart
/// "No se pudo eliminar el artículo."
/// ```
String get permanentlyDeleteFailed => """No se pudo eliminar el artículo.""";
/// ```dart
/// "Artículo restaurado"
/// ```
String get itemRestored => """Artículo restaurado""";
/// ```dart
/// "Nueva lista"
/// ```
@@ -1599,6 +1677,25 @@ Por favor, completa el inicio de sesión en tu navegador.""",
"""checklists.moveFailed""": """No se pudo mover el artículo.""",
"""checklists.itemMarkedDone""": """Artículo marcado como hecho""",
"""checklists.undo""": """Deshacer""",
"""checklists.viewTrash""": """Ver papelera""",
"""checklists.exitTrash""": """Salir de la papelera""",
"""checklists.trashTitle""": """Papelera""",
"""checklists.noTrashedItems""": """La papelera está vacía.""",
"""checklists.emptyTrash""": """Vaciar papelera""",
"""checklists.emptyTrashConfirm""": """¿Vaciar la papelera?""",
"""checklists.emptyTrashConfirmBody""":
"""Todos los artículos de la papelera se eliminarán permanentemente. Esta acción no se puede deshacer.""",
"""checklists.emptyTrashFailed""": """No se pudo vaciar la papelera.""",
"""checklists.restoreItem""": """Restaurar""",
"""checklists.permanentlyDeleteItem""": """Eliminar permanentemente""",
"""checklists.permanentlyDeleteConfirm""":
"""¿Eliminar este artículo permanentemente?""",
"""checklists.permanentlyDeleteConfirmBody""":
"""Esta acción no se puede deshacer.""",
"""checklists.restoreFailed""": """No se pudo restaurar el artículo.""",
"""checklists.permanentlyDeleteFailed""":
"""No se pudo eliminar el artículo.""",
"""checklists.itemRestored""": """Artículo restaurado""",
"""checklists.createList""": """Nueva lista""",
"""checklists.listName""": """Nombre de la lista""",
"""checklists.listDescription""": """Descripción (opcional)""",

View File

@@ -131,6 +131,21 @@ checklists:
moveFailed: "No se pudo mover el artículo."
itemMarkedDone: "Artículo marcado como hecho"
undo: Deshacer
viewTrash: Ver papelera
exitTrash: Salir de la papelera
trashTitle: Papelera
noTrashedItems: La papelera está vacía.
emptyTrash: Vaciar papelera
emptyTrashConfirm: ¿Vaciar la papelera?
emptyTrashConfirmBody: "Todos los artículos de la papelera se eliminarán permanentemente. Esta acción no se puede deshacer."
emptyTrashFailed: No se pudo vaciar la papelera.
restoreItem: Restaurar
permanentlyDeleteItem: Eliminar permanentemente
permanentlyDeleteConfirm: "¿Eliminar este artículo permanentemente?"
permanentlyDeleteConfirmBody: Esta acción no se puede deshacer.
restoreFailed: No se pudo restaurar el artículo.
permanentlyDeleteFailed: No se pudo eliminar el artículo.
itemRestored: Artículo restaurado
createList: Nueva lista
listName: Nombre de la lista
listDescription: "Descripción (opcional)"

View File

@@ -724,6 +724,85 @@ class ChecklistsMessagesFr extends ChecklistsMessages {
/// ```
String get undo => """Annuler""";
/// ```dart
/// "Afficher la corbeille"
/// ```
String get viewTrash => """Afficher la corbeille""";
/// ```dart
/// "Quitter la corbeille"
/// ```
String get exitTrash => """Quitter la corbeille""";
/// ```dart
/// "Corbeille"
/// ```
String get trashTitle => """Corbeille""";
/// ```dart
/// "La corbeille est vide."
/// ```
String get noTrashedItems => """La corbeille est vide.""";
/// ```dart
/// "Vider la corbeille"
/// ```
String get emptyTrash => """Vider la corbeille""";
/// ```dart
/// "Vider la corbeille ?"
/// ```
String get emptyTrashConfirm => """Vider la corbeille ?""";
/// ```dart
/// "Tous les articles de la corbeille seront supprimés définitivement. Cette action est irréversible."
/// ```
String get emptyTrashConfirmBody =>
"""Tous les articles de la corbeille seront supprimés définitivement. Cette action est irréversible.""";
/// ```dart
/// "Impossible de vider la corbeille."
/// ```
String get emptyTrashFailed => """Impossible de vider la corbeille.""";
/// ```dart
/// "Restaurer"
/// ```
String get restoreItem => """Restaurer""";
/// ```dart
/// "Supprimer définitivement"
/// ```
String get permanentlyDeleteItem => """Supprimer définitivement""";
/// ```dart
/// "Supprimer définitivement cet article ?"
/// ```
String get permanentlyDeleteConfirm =>
"""Supprimer définitivement cet article ?""";
/// ```dart
/// "Cette action est irréversible."
/// ```
String get permanentlyDeleteConfirmBody =>
"""Cette action est irréversible.""";
/// ```dart
/// "Impossible de restaurer l'article."
/// ```
String get restoreFailed => """Impossible de restaurer l'article.""";
/// ```dart
/// "Impossible de supprimer l'article."
/// ```
String get permanentlyDeleteFailed =>
"""Impossible de supprimer l'article.""";
/// ```dart
/// "Article restauré"
/// ```
String get itemRestored => """Article restauré""";
/// ```dart
/// "Nouvelle liste"
/// ```
@@ -1604,6 +1683,25 @@ Veuillez terminer la connexion dans votre navigateur.""",
"""checklists.moveFailed""": """Impossible de déplacer l'article.""",
"""checklists.itemMarkedDone""": """Article marqué comme fait""",
"""checklists.undo""": """Annuler""",
"""checklists.viewTrash""": """Afficher la corbeille""",
"""checklists.exitTrash""": """Quitter la corbeille""",
"""checklists.trashTitle""": """Corbeille""",
"""checklists.noTrashedItems""": """La corbeille est vide.""",
"""checklists.emptyTrash""": """Vider la corbeille""",
"""checklists.emptyTrashConfirm""": """Vider la corbeille ?""",
"""checklists.emptyTrashConfirmBody""":
"""Tous les articles de la corbeille seront supprimés définitivement. Cette action est irréversible.""",
"""checklists.emptyTrashFailed""": """Impossible de vider la corbeille.""",
"""checklists.restoreItem""": """Restaurer""",
"""checklists.permanentlyDeleteItem""": """Supprimer définitivement""",
"""checklists.permanentlyDeleteConfirm""":
"""Supprimer définitivement cet article ?""",
"""checklists.permanentlyDeleteConfirmBody""":
"""Cette action est irréversible.""",
"""checklists.restoreFailed""": """Impossible de restaurer l'article.""",
"""checklists.permanentlyDeleteFailed""":
"""Impossible de supprimer l'article.""",
"""checklists.itemRestored""": """Article restauré""",
"""checklists.createList""": """Nouvelle liste""",
"""checklists.listName""": """Nom de la liste""",
"""checklists.listDescription""": """Description (facultatif)""",

View File

@@ -131,6 +131,21 @@ checklists:
moveFailed: "Impossible de déplacer l'article."
itemMarkedDone: "Article marqué comme fait"
undo: Annuler
viewTrash: "Afficher la corbeille"
exitTrash: "Quitter la corbeille"
trashTitle: Corbeille
noTrashedItems: "La corbeille est vide."
emptyTrash: "Vider la corbeille"
emptyTrashConfirm: "Vider la corbeille ?"
emptyTrashConfirmBody: "Tous les articles de la corbeille seront supprimés définitivement. Cette action est irréversible."
emptyTrashFailed: "Impossible de vider la corbeille."
restoreItem: Restaurer
permanentlyDeleteItem: "Supprimer définitivement"
permanentlyDeleteConfirm: "Supprimer définitivement cet article ?"
permanentlyDeleteConfirmBody: "Cette action est irréversible."
restoreFailed: "Impossible de restaurer l'article."
permanentlyDeleteFailed: "Impossible de supprimer l'article."
itemRestored: "Article restauré"
createList: Nouvelle liste
listName: Nom de la liste
listDescription: Description (facultatif)

View File

@@ -718,6 +718,82 @@ class ChecklistsMessagesHe extends ChecklistsMessages {
/// ```
String get undo => """בטל""";
/// ```dart
/// "הצג אשפה"
/// ```
String get viewTrash => """הצג אשפה""";
/// ```dart
/// "צא מהאשפה"
/// ```
String get exitTrash => """צא מהאשפה""";
/// ```dart
/// "אשפה"
/// ```
String get trashTitle => """אשפה""";
/// ```dart
/// "האשפה ריקה."
/// ```
String get noTrashedItems => """האשפה ריקה.""";
/// ```dart
/// "רוקן אשפה"
/// ```
String get emptyTrash => """רוקן אשפה""";
/// ```dart
/// "לרוקן את האשפה?"
/// ```
String get emptyTrashConfirm => """לרוקן את האשפה?""";
/// ```dart
/// "כל הפריטים באשפה יימחקו לצמיתות. לא ניתן לבטל פעולה זו."
/// ```
String get emptyTrashConfirmBody =>
"""כל הפריטים באשפה יימחקו לצמיתות. לא ניתן לבטל פעולה זו.""";
/// ```dart
/// "ריקון האשפה נכשל."
/// ```
String get emptyTrashFailed => """ריקון האשפה נכשל.""";
/// ```dart
/// "שחזר"
/// ```
String get restoreItem => """שחזר""";
/// ```dart
/// "מחק לצמיתות"
/// ```
String get permanentlyDeleteItem => """מחק לצמיתות""";
/// ```dart
/// "למחוק את הפריט לצמיתות?"
/// ```
String get permanentlyDeleteConfirm => """למחוק את הפריט לצמיתות?""";
/// ```dart
/// "לא ניתן לבטל פעולה זו."
/// ```
String get permanentlyDeleteConfirmBody => """לא ניתן לבטל פעולה זו.""";
/// ```dart
/// "שחזור הפריט נכשל."
/// ```
String get restoreFailed => """שחזור הפריט נכשל.""";
/// ```dart
/// "מחיקת הפריט נכשלה."
/// ```
String get permanentlyDeleteFailed => """מחיקת הפריט נכשלה.""";
/// ```dart
/// "הפריט שוחזר"
/// ```
String get itemRestored => """הפריט שוחזר""";
/// ```dart
/// "רשימה חדשה"
/// ```
@@ -1585,6 +1661,22 @@ Map<String, String> get messagesHeMap => {
"""checklists.moveFailed""": """העברת הפריט נכשלה.""",
"""checklists.itemMarkedDone""": """הפריט סומן כהושלם""",
"""checklists.undo""": """בטל""",
"""checklists.viewTrash""": """הצג אשפה""",
"""checklists.exitTrash""": """צא מהאשפה""",
"""checklists.trashTitle""": """אשפה""",
"""checklists.noTrashedItems""": """האשפה ריקה.""",
"""checklists.emptyTrash""": """רוקן אשפה""",
"""checklists.emptyTrashConfirm""": """לרוקן את האשפה?""",
"""checklists.emptyTrashConfirmBody""":
"""כל הפריטים באשפה יימחקו לצמיתות. לא ניתן לבטל פעולה זו.""",
"""checklists.emptyTrashFailed""": """ריקון האשפה נכשל.""",
"""checklists.restoreItem""": """שחזר""",
"""checklists.permanentlyDeleteItem""": """מחק לצמיתות""",
"""checklists.permanentlyDeleteConfirm""": """למחוק את הפריט לצמיתות?""",
"""checklists.permanentlyDeleteConfirmBody""": """לא ניתן לבטל פעולה זו.""",
"""checklists.restoreFailed""": """שחזור הפריט נכשל.""",
"""checklists.permanentlyDeleteFailed""": """מחיקת הפריט נכשלה.""",
"""checklists.itemRestored""": """הפריט שוחזר""",
"""checklists.createList""": """רשימה חדשה""",
"""checklists.listName""": """שם הרשימה""",
"""checklists.listDescription""": """תיאור (אופציונלי)""",

View File

@@ -131,6 +131,21 @@ checklists:
moveFailed: העברת הפריט נכשלה.
itemMarkedDone: הפריט סומן כהושלם
undo: בטל
viewTrash: הצג אשפה
exitTrash: צא מהאשפה
trashTitle: אשפה
noTrashedItems: האשפה ריקה.
emptyTrash: רוקן אשפה
emptyTrashConfirm: לרוקן את האשפה?
emptyTrashConfirmBody: כל הפריטים באשפה יימחקו לצמיתות. לא ניתן לבטל פעולה זו.
emptyTrashFailed: ריקון האשפה נכשל.
restoreItem: שחזר
permanentlyDeleteItem: מחק לצמיתות
permanentlyDeleteConfirm: למחוק את הפריט לצמיתות?
permanentlyDeleteConfirmBody: לא ניתן לבטל פעולה זו.
restoreFailed: שחזור הפריט נכשל.
permanentlyDeleteFailed: מחיקת הפריט נכשלה.
itemRestored: הפריט שוחזר
createList: רשימה חדשה
listName: שם הרשימה
listDescription: תיאור (אופציונלי)

View File

@@ -61,6 +61,7 @@ class ListItem {
final int sortOrder;
final int createdAt;
final int updatedAt;
final int? deletedAt;
const ListItem({
required this.id,
@@ -81,6 +82,7 @@ class ListItem {
required this.sortOrder,
required this.createdAt,
required this.updatedAt,
this.deletedAt,
});
factory ListItem.fromJson(Map<String, dynamic> json) => ListItem(
@@ -102,6 +104,7 @@ class ListItem {
sortOrder: json['sortOrder'] as int,
createdAt: json['createdAt'] as int,
updatedAt: json['updatedAt'] as int,
deletedAt: json['deletedAt'] as int?,
);
Map<String, dynamic> toJson() => {
@@ -123,6 +126,7 @@ class ListItem {
'sortOrder': sortOrder,
'createdAt': createdAt,
'updatedAt': updatedAt,
'deletedAt': deletedAt,
};
ListItem copyWith({bool? done, int? doneAt, String? doneBy}) => ListItem(
@@ -144,5 +148,6 @@ class ListItem {
sortOrder: sortOrder,
createdAt: createdAt,
updatedAt: updatedAt,
deletedAt: deletedAt,
);
}

View File

@@ -186,6 +186,44 @@ class ChecklistService {
);
}
Future<List<ListItem>> getDeletedItems(
int houseId,
int listId, {
int limit = 200,
int offset = 0,
}) async {
return ApiClient.instance.get<List, List<ListItem>>(
'/houses/$houseId/lists/$listId/items/trash',
query: {'limit': limit.toString(), 'offset': offset.toString()},
fromJson: (data) => data
.map((e) => ListItem.fromJson(e as Map<String, dynamic>))
.toList(),
);
}
Future<ListItem> restoreItem(int houseId, int listId, int itemId) async {
return ApiClient.instance.post<Map<String, dynamic>, ListItem>(
'/houses/$houseId/lists/$listId/items/$itemId/restore',
fromJson: (data) => ListItem.fromJson(data),
);
}
Future<void> permanentlyDeleteItem(
int houseId,
int listId,
int itemId,
) async {
await ApiClient.instance.delete(
'/houses/$houseId/lists/$listId/items/$itemId/permanent',
);
}
Future<void> emptyTrash(int houseId, int listId) async {
await ApiClient.instance.delete(
'/houses/$houseId/lists/$listId/items/trash',
);
}
Future<ListItem> toggleItem(int houseId, int listId, int itemId) async {
return ApiClient.instance.post<Map<String, dynamic>, ListItem>(
'/houses/$houseId/lists/$listId/items/$itemId/toggle',

View File

@@ -16,22 +16,28 @@ class ChecklistItemTile extends StatelessWidget {
final ListItem item;
final models.Category? category;
final int houseId;
final bool trashMode;
final ValueChanged<ListItem> onToggle;
final ValueChanged<ListItem> onView;
final ValueChanged<ListItem> onEdit;
final ValueChanged<ListItem> onMove;
final ValueChanged<ListItem> onDelete;
final ValueChanged<ListItem>? onRestore;
final ValueChanged<ListItem>? onPermanentDelete;
const ChecklistItemTile({
super.key,
required this.item,
this.category,
required this.houseId,
this.trashMode = false,
required this.onToggle,
required this.onView,
required this.onEdit,
required this.onMove,
required this.onDelete,
this.onRestore,
this.onPermanentDelete,
});
@override
@@ -50,12 +56,29 @@ class ChecklistItemTile extends StatelessWidget {
itemBuilder: _menuItems,
onSelected: (value) => _onMenuSelected(value),
child: InkWell(
onTap: tapRowToToggle ? () => onToggle(item) : null,
onTap: trashMode
? () => onView(item)
: (tapRowToToggle ? () => onToggle(item) : null),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
child: Row(
children: [
Checkbox(value: item.done, onChanged: (_) => onToggle(item)),
if (trashMode)
Padding(
padding: const EdgeInsetsDirectional.only(
start: 12,
end: 12,
),
child: Icon(
Icons.delete_outline,
color: theme.colorScheme.onSurfaceVariant,
),
)
else
Checkbox(
value: item.done,
onChanged: (_) => onToggle(item),
),
if (item.imageFileId != null) ...[
GestureDetector(
onTap: () => _showImagePreview(context),
@@ -124,38 +147,64 @@ class ChecklistItemTile extends StatelessWidget {
);
}
List<PopupMenuEntry<String>> _menuItems() => [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Icons.edit, size: 18),
const SizedBox(width: 8),
Text(m.checklists.editItem),
],
List<PopupMenuEntry<String>> _menuItems() {
if (trashMode) {
return [
PopupMenuItem(
value: 'restore',
child: Row(
children: [
const Icon(Icons.restore_from_trash, size: 18),
const SizedBox(width: 8),
Text(m.checklists.restoreItem),
],
),
),
PopupMenuItem(
value: 'permanent',
child: Row(
children: [
const Icon(Icons.delete_forever, size: 18),
const SizedBox(width: 8),
Text(m.checklists.permanentlyDeleteItem),
],
),
),
];
}
return [
PopupMenuItem(
value: 'edit',
child: Row(
children: [
const Icon(Icons.edit, size: 18),
const SizedBox(width: 8),
Text(m.checklists.editItem),
],
),
),
),
PopupMenuItem(
value: 'move',
child: Row(
children: [
const Icon(Icons.drive_file_move_outlined, size: 18),
const SizedBox(width: 8),
Text(m.checklists.moveItem),
],
PopupMenuItem(
value: 'move',
child: Row(
children: [
const Icon(Icons.drive_file_move_outlined, size: 18),
const SizedBox(width: 8),
Text(m.checklists.moveItem),
],
),
),
),
PopupMenuItem(
value: 'remove',
child: Row(
children: [
const Icon(Icons.delete, size: 18),
const SizedBox(width: 8),
Text(m.checklists.removeItem),
],
PopupMenuItem(
value: 'remove',
child: Row(
children: [
const Icon(Icons.delete, size: 18),
const SizedBox(width: 8),
Text(m.checklists.removeItem),
],
),
),
),
];
];
}
void _onMenuSelected(String value) {
switch (value) {
@@ -165,6 +214,10 @@ class ChecklistItemTile extends StatelessWidget {
onMove(item);
case 'remove':
onDelete(item);
case 'restore':
onRestore?.call(item);
case 'permanent':
onPermanentDelete?.call(item);
}
}

View File

@@ -41,6 +41,9 @@ class ChecklistsController extends ChangeNotifier {
String _sortBy = 'custom';
String get sortBy => _sortBy;
bool _isTrashMode = false;
bool get isTrashMode => _isTrashMode;
bool _isLoading = true;
bool get isLoading => _isLoading;
@@ -142,6 +145,14 @@ class ChecklistsController extends ChangeNotifier {
_currentList = list;
_checklistService.selectedListId = list.id;
if (_isTrashMode) {
_items = [];
_isLoading = true;
notifyListeners();
await _loadTrashItems(list);
return;
}
// Show cached items immediately, or spinner if no cache for this list
final cached = _checklistService.getCachedItems(list.id);
if (cached != null) {
@@ -162,7 +173,7 @@ class ChecklistsController extends ChangeNotifier {
sortBy: _sortBy,
);
_checklistService.cacheItems(list.id, freshItems);
if (_currentList?.id == list.id) {
if (_currentList?.id == list.id && !_isTrashMode) {
_items = freshItems;
_isLoading = false;
notifyListeners();
@@ -177,6 +188,38 @@ class ChecklistsController extends ChangeNotifier {
}
}
Future<void> _loadTrashItems(ChecklistList list) async {
try {
final trashItems = await _checklistService.getDeletedItems(
houseId,
list.id,
);
if (_currentList?.id == list.id && _isTrashMode) {
_items = trashItems;
_error = null;
_isLoading = false;
notifyListeners();
}
} catch (e) {
debugPrint('[ChecklistsController] Failed to load trash: $e');
if (_currentList?.id == list.id && _isTrashMode) {
_error = m.checklists.failedToLoadItems;
_isLoading = false;
notifyListeners();
}
}
}
Future<void> setTrashMode(bool enabled) async {
if (_isTrashMode == enabled) return;
_isTrashMode = enabled;
if (_currentList != null) {
await selectList(_currentList!);
} else {
notifyListeners();
}
}
Future<void> setSortBy(String sort) async {
if (sort == _sortBy) return;
_sortBy = sort;
@@ -389,6 +432,43 @@ class ChecklistsController extends ChangeNotifier {
notifyListeners();
}
Future<ListItem> restoreItem(ListItem item) async {
final restored = await _checklistService.restoreItem(
houseId,
item.listId,
item.id,
);
_items.removeWhere((i) => i.id == item.id);
if (!_isTrashMode) {
_items.add(restored);
_checklistService.cacheItems(_currentList!.id, List.of(_items));
}
notifyListeners();
return restored;
}
Future<void> permanentlyDeleteItem(ListItem item) async {
await _checklistService.permanentlyDeleteItem(
houseId,
item.listId,
item.id,
);
_items.removeWhere((i) => i.id == item.id);
if (!_isTrashMode) {
_checklistService.cacheItems(_currentList!.id, List.of(_items));
}
notifyListeners();
}
Future<void> emptyTrash() async {
if (_currentList == null) return;
await _checklistService.emptyTrash(houseId, _currentList!.id);
if (_isTrashMode) {
_items = [];
notifyListeners();
}
}
Future<void> toggleItem(ListItem item) async {
final index = _items.indexWhere((i) => i.id == item.id);
if (index == -1) return;
@@ -404,12 +484,23 @@ class ChecklistsController extends ChangeNotifier {
item.listId,
item.id,
);
_items[index] = updated;
// If toggling caused a soft-delete (deleteOnDone), drop it from active list.
if (updated.deletedAt != null) {
_items.removeWhere((i) => i.id == item.id);
} else {
final i = _items.indexWhere((x) => x.id == item.id);
if (i != -1) _items[i] = updated;
}
_checklistService.cacheItems(item.listId, List.of(_items));
notifyListeners();
} catch (e) {
// Revert on failure
_items[index] = item;
final i = _items.indexWhere((x) => x.id == item.id);
if (i != -1) {
_items[i] = item;
} else {
_items.insert(index.clamp(0, _items.length), item);
}
_checklistService.cacheItems(item.listId, List.of(_items));
notifyListeners();
}

View File

@@ -180,21 +180,36 @@ class _ChecklistsBodyState extends State<_ChecklistsBody> {
onCreateNew: () => _createList(context, controller),
),
),
IconButton(
icon: Icon(_searchOpen ? Icons.search_off : Icons.search),
onPressed: _toggleSearch,
),
ChecklistSortButton(
currentSort: controller.sortBy,
onSelected: controller.setSortBy,
if (!controller.isTrashMode)
IconButton(
icon: Icon(_searchOpen ? Icons.search_off : Icons.search),
onPressed: _toggleSearch,
),
if (!controller.isTrashMode)
ChecklistSortButton(
currentSort: controller.sortBy,
onSelected: controller.setSortBy,
),
PopupMenuButton<String>(
icon: Icon(
controller.isTrashMode ? Icons.delete : Icons.more_vert,
),
tooltip: controller.isTrashMode
? m.checklists.trashTitle
: null,
onSelected: (value) =>
_onListMenuSelected(context, controller, value),
itemBuilder: (_) => _listMenuItems(controller),
),
],
),
if (controller.isTrashMode && controller.currentList != null)
_TrashBanner(onExit: () => controller.setTrashMode(false)),
AnimatedSize(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
alignment: Alignment.topCenter,
child: _searchOpen
child: (_searchOpen && !controller.isTrashMode)
? _SearchPanel(
searchController: _searchController,
selectedCategoryIds: _selectedCategoryIds,
@@ -207,11 +222,12 @@ class _ChecklistsBodyState extends State<_ChecklistsBody> {
Expanded(child: itemsArea),
],
),
if (controller.currentList != null)
if (controller.currentList != null && !controller.isTrashMode)
PositionedDirectional(
end: 16,
bottom: 16,
child: FloatingActionButton(
heroTag: 'checklists-fab',
onPressed: () async {
final added = await Navigator.of(context).push<bool>(
MaterialPageRoute(
@@ -238,6 +254,139 @@ class _ChecklistsBodyState extends State<_ChecklistsBody> {
await controller.selectList(created);
}
}
List<PopupMenuEntry<String>> _listMenuItems(ChecklistsController controller) {
if (controller.isTrashMode) {
return [
PopupMenuItem(
value: 'exit_trash',
child: Row(
children: [
const Icon(Icons.arrow_back, size: 18),
const SizedBox(width: 8),
Text(m.checklists.exitTrash),
],
),
),
PopupMenuItem(
value: 'empty_trash',
child: Row(
children: [
const Icon(Icons.delete_forever, size: 18),
const SizedBox(width: 8),
Text(m.checklists.emptyTrash),
],
),
),
];
}
return [
PopupMenuItem(
value: 'view_trash',
child: Row(
children: [
const Icon(Icons.delete_outline, size: 18),
const SizedBox(width: 8),
Text(m.checklists.viewTrash),
],
),
),
];
}
Future<void> _onListMenuSelected(
BuildContext context,
ChecklistsController controller,
String value,
) async {
switch (value) {
case 'view_trash':
if (_searchOpen) {
setState(() {
_searchOpen = false;
_searchController.clear();
_selectedCategoryIds.clear();
});
}
await controller.setTrashMode(true);
case 'exit_trash':
await controller.setTrashMode(false);
case 'empty_trash':
await _confirmEmptyTrash(context, controller);
}
}
Future<void> _confirmEmptyTrash(
BuildContext context,
ChecklistsController controller,
) async {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(m.checklists.emptyTrashConfirm),
content: Text(m.checklists.emptyTrashConfirmBody),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(m.common.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(m.checklists.emptyTrash),
),
],
),
);
if (confirmed != true) return;
try {
await controller.emptyTrash();
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(m.checklists.emptyTrashFailed)));
}
}
}
}
class _TrashBanner extends StatelessWidget {
final VoidCallback onExit;
const _TrashBanner({required this.onExit});
@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Container(
width: double.infinity,
color: theme.colorScheme.surfaceContainerHighest,
padding: const EdgeInsetsDirectional.fromSTEB(16, 8, 8, 8),
child: Row(
children: [
Icon(
Icons.delete_outline,
size: 18,
color: theme.colorScheme.onSurfaceVariant,
),
const SizedBox(width: 8),
Expanded(
child: Text(
m.checklists.trashTitle,
style: theme.textTheme.labelLarge?.copyWith(
color: theme.colorScheme.onSurfaceVariant,
),
),
),
TextButton.icon(
onPressed: onExit,
icon: const Icon(Icons.close, size: 16),
label: Text(m.checklists.exitTrash),
),
],
),
);
}
}
class _SearchPanel extends StatelessWidget {
@@ -453,22 +602,40 @@ class _ItemList extends StatelessWidget {
@override
Widget build(BuildContext context) {
final unchecked = items.where((i) => !i.done).toList();
final checked = items.where((i) => i.done).toList();
if (items.isEmpty) {
return ListView(
children: [
const SizedBox(height: 100),
Center(
child: Text(
isFiltering ? m.checklists.noSearchResults : m.checklists.noItems,
controller.isTrashMode
? m.checklists.noTrashedItems
: (isFiltering
? m.checklists.noSearchResults
: m.checklists.noItems),
),
),
],
);
}
if (controller.isTrashMode) {
return CustomScrollView(
slivers: [
_ReorderablePartition(
items: items,
controller: controller,
categorySpacing: 'disabled',
allowReorder: false,
),
const SliverToBoxAdapter(child: SizedBox(height: 88)),
],
);
}
final unchecked = items.where((i) => !i.done).toList();
final checked = items.where((i) => i.done).toList();
final spacingPref = context.watch<PrefsService>().checklistCategorySpacing;
final categorySpacing = controller.sortBy == 'category'
? spacingPref
@@ -518,11 +685,13 @@ class _ReorderablePartition extends StatelessWidget {
final List<ListItem> items;
final ChecklistsController controller;
final String categorySpacing;
final bool allowReorder;
const _ReorderablePartition({
required this.items,
required this.controller,
this.categorySpacing = 'disabled',
this.allowReorder = true,
});
void _toggleItem(
@@ -531,6 +700,7 @@ class _ReorderablePartition extends StatelessWidget {
ListItem item,
) {
final wasDone = item.done;
final wasDeleteOnDone = item.deleteOnDone;
controller.toggleItem(item);
if (wasDone) return;
@@ -541,7 +711,19 @@ class _ReorderablePartition extends StatelessWidget {
content: Text(m.checklists.itemMarkedDone),
action: SnackBarAction(
label: m.checklists.undo,
onPressed: () {
onPressed: () async {
if (wasDeleteOnDone) {
try {
await controller.restoreItem(item);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(m.checklists.restoreFailed)),
);
}
}
return;
}
final current = controller.items.firstWhere(
(i) => i.id == item.id,
orElse: () => item.copyWith(done: true),
@@ -553,6 +735,62 @@ class _ReorderablePartition extends StatelessWidget {
);
}
Future<void> _restoreItem(
BuildContext context,
ChecklistsController controller,
ListItem item,
) async {
try {
await controller.restoreItem(item);
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(m.checklists.itemRestored)));
}
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(m.checklists.restoreFailed)));
}
}
}
void _permanentlyDelete(
BuildContext context,
ChecklistsController controller,
ListItem item,
) {
showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(m.checklists.permanentlyDeleteConfirm),
content: Text(m.checklists.permanentlyDeleteConfirmBody),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: Text(m.common.cancel),
),
FilledButton(
onPressed: () => Navigator.pop(ctx, true),
child: Text(m.common.delete),
),
],
),
).then((confirmed) async {
if (confirmed != true) return;
try {
await controller.permanentlyDeleteItem(item);
} catch (e) {
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(m.checklists.permanentlyDeleteFailed)),
);
}
}
});
}
void _viewItem(
BuildContext context,
ChecklistsController controller,
@@ -692,8 +930,52 @@ class _ReorderablePartition extends StatelessWidget {
});
}
Widget _tileFor(BuildContext context, int index) {
final item = items[index];
final showSeparator =
categorySpacing != 'disabled' &&
index > 0 &&
items[index - 1].categoryId != item.categoryId;
final tile = ChecklistItemTile(
key: allowReorder ? null : ValueKey(item.id),
item: item,
category: item.categoryId != null
? controller.categories[item.categoryId]
: null,
houseId: controller.houseId,
trashMode: controller.isTrashMode,
onToggle: (item) => _toggleItem(context, controller, item),
onView: (item) => _viewItem(context, controller, item),
onEdit: (item) => _editItem(context, controller, item),
onMove: (item) => _moveItem(context, controller, item),
onDelete: (item) => _deleteItem(context, controller, item),
onRestore: (item) => _restoreItem(context, controller, item),
onPermanentDelete: (item) =>
_permanentlyDelete(context, controller, item),
);
return showSeparator
? Column(
children: [
if (categorySpacing == 'divider')
const Divider(height: 25)
else
const SizedBox(height: 20),
tile,
],
)
: tile;
}
@override
Widget build(BuildContext context) {
if (!allowReorder) {
return SliverList(
delegate: SliverChildBuilderDelegate(
(context, index) => _tileFor(context, index),
childCount: items.length,
),
);
}
return SliverReorderableList(
itemCount: items.length,
onReorder: (oldIndex, newIndex) {
@@ -702,36 +984,10 @@ class _ReorderablePartition extends StatelessWidget {
},
itemBuilder: (context, index) {
final item = items[index];
final showSeparator =
categorySpacing != 'disabled' &&
index > 0 &&
items[index - 1].categoryId != item.categoryId;
final tile = ChecklistItemTile(
item: item,
category: item.categoryId != null
? controller.categories[item.categoryId]
: null,
houseId: controller.houseId,
onToggle: (item) => _toggleItem(context, controller, item),
onView: (item) => _viewItem(context, controller, item),
onEdit: (item) => _editItem(context, controller, item),
onMove: (item) => _moveItem(context, controller, item),
onDelete: (item) => _deleteItem(context, controller, item),
);
return ReorderableDelayedDragStartListener(
key: ValueKey(item.id),
index: index,
child: showSeparator
? Column(
children: [
if (categorySpacing == 'divider')
const Divider(height: 25)
else
const SizedBox(height: 20),
tile,
],
)
: tile,
child: _tileFor(context, index),
);
},
);

View File

@@ -125,6 +125,7 @@ class _NotesWallBody extends StatelessWidget {
end: 16,
bottom: 16,
child: FloatingActionButton(
heroTag: 'notes-fab',
onPressed: () => _createNote(context, controller),
child: const Icon(Icons.add),
),