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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.offset
|
import androidx.compose.foundation.layout.offset
|
||||||
@@ -36,8 +37,8 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@@ -51,7 +52,8 @@ import androidx.compose.ui.unit.IntOffset
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.gaudian.translator.R
|
import eu.gaudian.translator.R
|
||||||
import eu.gaudian.translator.view.composable.AppIcons
|
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
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private enum class DragState { Minimized, Extended }
|
private enum class DragState { Minimized, Extended }
|
||||||
@@ -60,6 +62,8 @@ private enum class DragState { Minimized, Extended }
|
|||||||
@Composable
|
@Composable
|
||||||
internal fun DraggableActionPanel(
|
internal fun DraggableActionPanel(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
isOpen: Boolean,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
isEditing: Boolean,
|
isEditing: Boolean,
|
||||||
onEditClick: () -> Unit,
|
onEditClick: () -> Unit,
|
||||||
onSaveClick: () -> Unit,
|
onSaveClick: () -> Unit,
|
||||||
@@ -71,7 +75,6 @@ internal fun DraggableActionPanel(
|
|||||||
showAnalyzeGrammarButton: Boolean,
|
showAnalyzeGrammarButton: Boolean,
|
||||||
onAnalyzeGrammarClick: () -> Unit,
|
onAnalyzeGrammarClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
|
|
||||||
val positionalThreshold = { totalDistance: Float -> totalDistance * 0.5f }
|
val positionalThreshold = { totalDistance: Float -> totalDistance * 0.5f }
|
||||||
@@ -92,14 +95,13 @@ internal fun DraggableActionPanel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
var panelWidth by remember { mutableFloatStateOf(0f) }
|
var panelWidth by remember { mutableFloatStateOf(0f) }
|
||||||
val minimizedWidth = with(density) { 64.dp.toPx() }
|
|
||||||
val isExtended = state.targetValue == DragState.Extended
|
val isExtended = state.targetValue == DragState.Extended
|
||||||
|
|
||||||
LaunchedEffect(panelWidth) {
|
LaunchedEffect(panelWidth) {
|
||||||
if (panelWidth > 0) {
|
if (panelWidth > 0) {
|
||||||
val anchors = DraggableAnchors {
|
val anchors = DraggableAnchors {
|
||||||
DragState.Extended at 0f
|
DragState.Extended at 0f
|
||||||
DragState.Minimized at panelWidth - minimizedWidth
|
DragState.Minimized at panelWidth
|
||||||
}
|
}
|
||||||
if (state.anchors != anchors) {
|
if (state.anchors != anchors) {
|
||||||
state.updateAnchors(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(
|
Card(
|
||||||
shape = RoundedCornerShape(topStart = 20.dp, bottomStart = 20.dp),
|
shape = RoundedCornerShape(topStart = 20.dp, bottomStart = 20.dp),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp),
|
||||||
@@ -148,39 +168,37 @@ internal fun DraggableActionPanel(
|
|||||||
verticalArrangement = Arrangement.Center
|
verticalArrangement = Arrangement.Center
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val actionClickHandler: (() -> Unit) -> () -> Unit = { action ->
|
val actionClickHandler: (() -> Unit) -> () -> Unit = { action ->
|
||||||
{
|
{
|
||||||
action()
|
action()
|
||||||
coroutineScope.launch {
|
onDismiss()
|
||||||
state.animateTo(DragState.Minimized)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
ActionItem(icon = AppIcons.Check, label = stringResource(R.string.label_save), isExtended = isExtended, onClick = actionClickHandler(onSaveClick))
|
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))
|
ActionItem(icon = AppIcons.Close, stringResource(R.string.label_cancel), isExtended = isExtended, onClick = actionClickHandler(onCancelClick))
|
||||||
} else {
|
} else {
|
||||||
ActionItem(icon = AppIcons.Edit, label = stringResource(R.string.edit), isExtended = isExtended, onClick = actionClickHandler(onEditClick))
|
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.Category, label = stringResource(R.string.move_to_category), isExtended = isExtended, onClick = actionClickHandler(onMoveToCategoryClick))
|
if (showAnalyzeGrammarButton) {
|
||||||
ActionItem(icon = AppIcons.Stages, label = stringResource(R.string.move_to_stage), isExtended = isExtended, onClick = actionClickHandler(onMoveToStageClick))
|
ActionItem(
|
||||||
ActionItem(icon = AppIcons.Statistics, label = stringResource(R.string.label_statistics), isExtended = isExtended, onClick = actionClickHandler(onStatisticsClick))
|
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)
|
Icon(icon, contentDescription = label)
|
||||||
}
|
}
|
||||||
if (isExtended) {
|
if (isExtended) {
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
Text(text = label, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -229,6 +248,8 @@ private fun ActionItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun DraggableActionPanelPreview() {
|
fun DraggableActionPanelPreview() {
|
||||||
DraggableActionPanel(
|
DraggableActionPanel(
|
||||||
|
isOpen = true,
|
||||||
|
onDismiss = {},
|
||||||
isEditing = false,
|
isEditing = false,
|
||||||
onEditClick = {},
|
onEditClick = {},
|
||||||
onSaveClick = {},
|
onSaveClick = {},
|
||||||
|
|||||||
@@ -113,9 +113,9 @@ fun VocabularyCard(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
StatusMessageService.triggerNonSuspend(
|
StatusMessageService.triggerNonSuspend(
|
||||||
StatusAction.ShowMessage(
|
StatusAction.ShowMessage(
|
||||||
text = e.toString(),
|
text = e.toString(),
|
||||||
type = MessageDisplayType.ERROR,
|
type = MessageDisplayType.ERROR,
|
||||||
timeoutInSeconds = 3
|
timeoutInSeconds = 3
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
VocabularyFeatures()
|
VocabularyFeatures()
|
||||||
@@ -130,6 +130,7 @@ fun VocabularyCard(
|
|||||||
.collectAsState(initial = null)
|
.collectAsState(initial = null)
|
||||||
|
|
||||||
var isEditing by remember(item.id) { mutableStateOf(false) }
|
var isEditing by remember(item.id) { mutableStateOf(false) }
|
||||||
|
var showActionPanel by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(key1 = item.features, key2 = isEditing) {
|
LaunchedEffect(key1 = item.features, key2 = isEditing) {
|
||||||
if (!isEditing) {
|
if (!isEditing) {
|
||||||
@@ -139,9 +140,9 @@ fun VocabularyCard(
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
StatusMessageService.triggerNonSuspend(
|
StatusMessageService.triggerNonSuspend(
|
||||||
StatusAction.ShowMessage(
|
StatusAction.ShowMessage(
|
||||||
text = e.toString(),
|
text = e.toString(),
|
||||||
type = MessageDisplayType.ERROR,
|
type = MessageDisplayType.ERROR,
|
||||||
timeoutInSeconds = 3
|
timeoutInSeconds = 3
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
VocabularyFeatures()
|
VocabularyFeatures()
|
||||||
@@ -170,7 +171,7 @@ fun VocabularyCard(
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
val entryToNavigate by dictionaryViewModel.navigateToEntry.collectAsState()
|
val entryToNavigate by dictionaryViewModel.navigateToEntry.collectAsState()
|
||||||
LaunchedEffect(key1 = entryToNavigate) {
|
LaunchedEffect(key1 = entryToNavigate) {
|
||||||
entryToNavigate?.let { entry ->
|
entryToNavigate?.let { entry ->
|
||||||
// Set flag indicating navigation is from external source (not DictionaryResultScreen)
|
// Set flag indicating navigation is from external source (not DictionaryResultScreen)
|
||||||
@@ -237,13 +238,11 @@ fun VocabularyCard(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize()
|
modifier = Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
val panelVisible = isFlipped || !exerciseMode
|
|
||||||
val panelMargin = if (panelVisible) 64.dp else 0.dp
|
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.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 },
|
.graphicsLayer { this.rotationY = rotationY },
|
||||||
shape = RoundedCornerShape(24.dp),
|
shape = RoundedCornerShape(24.dp),
|
||||||
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
|
||||||
@@ -294,10 +293,25 @@ fun VocabularyCard(
|
|||||||
showBottomSheet = true
|
showBottomSheet = true
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.padding(horizontal = 4.dp),
|
Row(
|
||||||
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
|
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(
|
CardFace(
|
||||||
modifier = backFaceModifier,
|
modifier = backFaceModifier,
|
||||||
@@ -336,6 +350,8 @@ fun VocabularyCard(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.height((IntrinsicSize.Min)),
|
.height((IntrinsicSize.Min)),
|
||||||
|
isOpen = showActionPanel,
|
||||||
|
onDismiss = { showActionPanel = false },
|
||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
onEditClick = { isEditing = true },
|
onEditClick = { isEditing = true },
|
||||||
onSaveClick = { handleSave() },
|
onSaveClick = { handleSave() },
|
||||||
@@ -580,22 +596,22 @@ private fun CardFace(
|
|||||||
|
|
||||||
val showSpellingCorrection = !isEditing && isRevealed && (userSpellingAnswer != null)
|
val showSpellingCorrection = !isEditing && isRevealed && (userSpellingAnswer != null)
|
||||||
if(!isExerciseMode) {
|
if(!isExerciseMode) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.Top,
|
verticalAlignment = Alignment.Top,
|
||||||
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 0.dp)
|
modifier = Modifier.padding(start = 8.dp, end = 8.dp, top = 8.dp, bottom = 0.dp)
|
||||||
){
|
){
|
||||||
val zipfFrequency = if(isFirst) vocabularyItem?.zipfFrequencyFirst else vocabularyItem?.zipfFrequencySecond
|
val zipfFrequency = if(isFirst) vocabularyItem?.zipfFrequencyFirst else vocabularyItem?.zipfFrequencySecond
|
||||||
|
|
||||||
FrequencyPill(zipfFrequency = zipfFrequency)
|
FrequencyPill(zipfFrequency = zipfFrequency)
|
||||||
|
|
||||||
GrammarPill(
|
GrammarPill(
|
||||||
wordDetails = wordDetails,
|
wordDetails = wordDetails,
|
||||||
language = language,
|
language = language,
|
||||||
isEditing = isEditing,
|
isEditing = isEditing,
|
||||||
isRevealed = isRevealed,
|
isRevealed = isRevealed,
|
||||||
onEditGrammarClick = onEditGrammarClick
|
onEditGrammarClick = onEditGrammarClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(modifier = modifier.fillMaxSize()) {
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
@@ -628,11 +644,11 @@ private fun CardFace(
|
|||||||
if (!isEditing && playable && isRevealed) {
|
if (!isEditing && playable && isRevealed) {
|
||||||
IconButton(onClick = {
|
IconButton(onClick = {
|
||||||
coroutineScope.launch {
|
coroutineScope.launch {
|
||||||
settingsViewModel.speakingSpeed.value
|
settingsViewModel.speakingSpeed.value
|
||||||
val voice = settingsViewModel.getTtsVoiceForLanguage(language!!.code, language.region)
|
val voice = settingsViewModel.getTtsVoiceForLanguage(language!!.code, language.region)
|
||||||
TextToSpeechHelper.speakOut(context, word,
|
TextToSpeechHelper.speakOut(context, word,
|
||||||
language, voice)
|
language, voice)
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Icon(AppIcons.TextToSpeech, stringResource(R.string.cd_text_to_speech))
|
Icon(AppIcons.TextToSpeech, stringResource(R.string.cd_text_to_speech))
|
||||||
}
|
}
|
||||||
@@ -712,7 +728,7 @@ private fun CardFace(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Info icon in bottom left
|
// Info icon in bottom left
|
||||||
if (onMoreClick != null && !isEditing) {
|
if (onMoreClick != null && !isEditing && (!isExerciseMode)) {
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = onMoreClick,
|
onClick = onMoreClick,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
Reference in New Issue
Block a user