feat: exclusão de notas na UI (lista e editor) com confirmação e sync

- Botão 🗑 no card da lista e na barra do editor; AlertDialog de confirmação
  explicando o efeito (remoto sobe na sync; local-only some direto)
- Exclusões já eram enfileiradas (pendingDelete + op delete) e o engine já
  tratava 404; propagação via SSE 'deleted' para outras sessões
- versionCode 3 / versionName 0.2.1; README atualizado
- Testado no emulador: delete pela lista → servidor 0 notas; delete pelo
  editor → idem, com volta à lista; pull de nota da Web e exclusão seguinte ok
This commit is contained in:
lgcosta committed 2026-11-05 07:48:57 -03:00
1 parent 9508dbf24a
commit 57edfbe4df
3 files changed
+67 -5

No files matched your search

@@ -30,6 +30,7 @@ import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.CloudDone
import androidx.compose.material.icons.filled.CloudOff
import androidx.compose.material.icons.filled.CloudSync
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Description
import androidx.compose.material.icons.filled.Edit
import androidx.compose.material.icons.filled.Visibility
@@ -195,7 +196,32 @@ fun NotesListScreen(
val remoteChanged by vm.remoteChanged.collectAsState()
var filterCat by remember { mutableStateOf<String?>(null) }
var filterTag by remember { mutableStateOf<String?>(null) }
var deleteTarget by remember { mutableStateOf<NoteEntity?>(null) }
val snackbar = remember { SnackbarHostState() }
val scope = rememberCoroutineScope()
deleteTarget?.let { target ->
androidx.compose.material3.AlertDialog(
onDismissRequest = { deleteTarget = null },
icon = { Icon(Icons.Default.Delete, null) },
title = { Text("Excluir nota?") },
text = {
Text("“${target.title.ifBlank { "Sem título" }}” será removida deste aparelho " +
"e do servidor na próxima sincronização (o histórico dela fica nas revisões " +
"do servidor até a exclusão subir). Esta ação não pode ser desfeita pelo app.")
},
confirmButton = {
TextButton(onClick = {
vm.deleteNote(target.id)
deleteTarget = null
scope.launch { snackbar.showSnackbar("Exclusão registrada — sincronizando") }
}) { Text("Excluir", color = MaterialTheme.colorScheme.error) }
},
dismissButton = {
TextButton(onClick = { deleteTarget = null }) { Text("Cancelar") }
},
)
}
val catPaths = remember(cats) {
val byId = cats.associateBy { it.id }
@@ -301,6 +327,7 @@ fun NotesListScreen(
others = presence[note.id] ?: 0,
remotelyChanged = remoteChanged.contains(note.id),
onClick = { onOpen(note.id) },
onDelete = { deleteTarget = note },
)
}
}
@@ -316,6 +343,7 @@ private fun NoteCard(
others: Int,
remotelyChanged: Boolean,
onClick: () -> Unit,
onDelete: () -> Unit,
) {
Card(onClick = onClick, shape = RoundedCornerShape(18.dp), elevation = CardDefaults.cardElevation(2.dp)) {
Column(Modifier.padding(14.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {
@@ -337,6 +365,10 @@ private fun NoteCard(
Text("👥$others", style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.primary)
}
IconButton(onClick = onDelete, modifier = Modifier.size(32.dp)) {
Icon(Icons.Default.Delete, "Excluir", Modifier.size(18.dp),
tint = MaterialTheme.colorScheme.outline)
}
}
Text(
ago(note.updatedAt) + if (note.tags.isNotBlank())
@@ -399,6 +431,7 @@ fun EditorScreen(vm: NotasViewModel, noteId: String, onBack: () -> Unit) {
var selectedCats by remember { mutableStateOf<List<String>>(emptyList()) }
var savedAt by remember { mutableStateOf("") }
var mode by remember { mutableStateOf(EditorMode.Edit) }
var confirmDelete by remember { mutableStateOf(false) }
val cats by vm.categories.collectAsState()
val remoteChanged by vm.remoteChanged.collectAsState()
val presence by vm.presence.collectAsState()
@@ -406,6 +439,28 @@ fun EditorScreen(vm: NotasViewModel, noteId: String, onBack: () -> Unit) {
val keyboard = LocalSoftwareKeyboardController.current
val dark = isSystemInDarkTheme()
if (confirmDelete) {
androidx.compose.material3.AlertDialog(
onDismissRequest = { confirmDelete = false },
icon = { Icon(Icons.Default.Delete, null) },
title = { Text("Excluir nota?") },
text = {
Text("“${title.text.ifBlank { "Sem título" }}” será removida deste aparelho e do " +
"servidor na próxima sincronização. Esta ação não pode ser desfeita pelo app.")
},
confirmButton = {
TextButton(onClick = {
confirmDelete = false
vm.deleteNote(noteId)
onBack()
}) { Text("Excluir", color = MaterialTheme.colorScheme.error) }
},
dismissButton = {
TextButton(onClick = { confirmDelete = false }) { Text("Cancelar") }
},
)
}
LaunchedEffect(noteId) {
vm.noteById(noteId)?.let {
title = TextFieldValue(it.title)
@@ -493,6 +548,9 @@ fun EditorScreen(vm: NotasViewModel, noteId: String, onBack: () -> Unit) {
IconButton(onClick = { persist(); keyboard?.hide() }) {
Icon(Icons.Default.CloudDone, "Salvar")
}
IconButton(onClick = { confirmDelete = true }) {
Icon(Icons.Default.Delete, "Excluir", tint = MaterialTheme.colorScheme.outline)
}
},
)
},