Refactor the WeeklyActivityChartWidget into an interactive smooth line chart and update vocabulary import labels.

This commit is contained in:
jonasgaudian
2026-02-19 16:16:24 +01:00
parent 15d03ef57f
commit 863920143d
10 changed files with 374 additions and 126 deletions

View File

@@ -59,7 +59,7 @@ data class VocabularyItem(
}
fun hasFeatures(): Boolean {
return !features.isNullOrBlank()
return !features.isNullOrBlank() && features != "{}"
}
}

View File

@@ -267,7 +267,8 @@ fun TranslatorApp(
"new_word",
"new_word_review",
"vocabulary_detail/{itemId}",
"daily_review"
"daily_review",
"explore_packs"
) || currentRoute?.startsWith("start_exercise") == true
|| currentRoute?.startsWith("vocabulary_exercise") == true
val showBottomNavLabels by settingsViewModel.showBottomNavLabels.collectAsStateWithLifecycle(initialValue = false)

View File

@@ -5,8 +5,10 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -397,12 +399,16 @@ fun BottomStatsSection(
val learnedWords by viewModel.totalWordsCompleted.collectAsState()
Row(
modifier = Modifier.fillMaxWidth(),
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Min),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
// Total Words
AppCard(
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
onClick = { navController.navigate(Screen.Library.route) }
) {
Column(modifier = Modifier.padding(20.dp)) {
@@ -415,7 +421,9 @@ fun BottomStatsSection(
// Learned
AppCard(
modifier = Modifier.weight(1f),
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
onClick = { navController.navigate(NavigationRoutes.STATS_LANGUAGE_PROGRESS) }
) {
Column(modifier = Modifier.padding(20.dp)) {

View File

@@ -4,14 +4,15 @@ package eu.gaudian.translator.view.stats.widgets
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
@@ -31,19 +32,31 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import eu.gaudian.translator.R
import eu.gaudian.translator.ui.theme.ThemePreviews
import eu.gaudian.translator.ui.theme.semanticColors
import eu.gaudian.translator.viewmodel.WeeklyActivityStat
import kotlinx.coroutines.delay
import kotlin.math.roundToInt
/**
* A widget that displays weekly activity statistics in a visually appealing bar chart.
* It's designed to be consistent with the app's modern, floating UI style.
* A widget that displays weekly activity statistics in a visually appealing smooth line chart.
* It's designed to be consistent with the app's modern UI style using the theme's colors.
*
* @param weeklyStats A list of [WeeklyActivityStat] for the last 7 days.
*/
@@ -51,20 +64,15 @@ import kotlinx.coroutines.delay
fun WeeklyActivityChartWidget(
weeklyStats: List<WeeklyActivityStat>
) {
val maxValue = remember(weeklyStats) {
(weeklyStats.flatMap { listOf(it.newlyAdded, it.completed, it.answeredRight) }.maxOrNull() ?: 0).let {
if (it < 10) 10 else ((it / 5) + 1) * 5
}
}
val hasNoData = remember(weeklyStats) {
weeklyStats.all { it.newlyAdded == 0 && it.completed == 0 && it.answeredRight == 0 }
weeklyStats.all { it.completed == 0 && it.answeredRight == 0 }
}
if (hasNoData) {
Box(
modifier = Modifier
.fillMaxWidth(),
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
Text(
@@ -77,103 +85,306 @@ fun WeeklyActivityChartWidget(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(8.dp)
// Reduced horizontal padding to give the chart more space
.padding(vertical = 24.dp, horizontal = 12.dp)
) {
WeeklyChartLegend()
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(24.dp))
Row(
modifier = Modifier
.fillMaxWidth()
.height(220.dp),
verticalAlignment = Alignment.Bottom
) {
// Y-Axis Labels
Column(
modifier = Modifier
.fillMaxHeight()
.padding(end = 8.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.End
) {
Text(maxValue.toString(), style = MaterialTheme.typography.labelSmall)
Text((maxValue / 2).toString(), style = MaterialTheme.typography.labelSmall)
Text("0", style = MaterialTheme.typography.labelSmall)
}
InteractiveLineChart(weeklyStats = weeklyStats)
// Chart Bars
Row(
modifier = Modifier
.weight(1f)
.fillMaxHeight(),
horizontalArrangement = Arrangement.SpaceAround,
verticalAlignment = Alignment.Bottom
) {
weeklyStats.forEach { stat ->
Column(
modifier = Modifier.weight(1f),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Bottom
) {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.weight(1f)
.fillMaxWidth(0.8f)
) {
Bar(value = stat.newlyAdded, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient1)
Bar(value = stat.completed, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient3)
Bar(value = stat.answeredRight, maxValue = maxValue, color = MaterialTheme.semanticColors.stageGradient5)
}
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stat.day,
style = MaterialTheme.typography.bodySmall
)
}
}
}
}
Spacer(modifier = Modifier.height(24.dp))
ChartFooter(weeklyStats = weeklyStats)
}
}
}
@Composable
private fun RowScope.Bar(value: Int, maxValue: Int, color: Color) {
private fun InteractiveLineChart(weeklyStats: List<WeeklyActivityStat>) {
var selectedIndex by remember { mutableStateOf<Int?>(3) } // Default selection
val textMeasurer = rememberTextMeasurer()
val colorCompleted = MaterialTheme.colorScheme.primary
val colorCorrect = MaterialTheme.colorScheme.tertiary
val gridColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
val tooltipLineColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f)
val dotCenterColor = MaterialTheme.colorScheme.surfaceVariant
val tooltipBgColor = MaterialTheme.colorScheme.inverseSurface
val tooltipTextColor = MaterialTheme.colorScheme.inverseOnSurface
var startAnimation by remember { mutableStateOf(false) }
val barHeight by animateFloatAsState(
targetValue = if (startAnimation) value.toFloat() / maxValue.toFloat() else 0f,
val animationProgress by animateFloatAsState(
targetValue = if (startAnimation) 1f else 0f,
animationSpec = tween(durationMillis = 1000),
label = "barHeightAnimation"
label = "chartAnimation"
)
LaunchedEffect(Unit) {
delay(200) // Small delay to ensure the UI is ready before animating
@Suppress("AssignedValueIsNeverRead")
delay(100)
startAnimation = true
}
val yAxisMax = remember(weeklyStats) {
val max = weeklyStats.flatMap { listOf(it.completed, it.answeredRight) }.maxOrNull() ?: 0
if (max < 10) 10 else ((max / 10) + 1) * 10
}
val yMax = yAxisMax.toFloat()
Row(modifier = Modifier.fillMaxWidth()) {
// Left Side: Y-Axis Amounts
Column(
modifier = Modifier
.height(180.dp)
// Reduced end padding to save space
.padding(end = 8.dp, top = 2.dp, bottom = 2.dp),
verticalArrangement = Arrangement.SpaceBetween,
horizontalAlignment = Alignment.End
) {
Text(
text = yAxisMax.toString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = (yAxisMax / 2).toString(),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Text(
text = "0",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Right Side: Chart Area
Column(modifier = Modifier.weight(1f)) {
Box(
modifier = Modifier
.weight(1f)
.fillMaxHeight(barHeight)
.clip(RoundedCornerShape(topStart = 4.dp, topEnd = 4.dp))
.background(color)
.fillMaxWidth()
.height(180.dp)
.pointerInput(Unit) {
detectTapGestures { offset ->
val itemWidth = size.width / (weeklyStats.size - 1).coerceAtLeast(1)
selectedIndex = (offset.x / itemWidth).roundToInt().coerceIn(0, weeklyStats.lastIndex)
}
}
.pointerInput(Unit) {
detectHorizontalDragGestures { change, _ ->
val itemWidth = size.width / (weeklyStats.size - 1).coerceAtLeast(1)
selectedIndex = (change.position.x / itemWidth).roundToInt().coerceIn(0, weeklyStats.lastIndex)
}
}
) {
Canvas(modifier = Modifier.fillMaxSize()) {
val width = size.width
val height = size.height
val xSpacing = width / (weeklyStats.size - 1).coerceAtLeast(1)
drawLine(gridColor, Offset(0f, 0f), Offset(width, 0f), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
drawLine(gridColor, Offset(0f, height / 2f), Offset(width, height / 2f), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
drawLine(gridColor, Offset(0f, height), Offset(width, height), strokeWidth = 2f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f)))
if (animationProgress == 0f) return@Canvas
val pointsCompleted = weeklyStats.mapIndexed { i, stat ->
Offset(i * xSpacing, height - ((stat.completed * animationProgress) / yMax) * height)
}
val pointsCorrect = weeklyStats.mapIndexed { i, stat ->
Offset(i * xSpacing, height - ((stat.answeredRight * animationProgress) / yMax) * height)
}
// Path 1: Correct (Bottom, Dashed)
val pathCorrect = Path().apply { smoothCurve(pointsCorrect) }
drawPath(
path = pathCorrect,
color = colorCorrect,
style = Stroke(width = 6f, pathEffect = PathEffect.dashPathEffect(floatArrayOf(12f, 12f), 0f))
)
// Path 2: Completed (Top, Solid with Fill)
val pathCompleted = Path().apply { smoothCurve(pointsCompleted) }
val fillPathCompleted = Path().apply {
smoothCurve(pointsCompleted)
lineTo(width, height)
lineTo(0f, height)
close()
}
drawPath(
path = fillPathCompleted,
brush = Brush.verticalGradient(
colors = listOf(colorCompleted.copy(alpha = 0.3f), Color.Transparent),
startY = 0f,
endY = height
)
)
drawPath(
path = pathCompleted,
color = colorCompleted,
style = Stroke(width = 8f)
)
// Interactive Highlights & Dual Separated Tooltips
selectedIndex?.let { index ->
val stat = weeklyStats[index]
val x = index * xSpacing
val yCompleted = height - ((stat.completed * animationProgress) / yMax) * height
val yCorrect = height - ((stat.answeredRight * animationProgress) / yMax) * height
// Vertical line marker
drawLine(
color = tooltipLineColor,
start = Offset(x, 0f),
end = Offset(x, height),
strokeWidth = 3f
)
// Dots on lines
drawCircle(color = dotCenterColor, radius = 12f, center = Offset(x, yCompleted))
drawCircle(color = colorCompleted, radius = 7f, center = Offset(x, yCompleted))
drawCircle(color = dotCenterColor, radius = 12f, center = Offset(x, yCorrect))
drawCircle(color = colorCorrect, radius = 7f, center = Offset(x, yCorrect))
// Measure text
val textStyle = TextStyle(color = tooltipTextColor, fontWeight = FontWeight.Bold, fontSize = 13.sp)
val textResCompleted = textMeasurer.measure(stat.completed.toString(), textStyle)
val textResCorrect = textMeasurer.measure(stat.answeredRight.toString(), textStyle)
val dotRadius = 5f
val gap = 6f
val padX = 12f
val padY = 8f
val w1 = padX * 2 + dotRadius * 2 + gap + textResCompleted.size.width
val h1 = padY * 2 + textResCompleted.size.height
val w2 = padX * 2 + dotRadius * 2 + gap + textResCorrect.size.width
val h2 = padY * 2 + textResCorrect.size.height
// Tooltip Overlap Prevention Logic
val completedIsHigher = yCompleted <= yCorrect
var yPosCompleted = if (completedIsHigher) yCompleted - h1 - 12f else yCompleted + 12f
var yPosCorrect = if (completedIsHigher) yCorrect + 12f else yCorrect - h2 - 12f
// Prevent clipping out of canvas bounds natively first
if (yPosCompleted < 0f && completedIsHigher) yPosCompleted = 0f
if (yPosCorrect < 0f && !completedIsHigher) yPosCorrect = 0f
if (yPosCompleted + h1 > height && !completedIsHigher) yPosCompleted = height - h1
if (yPosCorrect + h2 > height && completedIsHigher) yPosCorrect = height - h2
// Overlap resolution
val topRectY = minOf(yPosCompleted, yPosCorrect)
val topRectH = if (topRectY == yPosCompleted) h1 else h2
val bottomRectY = maxOf(yPosCompleted, yPosCorrect)
val gapBetweenTooltips = 8f
if (topRectY + topRectH + gapBetweenTooltips > bottomRectY) {
val midPointY = (yCompleted + yCorrect) / 2f
val adjustedTopY = midPointY - (topRectH + gapBetweenTooltips / 2f)
val adjustedBottomY = midPointY + (gapBetweenTooltips / 2f)
if (topRectY == yPosCompleted) {
yPosCompleted = adjustedTopY
yPosCorrect = adjustedBottomY
} else {
yPosCorrect = adjustedTopY
yPosCompleted = adjustedBottomY
}
}
// Final Canvas Bounds Check post-resolution
val finalMinY = minOf(yPosCompleted, yPosCorrect)
if (finalMinY < 0f) {
yPosCompleted -= finalMinY
yPosCorrect -= finalMinY
}
val finalMaxY = maxOf(yPosCompleted + h1, yPosCorrect + h2)
if (finalMaxY > height) {
val shift = finalMaxY - height
yPosCompleted -= shift
yPosCorrect -= shift
}
// Draw Completed Tooltip
val t1X = (x - w1 / 2f).coerceIn(0f, width - w1)
drawRoundRect(color = tooltipBgColor, topLeft = Offset(t1X, yPosCompleted), size = Size(w1, h1), cornerRadius = CornerRadius(16f, 16f))
drawCircle(color = colorCompleted, radius = dotRadius, center = Offset(t1X + padX + dotRadius, yPosCompleted + h1 / 2f))
drawText(textLayoutResult = textResCompleted, topLeft = Offset(t1X + padX + dotRadius * 2 + gap, yPosCompleted + padY))
// Draw Correct Tooltip
val t2X = (x - w2 / 2f).coerceIn(0f, width - w2)
drawRoundRect(color = tooltipBgColor, topLeft = Offset(t2X, yPosCorrect), size = Size(w2, h2), cornerRadius = CornerRadius(16f, 16f))
drawCircle(color = colorCorrect, radius = dotRadius, center = Offset(t2X + padX + dotRadius, yPosCorrect + h2 / 2f))
drawText(textLayoutResult = textResCorrect, topLeft = Offset(t2X + padX + dotRadius * 2 + gap, yPosCorrect + padY))
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// X-Axis Labels (Freed from fixed widths, prevented from wrapping)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
) {
weeklyStats.forEachIndexed { index, stat ->
val isSelected = index == selectedIndex
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = stat.day.uppercase().take(3) + ".",
color = if (isSelected) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 11.sp, // Slightly smaller to ensure fit across all devices
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
maxLines = 1,
softWrap = false // Prevents the text from splitting into multiple lines
)
if (isSelected) {
Spacer(modifier = Modifier.height(4.dp))
Box(
modifier = Modifier
.height(2.dp)
.width(20.dp)
.background(MaterialTheme.colorScheme.primary)
)
} else {
// Invisible spacer to prevent layout jumping when line appears
Spacer(modifier = Modifier.height(6.dp))
}
}
}
}
}
}
}
private fun Path.smoothCurve(points: List<Offset>) {
if (points.isEmpty()) return
moveTo(points.first().x, points.first().y)
for (i in 1 until points.size) {
val prev = points[i - 1]
val curr = points[i]
val controlX = (prev.x + curr.x) / 2f
cubicTo(
controlX, prev.y,
controlX, curr.y,
curr.x, curr.y
)
}
}
@Composable
private fun WeeklyChartLegend() {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
LegendItem(color = MaterialTheme.semanticColors.stageGradient1, label = stringResource(R.string.label_added))
LegendItem(color = MaterialTheme.semanticColors.stageGradient3, label = stringResource(R.string.label_completed))
LegendItem(color = MaterialTheme.semanticColors.stageGradient5, label = stringResource(R.string.label_correct))
LegendItem(color = MaterialTheme.colorScheme.primary, label = stringResource(R.string.label_completed).uppercase())
LegendItem(color = MaterialTheme.colorScheme.tertiary, label = stringResource(R.string.label_correct).uppercase())
}
}
@@ -185,8 +396,51 @@ private fun LegendItem(color: Color, label: String) {
.size(10.dp)
.background(color, shape = CircleShape)
)
Spacer(modifier = Modifier.width(6.dp))
Text(text = label, style = MaterialTheme.typography.labelMedium, fontSize = 12.sp)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = label,
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontWeight = FontWeight.Bold,
fontSize = 11.sp,
letterSpacing = 0.5.sp
)
}
}
@Composable
private fun ChartFooter(weeklyStats: List<WeeklyActivityStat>) {
val bestDay = remember(weeklyStats) {
weeklyStats.maxByOrNull { it.completed + it.answeredRight }?.day?.uppercase() ?: ""
}
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "Melhor Dia:",
color = MaterialTheme.colorScheme.onSurfaceVariant,
fontSize = 13.sp
)
Spacer(modifier = Modifier.width(8.dp))
Box(
modifier = Modifier
.clip(RoundedCornerShape(8.dp))
.background(MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f))
.padding(horizontal = 10.dp, vertical = 6.dp)
) {
Text(
text = bestDay,
color = MaterialTheme.colorScheme.onSurface,
fontWeight = FontWeight.Bold,
fontSize = 12.sp
)
}
}
}
}
@@ -194,13 +448,13 @@ private fun LegendItem(color: Color, label: String) {
@Composable
fun WeeklyActivityChartWidgetPreview() {
val sampleStats = listOf(
WeeklyActivityStat("Mon", 10, 5, 20),
WeeklyActivityStat("Tue", 12, 3, 15),
WeeklyActivityStat("Wed", 8, 8, 25),
WeeklyActivityStat("Thu", 15, 2, 18),
WeeklyActivityStat("Fri", 5, 10, 30),
WeeklyActivityStat("Sat", 7, 6, 22),
WeeklyActivityStat("Sun", 9, 4, 17)
WeeklyActivityStat("Seg", 30, 15, 10),
WeeklyActivityStat("Ter", 45, 20, 12),
WeeklyActivityStat("Qua", 80, 25, 15),
WeeklyActivityStat("Qui", 84, 35, 18),
WeeklyActivityStat("Sex", 50, 40, 22),
WeeklyActivityStat("Sáb", 70, 30, 20),
WeeklyActivityStat("Dom", 60, 25, 18)
)
Box(
modifier = Modifier

View File

@@ -57,7 +57,6 @@ import eu.gaudian.translator.utils.StatusMessageId
import eu.gaudian.translator.utils.StatusMessageService
import eu.gaudian.translator.utils.findActivity
import eu.gaudian.translator.view.NavigationRoutes
import eu.gaudian.translator.view.composable.AppActionCard
import eu.gaudian.translator.view.composable.AppButton
import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.AppIconContainer
@@ -67,7 +66,6 @@ import eu.gaudian.translator.view.composable.AppOutlinedTextField
import eu.gaudian.translator.view.composable.AppSlider
import eu.gaudian.translator.view.composable.AppTopAppBar
import eu.gaudian.translator.view.composable.InspiringSearchField
import eu.gaudian.translator.view.composable.LabeledSection
import eu.gaudian.translator.view.composable.Screen
import eu.gaudian.translator.view.composable.SourceLanguageDropdown
import eu.gaudian.translator.view.composable.TargetLanguageDropdown
@@ -700,12 +698,6 @@ fun ExplorePacksProminentCard(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.Default.DriveFolderUpload,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
}
@@ -734,7 +726,7 @@ fun ImportCsvCard(
Spacer(modifier = Modifier.width(16.dp))
Column(modifier = Modifier.weight(1f)) {
Text(
text = stringResource(R.string.label_import_csv),
text = stringResource(R.string.label_import_csv_or_lists),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold
)
@@ -745,12 +737,6 @@ fun ImportCsvCard(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Icon(
imageVector = Icons.Default.DriveFolderUpload,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.primary
)
}
}
}

View File

@@ -435,7 +435,7 @@ private fun VocabularyCardContent(
onMoveToStageClick = onMoveToStageClick,
onDeleteClick = onDeleteClick,
showAnalyzeGrammarButton = item.features.isNullOrBlank(),
showAnalyzeGrammarButton = !item.hasFeatures(),
onAnalyzeGrammarClick = {
vocabularyViewModel.fetchAndApplyGrammaticalDetailsForList(listOf(item))
},

View File

@@ -203,7 +203,7 @@ class ProgressViewModel @Inject constructor(
// Calculate localized day name
val calendarDay = ((date.dayOfWeek.ordinal + 1) % 7) + 1
val localizedDay = DateFormatSymbols.getInstance(Locale.getDefault()).shortWeekdays[calendarDay].uppercase()
val localizedDay = DateFormatSymbols.getInstance(Locale.getDefault()).weekdays[calendarDay]
WeeklyActivityStat(
// 3. Get the actual day name from the date and take the first 3 letters.

View File

@@ -902,7 +902,6 @@
<string name="text_add_new_word_to_list">Extrahiere ein neues Wort in deine Liste</string>
<string name="cd_scroll_to_top">Nach oben scrollen</string>
<string name="cd_settings">Einstellungen</string>
<string name="label_import_csv">CSV importieren</string>
<string name="label_ai_generator">KI-Generator</string>
<string name="label_new_wordss">Neue Wörter</string>
<string name="label_recently_added">Kürzlich hinzugefügt</string>

View File

@@ -898,7 +898,6 @@
<string name="text_add_new_word_to_list">Extrair uma nova palavra para a sua lista</string>
<string name="cd_scroll_to_top">Rolar para o topo</string>
<string name="cd_settings">Configurações</string>
<string name="label_import_csv">Importar CSV</string>
<string name="label_ai_generator">Gerador de IA</string>
<string name="label_new_wordss">Novas Palavras</string>
<string name="label_recently_added">Adicionados recentemente</string>

View File

@@ -77,7 +77,7 @@
<string name="desc_daily_review_due">%1$d words need attention</string>
<string name="desc_expand_your_vocabulary">Expand your vocabulary</string>
<string name="desc_explore_packs">Discover curated vocabulary packs</string>
<string name="desc_explore_packs">Discover lists to download</string>
<string name="desc_import_csv">Import words from CSV or lists</string>
<string name="description">Description</string>
@@ -1168,4 +1168,5 @@
<!-- Explore Packs Hint -->
<string name="hint_explore_packs_title">About Vocabulary Packs</string>
<string name="label_import_csv_or_lists">Import Lists or CSV</string>
</resources>