feat(stats): implement a basic last month screen
This commit is contained in:
@@ -54,6 +54,9 @@ sealed class Screen : NavKey {
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object LastWeek : Stats()
|
object LastWeek : Stats()
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
object LastMonth : Stats()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
|||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.data.Stat
|
import org.nsh07.pomodoro.data.Stat
|
||||||
import org.nsh07.pomodoro.ui.Screen
|
import org.nsh07.pomodoro.ui.Screen
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.screens.LastMonthScreen
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.screens.LastWeekScreen
|
import org.nsh07.pomodoro.ui.statsScreen.screens.LastWeekScreen
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.screens.StatsMainScreen
|
import org.nsh07.pomodoro.ui.statsScreen.screens.StatsMainScreen
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
||||||
@@ -80,7 +81,7 @@ fun StatsScreenRoot(
|
|||||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
lastWeekAnalysisValues = lastWeekAnalysisValues,
|
lastWeekAnalysisValues = lastWeekAnalysisValues,
|
||||||
lastMonthAverageFocusTimes = lastMonthAnalysisValues,
|
lastMonthAnalysisValues = lastMonthAnalysisValues,
|
||||||
lastYearAverageFocusTimes = lastYearAnalysisValues,
|
lastYearAverageFocusTimes = lastYearAnalysisValues,
|
||||||
generateSampleData = viewModel::generateSampleData,
|
generateSampleData = viewModel::generateSampleData,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -101,7 +102,7 @@ fun StatsScreen(
|
|||||||
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
lastWeekAnalysisValues: Pair<List<Long>, Long>,
|
lastWeekAnalysisValues: Pair<List<Long>, Long>,
|
||||||
lastMonthAverageFocusTimes: List<Int>,
|
lastMonthAnalysisValues: Pair<List<Long>, Long>,
|
||||||
lastYearAverageFocusTimes: List<Int>,
|
lastYearAverageFocusTimes: List<Int>,
|
||||||
generateSampleData: () -> Unit,
|
generateSampleData: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -138,7 +139,7 @@ fun StatsScreen(
|
|||||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
lastWeekAverageFocusTimes = lastWeekAnalysisValues.first,
|
lastWeekAverageFocusTimes = lastWeekAnalysisValues.first,
|
||||||
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
|
lastMonthAverageFocusTimes = lastMonthAnalysisValues.first,
|
||||||
lastYearAverageFocusTimes = lastYearAverageFocusTimes,
|
lastYearAverageFocusTimes = lastYearAverageFocusTimes,
|
||||||
generateSampleData = generateSampleData,
|
generateSampleData = generateSampleData,
|
||||||
hoursFormat = hoursFormat,
|
hoursFormat = hoursFormat,
|
||||||
@@ -157,9 +158,23 @@ fun StatsScreen(
|
|||||||
entry<Screen.Stats.LastWeek> {
|
entry<Screen.Stats.LastWeek> {
|
||||||
LastWeekScreen(
|
LastWeekScreen(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastWeekAnalysisValues = lastWeekAnalysisValues,
|
focusBreakdownValues = lastWeekAnalysisValues,
|
||||||
lastWeekSummaryValues = lastWeekSummaryValues,
|
focusHistoryValues = lastWeekSummaryValues,
|
||||||
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
mainChartData = lastWeekSummaryChartData,
|
||||||
|
onBack = backStack::removeLastOrNull,
|
||||||
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
|
hoursFormat = hoursFormat,
|
||||||
|
minutesFormat = minutesFormat,
|
||||||
|
axisTypeface = axisTypeface,
|
||||||
|
markerTypeface = markerTypeface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<Screen.Stats.LastMonth> {
|
||||||
|
LastMonthScreen(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
lastMonthAnalysisValues = lastMonthAnalysisValues,
|
||||||
|
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||||
onBack = backStack::removeLastOrNull,
|
onBack = backStack::removeLastOrNull,
|
||||||
hoursMinutesFormat = hoursMinutesFormat,
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
hoursFormat = hoursFormat,
|
hoursFormat = hoursFormat,
|
||||||
|
|||||||
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* 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.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
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.fillMaxSize
|
||||||
|
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.ButtonDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TonalToggleButton
|
||||||
|
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.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
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.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
|
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.common.data.ExtraStore
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.mergePaddingValues
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakRatioVisualization
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakdownChart
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.HorizontalStackedBar
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.TimeColumnChart
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.sharedBoundsReveal
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToMinutes
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SharedTransitionScope.LastMonthScreen(
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
lastMonthAnalysisValues: Pair<List<Long>, Long>,
|
||||||
|
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
hoursMinutesFormat: String,
|
||||||
|
hoursFormat: String,
|
||||||
|
minutesFormat: String,
|
||||||
|
axisTypeface: Typeface,
|
||||||
|
markerTypeface: Typeface
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
|
||||||
|
var breakdownChartExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(lastMonthAnalysisValues.first) {
|
||||||
|
lastMonthSummaryAnalysisModelProducer.runTransaction {
|
||||||
|
columnSeries {
|
||||||
|
series(lastMonthAnalysisValues.first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rankList = remember(lastMonthAnalysisValues) {
|
||||||
|
val sortedIndices =
|
||||||
|
lastMonthAnalysisValues.first.indices.sortedByDescending { lastMonthAnalysisValues.first[it] }
|
||||||
|
val ranks = MutableList(lastMonthAnalysisValues.first.size) { 0 }
|
||||||
|
|
||||||
|
sortedIndices.forEachIndexed { rank, originalIndex ->
|
||||||
|
ranks[originalIndex] = rank
|
||||||
|
}
|
||||||
|
|
||||||
|
ranks
|
||||||
|
}
|
||||||
|
|
||||||
|
val focusDuration = remember(lastMonthAnalysisValues) {
|
||||||
|
lastMonthAnalysisValues.first.sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.last_month),
|
||||||
|
fontFamily = robotoFlexTopBar,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@LastMonthScreen
|
||||||
|
.rememberSharedContentState("last month heading"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text(stringResource(R.string.stats))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
FilledTonalIconButton(
|
||||||
|
onClick = onBack,
|
||||||
|
shapes = IconButtonDefaults.shapes()
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
.sharedBoundsReveal(
|
||||||
|
sharedTransitionScope = this@LastMonthScreen,
|
||||||
|
sharedContentState = this@LastMonthScreen.rememberSharedContentState(
|
||||||
|
"last month card"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||||
|
clipShape = middleListItemShape
|
||||||
|
)
|
||||||
|
) { innerPadding ->
|
||||||
|
val insets = mergePaddingValues(innerPadding, contentPadding)
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
contentPadding = insets,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
millisecondsToHoursMinutes(
|
||||||
|
focusDuration,
|
||||||
|
hoursMinutesFormat
|
||||||
|
),
|
||||||
|
style = typography.displaySmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = this@LastMonthScreen
|
||||||
|
.rememberSharedContentState("last month average focus timer"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus_per_day_avg),
|
||||||
|
style = typography.titleSmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(bottom = 5.2.dp)
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = this@LastMonthScreen
|
||||||
|
.rememberSharedContentState("focus per day average (month)"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
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()]
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = this@LastMonthScreen
|
||||||
|
.rememberSharedContentState("last month chart"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(8.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus_breakdown),
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus_breakdown_desc),
|
||||||
|
style = typography.bodySmall,
|
||||||
|
color = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { HorizontalStackedBar(lastMonthAnalysisValues.first, rankList = rankList) }
|
||||||
|
item {
|
||||||
|
Row {
|
||||||
|
lastMonthAnalysisValues.first.fastForEach {
|
||||||
|
Text(
|
||||||
|
if (it <= 60 * 60 * 1000)
|
||||||
|
millisecondsToMinutes(it, minutesFormat)
|
||||||
|
else millisecondsToHoursMinutes(it, hoursMinutesFormat),
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
val iconRotation by animateFloatAsState(
|
||||||
|
if (breakdownChartExpanded) 180f else 0f
|
||||||
|
)
|
||||||
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
TonalToggleButton(
|
||||||
|
checked = breakdownChartExpanded,
|
||||||
|
onCheckedChange = { breakdownChartExpanded = it },
|
||||||
|
modifier = Modifier.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_down),
|
||||||
|
stringResource(R.string.more_info),
|
||||||
|
modifier = Modifier.rotate(iconRotation)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
|
||||||
|
Text("Show chart")
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusBreakdownChart(
|
||||||
|
expanded = breakdownChartExpanded,
|
||||||
|
modelProducer = lastMonthSummaryAnalysisModelProducer,
|
||||||
|
modifier = Modifier.padding(top = 16.dp, bottom = 24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus_break_ratio),
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
FocusBreakRatioVisualization(
|
||||||
|
focusDuration = focusDuration,
|
||||||
|
breakDuration = lastMonthAnalysisValues.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(8.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Focus history heatmap",
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Focus history of the past year. Brighter colors represent a longer focus duration.",
|
||||||
|
style = typography.bodySmall,
|
||||||
|
color = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -81,9 +81,9 @@ import org.nsh07.pomodoro.utils.millisecondsToMinutes
|
|||||||
@Composable
|
@Composable
|
||||||
fun SharedTransitionScope.LastWeekScreen(
|
fun SharedTransitionScope.LastWeekScreen(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastWeekAnalysisValues: Pair<List<Long>, Long>,
|
focusBreakdownValues: Pair<List<Long>, Long>,
|
||||||
lastWeekSummaryValues: List<Pair<String, List<Long>>>,
|
focusHistoryValues: List<Pair<String, List<Long>>>,
|
||||||
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
mainChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
hoursMinutesFormat: String,
|
hoursMinutesFormat: String,
|
||||||
@@ -97,18 +97,18 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
|
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
|
||||||
var breakdownChartExpanded by remember { mutableStateOf(false) }
|
var breakdownChartExpanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LaunchedEffect(lastWeekAnalysisValues.first) {
|
LaunchedEffect(focusBreakdownValues.first) {
|
||||||
lastWeekSummaryAnalysisModelProducer.runTransaction {
|
lastWeekSummaryAnalysisModelProducer.runTransaction {
|
||||||
columnSeries {
|
columnSeries {
|
||||||
series(lastWeekAnalysisValues.first)
|
series(focusBreakdownValues.first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val rankList = remember(lastWeekAnalysisValues) {
|
val rankList = remember(focusBreakdownValues) {
|
||||||
val sortedIndices =
|
val sortedIndices =
|
||||||
lastWeekAnalysisValues.first.indices.sortedByDescending { lastWeekAnalysisValues.first[it] }
|
focusBreakdownValues.first.indices.sortedByDescending { focusBreakdownValues.first[it] }
|
||||||
val ranks = MutableList(lastWeekAnalysisValues.first.size) { 0 }
|
val ranks = MutableList(focusBreakdownValues.first.size) { 0 }
|
||||||
|
|
||||||
sortedIndices.forEachIndexed { rank, originalIndex ->
|
sortedIndices.forEachIndexed { rank, originalIndex ->
|
||||||
ranks[originalIndex] = rank
|
ranks[originalIndex] = rank
|
||||||
@@ -117,8 +117,8 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
ranks
|
ranks
|
||||||
}
|
}
|
||||||
|
|
||||||
val focusDuration = remember(lastWeekAnalysisValues) {
|
val focusDuration = remember(focusBreakdownValues) {
|
||||||
lastWeekAnalysisValues.first.sum()
|
focusBreakdownValues.first.sum()
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
@@ -205,14 +205,14 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
}
|
}
|
||||||
item {
|
item {
|
||||||
TimeColumnChart(
|
TimeColumnChart(
|
||||||
modelProducer = lastWeekSummaryChartData.first,
|
modelProducer = mainChartData.first,
|
||||||
hoursFormat = hoursFormat,
|
hoursFormat = hoursFormat,
|
||||||
hoursMinutesFormat = hoursMinutesFormat,
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
minutesFormat = minutesFormat,
|
minutesFormat = minutesFormat,
|
||||||
axisTypeface = axisTypeface,
|
axisTypeface = axisTypeface,
|
||||||
markerTypeface = markerTypeface,
|
markerTypeface = markerTypeface,
|
||||||
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
||||||
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
|
context.model.extraStore[mainChartData.second][x.toInt()]
|
||||||
},
|
},
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.sharedElement(
|
.sharedElement(
|
||||||
@@ -237,10 +237,10 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { HorizontalStackedBar(lastWeekAnalysisValues.first, rankList = rankList) }
|
item { HorizontalStackedBar(focusBreakdownValues.first, rankList = rankList) }
|
||||||
item {
|
item {
|
||||||
Row {
|
Row {
|
||||||
lastWeekAnalysisValues.first.fastForEach {
|
focusBreakdownValues.first.fastForEach {
|
||||||
Text(
|
Text(
|
||||||
if (it <= 60 * 60 * 1000)
|
if (it <= 60 * 60 * 1000)
|
||||||
millisecondsToMinutes(it, minutesFormat)
|
millisecondsToMinutes(it, minutesFormat)
|
||||||
@@ -290,7 +290,7 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
item {
|
item {
|
||||||
FocusBreakRatioVisualization(
|
FocusBreakRatioVisualization(
|
||||||
focusDuration = focusDuration,
|
focusDuration = focusDuration,
|
||||||
breakDuration = lastWeekAnalysisValues.second
|
breakDuration = focusBreakdownValues.second
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,7 +320,7 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lastWeekSummaryValues.fastForEach {
|
focusHistoryValues.fastForEach {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
Text(
|
Text(
|
||||||
it.first,
|
it.first,
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
lastWeekAverageFocusTimes: List<Long>,
|
lastWeekAverageFocusTimes: List<Long>,
|
||||||
lastMonthAverageFocusTimes: List<Int>,
|
lastMonthAverageFocusTimes: List<Long>,
|
||||||
lastYearAverageFocusTimes: List<Int>,
|
lastYearAverageFocusTimes: List<Int>,
|
||||||
generateSampleData: () -> Unit,
|
generateSampleData: () -> Unit,
|
||||||
hoursMinutesFormat: String,
|
hoursMinutesFormat: String,
|
||||||
@@ -303,9 +303,17 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBoundsReveal(
|
||||||
|
sharedTransitionScope = this@StatsMainScreen,
|
||||||
|
sharedContentState = this@StatsMainScreen.rememberSharedContentState(
|
||||||
|
"last month card"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||||
|
clipShape = middleListItemShape
|
||||||
|
)
|
||||||
.clip(middleListItemShape)
|
.clip(middleListItemShape)
|
||||||
.background(listItemColors.containerColor)
|
.background(listItemColors.containerColor)
|
||||||
.clickable {}
|
.clickable { onNavigate(Screen.Stats.LastMonth) }
|
||||||
.padding(
|
.padding(
|
||||||
start = 20.dp,
|
start = 20.dp,
|
||||||
top = 20.dp,
|
top = 20.dp,
|
||||||
@@ -314,7 +322,12 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
stringResource(R.string.last_month),
|
stringResource(R.string.last_month),
|
||||||
style = typography.headlineSmall
|
style = typography.headlineSmall,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@StatsMainScreen
|
||||||
|
.rememberSharedContentState("last month heading"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -324,16 +337,28 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
Text(
|
Text(
|
||||||
millisecondsToHoursMinutes(
|
millisecondsToHoursMinutes(
|
||||||
remember(lastMonthAverageFocusTimes) {
|
remember(lastMonthAverageFocusTimes) {
|
||||||
lastMonthAverageFocusTimes.sum().toLong()
|
lastMonthAverageFocusTimes.sum()
|
||||||
},
|
},
|
||||||
hoursMinutesFormat
|
hoursMinutesFormat
|
||||||
),
|
),
|
||||||
style = typography.displaySmall
|
style = typography.displaySmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = this@StatsMainScreen
|
||||||
|
.rememberSharedContentState("last month average focus timer"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.focus_per_day_avg),
|
text = stringResource(R.string.focus_per_day_avg),
|
||||||
style = typography.titleSmall,
|
style = typography.titleSmall,
|
||||||
modifier = Modifier.padding(bottom = 5.2.dp)
|
modifier = Modifier
|
||||||
|
.padding(bottom = 5.2.dp)
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = this@StatsMainScreen
|
||||||
|
.rememberSharedContentState("focus per day average (month)"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +372,13 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
thickness = 8.dp,
|
thickness = 8.dp,
|
||||||
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
||||||
context.model.extraStore[lastMonthSummaryChartData.second][x.toInt()]
|
context.model.extraStore[lastMonthSummaryChartData.second][x.toInt()]
|
||||||
}
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedElement(
|
||||||
|
sharedContentState = this@StatsMainScreen
|
||||||
|
.rememberSharedContentState("last month chart"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -158,21 +158,24 @@ class StatsViewModel(
|
|||||||
initialValue = lastMonthSummary
|
initialValue = lastMonthSummary
|
||||||
)
|
)
|
||||||
|
|
||||||
val lastMonthAverageFocusTimes: StateFlow<List<Int>> =
|
val lastMonthAverageFocusTimes: StateFlow<Pair<List<Long>, Long>> =
|
||||||
statRepository.getLastNDaysAverageFocusTimes(30)
|
statRepository.getLastNDaysAverageFocusTimes(30)
|
||||||
.map {
|
.map {
|
||||||
listOf(
|
Pair(
|
||||||
it?.focusTimeQ1?.toInt() ?: 0,
|
listOf(
|
||||||
it?.focusTimeQ2?.toInt() ?: 0,
|
it?.focusTimeQ1 ?: 0L,
|
||||||
it?.focusTimeQ3?.toInt() ?: 0,
|
it?.focusTimeQ2 ?: 0L,
|
||||||
it?.focusTimeQ4?.toInt() ?: 0
|
it?.focusTimeQ3 ?: 0L,
|
||||||
|
it?.focusTimeQ4 ?: 0L
|
||||||
|
),
|
||||||
|
it?.breakTime ?: 0L
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
.flowOn(Dispatchers.IO)
|
.flowOn(Dispatchers.IO)
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
started = SharingStarted.WhileSubscribed(5000),
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
initialValue = listOf(0, 0, 0, 0)
|
initialValue = Pair(listOf(0L, 0L, 0L, 0L), 0L)
|
||||||
)
|
)
|
||||||
|
|
||||||
val lastYearSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
val lastYearSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
|
|||||||
Reference in New Issue
Block a user