feat: Implement monthly stats

This commit is contained in:
Nishant Mishra
2025-07-12 22:37:26 +05:30
parent ee0c69e01f
commit aa5160148b
6 changed files with 113 additions and 21 deletions

View File

@@ -37,8 +37,8 @@ interface StatDao {
@Query("SELECT * FROM stat WHERE date = :date")
fun getStat(date: LocalDate): Flow<Stat?>
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7")
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT :n")
fun getLastNDaysStatsSummary(n: Int): Flow<List<StatSummary>>
@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<StatFocusTime?>
fun getLastNDaysAvgFocusTimes(n: Int): Flow<StatFocusTime?>
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
suspend fun statExists(date: LocalDate): Boolean

View File

@@ -28,9 +28,9 @@ interface StatRepository {
fun getTodayStat(): Flow<Stat?>
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
fun getLastNDaysStatsSummary(n: Int): Flow<List<StatSummary>>
fun getLastWeekAverageFocusTimes(): Flow<StatFocusTime?>
fun getLastNDaysAverageFocusTimes(n: Int): Flow<StatFocusTime?>
suspend fun getLastDate(): LocalDate?
}
@@ -101,11 +101,11 @@ class AppStatRepository(
return statDao.getStat(currentDate)
}
override fun getLastWeekStatsSummary(): Flow<List<StatSummary>> =
statDao.getLastWeekStatsSummary()
override fun getLastNDaysStatsSummary(n: Int): Flow<List<StatSummary>> =
statDao.getLastNDaysStatsSummary(n)
override fun getLastWeekAverageFocusTimes(): Flow<StatFocusTime?> =
statDao.getLastWeekAvgFocusTimes()
override fun getLastNDaysAverageFocusTimes(n: Int): Flow<StatFocusTime?> =
statDao.getLastNDaysAvgFocusTimes(n)
override suspend fun getLastDate(): LocalDate? = statDao.getLastDate()
}

View File

@@ -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
}
)
}
}

View File

@@ -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<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer,
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
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

View File

@@ -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<Float>? = tween(500)
animationSpec: AnimationSpec<Float>? = 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,
)
}

View File

@@ -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<List<String>>())
val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer()
val lastMonthSummaryChartData =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
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 {