feat(deck): меню колонки — Переименовать / Удалить список (P1 diff)

DeckRepository.renameStack/deleteStack (PUT/DELETE /stacks/{id}); VM renameStack/deleteStack;
overflow-меню (⋯) в шапке колонки → DropdownMenu; диалог переименования (префилл) +
DeckConfirmDialog для удаления. Веб-паритет меню Stack.
This commit is contained in:
b-mob-deck
2026-07-13 07:53:54 +00:00
parent e3725b7339
commit 73762d80c5
4 changed files with 95 additions and 1 deletions
@@ -236,8 +236,9 @@ fun DeckTextPromptDialog(
confirmText: String,
onConfirm: (String) -> Unit,
onDismiss: () -> Unit,
initial: String = "",
) {
var text by remember { mutableStateOf("") }
var text by remember { mutableStateOf(initial) }
Dialog(onDismissRequest = onDismiss) {
Surface(shape = RoundedCornerShape(16.dp), color = Color.White) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
@@ -263,6 +264,33 @@ fun DeckTextPromptDialog(
}
@Composable
fun DeckConfirmDialog(
title: String,
message: String,
confirmText: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
Dialog(onDismissRequest = onDismiss) {
Surface(shape = RoundedCornerShape(16.dp), color = Color.White) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
Text(
title,
style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp),
fontWeight = FontWeight.SemiBold,
color = F7Colors.TextPrimary,
)
Text(message, style = MaterialTheme.typography.bodyMedium, color = F7Colors.TextSecondary)
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
F7SecondaryButton("Отмена", onClick = onDismiss, modifier = Modifier.weight(1f))
F7PrimaryButton(text = confirmText, onClick = onConfirm, modifier = Modifier.weight(1f))
}
}
}
}
}
/** Парсит ISO-строку срока Deck в миллисекунды (для инициализации пикера). */
internal fun parseDeckDueMillis(iso: String): Long? = runCatching {
Instant.parse(iso).toEpochMilli()
@@ -215,6 +215,15 @@ class DeckRepository {
return DeckStack(id = id, boardId = boardId, title = title, order = 999, cards = emptyList())
}
fun renameStack(session: AuthSession, stack: DeckStack, title: String) {
val body = JSONObject().put("title", title).put("boardId", stack.boardId).put("order", stack.order)
sendJson(session, "PUT", "${apiBase(session)}/stacks/${stack.id}", body)
}
fun deleteStack(session: AuthSession, stackId: Int) {
sendJson(session, "DELETE", "${apiBase(session)}/stacks/$stackId", null)
}
// --- HTTP ---
private fun getJson(client: OkHttpClient, url: String): Any {
@@ -22,7 +22,10 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -62,6 +65,8 @@ fun DeckScreen(
val state by vm.state.collectAsState()
var addStackOpen by remember { mutableStateOf(false) }
var addCardStackId by remember { mutableStateOf<Int?>(null) }
var renameStackTarget by remember { mutableStateOf<DeckStack?>(null) }
var deleteStackTarget by remember { mutableStateOf<DeckStack?>(null) }
LaunchedEffect(session.serverUrl, session.username) { vm.load(session) }
LaunchedEffect(openCardId) {
@@ -110,6 +115,8 @@ fun DeckScreen(
onCardClick = { vm.openCard(it) },
onToggleDone = { vm.toggleDone(session, it) },
onAddCard = { addCardStackId = stack.id },
onRename = { renameStackTarget = stack },
onDelete = { deleteStackTarget = stack },
)
}
}
@@ -165,6 +172,25 @@ fun DeckScreen(
onDismiss = { addCardStackId = null },
)
}
renameStackTarget?.let { stack ->
DeckTextPromptDialog(
title = "Переименовать колонку",
label = "Название колонки",
confirmText = "Сохранить",
initial = stack.title,
onConfirm = { vm.renameStack(session, stack, it); renameStackTarget = null },
onDismiss = { renameStackTarget = null },
)
}
deleteStackTarget?.let { stack ->
DeckConfirmDialog(
title = "Удалить колонку?",
message = "Колонка «${stack.title}» и её карточки будут удалены.",
confirmText = "Удалить",
onConfirm = { vm.deleteStack(session, stack); deleteStackTarget = null },
onDismiss = { deleteStackTarget = null },
)
}
}
@Composable
@@ -206,7 +232,10 @@ private fun DeckStackSection(
onCardClick: (DeckCard) -> Unit,
onToggleDone: (DeckCard) -> Unit,
onAddCard: () -> Unit,
onRename: () -> Unit,
onDelete: () -> Unit,
) {
var menuOpen by remember { mutableStateOf(false) }
Column(
modifier = Modifier
.fillMaxWidth()
@@ -240,6 +269,28 @@ private fun DeckStackSection(
) {
Icon(Icons.Filled.Add, contentDescription = "Добавить карточку", tint = F7Colors.Primary, modifier = Modifier.size(18.dp))
}
Box {
Icon(
Icons.Filled.MoreVert,
contentDescription = "Меню колонки",
tint = F7Colors.TextSecondary,
modifier = Modifier
.size(28.dp)
.clip(CircleShape)
.clickable { menuOpen = true }
.padding(5.dp),
)
DropdownMenu(expanded = menuOpen, onDismissRequest = { menuOpen = false }) {
DropdownMenuItem(
text = { Text("Переименовать") },
onClick = { menuOpen = false; onRename() },
)
DropdownMenuItem(
text = { Text("Удалить список") },
onClick = { menuOpen = false; onDelete() },
)
}
}
}
}
stack.cards.forEach { card ->
@@ -124,6 +124,12 @@ class DeckViewModel(
mutate(session) { repository.createStack(session, boardId, title.trim()) }
}
fun renameStack(session: AuthSession, stack: DeckStack, title: String) =
mutate(session) { repository.renameStack(session, stack, title.trim()) }
fun deleteStack(session: AuthSession, stack: DeckStack) =
mutate(session) { repository.deleteStack(session, stack.id) }
private fun mutate(
session: AuthSession,
closeCardAfter: Boolean = false,