feat: Add stats for the current day, update typography

The arrow button can be used to get a detailed analysis of the focus durations
This commit is contained in:
Nishant Mishra
2025-07-12 12:17:45 +05:30
parent 2c6c00b24f
commit 2101b0465e
9 changed files with 283 additions and 37 deletions

View File

@@ -1,3 +1,10 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.compose.animation.AnimatedVisibility
@@ -52,7 +59,7 @@ import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@OptIn(ExperimentalMaterial3Api::class)
@@ -106,7 +113,7 @@ private fun SettingsScreen(
Text(
"Settings",
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTitle,
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)

View File

@@ -7,18 +7,41 @@
package org.nsh07.pomodoro.ui.statsScreen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
@@ -27,17 +50,23 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import kotlinx.coroutines.runBlocking
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
@Composable
fun StatsScreenRoot(
modifier: Modifier = Modifier,
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) {
val allStatsSummaryModelProducer = viewModel.allStatsSummaryModelProducer
val todayStat by viewModel.todayStat.collectAsState(null)
StatsScreen(
allStatsSummaryModelProducer = allStatsSummaryModelProducer,
allStatsSummaryModelProducer = viewModel.allStatsSummaryModelProducer,
todayStatModelProducer = viewModel.todayStatModelProducer,
todayStat = todayStat,
modifier = modifier
)
}
@@ -46,25 +75,150 @@ fun StatsScreenRoot(
@Composable
fun StatsScreen(
allStatsSummaryModelProducer: CartesianChartModelProducer,
todayStatModelProducer: CartesianChartModelProducer,
todayStat: Stat?,
modifier: Modifier = Modifier
) {
Column(modifier) {
TopAppBar(
title = {
Text(
"Stats",
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTitle,
fontSize = 32.sp,
lineHeight = 32.sp
var todayStatExpanded by rememberSaveable { mutableStateOf(false) }
LazyColumn(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
item {
TopAppBar(
title = {
Text(
"Stats",
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)
)
)
},
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally
)
Text("This week", style = typography.headlineSmall, modifier = Modifier.padding(16.dp))
TimeColumnChart(allStatsSummaryModelProducer, modifier = Modifier.padding(start = 16.dp))
},
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally
)
}
item {
Text(
"Today",
style = typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
Spacer(Modifier.height(16.dp))
}
item {
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
Box(
modifier = Modifier
.background(
colorScheme.primaryContainer,
MaterialTheme.shapes.largeIncreased
)
.weight(1f)
) {
Column(Modifier.padding(16.dp)) {
Text(
"Focus",
style = typography.titleMedium,
color = colorScheme.onPrimaryContainer
)
Text(
if (todayStat != null) remember(todayStat) {
millisecondsToHoursMinutes(
todayStat.focusTimeQ1 +
todayStat.focusTimeQ2 +
todayStat.focusTimeQ3 +
todayStat.focusTimeQ4
)
} else "0h 0m",
style = typography.displaySmall,
fontFamily = openRundeClock,
color = colorScheme.onPrimaryContainer
)
}
}
Spacer(Modifier.width(8.dp))
Box(
modifier = Modifier
.background(
colorScheme.tertiaryContainer,
MaterialTheme.shapes.largeIncreased
)
.weight(1f)
) {
Column(Modifier.padding(16.dp)) {
Text(
"Break",
style = typography.titleMedium,
color = colorScheme.onTertiaryContainer
)
Text(
if (todayStat != null) remember(todayStat) {
millisecondsToHoursMinutes(todayStat.breakTime)
} else "0h 0m",
style = typography.displaySmall,
fontFamily = openRundeClock,
color = colorScheme.onTertiaryContainer
)
}
}
}
}
item {
val iconRotation by animateFloatAsState(
if (todayStatExpanded) 180f else 0f,
animationSpec = motionScheme.defaultSpatialSpec()
)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth()
) {
FilledTonalIconToggleButton(
checked = todayStatExpanded,
onCheckedChange = { todayStatExpanded = 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)
)
}
AnimatedVisibility(todayStatExpanded) {
TimeColumnChart(
todayStatModelProducer,
timeConverter = ::millisecondsToHoursMinutes,
modifier = Modifier.padding(start = 16.dp)
)
}
}
Spacer(Modifier.height(16.dp))
}
item {
Text(
"This week",
style = typography.headlineSmall,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
Spacer(Modifier.height(16.dp))
}
item {
TimeColumnChart(
allStatsSummaryModelProducer,
modifier = Modifier.padding(start = 16.dp)
)
}
}
}
@@ -78,11 +232,15 @@ fun StatsScreenPreview() {
runBlocking {
modelProducer.runTransaction {
columnSeries { series(5, 6) }
columnSeries {
series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7)
}
}
}
StatsScreen(
allStatsSummaryModelProducer = modelProducer
modelProducer,
modelProducer,
null
)
}

View File

@@ -10,10 +10,10 @@ package org.nsh07.pomodoro.ui.statsScreen
import android.graphics.Path
import android.graphics.RectF
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
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.patrykandpatrick.vico.compose.cartesian.CartesianChartHost
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberBottom
@@ -40,9 +40,11 @@ import org.nsh07.pomodoro.utils.millisecondsToHours
internal fun TimeColumnChart(
modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier,
thickness: Dp = 40.dp,
timeConverter: (Long) -> String = ::millisecondsToHours
) {
val radius = with(LocalDensity.current) {
(48.dp / 2).toPx()
(thickness / 2).toPx()
}
ProvideVicoTheme(rememberM3VicoTheme()) {
CartesianChartHost(
@@ -53,7 +55,7 @@ internal fun TimeColumnChart(
vicoTheme.columnCartesianLayerColors.map { color ->
rememberLineComponent(
fill = fill(color),
thickness = 48.dp,
thickness = thickness,
shape = { _, path, left, top, right, bottom ->
if (top + radius <= bottom - radius) {
path.arcTo(
@@ -87,21 +89,21 @@ internal fun TimeColumnChart(
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent),
valueFormatter = CartesianValueFormatter { measuringContext, value, _ ->
millisecondsToHours(value.toLong())
timeConverter(value.toLong())
}
),
bottomAxis = HorizontalAxis.rememberBottom(
rememberLineComponent(Fill.Transparent),
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent)
),
)
),
modelProducer = modelProducer,
zoomState = rememberVicoZoomState(
zoomEnabled = false,
initialZoom = Zoom.fixed(),
minZoom = Zoom.min(Zoom.fixed(), Zoom.Content)
minZoom = Zoom.min(Zoom.Content, Zoom.fixed())
),
animationSpec = motionScheme.defaultSpatialSpec(),
modifier = modifier,
)
}

View File

@@ -16,6 +16,7 @@ import androidx.lifecycle.viewmodel.viewModelFactory
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.StatRepository
@@ -23,11 +24,12 @@ import org.nsh07.pomodoro.data.StatRepository
class StatsViewModel(
statRepository: StatRepository
) : ViewModel() {
private val todayStat = statRepository.getTodayStat()
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
private val allStatsSummary = statRepository.getLastWeekStatsSummary()
private val averageFocusTimes = statRepository.getAverageFocusTimes()
val allStatsSummaryModelProducer = CartesianChartModelProducer()
val todayStatModelProducer = CartesianChartModelProducer()
init {
viewModelScope.launch(Dispatchers.IO) {
@@ -38,6 +40,21 @@ class StatsViewModel(
}
}
}
viewModelScope.launch(Dispatchers.IO) {
todayStat
.collect {
todayStatModelProducer.runTransaction {
columnSeries {
series(
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0,
it?.focusTimeQ3 ?: 0,
it?.focusTimeQ4 ?: 0
)
}
}
}
}
}
companion object {

View File

@@ -1,3 +1,10 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.theme
import androidx.compose.material3.Typography
@@ -9,6 +16,8 @@ import androidx.compose.ui.text.font.FontVariation
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexHeadline
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
// Set of Material typography styles to start with
val Typography = Typography(
@@ -18,6 +27,18 @@ val Typography = Typography(
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
headlineSmall = TextStyle(
fontFamily = robotoFlexHeadline,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp,
),
titleMedium = TextStyle(
fontFamily = robotoFlexTitle,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp,
)
)
@@ -27,7 +48,7 @@ object AppFonts {
)
@OptIn(ExperimentalTextApi::class)
val robotoFlexTitle = FontFamily(
val robotoFlexTopBar = FontFamily(
Font(
R.font.roboto_flex_variable,
variationSettings = FontVariation.Settings(
@@ -45,4 +66,28 @@ object AppFonts {
)
)
)
@OptIn(ExperimentalTextApi::class)
val robotoFlexHeadline = FontFamily(
Font(
R.font.roboto_flex_variable,
variationSettings = FontVariation.Settings(
FontVariation.width(130f),
FontVariation.weight(600),
FontVariation.grade(0)
)
)
)
@OptIn(ExperimentalTextApi::class)
val robotoFlexTitle = FontFamily(
Font(
R.font.roboto_flex_variable,
variationSettings = FontVariation.Settings(
FontVariation.width(130f),
FontVariation.weight(700),
FontVariation.grade(0)
)
)
)
}

View File

@@ -58,7 +58,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
@@ -112,7 +112,7 @@ fun TimerScreen(
Text(
"Tomato",
style = TextStyle(
fontFamily = robotoFlexTitle,
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.onErrorContainer
@@ -125,7 +125,7 @@ fun TimerScreen(
Text(
"Focus",
style = TextStyle(
fontFamily = robotoFlexTitle,
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.onPrimaryContainer
@@ -137,7 +137,7 @@ fun TimerScreen(
TimerMode.SHORT_BREAK -> Text(
"Short Break",
style = TextStyle(
fontFamily = robotoFlexTitle,
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.onTertiaryContainer
@@ -149,7 +149,7 @@ fun TimerScreen(
TimerMode.LONG_BREAK -> Text(
"Long Break",
style = TextStyle(
fontFamily = robotoFlexTitle,
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp,
color = colorScheme.onTertiaryContainer

View File

@@ -23,4 +23,11 @@ fun millisecondsToHours(t: Long): String =
Locale.getDefault(),
"%dh",
TimeUnit.MILLISECONDS.toHours(t)
)
fun millisecondsToHoursMinutes(t: Long): String =
String.format(
Locale.getDefault(),
"%dh %dm", TimeUnit.MILLISECONDS.toHours(t),
TimeUnit.MILLISECONDS.toMinutes(t) % TimeUnit.HOURS.toMinutes(1)
)

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#000000"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="@android:color/white"
android:pathData="M480,599q-8,0 -15,-2.5t-13,-8.5L268,404q-11,-11 -11,-28t11,-28q11,-11 28,-11t28,11l156,156 156,-156q11,-11 28,-11t28,11q11,11 11,28t-11,28L508,588q-6,6 -13,8.5t-15,2.5Z" />
</vector>