diff --git a/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt index 7205a5a..462c0e4 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/DailyReviewScreen.kt @@ -101,7 +101,7 @@ fun DailyReviewScreen( state = listState, modifier = Modifier .fillMaxSize(), - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), contentPadding = PaddingValues(16.dp) ) { items( diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt index fc69400..c1c9e56 100644 --- a/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryComponents.kt @@ -33,11 +33,13 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.AddCircleOutline import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.LocalMall -import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Tune +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenu import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon @@ -58,6 +60,7 @@ import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.CornerRadius import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.PathEffect +import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource @@ -70,6 +73,8 @@ import eu.gaudian.translator.R import eu.gaudian.translator.model.Language import eu.gaudian.translator.model.VocabularyCategory import eu.gaudian.translator.model.VocabularyItem +import eu.gaudian.translator.model.VocabularyStage +import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppIcons import eu.gaudian.translator.view.composable.insertBreakOpportunities @@ -127,22 +132,29 @@ fun SelectionTopBar( Row( modifier = modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Row(verticalAlignment = Alignment.CenterVertically) { - IconButton(onClick = onCloseClick) { - Icon(imageVector = AppIcons.Close, contentDescription = stringResource(R.string.label_close_selection_mode)) - } - Spacer(modifier = Modifier.width(8.dp)) - Text( - text = stringResource(R.string.d_selected, selectionCount), - style = MaterialTheme.typography.titleLarge, - fontWeight = FontWeight.Bold + // 1. Close Button + IconButton(onClick = onCloseClick) { + Icon( + imageVector = AppIcons.Close, + contentDescription = stringResource(R.string.label_close_selection_mode) ) } - Row { + + // 2. Title Text (Gets weight to prevent pushing icons off-screen) + Text( + text = stringResource(R.string.d_selected, selectionCount), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.Bold, + modifier = Modifier.weight(1f), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + + // 3. Action Icons Group + Row(verticalAlignment = Alignment.CenterVertically) { IconButton(onClick = onSelectAllClick) { Icon( imageVector = AppIcons.SelectAll, @@ -320,6 +332,7 @@ fun AllCardsView( vocabularyItems: List, allLanguages: List, selection: Set, + stageMapping: Map = emptyMap(), onItemClick: (VocabularyItem) -> Unit, onItemLongClick: (VocabularyItem) -> Unit, onDeleteClick: (VocabularyItem) -> Unit, @@ -350,7 +363,7 @@ fun AllCardsView( } else { LazyColumn( state = listState, - verticalArrangement = Arrangement.spacedBy(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), modifier = modifier.fillMaxSize(), contentPadding = PaddingValues(bottom = 100.dp) ) { @@ -359,10 +372,12 @@ fun AllCardsView( key = { it.id } ) { item -> val isSelected = selection.contains(item.id.toLong()) + val stage = stageMapping[item.id] ?: VocabularyStage.NEW VocabularyCard( item = item, allLanguages = allLanguages, isSelected = isSelected, + stage = stage, onItemClick = { onItemClick(item) }, onItemLongClick = { onItemLongClick(item) }, onDeleteClick = { onDeleteClick(item) } @@ -372,14 +387,12 @@ fun AllCardsView( } } -/** - * Individual vocabulary card component - */ @Composable fun VocabularyCard( item: VocabularyItem, allLanguages: List, isSelected: Boolean, + stage: VocabularyStage = VocabularyStage.NEW, onItemClick: () -> Unit, onItemLongClick: () -> Unit, onDeleteClick: () -> Unit, @@ -392,14 +405,15 @@ fun VocabularyCard( Card( modifier = modifier .fillMaxWidth() - .clip(RoundedCornerShape(8.dp)) + .clip(RoundedCornerShape(12.dp)) // Slightly rounder for a modern look .combinedClickable( onClick = onItemClick, onLongClick = onItemLongClick ), colors = CardDefaults.cardColors( - containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceContainer, - contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.surfaceContainer + containerColor = if (isSelected) MaterialTheme.colorScheme.primaryContainer else MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f), + // Fixed the contentColor bug here: + contentColor = if (isSelected) MaterialTheme.colorScheme.onPrimaryContainer else MaterialTheme.colorScheme.onSurface ), border = if (isSelected) BorderStroke(2.dp, MaterialTheme.colorScheme.primary) else null ) { @@ -410,50 +424,46 @@ fun VocabularyCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - Column(modifier = Modifier.weight(1f)) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 12.dp) // Ensures text doesn't bleed into the trailing icon + ) { // Top row: First word + Language Pill Row(verticalAlignment = Alignment.CenterVertically) { Text( text = insertBreakOpportunities(item.wordFirst), style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface + color = MaterialTheme.colorScheme.onSurface, + // This modifier allows the text to wrap without squishing the pill + modifier = Modifier.weight(1f, fill = false) ) Spacer(modifier = Modifier.width(8.dp)) - Surface( - color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), - shape = RoundedCornerShape(4.dp) - ) { - Text( - text = langFirst, - style = MaterialTheme.typography.labelSmall, - modifier = Modifier.padding(horizontal = 6.dp, vertical = 2.dp), - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + LanguagePill( + text = langFirst, + backgroundColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f), + textColor = MaterialTheme.colorScheme.onSurfaceVariant + ) } - Spacer(modifier = Modifier.height(4.dp)) + Spacer(modifier = Modifier.height(6.dp)) // Slightly more breathing room // Bottom row: Second word + Language Pill Row(verticalAlignment = Alignment.CenterVertically) { Text( text = insertBreakOpportunities(item.wordSecond), style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant + color = MaterialTheme.colorScheme.onSurfaceVariant, + // Applied to the second text as well for consistency + modifier = Modifier.weight(1f, fill = false) ) Spacer(modifier = Modifier.width(8.dp)) - Surface( - color = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f), - shape = RoundedCornerShape(8.dp) - ) { - Text( - text = langSecond, - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.primary, - modifier = Modifier.padding(horizontal = 8.dp, vertical = 2.dp) - ) - } + LanguagePill( + text = langSecond, + backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.15f), + textColor = MaterialTheme.colorScheme.primary + ) } } @@ -464,18 +474,124 @@ fun VocabularyCard( tint = MaterialTheme.colorScheme.primary ) } else { - IconButton(onClick = { /* Options menu could go here */ }) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.cd_options), - tint = MaterialTheme.colorScheme.onSurfaceVariant - ) - } + // Stage indicator showing the vocabulary item's learning stage + StageIndicator(stage = stage) } } } } +@Composable +fun StageIndicator( + stage: VocabularyStage, + modifier: Modifier = Modifier +) { + // Convert VocabularyStage to a step number (0-6) + val step = when (stage) { + VocabularyStage.NEW -> 0 + VocabularyStage.STAGE_1 -> 1 + VocabularyStage.STAGE_2 -> 2 + VocabularyStage.STAGE_3 -> 3 + VocabularyStage.STAGE_4 -> 4 + VocabularyStage.STAGE_5 -> 5 + VocabularyStage.LEARNED -> 6 + } + + // 1. Calculate how full the ring should be (0.0 to 1.0) + val maxSteps = 6f + val progress = step / maxSteps + + // 2. Determine the ring color based on the stage + val indicatorColor = when (stage) { + VocabularyStage.NEW -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) + VocabularyStage.STAGE_1, VocabularyStage.STAGE_2 -> Color(0xFFE57373) // Soft Red + VocabularyStage.STAGE_3 -> Color(0xFFFFB74D) // Soft Orange + VocabularyStage.STAGE_4 -> Color(0xFFFFD54F) // Soft Yellow + VocabularyStage.STAGE_5 -> Color(0xFFAED581) // Light Green + VocabularyStage.LEARNED -> Color(0xFF81C784) // Solid Green + } + + Box( + contentAlignment = Alignment.Center, + modifier = modifier.size(36.dp) // Keeps it neatly sized within the row + ) { + // The background track (empty ring) + CircularProgressIndicator( + progress = { 1f }, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f), + strokeWidth = 3.dp, + modifier = Modifier.fillMaxSize() + ) + + // The colored progress ring + if (stage != VocabularyStage.NEW) { + CircularProgressIndicator( + progress = { progress }, + color = indicatorColor, + strokeWidth = 3.dp, + strokeCap = StrokeCap.Round, // Gives the progress bar nice rounded ends + modifier = Modifier.fillMaxSize() + ) + } + + // The center content (Number or Icon) + when (stage) { + VocabularyStage.NEW -> { + // An empty dot or small icon to denote it's untouched + Icon( + imageVector = Icons.Rounded.Star, // Or any generic 'new' icon + contentDescription = "New Word", + tint = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), + modifier = Modifier.size(16.dp) + ) + } + VocabularyStage.LEARNED -> { + // A checkmark for mastery + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = "Learned", + tint = indicatorColor, + modifier = Modifier.size(20.dp) + ) + } + else -> { + // Display the actual level number (1 through 5) + Text( + text = step.toString(), + style = MaterialTheme.typography.labelMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface + ) + } + } + } +} +// Extracted for consistency and cleaner code +@Composable +private fun LanguagePill( + text: String, + backgroundColor: Color, + textColor: Color +) { + if (text.isNotEmpty()) { + Surface( + color = backgroundColor, + shape = RoundedCornerShape(6.dp) // Consistent corner rounding for all pills + ) { + Text( + text = text, + style = MaterialTheme.typography.labelSmall, + color = textColor, + // Guaranteed to never wrap awkwardly + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp) + ) + } + } +} + /** * Grid view of categories */ @@ -515,13 +631,11 @@ fun CategoryCard( onClick: () -> Unit, modifier: Modifier = Modifier ) { - Card( + AppCard( modifier = modifier .fillMaxWidth() .height(140.dp) .clickable(onClick = onClick), - shape = RoundedCornerShape(16.dp), - colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceVariant) ) { Column( modifier = Modifier @@ -709,6 +823,7 @@ fun VocabularyCardPreview() { ), allLanguages = emptyList(), isSelected = false, + stage = VocabularyStage.NEW, onItemClick = {}, onItemLongClick = {}, onDeleteClick = {} @@ -716,6 +831,154 @@ fun VocabularyCardPreview() { } } +@Suppress("HardCodedStringLiteral") +@Preview(showBackground = true) +@Composable +fun VocabularyCardStage1Preview() { + MaterialTheme { + VocabularyCard( + item = VocabularyItem( + id = 2, + wordFirst = "Goodbye", + wordSecond = "Adiós", + languageFirstId = 1, + languageSecondId = 2, + createdAt = null, + features = null, + zipfFrequencyFirst = null, + zipfFrequencySecond = null + ), + allLanguages = emptyList(), + isSelected = false, + stage = VocabularyStage.STAGE_1, + onItemClick = {}, + onItemLongClick = {}, + onDeleteClick = {} + ) + } +} + +@Suppress("HardCodedStringLiteral") +@Preview(showBackground = true) +@Composable +fun VocabularyCardStage3Preview() { + MaterialTheme { + VocabularyCard( + item = VocabularyItem( + id = 3, + wordFirst = "Thank you", + wordSecond = "Gracias", + languageFirstId = 1, + languageSecondId = 2, + createdAt = null, + features = null, + zipfFrequencyFirst = null, + zipfFrequencySecond = null + ), + allLanguages = emptyList(), + isSelected = false, + stage = VocabularyStage.STAGE_3, + onItemClick = {}, + onItemLongClick = {}, + onDeleteClick = {} + ) + } +} + +@Suppress("HardCodedStringLiteral") +@Preview(showBackground = true) +@Composable +fun VocabularyCardStage5Preview() { + MaterialTheme { + VocabularyCard( + item = VocabularyItem( + id = 4, + wordFirst = "Please", + wordSecond = "Por favor", + languageFirstId = 1, + languageSecondId = 2, + createdAt = null, + features = null, + zipfFrequencyFirst = null, + zipfFrequencySecond = null + ), + allLanguages = emptyList(), + isSelected = false, + stage = VocabularyStage.STAGE_5, + onItemClick = {}, + onItemLongClick = {}, + onDeleteClick = {} + ) + } +} + +@Suppress("HardCodedStringLiteral") +@Preview(showBackground = true) +@Composable +fun VocabularyCardLearnedPreview() { + MaterialTheme { + VocabularyCard( + item = VocabularyItem( + id = 5, + wordFirst = "Yes", + wordSecond = "Sí", + languageFirstId = 1, + languageSecondId = 2, + createdAt = null, + features = null, + zipfFrequencyFirst = null, + zipfFrequencySecond = null + ), + allLanguages = emptyList(), + isSelected = false, + stage = VocabularyStage.LEARNED, + onItemClick = {}, + onItemLongClick = {}, + onDeleteClick = {} + ) + } +} + +@Preview(showBackground = true) +@Composable +fun StageIndicatorNewPreview() { + MaterialTheme { + StageIndicator(stage = VocabularyStage.NEW) + } +} + +@Preview(showBackground = true) +@Composable +fun StageIndicatorStage1Preview() { + MaterialTheme { + StageIndicator(stage = VocabularyStage.STAGE_1) + } +} + +@Preview(showBackground = true) +@Composable +fun StageIndicatorStage3Preview() { + MaterialTheme { + StageIndicator(stage = VocabularyStage.STAGE_3) + } +} + +@Preview(showBackground = true) +@Composable +fun StageIndicatorStage5Preview() { + MaterialTheme { + StageIndicator(stage = VocabularyStage.STAGE_5) + } +} + +@Preview(showBackground = true) +@Composable +fun StageIndicatorLearnedPreview() { + MaterialTheme { + StageIndicator(stage = VocabularyStage.LEARNED) + } +} + @Suppress("HardCodedStringLiteral") @Preview(showBackground = true) @Composable diff --git a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt index ccb1ef4..8aaf560 100644 --- a/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/library/LibraryScreen.kt @@ -139,6 +139,7 @@ fun LibraryScreen( } val vocabularyItems by vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()) + val stageMapping by vocabularyViewModel.stageMapping.collectAsStateWithLifecycle(initialValue = emptyMap()) // Handle export state LaunchedEffect(exportState) { @@ -263,6 +264,7 @@ fun LibraryScreen( vocabularyItems = vocabularyItems, allLanguages = allLanguages, selection = selection, + stageMapping = stageMapping, listState = lazyListState, onItemClick = { item -> if (isInSelectionMode) { diff --git a/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt index 33e47ef..de6a144 100644 --- a/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt +++ b/app/src/main/java/eu/gaudian/translator/view/stats/widgets/WeeklyActivityChartWidget.kt @@ -82,21 +82,21 @@ fun WeeklyActivityChartWidget( ) } } else { - Column( - modifier = Modifier - .fillMaxWidth() - // Reduced horizontal padding to give the chart more space - .padding(vertical = 24.dp, horizontal = 12.dp) - ) { - WeeklyChartLegend() - Spacer(modifier = Modifier.height(24.dp)) + Column( + modifier = Modifier + .fillMaxWidth() + // Reduced horizontal padding to give the chart more space + .padding(vertical = 24.dp, horizontal = 12.dp) + ) { + WeeklyChartLegend() + Spacer(modifier = Modifier.height(24.dp)) - InteractiveLineChart(weeklyStats = weeklyStats) + InteractiveLineChart(weeklyStats = weeklyStats) - Spacer(modifier = Modifier.height(24.dp)) - ChartFooter(weeklyStats = weeklyStats) - } + Spacer(modifier = Modifier.height(24.dp)) + ChartFooter(weeklyStats = weeklyStats) } + } } @Composable @@ -195,15 +195,15 @@ private fun InteractiveLineChart(weeklyStats: List) { Offset(i * xSpacing, height - ((stat.answeredRight * animationProgress) / yMax) * height) } - // Path 1: Correct (Bottom, Dashed) + // Define Paths val pathCorrect = Path().apply { smoothCurve(pointsCorrect) } - drawPath( - path = pathCorrect, - color = colorCorrect, - style = Stroke(width = 6f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f)) - ) + val fillPathCorrect = Path().apply { + smoothCurve(pointsCorrect) + lineTo(width, height) + lineTo(0f, height) + close() + } - // Path 2: Completed (Top, Solid with Fill) val pathCompleted = Path().apply { smoothCurve(pointsCompleted) } val fillPathCompleted = Path().apply { smoothCurve(pointsCompleted) @@ -212,14 +212,32 @@ private fun InteractiveLineChart(weeklyStats: List) { close() } + // Draw semi-transparent fills first drawPath( path = fillPathCompleted, brush = Brush.verticalGradient( - colors = listOf(colorCompleted.copy(alpha = 0.3f), Color.Transparent), + colors = listOf(colorCompleted.copy(alpha = 0.25f), Color.Transparent), startY = 0f, endY = height ) ) + + drawPath( + path = fillPathCorrect, + brush = Brush.verticalGradient( + colors = listOf(colorCorrect.copy(alpha = 0.25f), Color.Transparent), + startY = 0f, + endY = height + ) + ) + + // Draw solid strokes on top of the fills + drawPath( + path = pathCorrect, + color = colorCorrect, + style = Stroke(width = 6f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f)) + ) + drawPath( path = pathCompleted, color = colorCompleted, diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExplorePacksScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExplorePacksScreen.kt index d052d52..d46ccf2 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExplorePacksScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/ExplorePacksScreen.kt @@ -669,10 +669,20 @@ private fun PackCard( onGetClick: () -> Unit, onAddToLibraryClick: () -> Unit, modifier: Modifier = Modifier, + languageViewModel: LanguageViewModel = hiltViewModel(), ) { val info = packState.info val gradient = gradientForId(info.id) + // Get language names from language IDs + val languageIds = info.languageIds + val firstLanguageName by languageViewModel.getLanguageByIdFlow(languageIds.getOrNull(0)).collectAsState(initial = null) + val secondLanguageName by languageViewModel.getLanguageByIdFlow(languageIds.getOrNull(1)).collectAsState(initial = null) + + val languageDisplayText = listOfNotNull(firstLanguageName?.name, secondLanguageName?.name) + .joinToString(" ⇆ ") + .ifEmpty { info.category } + Surface( modifier = modifier .fillMaxWidth() @@ -785,7 +795,7 @@ private fun PackCard( ) Spacer(modifier = Modifier.height(2.dp)) Text( - text = info.category, + text = languageDisplayText, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1 diff --git a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt index 9c6843b..d2d18c7 100644 --- a/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/vocabulary/VocabularyListScreen.kt @@ -164,6 +164,7 @@ fun AllCardsListScreen( val vocabularyItems: List = itemsToShow.ifEmpty { vocabularyItemsFlow.collectAsStateWithLifecycle(initialValue = emptyList()).value } + val stageMapping by vocabularyViewModel.stageMapping.collectAsStateWithLifecycle(initialValue = emptyMap()) // Handle export state LaunchedEffect(exportState) { @@ -298,6 +299,7 @@ fun AllCardsListScreen( vocabularyItems = vocabularyItems, allLanguages = allLanguages, selection = selection, + stageMapping = stageMapping, listState = lazyListState, modifier = Modifier .fillMaxSize()