Refactor the WeeklyActivityChartWidget into an interactive smooth line chart and update vocabulary import labels.
This commit is contained in:
@@ -59,7 +59,7 @@ data class VocabularyItem(
|
||||
}
|
||||
|
||||
fun hasFeatures(): Boolean {
|
||||
return !features.isNullOrBlank()
|
||||
return !features.isNullOrBlank() && features != "{}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -435,7 +435,7 @@ private fun VocabularyCardContent(
|
||||
onMoveToStageClick = onMoveToStageClick,
|
||||
onDeleteClick = onDeleteClick,
|
||||
|
||||
showAnalyzeGrammarButton = item.features.isNullOrBlank(),
|
||||
showAnalyzeGrammarButton = !item.hasFeatures(),
|
||||
onAnalyzeGrammarClick = {
|
||||
vocabularyViewModel.fetchAndApplyGrammaticalDetailsForList(listOf(item))
|
||||
},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user