Merge pull request #72 from nsh07/year-stats

feat: implement a time line chart composable
This commit is contained in:
Nishant Mishra
2025-10-16 00:10:45 +05:30
committed by GitHub
5 changed files with 392 additions and 101 deletions

View File

@@ -35,6 +35,7 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -56,10 +57,8 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.runBlocking
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.data.StatFocusTime
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -72,20 +71,25 @@ fun StatsScreenRoot(
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory) viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) { ) {
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null) val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
val lastWeekAverageFocusTimes by viewModel
.lastWeekAverageFocusTimes.collectAsStateWithLifecycle(null) val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
val lastMonthAverageFocusTimes by viewModel val lastWeekAnalysisValues by viewModel.lastWeekAverageFocusTimes.collectAsStateWithLifecycle()
.lastMonthAverageFocusTimes.collectAsStateWithLifecycle(null)
val lastMonthSummaryChartData by viewModel.lastMonthSummaryChartData.collectAsStateWithLifecycle()
val lastMonthAnalysisValues by viewModel.lastMonthAverageFocusTimes.collectAsStateWithLifecycle()
val lastYearSummaryChartData by viewModel.lastYearSummaryChartData.collectAsStateWithLifecycle()
val lastYearAnalysisValues by viewModel.lastYearAverageFocusTimes.collectAsStateWithLifecycle()
StatsScreen( StatsScreen(
contentPadding = contentPadding, contentPadding = contentPadding,
lastWeekSummaryChartData = remember { viewModel.lastWeekSummaryChartData }, lastWeekSummaryChartData = lastWeekSummaryChartData,
lastWeekSummaryAnalysisModelProducer = remember { viewModel.lastWeekSummaryAnalysisModelProducer }, lastMonthSummaryChartData = lastMonthSummaryChartData,
lastMonthSummaryChartData = remember { viewModel.lastMonthSummaryChartData }, lastYearSummaryChartData = lastYearSummaryChartData,
lastMonthSummaryAnalysisModelProducer = remember { viewModel.lastMonthSummaryAnalysisModelProducer },
todayStat = todayStat, todayStat = todayStat,
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes, lastWeekAverageFocusTimes = lastWeekAnalysisValues,
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes, lastMonthAverageFocusTimes = lastMonthAnalysisValues,
lastYearAverageFocusTimes = lastYearAnalysisValues,
modifier = modifier modifier = modifier
) )
} }
@@ -95,12 +99,12 @@ fun StatsScreenRoot(
fun StatsScreen( fun StatsScreen(
contentPadding: PaddingValues, contentPadding: PaddingValues,
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer,
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>, lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer, lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
todayStat: Stat?, todayStat: Stat?,
lastWeekAverageFocusTimes: StatFocusTime?, lastWeekAverageFocusTimes: List<Int>,
lastMonthAverageFocusTimes: StatFocusTime?, lastMonthAverageFocusTimes: List<Int>,
lastYearAverageFocusTimes: List<Int>,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@@ -108,6 +112,25 @@ fun StatsScreen(
var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) } var lastWeekStatExpanded by rememberSaveable { mutableStateOf(false) }
var lastMonthStatExpanded by rememberSaveable { mutableStateOf(false) } var lastMonthStatExpanded by rememberSaveable { mutableStateOf(false) }
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
LaunchedEffect(lastWeekAverageFocusTimes) {
lastWeekSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(lastWeekAverageFocusTimes)
}
}
}
LaunchedEffect(lastMonthAverageFocusTimes) {
lastMonthSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(lastMonthAverageFocusTimes)
}
}
}
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection) modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
@@ -218,7 +241,11 @@ fun StatsScreen(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Text( Text(
millisecondsToHoursMinutes(lastWeekAverageFocusTimes?.total() ?: 0), millisecondsToHoursMinutes(
remember(lastWeekAverageFocusTimes) {
lastWeekAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall, style = typography.displaySmall,
fontFamily = openRundeClock fontFamily = openRundeClock
) )
@@ -290,7 +317,11 @@ fun StatsScreen(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
Text( Text(
millisecondsToHoursMinutes(lastMonthAverageFocusTimes?.total() ?: 0), millisecondsToHoursMinutes(
remember(lastMonthAverageFocusTimes) {
lastMonthAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall, style = typography.displaySmall,
fontFamily = openRundeClock fontFamily = openRundeClock
) )
@@ -343,8 +374,51 @@ fun StatsScreen(
modifier = Modifier.padding(horizontal = 32.dp) modifier = Modifier.padding(horizontal = 32.dp)
) )
} }
Spacer(Modifier.height(16.dp))
} }
item { Spacer(Modifier) }
item {
Text(
stringResource(R.string.last_year),
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(lastYearAverageFocusTimes) {
lastYearAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall,
fontFamily = openRundeClock
)
Text(
text = stringResource(R.string.focus_per_day_avg),
style = typography.titleSmall,
modifier = Modifier.padding(bottom = 6.3.dp)
)
}
}
item {
TimeLineChart(
lastYearSummaryChartData.first,
modifier = Modifier.padding(start = 16.dp),
xValueFormatter = CartesianValueFormatter { context, x, _ ->
context.model.extraStore[lastYearSummaryChartData.second][x.toInt()]
}
)
}
item { Spacer(Modifier.height(16.dp)) }
} }
} }
} }
@@ -356,23 +430,25 @@ fun StatsScreen(
@Composable @Composable
fun StatsScreenPreview() { fun StatsScreenPreview() {
val modelProducer = remember { CartesianChartModelProducer() } val modelProducer = remember { CartesianChartModelProducer() }
val keys = remember { ExtraStore.Key<List<String>>() }
runBlocking { LaunchedEffect(Unit) {
modelProducer.runTransaction { modelProducer.runTransaction {
columnSeries { columnSeries {
series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7) series(5, 6, 5, 2, 11, 8, 5, 2, 15, 11, 8, 13, 12, 10, 2, 7)
} }
extras { it[keys] = listOf("M", "T", "W", "T", "F", "S", "S") }
} }
} }
StatsScreen( StatsScreen(
PaddingValues(), PaddingValues(),
Pair(modelProducer, ExtraStore.Key()), Pair(modelProducer, keys),
modelProducer, Pair(modelProducer, keys),
Pair(modelProducer, ExtraStore.Key()), Pair(modelProducer, keys),
modelProducer,
null, null,
null, listOf(0, 0, 0, 0),
null listOf(0, 0, 0, 0),
listOf(0, 0, 0, 0)
) )
} }

View File

@@ -10,8 +10,12 @@ package org.nsh07.pomodoro.ui.statsScreen
import androidx.compose.animation.core.AnimationSpec import androidx.compose.animation.core.AnimationSpec
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme.motionScheme import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
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.CartesianChartHost
@@ -25,20 +29,23 @@ import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent
import com.patrykandpatrick.vico.compose.common.fill import com.patrykandpatrick.vico.compose.common.fill
import com.patrykandpatrick.vico.compose.common.vicoTheme import com.patrykandpatrick.vico.compose.common.vicoTheme
import com.patrykandpatrick.vico.compose.m3.common.rememberM3VicoTheme import com.patrykandpatrick.vico.compose.m3.common.rememberM3VicoTheme
import com.patrykandpatrick.vico.core.cartesian.FadingEdges
import com.patrykandpatrick.vico.core.cartesian.Zoom import com.patrykandpatrick.vico.core.cartesian.Zoom
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer
import com.patrykandpatrick.vico.core.common.Fill import com.patrykandpatrick.vico.core.common.Fill
import com.patrykandpatrick.vico.core.common.shape.CorneredShape import com.patrykandpatrick.vico.core.common.shape.CorneredShape
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.millisecondsToHours import org.nsh07.pomodoro.utils.millisecondsToHours
import org.nsh07.pomodoro.utils.millisecondsToMinutes import org.nsh07.pomodoro.utils.millisecondsToMinutes
@OptIn(ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
internal fun TimeColumnChart( fun TimeColumnChart(
modelProducer: CartesianChartModelProducer, modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
thickness: Dp = 40.dp, thickness: Dp = 40.dp,
@@ -80,7 +87,8 @@ internal fun TimeColumnChart(
tick = rememberLineComponent(Fill.Transparent), tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent), guideline = rememberLineComponent(Fill.Transparent),
valueFormatter = xValueFormatter valueFormatter = xValueFormatter
) ),
fadingEdges = FadingEdges()
), ),
modelProducer = modelProducer, modelProducer = modelProducer,
zoomState = rememberVicoZoomState( zoomState = rememberVicoZoomState(
@@ -92,4 +100,26 @@ internal fun TimeColumnChart(
modifier = modifier, modifier = modifier,
) )
} }
} }
@Preview
@Composable
private fun TimeColumnChartPreview() {
val modelProducer = remember { CartesianChartModelProducer() }
val values = mutableListOf<Int>()
LaunchedEffect(Unit) {
repeat(30) {
values.add((0..120).random() * 60 * 1000)
}
modelProducer.runTransaction {
columnSeries {
series(values)
}
}
}
TomatoTheme {
Surface {
TimeColumnChart(thickness = 8.dp, modelProducer = modelProducer)
}
}
}

View File

@@ -0,0 +1,140 @@
/*
* 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.statsScreen
import androidx.compose.animation.core.AnimationSpec
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.tooling.preview.Preview
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
import com.patrykandpatrick.vico.compose.cartesian.axis.rememberStart
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLine
import com.patrykandpatrick.vico.compose.cartesian.layer.rememberLineCartesianLayer
import com.patrykandpatrick.vico.compose.cartesian.rememberCartesianChart
import com.patrykandpatrick.vico.compose.cartesian.rememberVicoZoomState
import com.patrykandpatrick.vico.compose.common.ProvideVicoTheme
import com.patrykandpatrick.vico.compose.common.component.rememberLineComponent
import com.patrykandpatrick.vico.compose.common.fill
import com.patrykandpatrick.vico.compose.common.vicoTheme
import com.patrykandpatrick.vico.compose.m3.common.rememberM3VicoTheme
import com.patrykandpatrick.vico.core.cartesian.FadingEdges
import com.patrykandpatrick.vico.core.cartesian.Zoom
import com.patrykandpatrick.vico.core.cartesian.axis.HorizontalAxis
import com.patrykandpatrick.vico.core.cartesian.axis.VerticalAxis
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer
import com.patrykandpatrick.vico.core.cartesian.layer.LineCartesianLayer.LineFill.Companion.single
import com.patrykandpatrick.vico.core.common.Fill
import com.patrykandpatrick.vico.core.common.shader.ShaderProvider
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.millisecondsToHours
import org.nsh07.pomodoro.utils.millisecondsToMinutes
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TimeLineChart(
modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier,
thickness: Float = 2f,
pointSpacing: Dp = 12.dp,
xValueFormatter: CartesianValueFormatter = CartesianValueFormatter.Default,
yValueFormatter: CartesianValueFormatter = CartesianValueFormatter { _, value, _ ->
if (value >= 60 * 60 * 1000) {
millisecondsToHours(value.toLong())
} else {
millisecondsToMinutes(value.toLong())
}
},
animationSpec: AnimationSpec<Float>? = motionScheme.slowEffectsSpec()
) {
ProvideVicoTheme(rememberM3VicoTheme()) {
CartesianChartHost(
chart =
rememberCartesianChart(
rememberLineCartesianLayer(
LineCartesianLayer.LineProvider.series(
vicoTheme.lineCartesianLayerColors.map { color ->
LineCartesianLayer.rememberLine(
fill = single(fill(color)),
stroke = LineCartesianLayer.LineStroke.Continuous(
thicknessDp = thickness,
),
areaFill = LineCartesianLayer.AreaFill.single(
fill(
ShaderProvider.verticalGradient(
color.toArgb(),
Color.Transparent.toArgb()
)
)
),
pointConnector = LineCartesianLayer.PointConnector.cubic(0.5f)
)
}
),
pointSpacing = pointSpacing
),
startAxis = VerticalAxis.rememberStart(
line = rememberLineComponent(Fill.Transparent),
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent),
valueFormatter = yValueFormatter
),
bottomAxis = HorizontalAxis.rememberBottom(
rememberLineComponent(Fill.Transparent),
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent),
valueFormatter = xValueFormatter
),
fadingEdges = FadingEdges()
),
modelProducer = modelProducer,
zoomState = rememberVicoZoomState(
zoomEnabled = true,
initialZoom = Zoom.fixed(),
minZoom = Zoom.min(Zoom.Content, Zoom.fixed())
),
animationSpec = animationSpec,
modifier = modifier,
)
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
private fun TimeLineChartPreview() {
val modelProducer = remember { CartesianChartModelProducer() }
val values = mutableListOf<Int>()
LaunchedEffect(Unit) {
repeat(365) {
values.add((0..120).random() * 60 * 1000)
}
modelProducer.runTransaction {
lineSeries {
series(values)
}
}
}
TomatoTheme {
Surface {
TimeLineChart(modelProducer = modelProducer)
}
}
}

View File

@@ -15,12 +15,16 @@ import androidx.lifecycle.viewmodel.initializer
import androidx.lifecycle.viewmodel.viewModelFactory import androidx.lifecycle.viewmodel.viewModelFactory
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.StatRepository import org.nsh07.pomodoro.data.StatRepository
import java.time.format.DateTimeFormatter
import java.time.format.TextStyle import java.time.format.TextStyle
import java.util.Locale import java.util.Locale
@@ -29,83 +33,123 @@ class StatsViewModel(
) : ViewModel() { ) : ViewModel() {
val todayStat = statRepository.getTodayStat().distinctUntilChanged() val todayStat = statRepository.getTodayStat().distinctUntilChanged()
private val lastWeekStatsSummary = statRepository.getLastNDaysStatsSummary(7)
val lastWeekAverageFocusTimes =
statRepository.getLastNDaysAverageFocusTimes(7).distinctUntilChanged()
private val lastMonthStatsSummary = statRepository.getLastNDaysStatsSummary(30)
val lastMonthAverageFocusTimes =
statRepository.getLastNDaysAverageFocusTimes(30).distinctUntilChanged()
val lastWeekSummaryChartData = private val lastWeekSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>()) Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer() private val lastMonthSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
val lastMonthSummaryChartData = private val lastYearSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>()) Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
val lastMonthSummaryAnalysisModelProducer = CartesianChartModelProducer()
init { private val yearDayFormatter = DateTimeFormatter.ofPattern("d MMM")
viewModelScope.launch(Dispatchers.IO) {
lastWeekStatsSummary val lastWeekSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
.collect { list -> statRepository.getLastNDaysStatsSummary(7)
// reversing is required because we need ascending order while the DB returns descending order .map { list ->
val reversed = list.reversed() // reversing is required because we need ascending order while the DB returns descending order
val keys = reversed.map { val reversed = list.reversed()
it.date.dayOfWeek.getDisplayName( val keys = reversed.map {
TextStyle.NARROW, it.date.dayOfWeek.getDisplayName(
Locale.getDefault() TextStyle.NARROW,
) Locale.getDefault()
} )
val values = reversed.map { it.focusTime }
lastWeekSummaryChartData.first.runTransaction {
columnSeries { series(values) }
extras { it[lastWeekSummaryChartData.second] = keys }
}
} }
} val values = reversed.map { it.focusTime }
viewModelScope.launch(Dispatchers.IO) { lastWeekSummary.first.runTransaction {
lastWeekAverageFocusTimes columnSeries { series(values) }
.collect { extras { it[lastWeekSummary.second] = keys }
lastWeekSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0,
it?.focusTimeQ3 ?: 0,
it?.focusTimeQ4 ?: 0
)
}
}
} }
} lastWeekSummary
viewModelScope.launch(Dispatchers.IO) { }
lastMonthStatsSummary .stateIn(
.collect { list -> scope = viewModelScope,
val reversed = list.reversed() started = SharingStarted.WhileSubscribed(5000),
val keys = reversed.map { it.date.dayOfMonth.toString() } initialValue = lastWeekSummary
val values = reversed.map { it.focusTime } )
lastMonthSummaryChartData.first.runTransaction {
columnSeries { series(values) } val lastWeekAverageFocusTimes: StateFlow<List<Int>> =
extras { it[lastMonthSummaryChartData.second] = keys } statRepository.getLastNDaysAverageFocusTimes(7)
} .map {
listOf(
it?.focusTimeQ1?.toInt() ?: 0,
it?.focusTimeQ2?.toInt() ?: 0,
it?.focusTimeQ3?.toInt() ?: 0,
it?.focusTimeQ4?.toInt() ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = listOf(0, 0, 0, 0)
)
val lastMonthSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
statRepository.getLastNDaysStatsSummary(30)
.map { list ->
val reversed = list.reversed()
val keys = reversed.map { it.date.dayOfMonth.toString() }
val values = reversed.map { it.focusTime }
lastMonthSummary.first.runTransaction {
columnSeries { series(values) }
extras { it[lastMonthSummary.second] = keys }
} }
} lastMonthSummary
viewModelScope.launch(Dispatchers.IO) { }
lastMonthAverageFocusTimes .stateIn(
.collect { scope = viewModelScope,
lastMonthSummaryAnalysisModelProducer.runTransaction { started = SharingStarted.WhileSubscribed(5000),
columnSeries { initialValue = lastMonthSummary
series( )
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0, val lastMonthAverageFocusTimes: StateFlow<List<Int>> =
it?.focusTimeQ3 ?: 0, statRepository.getLastNDaysAverageFocusTimes(30)
it?.focusTimeQ4 ?: 0 .map {
) listOf(
} it?.focusTimeQ1?.toInt() ?: 0,
} it?.focusTimeQ2?.toInt() ?: 0,
it?.focusTimeQ3?.toInt() ?: 0,
it?.focusTimeQ4?.toInt() ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = listOf(0, 0, 0, 0)
)
val lastYearSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
statRepository.getLastNDaysStatsSummary(365)
.map { list ->
val reversed = list.reversed()
val keys = reversed.map { it.date.format(yearDayFormatter) }
val values = reversed.map { it.focusTime }
lastYearSummary.first.runTransaction {
lineSeries { series(values) }
extras { it[lastYearSummary.second] = keys }
} }
} lastYearSummary
} }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = lastYearSummary
)
val lastYearAverageFocusTimes: StateFlow<List<Int>> =
statRepository.getLastNDaysAverageFocusTimes(365)
.map {
listOf(
it?.focusTimeQ1?.toInt() ?: 0,
it?.focusTimeQ2?.toInt() ?: 0,
it?.focusTimeQ3?.toInt() ?: 0,
it?.focusTimeQ4?.toInt() ?: 0
)
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = listOf(0, 0, 0, 0)
)
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {

View File

@@ -56,4 +56,5 @@
<string name="up_next">Up next</string> <string name="up_next">Up next</string>
<string name="timer">Timer</string> <string name="timer">Timer</string>
<string name="timer_progress">Timer progress</string> <string name="timer_progress">Timer progress</string>
<string name="last_year">Last year</string>
</resources> </resources>