implement manual visibility control for DraggableActionPanel via isOpen and onDismiss props, and add a "more" options button to VocabularyCard to trigger the panel.

This commit is contained in:
jonasgaudian
2026-02-13 17:41:49 +01:00
parent 4a014e6206
commit 99d379071b
2 changed files with 104 additions and 67 deletions

View File

@@ -14,6 +14,7 @@ import androidx.compose.foundation.gestures.animateTo
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
@@ -36,8 +37,8 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -51,7 +52,8 @@ import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import eu.gaudian.translator.R
import eu.gaudian.translator.view.composable.AppIcons
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlin.math.roundToInt
private enum class DragState { Minimized, Extended }
@@ -60,6 +62,8 @@ private enum class DragState { Minimized, Extended }
@Composable
internal fun DraggableActionPanel(
modifier: Modifier = Modifier,
isOpen: Boolean,
onDismiss: () -> Unit,
isEditing: Boolean,
onEditClick: () -> Unit,
onSaveClick: () -> Unit,
@@ -71,7 +75,6 @@ internal fun DraggableActionPanel(
showAnalyzeGrammarButton: Boolean,
onAnalyzeGrammarClick: () -> Unit,
) {
val coroutineScope = rememberCoroutineScope()
val density = LocalDensity.current
val positionalThreshold = { totalDistance: Float -> totalDistance * 0.5f }
@@ -92,14 +95,13 @@ internal fun DraggableActionPanel(
}
var panelWidth by remember { mutableFloatStateOf(0f) }
val minimizedWidth = with(density) { 64.dp.toPx() }
val isExtended = state.targetValue == DragState.Extended
LaunchedEffect(panelWidth) {
if (panelWidth > 0) {
val anchors = DraggableAnchors {
DragState.Extended at 0f
DragState.Minimized at panelWidth - minimizedWidth
DragState.Minimized at panelWidth
}
if (state.anchors != anchors) {
state.updateAnchors(anchors)
@@ -107,6 +109,24 @@ internal fun DraggableActionPanel(
}
}
LaunchedEffect(isOpen) {
if (isOpen) {
state.animateTo(DragState.Extended)
} else {
state.animateTo(DragState.Minimized)
}
}
LaunchedEffect(state) {
snapshotFlow { state.currentValue }
.distinctUntilChanged()
.collectLatest {
if (it == DragState.Minimized) {
onDismiss()
}
}
}
Card(
shape = RoundedCornerShape(topStart = 20.dp, bottomStart = 20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
@@ -151,9 +171,7 @@ internal fun DraggableActionPanel(
val actionClickHandler: (() -> Unit) -> () -> Unit = { action ->
{
action()
coroutineScope.launch {
state.animateTo(DragState.Minimized)
}
onDismiss()
}
}
@@ -220,6 +238,7 @@ private fun ActionItem(
Icon(icon, contentDescription = label)
}
if (isExtended) {
Spacer(modifier = Modifier.width(8.dp))
Text(text = label, style = MaterialTheme.typography.bodyMedium)
}
}
@@ -229,6 +248,8 @@ private fun ActionItem(
@Composable
fun DraggableActionPanelPreview() {
DraggableActionPanel(
isOpen = true,
onDismiss = {},
isEditing = false,
onEditClick = {},
onSaveClick = {},

View File

@@ -130,6 +130,7 @@ fun VocabularyCard(
.collectAsState(initial = null)
var isEditing by remember(item.id) { mutableStateOf(false) }
var showActionPanel by remember { mutableStateOf(false) }
LaunchedEffect(key1 = item.features, key2 = isEditing) {
if (!isEditing) {
@@ -237,13 +238,11 @@ fun VocabularyCard(
Box(
modifier = Modifier.fillMaxSize()
) {
val panelVisible = isFlipped || !exerciseMode
val panelMargin = if (panelVisible) 64.dp else 0.dp
Card(
modifier = Modifier
.fillMaxSize()
.padding(start = 0.dp, top = 0.dp, bottom = 0.dp, end = panelMargin)
.padding(start = 0.dp, top = 0.dp, bottom = 0.dp, end = 0.dp)
.graphicsLayer { this.rotationY = rotationY },
shape = RoundedCornerShape(24.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
@@ -294,10 +293,25 @@ fun VocabularyCard(
showBottomSheet = true
}
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 4.dp)
) {
HorizontalDivider(
modifier = Modifier.padding(horizontal = 4.dp),
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
)
if (!exerciseMode && !isFlipped) {
IconButton(onClick = { showActionPanel = true }) {
Icon(
imageVector = AppIcons.MoreVert,
contentDescription = stringResource(R.string.more),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
CardFace(
modifier = backFaceModifier,
@@ -336,6 +350,8 @@ fun VocabularyCard(
modifier = Modifier
.align(Alignment.CenterEnd)
.height((IntrinsicSize.Min)),
isOpen = showActionPanel,
onDismiss = { showActionPanel = false },
isEditing = isEditing,
onEditClick = { isEditing = true },
onSaveClick = { handleSave() },
@@ -712,7 +728,7 @@ private fun CardFace(
}
// Info icon in bottom left
if (onMoreClick != null && !isEditing) {
if (onMoreClick != null && !isEditing && (!isExerciseMode)) {
IconButton(
onClick = onMoreClick,
modifier = Modifier