refactor VocabularyMenu and FAB components to support dynamic text visibility based on scroll state and update Zipf frequency UI in VocabularyCard

This commit is contained in:
jonasgaudian
2026-02-14 02:01:00 +01:00
parent b65e16000c
commit b95a2de747
5 changed files with 75 additions and 26 deletions

View File

@@ -1,6 +1,7 @@
package eu.gaudian.translator.view.composable
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
@@ -55,7 +56,8 @@ data class FabMenuItem(
fun AppFabMenu(
items: List<FabMenuItem>,
modifier: Modifier = Modifier,
title: String? = null
title: String? = null,
showFabText: Boolean = true
) {
var isMenuExpanded by remember { mutableStateOf(false) }
@@ -111,14 +113,16 @@ fun AppFabMenu(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) {
Icon(
imageVector = AppIcons.Add,
contentDescription = stringResource(R.string.cd_toggle_menu),
modifier = Modifier.rotate(iconRotationAngle)
)
if (title != null) {
if (title != null && showFabText) {
Text(
text = title,
style = MaterialTheme.typography.labelLarge

View File

@@ -22,6 +22,7 @@ import eu.gaudian.translator.viewmodel.VocabularyViewModel
@Composable
fun VocabularyMenu(
modifier: Modifier = Modifier,
showFabText : Boolean = true
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -48,7 +49,7 @@ fun VocabularyMenu(
)
)
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary))
AppFabMenu(items = menuItems, modifier = modifier, title = stringResource(R.string.label_add_vocabulary), showFabText = showFabText)
if (showAddVocabularyDialog) {
AddVocabularyDialog(

View File

@@ -88,6 +88,7 @@ fun DashboardContent(
onNavigateToCategoryDetail: (Int) -> Unit,
onNavigateToCategoryList: () -> Unit,
onShowWordPairExerciseDialog: () -> Unit,
onScroll: (Boolean) -> Unit = {},
) {
val activity = LocalContext.current.findActivity()
val languageViewModel: LanguageViewModel = hiltViewModel(viewModelStoreOwner = activity)
@@ -167,6 +168,11 @@ fun DashboardContent(
lazyListState.firstVisibleItemScrollOffset
)
}
// Detect scroll and notify parent
LaunchedEffect(lazyListState.isScrollInProgress) {
onScroll(lazyListState.isScrollInProgress)
}
DisposableEffect(Unit) {
onDispose {
settingsViewModel.saveDashboardScrollState(

View File

@@ -4,6 +4,7 @@ package eu.gaudian.translator.view.vocabulary
import androidx.activity.ComponentActivity
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -112,6 +113,9 @@ fun MainVocabularyScreen(
var wpTrainingMode by remember { mutableStateOf(false) }
var wpDueTodayOnly by remember { mutableStateOf(false) }
var isScrolling by remember { mutableStateOf(false) }
if (showCustomExerciseDialog) {
StartExerciseDialog(
onDismiss = { showCustomExerciseDialog = false },
@@ -293,6 +297,8 @@ fun MainVocabularyScreen(
VocabularyTab.entries.find { it.route == currentRoute } ?: VocabularyTab.Dashboard
}
val showFabText = selectedTab == VocabularyTab.Dashboard && !isScrolling
val repoEmpty =
vocabularyViewModel.vocabularyItems.collectAsState(initial = emptyList()).value.isEmpty()
@@ -333,10 +339,11 @@ fun MainVocabularyScreen(
onNavigateToCategoryList = {
navController.navigate("category_list_screen")
},
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true }
)
}
composable(VocabularyTab.Statistics.route) {
onShowWordPairExerciseDialog = { showWordPairExerciseDialog = true },
onScroll = { isScrolling = it }
)
}
composable(VocabularyTab.Statistics.route) {
StatisticsContent(navController = navController)
}
composable("category_detail/{categoryId}") { backStackEntry ->
@@ -390,7 +397,7 @@ fun MainVocabularyScreen(
.padding(16.dp),
horizontalAlignment = Alignment.End
) {
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height })
VocabularyMenu(modifier = Modifier.onSizeChanged { menuHeightPx = it.height }, showFabText = showFabText)
}
// Place the FAB separately and animate its bottom padding based on the menu height
@@ -405,16 +412,19 @@ fun MainVocabularyScreen(
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 16.dp)
modifier = Modifier
.padding(horizontal = 16.dp)
.animateContentSize()
) {
Icon(
imageVector = AppIcons.Quiz,
contentDescription = null
)
if(showFabText) {
Text(
text = stringResource(R.string.label_start_exercise),
style = MaterialTheme.typography.labelLarge
)
)}
}
}
}

View File

@@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Card
@@ -25,6 +26,7 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.OutlinedCard
@@ -43,7 +45,9 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@@ -454,26 +458,50 @@ private fun FrequencyPill(zipfFrequency: Float?) {
if (zipfFrequency == null) return
val semanticColors = MaterialTheme.semanticColors
// Determine the label and color based on the Zipf value
val (label, color) = when {
zipfFrequency <= 3f -> Pair(stringResource(R.string.text_rare), semanticColors.wrongContainer)
zipfFrequency <= 4f -> Pair(stringResource(R.string.text_infrequent), semanticColors.stageGradient2)
zipfFrequency <= 4.5f -> Pair(stringResource(R.string.text_uncommon), semanticColors.stageGradient3)
zipfFrequency <= 2.0f -> Pair(stringResource(R.string.text_rare), semanticColors.wrongContainer)
zipfFrequency <= 3.0f -> Pair(stringResource(R.string.text_infrequent), semanticColors.stageGradient2)
zipfFrequency <= 4.0f -> Pair(stringResource(R.string.text_uncommon), semanticColors.stageGradient3)
zipfFrequency <= 5.5f -> Pair(stringResource(R.string.text_common), semanticColors.stageGradient4)
zipfFrequency <= 6.5f -> Pair(stringResource(R.string.text_frequent), semanticColors.stageGradient5)
zipfFrequency <= 7.5f -> Pair(stringResource(R.string.text_very_frequent), semanticColors.stageGradient6)
zipfFrequency <= 6.0f -> Pair(stringResource(R.string.text_frequent), semanticColors.stageGradient5)
else -> Pair(stringResource(R.string.text_very_frequent), semanticColors.successContainer)
}
Surface(
shape = RoundedCornerShape(16.dp),
color = color.copy(alpha = 0.5f),
modifier = Modifier.padding(horizontal = 4.dp)
// Normalize progress: Zipf 1.0 to 8.0 mapped to 0f to 1f
val progress = ((zipfFrequency - 1f) / (8f - 1f)).coerceIn(0f, 1f)
Column(
modifier = Modifier
.padding(horizontal = 4.dp)
.width(80.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
Surface(
shape = RoundedCornerShape(16.dp),
color = color.copy(alpha = 0.2f),
) {
Text(
text = label,
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp)
)
}
Spacer(modifier = Modifier.height(4.dp))
// The "Slider" representation
LinearProgressIndicator(
progress = { progress },
modifier = Modifier
.fillMaxWidth()
.height(4.dp)
.clip(CircleShape),
color = color,
trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f),
strokeCap = StrokeCap.Round
)
}
}