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:
1 parent
9508dbf24a
commit
57edfbe4df
3 files changed
+67
-5
No files matched your search
@@ -112,11 +112,15 @@ notas-online-android/
|
||||
pendências/Erro), chips de filtro (Todas / 📁 categoria com caminho da árvore / #tag), cards
|
||||
com título+extensão, tempo relativo, tags, chip de categoria(s) e ícone de status por nota
|
||||
(☁✓ sincronizada, ● âmbar não enviada, 🗑 exclusão na fila, ⚠ conflito/mudança remota, 👥N
|
||||
presença), FAB estendido "Nova".
|
||||
presença), **botão de excluir por card** (🗑) com diálogo de confirmação, FAB estendido "Nova".
|
||||
- **Editor**: campo de título na top bar, seletor de modo (Editar / WYSIWYG / Preview), chips de
|
||||
formato (TXT/MD/HTML) e de categorias (multi-seleção), corpo monoespaçado com toolbar de
|
||||
Markdown (B I S H1 H2 • ❝ 🔗 ` { }) que envolve/prefixa a seleção, selo de presença 👥, e
|
||||
horário do último salvamento.
|
||||
Markdown (B I S H1 H2 • ❝ 🔗 ` { }) que envolve/prefixa a seleção, selo de presença 👥,
|
||||
horário do último salvamento e **botão de excluir** (com confirmação; volta para a lista).
|
||||
- **Exclusões sincronizam**: apagar marca `pendingDelete`, entra `delete` na fila e sobe na
|
||||
próxima sync (nota local-only some direto); outras sessões recebem o evento SSE `deleted` e a
|
||||
nota some delas também se estiver limpa. Se a nota já sumiu do servidor (404), a fila limpa
|
||||
sozinha.
|
||||
- **Servidor**: URL + token + autor com *Testar conexão* e validação de esquema http(s).
|
||||
|
||||
## Compilar e instalar
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.labexposed.notas"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 2
|
||||
versionName = "0.2.0"
|
||||
versionCode = 3
|
||||
versionName = "0.2.1"
|
||||
}
|
||||
|
||||
buildTypes {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
|
||||
Reference in new issue
Block a user