feat(ui): clean up stats screen for upcoming changes

This commit is contained in:
Nishant Mishra
2025-12-10 18:49:25 +05:30
parent 7a83b39b49
commit 13e4689546
2 changed files with 153 additions and 289 deletions

View File

@@ -1,72 +0,0 @@
/*
* 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
import android.graphics.Typeface
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import org.nsh07.pomodoro.R
@Composable
fun ColumnScope.ProductivityGraph(
expanded: Boolean,
modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier,
axisTypeface: Typeface = Typeface.DEFAULT,
markerTypeface: Typeface = Typeface.DEFAULT,
label: String = stringResource(R.string.productivity_analysis)
) {
AnimatedVisibility(expanded) {
Column(modifier = modifier) {
Text(label, style = typography.titleMedium)
Text(
stringResource(R.string.productivity_analysis_desc),
style = typography.bodySmall
)
Spacer(Modifier.height(8.dp))
TimeColumnChart(
modelProducer,
hoursFormat = stringResource(R.string.hours_format),
hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format),
minutesFormat = stringResource(R.string.minutes_format),
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { _, value, _ ->
when (value) {
0.0 -> "0 - 6"
1.0 -> "6 - 12"
2.0 -> "12 - 18"
3.0 -> "18 - 24"
else -> ""
}
}
)
}
}
}

View File

@@ -18,16 +18,14 @@
package org.nsh07.pomodoro.ui.statsScreen package org.nsh07.pomodoro.ui.statsScreen
import android.graphics.Typeface import android.graphics.Typeface
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
@@ -36,14 +34,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.text.TextAutoSize 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.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
@@ -53,16 +47,12 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalFontFamilyResolver import androidx.compose.ui.platform.LocalFontFamilyResolver
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
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
@@ -82,6 +72,11 @@ 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.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.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
@@ -130,15 +125,12 @@ fun StatsScreen(
generateSampleData: () -> Unit, generateSampleData: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() 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)
var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) }
var lastMonthStatExpanded by rememberSaveable { mutableStateOf(false) }
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() } val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() } val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
@@ -172,10 +164,7 @@ fun StatsScreen(
fontFamily = robotoFlexTopBar, fontFamily = robotoFlexTopBar,
fontSize = 32.sp, fontSize = 32.sp,
lineHeight = 32.sp lineHeight = 32.sp
), )
modifier = Modifier
.padding(top = contentPadding.calculateTopPadding())
.padding(vertical = 14.dp)
) )
}, },
actions = if (BuildConfig.DEBUG) { actions = if (BuildConfig.DEBUG) {
@@ -192,34 +181,38 @@ fun StatsScreen(
subtitle = {}, subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally, titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior, scrollBehavior = scrollBehavior,
windowInsets = WindowInsets() colors = topBarColors
) )
}, },
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding -> ) { innerPadding ->
val insets = mergePaddingValues(innerPadding, contentPadding) val insets = mergePaddingValues(innerPadding, contentPadding)
LazyColumn( LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = insets, contentPadding = insets,
verticalArrangement = Arrangement.spacedBy(16.dp) verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.padding(horizontal = 16.dp)
) { ) {
item { Spacer(Modifier) } item { Spacer(Modifier.height(14.dp)) }
item { item {
Text( Text(
stringResource(R.string.today), stringResource(R.string.today),
style = typography.headlineSmall, style = typography.headlineSmall,
modifier = Modifier modifier = Modifier.padding(horizontal = 16.dp)
.fillMaxWidth()
.padding(horizontal = 16.dp)
) )
} }
item { Spacer(Modifier.height(12.dp)) }
item { item {
Row(modifier = Modifier.padding(horizontal = 16.dp)) { Row {
Box( Box(
modifier = Modifier modifier = Modifier
.background( .background(
colorScheme.primaryContainer, colorScheme.primaryContainer,
MaterialTheme.shapes.largeIncreased shapes.largeIncreased
) )
.weight(1f) .weight(1f)
) { ) {
@@ -248,7 +241,7 @@ fun StatsScreen(
modifier = Modifier modifier = Modifier
.background( .background(
colorScheme.tertiaryContainer, colorScheme.tertiaryContainer,
MaterialTheme.shapes.largeIncreased shapes.largeIncreased
) )
.weight(1f) .weight(1f)
) { ) {
@@ -274,222 +267,165 @@ fun StatsScreen(
} }
} }
} }
item { Spacer(Modifier) }
item { Spacer(Modifier.height(12.dp)) }
item { item {
Text(
stringResource(R.string.last_week),
style = typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
item {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.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)
)
}
}
item {
TimeColumnChart(
modelProducer = lastWeekSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
modifier = Modifier.padding(start = 16.dp),
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
}
)
}
item {
val iconRotation by animateFloatAsState(
if (lastWeekStatExpanded) 180f else 0f,
animationSpec = motionScheme.defaultSpatialSpec()
)
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth() 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
) { ) {
Spacer(Modifier.height(2.dp)) Text(
FilledTonalIconToggleButton( stringResource(R.string.last_week),
checked = lastWeekStatExpanded, style = typography.headlineSmall
onCheckedChange = { lastWeekStatExpanded = it }, )
shapes = IconButtonDefaults.toggleableShapes(),
modifier = Modifier Row(
.padding(horizontal = 16.dp) verticalAlignment = Alignment.Bottom,
.width(52.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
.align(Alignment.End)
) { ) {
Icon( Text(
painterResource(R.drawable.arrow_down), millisecondsToHoursMinutes(
stringResource(R.string.more_info), remember(lastWeekAverageFocusTimes) {
modifier = Modifier.rotate(iconRotation) 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)
) )
} }
ProductivityGraph(
lastWeekStatExpanded, TimeColumnChart(
lastWeekSummaryAnalysisModelProducer, modelProducer = lastWeekSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface, axisTypeface = axisTypeface,
markerTypeface = markerTypeface, markerTypeface = markerTypeface,
label = stringResource(R.string.weekly_productivity_analysis), xValueFormatter = CartesianValueFormatter { context, x, _ ->
modifier = Modifier.padding(horizontal = 32.dp) context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
}
) )
} }
} }
item { Spacer(Modifier) }
item { item {
Text(
stringResource(R.string.last_month),
style = typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
item {
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.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)
)
}
}
item {
TimeColumnChart(
modelProducer = lastMonthSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
modifier = Modifier.padding(start = 16.dp),
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
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( Column(
horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.fillMaxWidth() 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
) { ) {
Spacer(Modifier.height(2.dp)) Text(
FilledTonalIconToggleButton( stringResource(R.string.last_month),
checked = lastMonthStatExpanded, style = typography.headlineSmall
onCheckedChange = { lastMonthStatExpanded = it }, )
shapes = IconButtonDefaults.toggleableShapes(),
modifier = Modifier Row(
.padding(horizontal = 16.dp) verticalAlignment = Alignment.Bottom,
.width(52.dp) horizontalArrangement = Arrangement.spacedBy(8.dp),
.align(Alignment.End)
) { ) {
Icon( Text(
painterResource(R.drawable.arrow_down), millisecondsToHoursMinutes(
stringResource(R.string.more_info), remember(lastMonthAverageFocusTimes) {
modifier = Modifier.rotate(iconRotation) 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)
) )
} }
ProductivityGraph(
lastMonthStatExpanded, TimeColumnChart(
lastMonthSummaryAnalysisModelProducer, modelProducer = lastMonthSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
axisTypeface = axisTypeface, axisTypeface = axisTypeface,
markerTypeface = markerTypeface, markerTypeface = markerTypeface,
label = stringResource(R.string.monthly_productivity_analysis), thickness = 8.dp,
modifier = Modifier.padding(horizontal = 32.dp) xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastMonthSummaryChartData.second][x.toInt()]
}
) )
} }
} }
item { Spacer(Modifier) }
item { item {
Text( Column(
stringResource(R.string.last_year), verticalArrangement = Arrangement.spacedBy(16.dp),
style = typography.headlineSmall,
modifier = Modifier modifier = Modifier
.fillMaxWidth() .clip(bottomListItemShape)
.padding(horizontal = 16.dp) .background(listItemColors.containerColor)
) .clickable {}
} .padding(
item { start = 20.dp,
Row( top = 20.dp,
verticalAlignment = Alignment.Bottom, bottom = 20.dp
horizontalArrangement = Arrangement.spacedBy(8.dp), ) // end = 0 to let the chart touch the end
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
) { ) {
Text( Text(
millisecondsToHoursMinutes( stringResource(R.string.last_year),
remember(lastYearAverageFocusTimes) { style = typography.headlineSmall
lastYearAverageFocusTimes.sum().toLong()
},
hoursMinutesFormat
),
style = typography.displaySmall
) )
Text(
text = stringResource(R.string.focus_per_day_avg), Row(
style = typography.titleSmall, verticalAlignment = Alignment.Bottom,
modifier = Modifier.padding(bottom = 5.2.dp) 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()]
}
) )
} }
} }
item {
TimeLineChart(
modelProducer = lastYearSummaryChartData.first,
hoursFormat = hoursFormat,
hoursMinutesFormat = hoursMinutesFormat,
minutesFormat = minutesFormat,
modifier = Modifier.padding(start = 16.dp),
axisTypeface = axisTypeface,
markerTypeface = markerTypeface,
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastYearSummaryChartData.second][x.toInt()]
}
)
}
item { Spacer(Modifier.height(16.dp)) }
} }
} }
} }