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 9c1b2b4..1c18ecf 100644 Binary files a/app/src/main/res/font/open_runde_bold_clock_only.otf and b/app/src/main/res/font/open_runde_bold_clock_only.otf differ