feat: show average focus hours within sections in stats screen

This commit is contained in:
Nishant Mishra
2025-07-13 10:20:29 +05:30
parent 7541bd84dd
commit e6e21ea7d1
4 changed files with 84 additions and 20 deletions

View File

@@ -25,7 +25,9 @@ data class Stat(
val focusTimeQ3: Long, val focusTimeQ3: Long,
val focusTimeQ4: Long, val focusTimeQ4: Long,
val breakTime: Long val breakTime: Long
) ) {
fun totalFocusTime() = focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4
}
data class StatSummary( data class StatSummary(
val date: LocalDate, val date: LocalDate,
@@ -38,4 +40,6 @@ data class StatFocusTime(
val focusTimeQ2: Long, val focusTimeQ2: Long,
val focusTimeQ3: Long, val focusTimeQ3: Long,
val focusTimeQ4: Long val focusTimeQ4: Long
) ) {
fun total() = focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4
}

View File

@@ -33,7 +33,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -48,6 +47,7 @@ import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
@@ -56,6 +56,7 @@ import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.data.StatFocusTime
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -66,13 +67,20 @@ fun StatsScreenRoot(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory) viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) { ) {
val todayStat by viewModel.todayStat.collectAsState(null) val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
val lastWeekAverageFocusTimes by viewModel
.lastWeekAverageFocusTimes.collectAsStateWithLifecycle(null)
val lastMonthAverageFocusTimes by viewModel
.lastMonthAverageFocusTimes.collectAsStateWithLifecycle(null)
StatsScreen( StatsScreen(
lastWeekSummaryChartData = remember { viewModel.lastWeekSummaryChartData }, lastWeekSummaryChartData = remember { viewModel.lastWeekSummaryChartData },
lastWeekSummaryAnalysisModelProducer = remember { viewModel.lastWeekSummaryAnalysisModelProducer }, lastWeekSummaryAnalysisModelProducer = remember { viewModel.lastWeekSummaryAnalysisModelProducer },
lastMonthSummaryChartData = remember { viewModel.lastMonthSummaryChartData }, lastMonthSummaryChartData = remember { viewModel.lastMonthSummaryChartData },
lastMonthSummaryAnalysisModelProducer = remember { viewModel.lastMonthSummaryAnalysisModelProducer }, lastMonthSummaryAnalysisModelProducer = remember { viewModel.lastMonthSummaryAnalysisModelProducer },
todayStat = todayStat, todayStat = todayStat,
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
modifier = modifier modifier = modifier
) )
} }
@@ -85,6 +93,8 @@ fun StatsScreen(
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer, lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer,
todayStat: Stat?, todayStat: Stat?,
lastWeekAverageFocusTimes: StatFocusTime?,
lastMonthAverageFocusTimes: StatFocusTime?,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@@ -143,14 +153,9 @@ fun StatsScreen(
color = colorScheme.onPrimaryContainer color = colorScheme.onPrimaryContainer
) )
Text( Text(
if (todayStat != null) remember(todayStat) { remember(todayStat) {
millisecondsToHoursMinutes( millisecondsToHoursMinutes(todayStat?.totalFocusTime() ?: 0)
todayStat.focusTimeQ1 + },
todayStat.focusTimeQ2 +
todayStat.focusTimeQ3 +
todayStat.focusTimeQ4
)
} else "0h 0m",
style = typography.displaySmall, style = typography.displaySmall,
fontFamily = openRundeClock, fontFamily = openRundeClock,
color = colorScheme.onPrimaryContainer color = colorScheme.onPrimaryContainer
@@ -173,9 +178,9 @@ fun StatsScreen(
color = colorScheme.onTertiaryContainer color = colorScheme.onTertiaryContainer
) )
Text( Text(
if (todayStat != null) remember(todayStat) { remember(todayStat) {
millisecondsToHoursMinutes(todayStat.breakTime) millisecondsToHoursMinutes(todayStat?.breakTime ?: 0)
} else "0h 0m", },
style = typography.displaySmall, style = typography.displaySmall,
fontFamily = openRundeClock, fontFamily = openRundeClock,
color = colorScheme.onTertiaryContainer color = colorScheme.onTertiaryContainer
@@ -194,6 +199,26 @@ fun StatsScreen(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) )
} }
item {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text(
millisecondsToHoursMinutes(lastWeekAverageFocusTimes?.total() ?: 0),
style = typography.displaySmall,
fontFamily = openRundeClock
)
Text(
"focus per day (avg)",
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 6.3.dp)
)
}
}
item { item {
TimeColumnChart( TimeColumnChart(
lastWeekSummaryChartData.first, lastWeekSummaryChartData.first,
@@ -246,6 +271,26 @@ fun StatsScreen(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) )
} }
item {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) {
Text(
millisecondsToHoursMinutes(lastMonthAverageFocusTimes?.total() ?: 0),
style = typography.displaySmall,
fontFamily = openRundeClock
)
Text(
"focus per day (avg)",
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 6.3.dp)
)
}
}
item { item {
TimeColumnChart( TimeColumnChart(
lastMonthSummaryChartData.first, lastMonthSummaryChartData.first,
@@ -315,6 +360,8 @@ fun StatsScreenPreview() {
modelProducer, modelProducer,
Pair(modelProducer, ExtraStore.Key()), Pair(modelProducer, ExtraStore.Key()),
modelProducer, modelProducer,
null,
null,
null null
) )
} }

View File

@@ -21,18 +21,20 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.StatRepository
import java.time.format.DateTimeFormatter import java.time.format.TextStyle
import java.util.Locale
class StatsViewModel( class StatsViewModel(
statRepository: StatRepository statRepository: StatRepository
) : ViewModel() { ) : ViewModel() {
private val dayFormatter = DateTimeFormatter.ofPattern("E")
val todayStat = statRepository.getTodayStat().distinctUntilChanged() val todayStat = statRepository.getTodayStat().distinctUntilChanged()
private val lastWeekStatsSummary = statRepository.getLastNDaysStatsSummary(7) private val lastWeekStatsSummary = statRepository.getLastNDaysStatsSummary(7)
private val lastWeekAverageFocusTimes = statRepository.getLastNDaysAverageFocusTimes(7) val lastWeekAverageFocusTimes =
statRepository.getLastNDaysAverageFocusTimes(7).distinctUntilChanged()
private val lastMonthStatsSummary = statRepository.getLastNDaysStatsSummary(30) private val lastMonthStatsSummary = statRepository.getLastNDaysStatsSummary(30)
private val lastMonthAverageFocusTimes = statRepository.getLastNDaysAverageFocusTimes(30) val lastMonthAverageFocusTimes =
statRepository.getLastNDaysAverageFocusTimes(30).distinctUntilChanged()
val lastWeekSummaryChartData = val lastWeekSummaryChartData =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>()) Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
@@ -48,7 +50,12 @@ class StatsViewModel(
.collect { list -> .collect { list ->
// reversing is required because we need ascending order while the DB returns descending order // reversing is required because we need ascending order while the DB returns descending order
val reversed = list.reversed() val reversed = list.reversed()
val keys = reversed.map { it.date.format(dayFormatter) } val keys = reversed.map {
it.date.dayOfWeek.getDisplayName(
TextStyle.NARROW,
Locale.getDefault()
)
}
val values = reversed.map { it.focusTime } val values = reversed.map { it.focusTime }
lastWeekSummaryChartData.first.runTransaction { lastWeekSummaryChartData.first.runTransaction {
columnSeries { series(values) } columnSeries { series(values) }

View File

@@ -39,6 +39,12 @@ val Typography = Typography(
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.15.sp, letterSpacing = 0.15.sp,
),
titleSmall = TextStyle(
fontFamily = robotoFlexTitle,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
) )
) )