feat(ui): implement a navigation system in the stats screen

This commit is contained in:
Nishant Mishra
2025-12-10 19:22:08 +05:30
parent 13e4689546
commit 4f44e4c23e
6 changed files with 423 additions and 372 deletions

View File

@@ -297,7 +297,7 @@ fun AppScreen(
) )
} }
entry<Screen.Stats> { entry<Screen.Stats.Main> {
StatsScreenRoot(contentPadding = contentPadding) StatsScreenRoot(contentPadding = contentPadding)
} }
} }

View File

@@ -27,7 +27,7 @@ val mainScreens = listOf(
R.string.timer R.string.timer
), ),
NavItem( NavItem(
Screen.Stats, Screen.Stats.Main,
R.drawable.monitoring, R.drawable.monitoring,
R.drawable.monitoring_filled, R.drawable.monitoring_filled,
R.string.stats R.string.stats

View File

@@ -48,7 +48,10 @@ sealed class Screen : NavKey {
} }
@Serializable @Serializable
object Stats : Screen() sealed class Stats : Screen() {
@Serializable
object Main : Stats()
}
} }
data class NavItem( data class NavItem(

View File

@@ -18,67 +18,34 @@
package org.nsh07.pomodoro.ui.statsScreen package org.nsh07.pomodoro.ui.statsScreen
import android.graphics.Typeface import android.graphics.Typeface
import androidx.compose.foundation.background import androidx.compose.animation.fadeIn
import androidx.compose.foundation.clickable import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Arrangement import androidx.compose.animation.slideInHorizontally
import androidx.compose.foundation.layout.Box import androidx.compose.animation.slideOutHorizontally
import androidx.compose.foundation.layout.Column import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
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.columnSeries
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.ExtraStore
import org.nsh07.pomodoro.BuildConfig
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.ui.mergePaddingValues import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.statsScreen.screens.StatsMainScreen
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400 import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600 import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
@Composable @Composable
fun StatsScreenRoot( fun StatsScreenRoot(
@@ -86,6 +53,8 @@ fun StatsScreenRoot(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory) viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) { ) {
val backStack = viewModel.backStack
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null) val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle() val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
@@ -99,6 +68,7 @@ fun StatsScreenRoot(
StatsScreen( StatsScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
backStack = backStack,
lastWeekSummaryChartData = lastWeekSummaryChartData, lastWeekSummaryChartData = lastWeekSummaryChartData,
lastMonthSummaryChartData = lastMonthSummaryChartData, lastMonthSummaryChartData = lastMonthSummaryChartData,
lastYearSummaryChartData = lastYearSummaryChartData, lastYearSummaryChartData = lastYearSummaryChartData,
@@ -115,6 +85,7 @@ fun StatsScreenRoot(
@Composable @Composable
fun StatsScreen( fun StatsScreen(
contentPadding: PaddingValues, contentPadding: PaddingValues,
backStack: SnapshotStateList<Screen.Stats>,
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
@@ -125,342 +96,49 @@ fun StatsScreen(
generateSampleData: () -> Unit, generateSampleData: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
val hoursFormat = stringResource(R.string.hours_format) val hoursFormat = stringResource(R.string.hours_format)
val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format) val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format)
val minutesFormat = stringResource(R.string.minutes_format) val minutesFormat = stringResource(R.string.minutes_format)
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
LaunchedEffect(lastWeekAverageFocusTimes) {
lastWeekSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(lastWeekAverageFocusTimes)
}
}
}
LaunchedEffect(lastMonthAverageFocusTimes) {
lastMonthSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(lastMonthAverageFocusTimes)
}
}
}
val resolver = LocalFontFamilyResolver.current val resolver = LocalFontFamilyResolver.current
val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface } val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface }
val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface } val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface }
Scaffold( NavDisplay(
topBar = { backStack = backStack,
TopAppBar( onBack = backStack::removeLastOrNull,
title = { transitionSpec = {
Text( (slideInHorizontally(initialOffsetX = { it }))
stringResource(R.string.stats), .togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)
)
},
actions = if (BuildConfig.DEBUG) {
{
IconButton(
onClick = generateSampleData
) {
Spacer(Modifier.size(24.dp))
}
}
} else {
{}
},
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior,
colors = topBarColors
)
}, },
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) popTransitionSpec = {
) { innerPadding -> (slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
val insets = mergePaddingValues(innerPadding, contentPadding) .togetherWith(slideOutHorizontally(targetOffsetX = { it }))
LazyColumn( },
contentPadding = insets, predictivePopTransitionSpec = {
verticalArrangement = Arrangement.spacedBy(2.dp), (slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
modifier = Modifier .togetherWith(slideOutHorizontally(targetOffsetX = { it }))
.background(topBarColors.containerColor) },
.padding(horizontal = 16.dp) entryProvider = entryProvider {
) { entry<Screen.Stats.Main> {
item { Spacer(Modifier.height(14.dp)) } StatsMainScreen(
contentPadding = contentPadding,
item { lastWeekSummaryChartData = lastWeekSummaryChartData,
Text( lastMonthSummaryChartData = lastMonthSummaryChartData,
stringResource(R.string.today), lastYearSummaryChartData = lastYearSummaryChartData,
style = typography.headlineSmall, todayStat = todayStat,
modifier = Modifier.padding(horizontal = 16.dp) lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
lastYearAverageFocusTimes = lastYearAverageFocusTimes,
generateSampleData = generateSampleData,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
modifier = modifier
) )
} }
item { Spacer(Modifier.height(12.dp)) }
item {
Row {
Box(
modifier = Modifier
.background(
colorScheme.primaryContainer,
shapes.largeIncreased
)
.weight(1f)
) {
Column(Modifier.padding(16.dp)) {
Text(
stringResource(R.string.focus),
style = typography.titleMedium,
color = colorScheme.onPrimaryContainer
)
Text(
remember(todayStat) {
millisecondsToHoursMinutes(
todayStat?.totalFocusTime() ?: 0,
hoursMinutesFormat
)
},
style = typography.displaySmall,
color = colorScheme.onPrimaryContainer,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
)
}
}
Spacer(Modifier.width(8.dp))
Box(
modifier = Modifier
.background(
colorScheme.tertiaryContainer,
shapes.largeIncreased
)
.weight(1f)
) {
Column(Modifier.padding(16.dp)) {
Text(
stringResource(R.string.break_),
style = typography.titleMedium,
color = colorScheme.onTertiaryContainer
)
Text(
remember(todayStat) {
millisecondsToHoursMinutes(
todayStat?.breakTime ?: 0,
hoursMinutesFormat
)
},
style = typography.displaySmall,
color = colorScheme.onTertiaryContainer,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
)
}
}
}
}
item { Spacer(Modifier.height(12.dp)) }
item {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clip(topListItemShape)
.background(listItemColors.containerColor)
.clickable {}
.padding(
start = 20.dp,
top = 20.dp,
bottom = 20.dp
) // end = 0 to let the chart touch the end
) {
Text(
stringResource(R.string.last_week),
style = typography.headlineSmall
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
millisecondsToHoursMinutes(
remember(lastWeekAverageFocusTimes) {
lastWeekAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
)
Text(
stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 5.2.dp)
)
}
TimeColumnChart(
modelProducer = lastWeekSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
}
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clip(middleListItemShape)
.background(listItemColors.containerColor)
.clickable {}
.padding(
start = 20.dp,
top = 20.dp,
bottom = 20.dp
) // end = 0 to let the chart touch the end
) {
Text(
stringResource(R.string.last_month),
style = typography.headlineSmall
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
millisecondsToHoursMinutes(
remember(lastMonthAverageFocusTimes) {
lastMonthAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
)
Text(
text = stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 5.2.dp)
)
}
TimeColumnChart(
modelProducer = lastMonthSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
thickness = 8.dp,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastMonthSummaryChartData.second][x.toInt()]
}
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clip(bottomListItemShape)
.background(listItemColors.containerColor)
.clickable {}
.padding(
start = 20.dp,
top = 20.dp,
bottom = 20.dp
) // end = 0 to let the chart touch the end
) {
Text(
stringResource(R.string.last_year),
style = typography.headlineSmall
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
millisecondsToHoursMinutes(
remember(lastYearAverageFocusTimes) {
lastYearAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
)
Text(
text = stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 5.2.dp)
)
}
TimeLineChart(
modelProducer = lastYearSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastYearSummaryChartData.second][x.toInt()]
}
)
}
}
} }
} )
} }
@Preview(
widthDp = 400
)
@Composable
fun StatsScreenPreview() {
val modelProducer = remember { CartesianChartModelProducer() }
val keys = remember { ExtraStore.Key<List<String>>() }
LaunchedEffect(Unit) {
modelProducer.runTransaction {
columnSeries {
series(5, 6, 5, 2, 11, 8, 5)
}
lineSeries {}
extras { it[keys] = listOf("M", "T", "W", "T", "F", "S", "S") }
}
}
TomatoTheme {
Surface {
StatsScreen(
PaddingValues(),
Pair(modelProducer, keys),
Pair(modelProducer, keys),
Pair(modelProducer, keys),
null,
listOf(0, 0, 0, 0),
listOf(0, 0, 0, 0),
listOf(0, 0, 0, 0),
{}
)
}
}
}

View File

@@ -0,0 +1,367 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.statsScreen.screens
import android.graphics.Typeface
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import org.nsh07.pomodoro.BuildConfig
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.ui.mergePaddingValues
import org.nsh07.pomodoro.ui.statsScreen.TimeColumnChart
import org.nsh07.pomodoro.ui.statsScreen.TimeLineChart
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun StatsMainScreen(
contentPadding: PaddingValues,
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
todayStat: Stat?,
lastWeekAverageFocusTimes: List<Int>,
lastMonthAverageFocusTimes: List<Int>,
lastYearAverageFocusTimes: List<Int>,
generateSampleData: () -> Unit,
modifier: Modifier = Modifier,
hoursMinutesFormat: String,
hoursFormat: String,
minutesFormat: String,
axisTypeface: Typeface,
markerTypeface: Typeface
) {
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
Scaffold(
topBar = {
TopAppBar(
title = {
Text(
stringResource(R.string.stats),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)
)
},
actions = if (BuildConfig.DEBUG) {
{
IconButton(
onClick = generateSampleData
) {
Spacer(Modifier.size(24.dp))
}
}
} else {
{}
},
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior,
colors = topBarColors
)
},
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = mergePaddingValues(innerPadding, contentPadding)
LazyColumn(
contentPadding = insets,
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.padding(horizontal = 16.dp)
) {
item { Spacer(Modifier.height(14.dp)) }
item {
Text(
stringResource(R.string.today),
style = typography.headlineSmall,
modifier = Modifier.padding(horizontal = 16.dp)
)
}
item { Spacer(Modifier.height(12.dp)) }
item {
Row {
Box(
modifier = Modifier
.background(
colorScheme.primaryContainer,
shapes.largeIncreased
)
.weight(1f)
) {
Column(Modifier.padding(16.dp)) {
Text(
stringResource(R.string.focus),
style = typography.titleMedium,
color = colorScheme.onPrimaryContainer
)
Text(
remember(todayStat) {
millisecondsToHoursMinutes(
todayStat?.totalFocusTime() ?: 0,
hoursMinutesFormat
)
},
style = typography.displaySmall,
color = colorScheme.onPrimaryContainer,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
)
}
}
Spacer(Modifier.width(8.dp))
Box(
modifier = Modifier
.background(
colorScheme.tertiaryContainer,
shapes.largeIncreased
)
.weight(1f)
) {
Column(Modifier.padding(16.dp)) {
Text(
stringResource(R.string.break_),
style = typography.titleMedium,
color = colorScheme.onTertiaryContainer
)
Text(
remember(todayStat) {
millisecondsToHoursMinutes(
todayStat?.breakTime ?: 0,
hoursMinutesFormat
)
},
style = typography.displaySmall,
color = colorScheme.onTertiaryContainer,
maxLines = 1,
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
)
}
}
}
}
item { Spacer(Modifier.height(12.dp)) }
item {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clip(topListItemShape)
.background(listItemColors.containerColor)
.clickable {}
.padding(
start = 20.dp,
top = 20.dp,
bottom = 20.dp
) // end = 0 to let the chart touch the end
) {
Text(
stringResource(R.string.last_week),
style = typography.headlineSmall
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
millisecondsToHoursMinutes(
remember(lastWeekAverageFocusTimes) {
lastWeekAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
)
Text(
stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 5.2.dp)
)
}
TimeColumnChart(
modelProducer = lastWeekSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
}
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clip(middleListItemShape)
.background(listItemColors.containerColor)
.clickable {}
.padding(
start = 20.dp,
top = 20.dp,
bottom = 20.dp
) // end = 0 to let the chart touch the end
) {
Text(
stringResource(R.string.last_month),
style = typography.headlineSmall
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
millisecondsToHoursMinutes(
remember(lastMonthAverageFocusTimes) {
lastMonthAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
)
Text(
text = stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 5.2.dp)
)
}
TimeColumnChart(
modelProducer = lastMonthSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
thickness = 8.dp,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastMonthSummaryChartData.second][x.toInt()]
}
)
}
}
item {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clip(bottomListItemShape)
.background(listItemColors.containerColor)
.clickable {}
.padding(
start = 20.dp,
top = 20.dp,
bottom = 20.dp
) // end = 0 to let the chart touch the end
) {
Text(
stringResource(R.string.last_year),
style = typography.headlineSmall
)
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
millisecondsToHoursMinutes(
remember(lastYearAverageFocusTimes) {
lastYearAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
)
Text(
text = stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 5.2.dp)
)
}
TimeLineChart(
modelProducer = lastYearSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastYearSummaryChartData.second][x.toInt()]
}
)
}
}
}
}
}

View File

@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.statsScreen.viewModel package org.nsh07.pomodoro.ui.statsScreen.viewModel
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
@@ -38,6 +39,7 @@ import org.nsh07.pomodoro.BuildConfig
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.ui.Screen
import java.time.LocalDate import java.time.LocalDate
import java.time.format.DateTimeFormatter import java.time.format.DateTimeFormatter
import java.time.format.TextStyle import java.time.format.TextStyle
@@ -46,6 +48,7 @@ import java.util.Locale
class StatsViewModel( class StatsViewModel(
private val statRepository: StatRepository private val statRepository: StatRepository
) : ViewModel() { ) : ViewModel() {
val backStack = mutableStateListOf<Screen.Stats>(Screen.Stats.Main)
val todayStat = statRepository val todayStat = statRepository
.getTodayStat() .getTodayStat()