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
|
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
|
||||||
) {
|
) {
|
||||||
@@ -201,7 +234,6 @@ fun StatCard(
|
|||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
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
|
||||||
@@ -209,13 +241,35 @@ 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
|
||||||
) {
|
) {
|
||||||
@@ -234,7 +288,6 @@ fun GoalCard(
|
|||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
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
|
||||||
@@ -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(
|
||||||
|
|||||||
@@ -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() }
|
||||||
|
|||||||
Reference in New Issue
Block a user