From aa5160148b6283aad017f83efb21835fd677e7e4 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sat, 12 Jul 2025 22:37:26 +0530 Subject: [PATCH] feat: Implement monthly stats --- .../java/org/nsh07/pomodoro/data/StatDao.kt | 10 ++- .../org/nsh07/pomodoro/data/StatRepository.kt | 12 ++-- .../ui/statsScreen/ProductivityGraph.kt | 5 +- .../pomodoro/ui/statsScreen/StatsScreen.kt | 65 ++++++++++++++++++- .../ui/statsScreen/TimeColumnChart.kt | 5 +- .../statsScreen/viewModel/StatsViewModel.kt | 37 ++++++++++- 6 files changed, 113 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt b/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt index 995106d..3dcba51 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt @@ -37,8 +37,8 @@ interface StatDao { @Query("SELECT * FROM stat WHERE date = :date") fun getStat(date: LocalDate): Flow - @Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7") - fun getLastWeekStatsSummary(): Flow> + @Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT :n") + fun getLastNDaysStatsSummary(n: Int): Flow> @Query( "SELECT " + @@ -46,11 +46,9 @@ interface StatDao { "AVG(focusTimeQ2) AS focusTimeQ2, " + "AVG(focusTimeQ3) AS focusTimeQ3, " + "AVG(focusTimeQ4) AS focusTimeQ4 " + - "FROM (" + - "SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4 FROM stat ORDER BY date DESC LIMIT 7" + - ")" + "FROM (SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4 FROM stat ORDER BY date DESC LIMIT :n)" ) - fun getLastWeekAvgFocusTimes(): Flow + fun getLastNDaysAvgFocusTimes(n: Int): Flow @Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)") suspend fun statExists(date: LocalDate): Boolean diff --git a/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt index ffaf6ef..7d2efbd 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt @@ -28,9 +28,9 @@ interface StatRepository { fun getTodayStat(): Flow - fun getLastWeekStatsSummary(): Flow> + fun getLastNDaysStatsSummary(n: Int): Flow> - fun getLastWeekAverageFocusTimes(): Flow + fun getLastNDaysAverageFocusTimes(n: Int): Flow suspend fun getLastDate(): LocalDate? } @@ -101,11 +101,11 @@ class AppStatRepository( return statDao.getStat(currentDate) } - override fun getLastWeekStatsSummary(): Flow> = - statDao.getLastWeekStatsSummary() + override fun getLastNDaysStatsSummary(n: Int): Flow> = + statDao.getLastNDaysStatsSummary(n) - override fun getLastWeekAverageFocusTimes(): Flow = - statDao.getLastWeekAvgFocusTimes() + override fun getLastNDaysAverageFocusTimes(n: Int): Flow = + statDao.getLastNDaysAvgFocusTimes(n) override suspend fun getLastDate(): LocalDate? = statDao.getLastDate() } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/ProductivityGraph.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/ProductivityGraph.kt index 7d47a63..f209e33 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/ProductivityGraph.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/ProductivityGraph.kt @@ -31,7 +31,7 @@ fun ColumnScope.ProductivityGraph( AnimatedVisibility(expanded) { Column(modifier = modifier) { Text(label, style = typography.titleMedium) - Text("Time of day versus focus hours", style = typography.bodySmall) + Text("Time of day versus focus duration", style = typography.bodySmall) Spacer(Modifier.height(8.dp)) TimeColumnChart( modelProducer, @@ -46,8 +46,7 @@ fun ColumnScope.ProductivityGraph( }, yValueFormatter = CartesianValueFormatter { _, value, _ -> millisecondsToHoursMinutes(value.toLong()) - }, - animationSpec = null + } ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt index 1305ea2..768d1ac 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt @@ -70,6 +70,8 @@ fun StatsScreenRoot( StatsScreen( lastWeekSummaryChartData = viewModel.lastWeekSummaryChartData, lastWeekSummaryAnalysisModelProducer = viewModel.lastWeekSummaryAnalysisModelProducer, + lastMonthSummaryChartData = viewModel.lastMonthSummaryChartData, + lastMonthSummaryAnalysisModelProducer = viewModel.lastMonthSummaryAnalysisModelProducer, todayStat = todayStat, modifier = modifier ) @@ -80,12 +82,15 @@ fun StatsScreenRoot( fun StatsScreen( lastWeekSummaryChartData: Pair>>, lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer, + lastMonthSummaryChartData: Pair>>, + lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer, todayStat: Stat?, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) } + var lastMonthStatExpanded by rememberSaveable { mutableStateOf(false) } Column( horizontalAlignment = Alignment.CenterHorizontally, @@ -179,13 +184,14 @@ fun StatsScreen( } } } + item { Spacer(Modifier) } item { Text( - "This week", + "Last week", style = typography.headlineSmall, modifier = Modifier .fillMaxWidth() - .padding(16.dp) + .padding(horizontal = 16.dp) ) } item { @@ -229,6 +235,59 @@ fun StatsScreen( modifier = Modifier.padding(horizontal = 32.dp) ) } + } + item { Spacer(Modifier) } + item { + Text( + "Last month", + style = typography.headlineSmall, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + ) + } + item { + TimeColumnChart( + lastMonthSummaryChartData.first, + modifier = Modifier.padding(start = 16.dp), + thickness = 8.dp, + xValueFormatter = CartesianValueFormatter { context, x, _ -> + context.model.extraStore[lastMonthSummaryChartData.second][x.toInt()] + } + ) + } + item { + val iconRotation by animateFloatAsState( + if (lastMonthStatExpanded) 180f else 0f, + animationSpec = motionScheme.defaultSpatialSpec() + ) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier.fillMaxWidth() + ) { + Spacer(Modifier.height(2.dp)) + FilledTonalIconToggleButton( + checked = lastMonthStatExpanded, + onCheckedChange = { lastMonthStatExpanded = it }, + shapes = IconButtonDefaults.toggleableShapes(), + modifier = Modifier + .padding(horizontal = 16.dp) + .width(52.dp) + .align(Alignment.End) + ) { + Icon( + painterResource(R.drawable.arrow_down), + "More info", + modifier = Modifier.rotate(iconRotation) + ) + } + ProductivityGraph( + lastMonthStatExpanded, + lastMonthSummaryAnalysisModelProducer, + label = "Monthly productivity analysis", + modifier = Modifier.padding(horizontal = 32.dp) + ) + } Spacer(Modifier.height(16.dp)) } } @@ -252,6 +311,8 @@ fun StatsScreenPreview() { } StatsScreen( + Pair(modelProducer, ExtraStore.Key()), + modelProducer, Pair(modelProducer, ExtraStore.Key()), modelProducer, null diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt index e622c16..36c8f41 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt @@ -10,8 +10,8 @@ package org.nsh07.pomodoro.ui.statsScreen import android.graphics.Path import android.graphics.RectF import androidx.compose.animation.core.AnimationSpec -import androidx.compose.animation.core.tween import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity @@ -48,7 +48,7 @@ internal fun TimeColumnChart( yValueFormatter: CartesianValueFormatter = CartesianValueFormatter { measuringContext, value, _ -> millisecondsToHours(value.toLong()) }, - animationSpec: AnimationSpec? = tween(500) + animationSpec: AnimationSpec? = motionScheme.slowEffectsSpec() ) { val radius = with(LocalDensity.current) { (thickness / 2).toPx() @@ -111,6 +111,7 @@ internal fun TimeColumnChart( minZoom = Zoom.min(Zoom.Content, Zoom.fixed()) ), animationSpec = animationSpec, + animateIn = false, modifier = modifier, ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt index 5d96e61..de12813 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt @@ -29,13 +29,19 @@ class StatsViewModel( private val dayFormatter = DateTimeFormatter.ofPattern("E") val todayStat = statRepository.getTodayStat().distinctUntilChanged() - private val lastWeekStatsSummary = statRepository.getLastWeekStatsSummary() - private val lastWeekAverageFocusTimes = statRepository.getLastWeekAverageFocusTimes() + private val lastWeekStatsSummary = statRepository.getLastNDaysStatsSummary(7) + private val lastWeekAverageFocusTimes = statRepository.getLastNDaysAverageFocusTimes(7) + private val lastMonthStatsSummary = statRepository.getLastNDaysStatsSummary(30) + private val lastMonthAverageFocusTimes = statRepository.getLastNDaysAverageFocusTimes(30) val lastWeekSummaryChartData = Pair(CartesianChartModelProducer(), ExtraStore.Key>()) val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer() + val lastMonthSummaryChartData = + Pair(CartesianChartModelProducer(), ExtraStore.Key>()) + val lastMonthSummaryAnalysisModelProducer = CartesianChartModelProducer() + init { viewModelScope.launch(Dispatchers.IO) { lastWeekStatsSummary @@ -65,6 +71,33 @@ class StatsViewModel( } } } + viewModelScope.launch(Dispatchers.IO) { + lastMonthStatsSummary + .collect { list -> + val reversed = list.reversed() + val keys = reversed.map { it.date.dayOfMonth.toString() } + val values = reversed.map { it.focusTime } + lastMonthSummaryChartData.first.runTransaction { + columnSeries { series(values) } + extras { it[lastMonthSummaryChartData.second] = keys } + } + } + } + viewModelScope.launch(Dispatchers.IO) { + lastMonthAverageFocusTimes + .collect { + lastMonthSummaryAnalysisModelProducer.runTransaction { + columnSeries { + series( + it?.focusTimeQ1 ?: 0, + it?.focusTimeQ2 ?: 0, + it?.focusTimeQ3 ?: 0, + it?.focusTimeQ4 ?: 0 + ) + } + } + } + } } companion object {