diff --git a/README.md b/README.md index 93f3707..4f8ca8d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b08fa0..e62ea71 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 { diff --git a/app/src/main/java/com/labexposed/notas/ui/Screens.kt b/app/src/main/java/com/labexposed/notas/ui/Screens.kt index d13b3f6..76c2b74 100644 --- a/app/src/main/java/com/labexposed/notas/ui/Screens.kt +++ b/app/src/main/java/com/labexposed/notas/ui/Screens.kt @@ -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(null) } var filterTag by remember { mutableStateOf(null) } + var deleteTarget by remember { mutableStateOf(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>(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) + } }, ) },