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:
@@ -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),
|
||||
@@ -148,39 +168,37 @@ internal fun DraggableActionPanel(
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
|
||||
val actionClickHandler: (() -> Unit) -> () -> Unit = { action ->
|
||||
{
|
||||
action()
|
||||
coroutineScope.launch {
|
||||
state.animateTo(DragState.Minimized)
|
||||
val actionClickHandler: (() -> Unit) -> () -> Unit = { action ->
|
||||
{
|
||||
action()
|
||||
onDismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isEditing) {
|
||||
ActionItem(icon = AppIcons.Check, label = stringResource(R.string.label_save), isExtended = isExtended, onClick = actionClickHandler(onSaveClick))
|
||||
ActionItem(icon = AppIcons.Close, stringResource(R.string.label_cancel), isExtended = isExtended, onClick = actionClickHandler(onCancelClick))
|
||||
} else {
|
||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||
}
|
||||
if (!isEditing) {
|
||||
|
||||
if (showAnalyzeGrammarButton) {
|
||||
ActionItem(
|
||||
icon = AppIcons.AI,
|
||||
label = stringResource(R.string.label_analyze_grammar),
|
||||
isExtended = isExtended,
|
||||
onClick = actionClickHandler(onAnalyzeGrammarClick)
|
||||
)
|
||||
if (isEditing) {
|
||||
ActionItem(icon = AppIcons.Check, label = stringResource(R.string.label_save), isExtended = isExtended, onClick = actionClickHandler(onSaveClick))
|
||||
ActionItem(icon = AppIcons.Close, stringResource(R.string.label_cancel), isExtended = isExtended, onClick = actionClickHandler(onCancelClick))
|
||||
} else {
|
||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
||||
}
|
||||
if (!isEditing) {
|
||||
|
||||
ActionItem(icon = AppIcons.Category, label = stringResource(R.string.move_to_category), isExtended = isExtended, onClick = actionClickHandler(onMoveToCategoryClick))
|
||||
ActionItem(icon = AppIcons.Stages, label = stringResource(R.string.move_to_stage), isExtended = isExtended, onClick = actionClickHandler(onMoveToStageClick))
|
||||
ActionItem(icon = AppIcons.Statistics, label = stringResource(R.string.label_statistics), isExtended = isExtended, onClick = actionClickHandler(onStatisticsClick))
|
||||
if (showAnalyzeGrammarButton) {
|
||||
ActionItem(
|
||||
icon = AppIcons.AI,
|
||||
label = stringResource(R.string.label_analyze_grammar),
|
||||
isExtended = isExtended,
|
||||
onClick = actionClickHandler(onAnalyzeGrammarClick)
|
||||
)
|
||||
}
|
||||
|
||||
ActionItem(icon = AppIcons.Category, label = stringResource(R.string.move_to_category), isExtended = isExtended, onClick = actionClickHandler(onMoveToCategoryClick))
|
||||
ActionItem(icon = AppIcons.Stages, label = stringResource(R.string.move_to_stage), isExtended = isExtended, onClick = actionClickHandler(onMoveToStageClick))
|
||||
ActionItem(icon = AppIcons.Statistics, label = stringResource(R.string.label_statistics), isExtended = isExtended, onClick = actionClickHandler(onStatisticsClick))
|
||||
|
||||
|
||||
ActionItem(icon = AppIcons.Delete, stringResource(R.string.label_delete), isExtended = isExtended, onClick = actionClickHandler(onDeleteClick))
|
||||
}
|
||||
ActionItem(icon = AppIcons.Delete, stringResource(R.string.label_delete), isExtended = isExtended, onClick = actionClickHandler(onDeleteClick))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 = {},
|
||||
|
||||
@@ -113,9 +113,9 @@ fun VocabularyCard(
|
||||
} catch (e: Exception) {
|
||||
StatusMessageService.triggerNonSuspend(
|
||||
StatusAction.ShowMessage(
|
||||
text = e.toString(),
|
||||
type = MessageDisplayType.ERROR,
|
||||
timeoutInSeconds = 3
|
||||
text = e.toString(),
|
||||
type = MessageDisplayType.ERROR,
|
||||
timeoutInSeconds = 3
|
||||
)
|
||||
)
|
||||
VocabularyFeatures()
|
||||
@@ -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) {
|
||||
@@ -139,9 +140,9 @@ fun VocabularyCard(
|
||||
} catch (e: Exception) {
|
||||
StatusMessageService.triggerNonSuspend(
|
||||
StatusAction.ShowMessage(
|
||||
text = e.toString(),
|
||||
type = MessageDisplayType.ERROR,
|
||||
timeoutInSeconds = 3
|
||||
text = e.toString(),
|
||||
type = MessageDisplayType.ERROR,
|
||||
timeoutInSeconds = 3
|
||||
)
|
||||
)
|
||||
VocabularyFeatures()
|
||||
@@ -170,7 +171,7 @@ fun VocabularyCard(
|
||||
|
||||
|
||||
|
||||
val entryToNavigate by dictionaryViewModel.navigateToEntry.collectAsState()
|
||||
val entryToNavigate by dictionaryViewModel.navigateToEntry.collectAsState()
|
||||
LaunchedEffect(key1 = entryToNavigate) {
|
||||
entryToNavigate?.let { entry ->
|
||||
// Set flag indicating navigation is from external source (not DictionaryResultScreen)
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
HorizontalDivider(
|
||||
modifier = Modifier.padding(horizontal = 4.dp),
|
||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
||||
)
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 4.dp)
|
||||
) {
|
||||
HorizontalDivider(
|
||||
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() },
|
||||
@@ -580,22 +596,22 @@ private fun CardFace(
|
||||
|
||||
val showSpellingCorrection = !isEditing && isRevealed && (userSpellingAnswer != null)
|
||||
if(!isExerciseMode) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 0.dp)
|
||||
){
|
||||
val zipfFrequency = if(isFirst) vocabularyItem?.zipfFrequencyFirst else vocabularyItem?.zipfFrequencySecond
|
||||
Row(
|
||||
verticalAlignment = Alignment.Top,
|
||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 0.dp)
|
||||
){
|
||||
val zipfFrequency = if(isFirst) vocabularyItem?.zipfFrequencyFirst else vocabularyItem?.zipfFrequencySecond
|
||||
|
||||
FrequencyPill(zipfFrequency = zipfFrequency)
|
||||
FrequencyPill(zipfFrequency = zipfFrequency)
|
||||
|
||||
GrammarPill(
|
||||
wordDetails = wordDetails,
|
||||
language = language,
|
||||
isEditing = isEditing,
|
||||
isRevealed = isRevealed,
|
||||
onEditGrammarClick = onEditGrammarClick
|
||||
)
|
||||
}
|
||||
GrammarPill(
|
||||
wordDetails = wordDetails,
|
||||
language = language,
|
||||
isEditing = isEditing,
|
||||
isRevealed = isRevealed,
|
||||
onEditGrammarClick = onEditGrammarClick
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
@@ -628,11 +644,11 @@ private fun CardFace(
|
||||
if (!isEditing && playable && isRevealed) {
|
||||
IconButton(onClick = {
|
||||
coroutineScope.launch {
|
||||
settingsViewModel.speakingSpeed.value
|
||||
val voice = settingsViewModel.getTtsVoiceForLanguage(language!!.code, language.region)
|
||||
TextToSpeechHelper.speakOut(context, word,
|
||||
language, voice)
|
||||
}
|
||||
settingsViewModel.speakingSpeed.value
|
||||
val voice = settingsViewModel.getTtsVoiceForLanguage(language!!.code, language.region)
|
||||
TextToSpeechHelper.speakOut(context, word,
|
||||
language, voice)
|
||||
}
|
||||
}) {
|
||||
Icon(AppIcons.TextToSpeech, stringResource(R.string.cd_text_to_speech))
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user