feat(ui): implement a navigation system in the stats screen
This commit is contained in:
@@ -297,7 +297,7 @@ fun AppScreen(
|
||||
)
|
||||
}
|
||||
|
||||
entry<Screen.Stats> {
|
||||
entry<Screen.Stats.Main> {
|
||||
StatsScreenRoot(contentPadding = contentPadding)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ val mainScreens = listOf(
|
||||
R.string.timer
|
||||
),
|
||||
NavItem(
|
||||
Screen.Stats,
|
||||
Screen.Stats.Main,
|
||||
R.drawable.monitoring,
|
||||
R.drawable.monitoring_filled,
|
||||
R.string.stats
|
||||
|
||||
@@ -48,7 +48,10 @@ sealed class Screen : NavKey {
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Stats : Screen()
|
||||
sealed class Stats : Screen() {
|
||||
@Serializable
|
||||
object Main : Stats()
|
||||
}
|
||||
}
|
||||
|
||||
data class NavItem(
|
||||
|
||||
@@ -18,67 +18,34 @@
|
||||
package org.nsh07.pomodoro.ui.statsScreen
|
||||
|
||||
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.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
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.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.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||
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.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.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 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.Screen
|
||||
import org.nsh07.pomodoro.ui.statsScreen.screens.StatsMainScreen
|
||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400
|
||||
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
|
||||
fun StatsScreenRoot(
|
||||
@@ -86,6 +53,8 @@ fun StatsScreenRoot(
|
||||
modifier: Modifier = Modifier,
|
||||
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
||||
) {
|
||||
val backStack = viewModel.backStack
|
||||
|
||||
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
|
||||
|
||||
val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
|
||||
@@ -99,6 +68,7 @@ fun StatsScreenRoot(
|
||||
|
||||
StatsScreen(
|
||||
contentPadding = contentPadding,
|
||||
backStack = backStack,
|
||||
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||
@@ -115,6 +85,7 @@ fun StatsScreenRoot(
|
||||
@Composable
|
||||
fun StatsScreen(
|
||||
contentPadding: PaddingValues,
|
||||
backStack: SnapshotStateList<Screen.Stats>,
|
||||
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||
@@ -125,342 +96,49 @@ fun StatsScreen(
|
||||
generateSampleData: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||
|
||||
val hoursFormat = stringResource(R.string.hours_format)
|
||||
val hoursMinutesFormat = stringResource(R.string.hours_and_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 axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface }
|
||||
val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface }
|
||||
|
||||
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
|
||||
)
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = backStack::removeLastOrNull,
|
||||
transitionSpec = {
|
||||
(slideInHorizontally(initialOffsetX = { it }))
|
||||
.togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
|
||||
},
|
||||
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)
|
||||
popTransitionSpec = {
|
||||
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||
},
|
||||
predictivePopTransitionSpec = {
|
||||
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Stats.Main> {
|
||||
StatsMainScreen(
|
||||
contentPadding = contentPadding,
|
||||
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||
todayStat = todayStat,
|
||||
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),
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()]
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
package org.nsh07.pomodoro.ui.statsScreen.viewModel
|
||||
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
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.data.Stat
|
||||
import org.nsh07.pomodoro.data.StatRepository
|
||||
import org.nsh07.pomodoro.ui.Screen
|
||||
import java.time.LocalDate
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.TextStyle
|
||||
@@ -46,6 +48,7 @@ import java.util.Locale
|
||||
class StatsViewModel(
|
||||
private val statRepository: StatRepository
|
||||
) : ViewModel() {
|
||||
val backStack = mutableStateListOf<Screen.Stats>(Screen.Stats.Main)
|
||||
|
||||
val todayStat = statRepository
|
||||
.getTodayStat()
|
||||
|
||||
Reference in New Issue
Block a user