From 2101b0465e305991ed354dd3fe4f6bdbddbf0806 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sat, 12 Jul 2025 12:17:45 +0530 Subject: [PATCH] feat: Add stats for the current day, update typography The arrow button can be used to get a detailed analysis of the focus durations --- .../ui/settingsScreen/SettingsScreen.kt | 11 +- .../pomodoro/ui/statsScreen/StatsScreen.kt | 200 ++++++++++++++++-- .../ui/statsScreen/TimeColumnChart.kt | 16 +- .../statsScreen/viewModel/StatsViewModel.kt | 19 +- .../java/org/nsh07/pomodoro/ui/theme/Type.kt | 47 +++- .../pomodoro/ui/timerScreen/TimerScreen.kt | 10 +- .../java/org/nsh07/pomodoro/utils/Utils.kt | 7 + app/src/main/res/drawable/arrow_down.xml | 10 + .../res/font/open_runde_bold_clock_only.otf | Bin 18732 -> 22180 bytes 9 files changed, 283 insertions(+), 37 deletions(-) create mode 100644 app/src/main/res/drawable/arrow_down.xml diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 60d29b5..496b3ef 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -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 . + */ + 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 ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt index 7317e54..7cd8f25 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt @@ -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 ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt index c49be99..1925d4f 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt @@ -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, ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt index 99257cf..9a887e5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt @@ -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 { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Type.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Type.kt index a2966f0..6fd8593 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Type.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Type.kt @@ -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 . + */ + 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) + ) + ) + ) } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt index 855217a..d731065 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt @@ -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 diff --git a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt index 9b6dfd7..47378e2 100644 --- a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt +++ b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt @@ -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) ) \ No newline at end of file diff --git a/app/src/main/res/drawable/arrow_down.xml b/app/src/main/res/drawable/arrow_down.xml new file mode 100644 index 0000000..d565e75 --- /dev/null +++ b/app/src/main/res/drawable/arrow_down.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/font/open_runde_bold_clock_only.otf b/app/src/main/res/font/open_runde_bold_clock_only.otf index 9c1b2b4e65cbf0c437f49061877523ab38f5266d..1c18ecf9cbb19b54d65518c06cedb507b803fe6e 100644 GIT binary patch delta 4086 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 SyQPpxd;(d?vu}PZ+4_Iz;yDKZ delta 567 zcmXv~T}YE*6n@@s-(TCe>A)cMoe4F&FzKr(a@$4C`eroS*p~f3*(hlwgGB{Kfk6=j z5zCjLx{OwhG$QFjNW1HzZZaW=x{w%@7u^(Tv^;O$KFM!@T06ejKkApCI@P8wfo$i@DB|p>O zUZ6=7&qLZaoi=lnJ(jk!IL+GeS0+jKzgR#TAPrToDlehIG&=Nq zgx^am`dJr?Ok8E3JmDCfP7>izi4rx?lQ`nKq;1!AXTZb_ZAg^Y-4}QD80!@u^v>3W zW8BcV8PYf&HgKSkV=BlI=lV|(GLE!a3XQRG2jAHxC8~^hGM+^>sae{j-cpZ>WuuEN zh>hkcrG8l4HnS`v=FOX|A}*|&W*r%u+w-i>t{SO$_hA034