implement daily goal tracking and integrate dynamic streak data into HomeScreen
This commit is contained in:
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -180,13 +191,35 @@ fun StatCard(
|
||||
modifier: Modifier = Modifier,
|
||||
icon: ImageVector,
|
||||
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(
|
||||
modifier = modifier,
|
||||
) {
|
||||
StatCardContent(icon = icon, title = title, subtitle = subtitle)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StatCardContent(
|
||||
icon: ImageVector,
|
||||
title: String,
|
||||
subtitle: String
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.height(120.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
@@ -202,20 +235,41 @@ fun StatCard(
|
||||
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
|
||||
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
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(20.dp),
|
||||
modifier = Modifier
|
||||
.padding(20.dp)
|
||||
.height(120.dp),
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
@@ -235,7 +289,6 @@ fun GoalCard(
|
||||
Text(text = subtitle, style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ActionCard(
|
||||
@@ -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(
|
||||
|
||||
@@ -96,6 +96,11 @@ class ProgressViewModel @Inject constructor(
|
||||
private val _dailyVocabularyStats = MutableStateFlow<Map<LocalDate, Int>>(emptyMap())
|
||||
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 {
|
||||
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() }
|
||||
|
||||
Reference in New Issue
Block a user