From 142eb5a31d9bc81b75d76574f25bd5a3453c7891 Mon Sep 17 00:00:00 2001 From: jonasgaudian <43753916+jonasgaudian@users.noreply.github.com> Date: Tue, 17 Feb 2026 10:57:59 +0100 Subject: [PATCH] implement daily goal tracking and integrate dynamic streak data into `HomeScreen` --- .../translator/view/home/HomeScreen.kt | 182 ++++++++++++------ .../translator/viewmodel/ProgressViewModel.kt | 14 ++ 2 files changed, 132 insertions(+), 64 deletions(-) diff --git a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt index d50bb6c..beeba03 100644 --- a/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt +++ b/app/src/main/java/eu/gaudian/translator/view/home/HomeScreen.kt @@ -1,6 +1,5 @@ package eu.gaudian.translator.view.home -import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -40,7 +39,6 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.res.painterResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel @@ -49,6 +47,7 @@ import eu.gaudian.translator.R import eu.gaudian.translator.utils.findActivity import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.Screen +import eu.gaudian.translator.view.settings.SettingsRoutes import eu.gaudian.translator.view.vocabulary.widgets.WeeklyActivityChartWidget import eu.gaudian.translator.viewmodel.ProgressViewModel @@ -57,6 +56,17 @@ fun HomeScreen( navController: NavHostController, modifier: Modifier = Modifier ) { + val activity = LocalContext.current.findActivity() + val viewModel: ProgressViewModel = hiltViewModel(activity) + val streak by viewModel.streak.collectAsState() + val dailyGoal by viewModel.dailyGoal.collectAsState() + val todayCompletedCount by viewModel.todayCompletedCount.collectAsState() + + // Calculate daily goal progress + val progress = if (dailyGoal > 0) { + (todayCompletedCount.toFloat() / dailyGoal).coerceIn(0f, 1f) + } else 0f + // A Box with TopCenter alignment keeps the UI centered on wide screens (tablets/foldables) Box( modifier = modifier.fillMaxSize(), @@ -71,7 +81,15 @@ fun HomeScreen( ) { item { Spacer(modifier = Modifier.height(16.dp)) } item { TopProfileSection(navController = navController) } - item { StreakAndGoalSection() } + item { + StreakAndGoalSection( + streak = streak, + progress = progress, + progressTitle = "$todayCompletedCount / $dailyGoal", + onGoalClick = { navController.navigate(SettingsRoutes.VOCABULARY_OPTIONS) }, + onStreakClick = { navController.navigate("stats/vocabulary_heatmap") } + ) + } item { ActionCard( title = "Daily Review", @@ -112,21 +130,6 @@ fun TopProfileSection(navController: NavHostController) { verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(8.dp) ) { - // Parrot App Icon - Box( - modifier = Modifier - .size(56.dp) - .clip(CircleShape) - .background(MaterialTheme.colorScheme.primaryContainer), - contentAlignment = Alignment.Center - ) { - Image( - painter = painterResource(id = R.mipmap.ic_launcher_foreground), - contentDescription = "Polly Parrot", - ) - } - - Spacer(modifier = Modifier.width(16.dp)) Column(modifier = Modifier.weight(1f)) { Text( @@ -153,7 +156,13 @@ fun TopProfileSection(navController: NavHostController) { } @Composable -fun StreakAndGoalSection() { +fun StreakAndGoalSection( + streak: Int, + progress: Float, + progressTitle: String, + onGoalClick: () -> Unit, + onStreakClick: () -> Unit +) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(16.dp) @@ -162,15 +171,17 @@ fun StreakAndGoalSection() { StatCard( modifier = Modifier.weight(1f), icon = Icons.Default.LocalFireDepartment, - title = "7 Days", - subtitle = "CURRENT STREAK" + title = "$streak Days", + subtitle = "CURRENT STREAK", + onClick = onStreakClick ) // Goal Card GoalCard( modifier = Modifier.weight(1f), - progress = 0.7f, - title = "14 / 20", - subtitle = "DAILY GOAL" + progress = progress, + title = progressTitle, + subtitle = "DAILY GOAL", + onClick = onGoalClick ) } } @@ -178,62 +189,104 @@ fun StreakAndGoalSection() { @Composable fun StatCard( modifier: Modifier = Modifier, + icon: ImageVector, + title: String, + subtitle: String, + onClick: (() -> Unit)? = null +) { + if (onClick != null) { + AppCard( + modifier = modifier, + onClick = onClick + ) { + StatCardContent(icon = icon, title = title, subtitle = subtitle) + } + } else { + AppCard( + modifier = modifier, + ) { + StatCardContent(icon = icon, title = title, subtitle = subtitle) + } + } +} + +@Composable +private fun StatCardContent( icon: ImageVector, title: String, subtitle: String ) { - AppCard( - modifier = modifier, + Column( + modifier = Modifier + .padding(20.dp) + .height(120.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column( - modifier = Modifier.padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(32.dp) - ) - Spacer(modifier = Modifier.height(12.dp)) - Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(4.dp)) - Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) - } + Icon( + imageVector = icon, + contentDescription = null, + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(32.dp) + ) + Spacer(modifier = Modifier.height(12.dp)) + Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) } } @Composable fun GoalCard( modifier: Modifier = Modifier, + progress: Float, + title: String, + subtitle: String, + onClick: (() -> Unit)? = null +) { + if (onClick != null) { + AppCard( + modifier = modifier, + onClick = onClick + ) { + GoalCardContent(progress = progress, title = title, subtitle = subtitle) + } + } else { + AppCard( + modifier = modifier, + ) { + GoalCardContent(progress = progress, title = title, subtitle = subtitle) + } + } +} + +@Composable +private fun GoalCardContent( progress: Float, title: String, subtitle: String ) { - AppCard( - modifier = modifier, + Column( + modifier = Modifier + .padding(20.dp) + .height(120.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - Column( - modifier = Modifier.padding(20.dp), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center - ) { - Box(contentAlignment = Alignment.Center) { - CircularProgressIndicator( - progress = { progress }, - modifier = Modifier.size(48.dp), - strokeWidth = 4.dp, - color = MaterialTheme.colorScheme.primary, - trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) - ) - Text(text = "${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold) - } - Spacer(modifier = Modifier.height(12.dp)) - Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) - Spacer(modifier = Modifier.height(4.dp)) - Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) + Box(contentAlignment = Alignment.Center) { + CircularProgressIndicator( + progress = { progress }, + modifier = Modifier.size(48.dp), + strokeWidth = 4.dp, + color = MaterialTheme.colorScheme.primary, + trackColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f) + ) + Text(text = "${(progress * 100).toInt()}%", style = MaterialTheme.typography.labelSmall, fontWeight = FontWeight.Bold) } + Spacer(modifier = Modifier.height(12.dp)) + Text(text = title, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold) + Spacer(modifier = Modifier.height(4.dp)) + Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) } } @@ -301,7 +354,7 @@ fun WeeklyProgressSection( verticalAlignment = Alignment.CenterVertically ) { Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) - TextButton(onClick = { navController.navigate("vocabulary_heatmap") }) { + TextButton(onClick = { navController.navigate("stats/vocabulary_heatmap") }) { Text("See History") } } @@ -310,6 +363,7 @@ fun WeeklyProgressSection( AppCard( modifier = Modifier.fillMaxWidth(), + onClick = { navController.navigate("stats/vocabulary_heatmap") } ) { if (weeklyActivityStats.isEmpty()) { Column( diff --git a/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt b/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt index 68f0bb0..02dc862 100644 --- a/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt +++ b/app/src/main/java/eu/gaudian/translator/viewmodel/ProgressViewModel.kt @@ -96,6 +96,11 @@ class ProgressViewModel @Inject constructor( private val _dailyVocabularyStats = MutableStateFlow>(emptyMap()) val dailyVocabularyStats: StateFlow> = _dailyVocabularyStats.asStateFlow() + private val _dailyGoal = MutableStateFlow(10) + val dailyGoal: StateFlow = _dailyGoal.asStateFlow() + + private val _todayCompletedCount = MutableStateFlow(0) + val todayCompletedCount: StateFlow = _todayCompletedCount.asStateFlow() init { viewModelScope.launch { @@ -255,6 +260,15 @@ class ProgressViewModel @Inject constructor( try { loadSelectedCategories() try { + // Load daily goal setting + val dailyGoalValue = settingsRepository.dailyGoal.flow.first() + _dailyGoal.value = dailyGoalValue + + // Get today's completed count + val today = kotlin.time.Clock.System.now().toLocalDateTime(kotlinx.datetime.TimeZone.currentSystemDefault()).date + val todayCompleted = vocabularyRepository.getCorrectAnswerCountForDate(today) + _todayCompletedCount.value = todayCompleted + val progressDeferred = viewModelScope.async { vocabularyRepository.calculateCategoryProgress() } val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() } val streakDeferred = viewModelScope.async { calculateDailyStreak() }