feat(deck): меню колонки — Переименовать / Удалить список (P1 diff)
DeckRepository.renameStack/deleteStack (PUT/DELETE /stacks/{id}); VM renameStack/deleteStack;
overflow-меню (⋯) в шапке колонки → DropdownMenu; диалог переименования (префилл) +
DeckConfirmDialog для удаления. Веб-паритет меню Stack.
This commit is contained in:
@@ -236,8 +236,9 @@ fun DeckTextPromptDialog(
|
|||||||
confirmText: String,
|
confirmText: String,
|
||||||
onConfirm: (String) -> Unit,
|
onConfirm: (String) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
|
initial: String = "",
|
||||||
) {
|
) {
|
||||||
var text by remember { mutableStateOf("") }
|
var text by remember { mutableStateOf(initial) }
|
||||||
Dialog(onDismissRequest = onDismiss) {
|
Dialog(onDismissRequest = onDismiss) {
|
||||||
Surface(shape = RoundedCornerShape(16.dp), color = Color.White) {
|
Surface(shape = RoundedCornerShape(16.dp), color = Color.White) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(12.dp)) {
|
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 в миллисекунды (для инициализации пикера). */
|
/** Парсит ISO-строку срока Deck в миллисекунды (для инициализации пикера). */
|
||||||
internal fun parseDeckDueMillis(iso: String): Long? = runCatching {
|
internal fun parseDeckDueMillis(iso: String): Long? = runCatching {
|
||||||
Instant.parse(iso).toEpochMilli()
|
Instant.parse(iso).toEpochMilli()
|
||||||
|
|||||||
@@ -215,6 +215,15 @@ class DeckRepository {
|
|||||||
return DeckStack(id = id, boardId = boardId, title = title, order = 999, cards = emptyList())
|
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 ---
|
// --- HTTP ---
|
||||||
|
|
||||||
private fun getJson(client: OkHttpClient, url: String): Any {
|
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.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Add
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -62,6 +65,8 @@ fun DeckScreen(
|
|||||||
val state by vm.state.collectAsState()
|
val state by vm.state.collectAsState()
|
||||||
var addStackOpen by remember { mutableStateOf(false) }
|
var addStackOpen by remember { mutableStateOf(false) }
|
||||||
var addCardStackId by remember { mutableStateOf<Int?>(null) }
|
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(session.serverUrl, session.username) { vm.load(session) }
|
||||||
LaunchedEffect(openCardId) {
|
LaunchedEffect(openCardId) {
|
||||||
@@ -110,6 +115,8 @@ fun DeckScreen(
|
|||||||
onCardClick = { vm.openCard(it) },
|
onCardClick = { vm.openCard(it) },
|
||||||
onToggleDone = { vm.toggleDone(session, it) },
|
onToggleDone = { vm.toggleDone(session, it) },
|
||||||
onAddCard = { addCardStackId = stack.id },
|
onAddCard = { addCardStackId = stack.id },
|
||||||
|
onRename = { renameStackTarget = stack },
|
||||||
|
onDelete = { deleteStackTarget = stack },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,6 +172,25 @@ fun DeckScreen(
|
|||||||
onDismiss = { addCardStackId = null },
|
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
|
@Composable
|
||||||
@@ -206,7 +232,10 @@ private fun DeckStackSection(
|
|||||||
onCardClick: (DeckCard) -> Unit,
|
onCardClick: (DeckCard) -> Unit,
|
||||||
onToggleDone: (DeckCard) -> Unit,
|
onToggleDone: (DeckCard) -> Unit,
|
||||||
onAddCard: () -> Unit,
|
onAddCard: () -> Unit,
|
||||||
|
onRename: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
var menuOpen by remember { mutableStateOf(false) }
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@@ -240,6 +269,28 @@ private fun DeckStackSection(
|
|||||||
) {
|
) {
|
||||||
Icon(Icons.Filled.Add, contentDescription = "Добавить карточку", tint = F7Colors.Primary, modifier = Modifier.size(18.dp))
|
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 ->
|
stack.cards.forEach { card ->
|
||||||
|
|||||||
@@ -124,6 +124,12 @@ class DeckViewModel(
|
|||||||
mutate(session) { repository.createStack(session, boardId, title.trim()) }
|
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(
|
private fun mutate(
|
||||||
session: AuthSession,
|
session: AuthSession,
|
||||||
closeCardAfter: Boolean = false,
|
closeCardAfter: Boolean = false,
|
||||||
|
|||||||
Reference in New Issue
Block a user