From b95a2de7472f8d8bcd2b19f048803aa02d346dd4 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Sat, 14 Feb 2026 02:01:00 +0100 Subject: [PATCH] refactor `VocabularyMenu` and FAB components to support dynamic text visibility based on scroll state and update `Zipf` frequency UI in `VocabularyCard` --- .../translator/view/composable/AppFabMenu.kt | 10 +++- .../translator/view/dialogs/VocabularyMenu.kt | 3 +- .../view/vocabulary/DashboardContent.kt | 8 ++- .../view/vocabulary/MainVocabularyScreen.kt | 24 +++++--- .../view/vocabulary/card/VocabularyCard.kt | 56 ++++++++++++++----- 5 files changed, 75 insertions(+), 26 deletions(-) diff --git a/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt b/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt index e6c8812..fd5575d 100644 --- a/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt +++ b/app/src/main/java/eu/gaudian/translator/view/composable/AppFabMenu.kt @@ -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, 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 diff --git a/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt b/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt index d9013f1..99743d4 100644 --- a/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt +++ b/app/src/main/java/eu/gaudian/translator/view/dialogs/VocabularyMenu.kt @@ -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( diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt index ce92ae0..b725b86 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/DashboardContent.kt @@ -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( @@ -674,4 +680,4 @@ fun WidgetContainerPreview() { Text("Preview Content") } } -} +} \ No newline at end of file diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt index 7943004..58a967a 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/MainVocabularyScreen.kt @@ -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 - ) + )} } } } diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt index 094ca84..a3fbdce 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/card/VocabularyCard.kt @@ -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 ) } }