implement daily goal tracking and integrate dynamic streak data into HomeScreen

This commit is contained in:
jonasgaudian
2026-02-17 10:57:59 +01:00
parent f50c0c08a5
commit 142eb5a31d
2 changed files with 132 additions and 64 deletions

View File

@@ -1,6 +1,5 @@
package eu.gaudian.translator.view.home package eu.gaudian.translator.view.home
import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box 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.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel 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.utils.findActivity
import eu.gaudian.translator.view.composable.AppCard import eu.gaudian.translator.view.composable.AppCard
import eu.gaudian.translator.view.composable.Screen 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.view.vocabulary.widgets.WeeklyActivityChartWidget
import eu.gaudian.translator.viewmodel.ProgressViewModel import eu.gaudian.translator.viewmodel.ProgressViewModel
@@ -57,6 +56,17 @@ fun HomeScreen(
navController: NavHostController, navController: NavHostController,
modifier: Modifier = Modifier 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) // A Box with TopCenter alignment keeps the UI centered on wide screens (tablets/foldables)
Box( Box(
modifier = modifier.fillMaxSize(), modifier = modifier.fillMaxSize(),
@@ -71,7 +81,15 @@ fun HomeScreen(
) { ) {
item { Spacer(modifier = Modifier.height(16.dp)) } item { Spacer(modifier = Modifier.height(16.dp)) }
item { TopProfileSection(navController = navController) } 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 { item {
ActionCard( ActionCard(
title = "Daily Review", title = "Daily Review",
@@ -112,21 +130,6 @@ fun TopProfileSection(navController: NavHostController) {
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth().padding(8.dp) 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)) { Column(modifier = Modifier.weight(1f)) {
Text( Text(
@@ -153,7 +156,13 @@ fun TopProfileSection(navController: NavHostController) {
} }
@Composable @Composable
fun StreakAndGoalSection() { fun StreakAndGoalSection(
streak: Int,
progress: Float,
progressTitle: String,
onGoalClick: () -> Unit,
onStreakClick: () -> Unit
) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp) horizontalArrangement = Arrangement.spacedBy(16.dp)
@@ -162,15 +171,17 @@ fun StreakAndGoalSection() {
StatCard( StatCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
icon = Icons.Default.LocalFireDepartment, icon = Icons.Default.LocalFireDepartment,
title = "7 Days", title = "$streak Days",
subtitle = "CURRENT STREAK" subtitle = "CURRENT STREAK",
onClick = onStreakClick
) )
// Goal Card // Goal Card
GoalCard( GoalCard(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
progress = 0.7f, progress = progress,
title = "14 / 20", title = progressTitle,
subtitle = "DAILY GOAL" subtitle = "DAILY GOAL",
onClick = onGoalClick
) )
} }
} }
@@ -180,13 +191,35 @@ fun StatCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
icon: ImageVector, icon: ImageVector,
title: String, title: String,
subtitle: String subtitle: String,
onClick: (() -> Unit)? = null
) { ) {
if (onClick != null) {
AppCard(
modifier = modifier,
onClick = onClick
) {
StatCardContent(icon = icon, title = title, subtitle = subtitle)
}
} else {
AppCard( AppCard(
modifier = modifier, modifier = modifier,
) {
StatCardContent(icon = icon, title = title, subtitle = subtitle)
}
}
}
@Composable
private fun StatCardContent(
icon: ImageVector,
title: String,
subtitle: String
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp), modifier = Modifier
.padding(20.dp)
.height(120.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -202,20 +235,41 @@ fun StatCard(
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
} }
} }
}
@Composable @Composable
fun GoalCard( fun GoalCard(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
progress: Float, progress: Float,
title: String, title: String,
subtitle: String subtitle: String,
onClick: (() -> Unit)? = null
) { ) {
if (onClick != null) {
AppCard(
modifier = modifier,
onClick = onClick
) {
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
}
} else {
AppCard( AppCard(
modifier = modifier, modifier = modifier,
) {
GoalCardContent(progress = progress, title = title, subtitle = subtitle)
}
}
}
@Composable
private fun GoalCardContent(
progress: Float,
title: String,
subtitle: String
) { ) {
Column( Column(
modifier = Modifier.padding(20.dp), modifier = Modifier
.padding(20.dp)
.height(120.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center verticalArrangement = Arrangement.Center
) { ) {
@@ -235,7 +289,6 @@ fun GoalCard(
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)) Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
} }
} }
}
@Composable @Composable
fun ActionCard( fun ActionCard(
@@ -301,7 +354,7 @@ fun WeeklyProgressSection(
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text(text = "Weekly Progress", style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.Bold) 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") Text("See History")
} }
} }
@@ -310,6 +363,7 @@ fun WeeklyProgressSection(
AppCard( AppCard(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = { navController.navigate("stats/vocabulary_heatmap") }
) { ) {
if (weeklyActivityStats.isEmpty()) { if (weeklyActivityStats.isEmpty()) {
Column( Column(

View File

@@ -96,6 +96,11 @@ class ProgressViewModel @Inject constructor(
private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap()) private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap())
val dailyVocabularyStats: StateFlow<Map<LocalDate, Int>> = _dailyVocabularyStats.asStateFlow() val dailyVocabularyStats: StateFlow<Map<LocalDate, Int>> = _dailyVocabularyStats.asStateFlow()
private val _dailyGoal = MutableStateFlow(10)
val dailyGoal: StateFlow<Int> = _dailyGoal.asStateFlow()
private val _todayCompletedCount = MutableStateFlow(0)
val todayCompletedCount: StateFlow<Int> = _todayCompletedCount.asStateFlow()
init { init {
viewModelScope.launch { viewModelScope.launch {
@@ -255,6 +260,15 @@ class ProgressViewModel @Inject constructor(
try { try {
loadSelectedCategories() loadSelectedCategories()
try { 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 progressDeferred = viewModelScope.async { vocabularyRepository.calculateCategoryProgress() }
val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() } val lastSevenDaysDeferred = viewModelScope.async { getLastSevenDays() }
val streakDeferred = viewModelScope.async { calculateDailyStreak() } val streakDeferred = viewModelScope.async { calculateDailyStreak() }