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.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
@@ -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.columnSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.runBlocking
import org.nsh07.pomodoro.R
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.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -72,20 +71,25 @@ fun StatsScreenRoot(
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) {
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
val lastWeekAverageFocusTimes by viewModel
.lastWeekAverageFocusTimes.collectAsStateWithLifecycle(null)
val lastMonthAverageFocusTimes by viewModel
.lastMonthAverageFocusTimes.collectAsStateWithLifecycle(null)
val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
val lastWeekAnalysisValues by viewModel.lastWeekAverageFocusTimes.collectAsStateWithLifecycle()
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(
contentPadding = contentPadding,
lastWeekSummaryChartData = remember { viewModel.lastWeekSummaryChartData },
lastWeekSummaryAnalysisModelProducer = remember { viewModel.lastWeekSummaryAnalysisModelProducer },
lastMonthSummaryChartData = remember { viewModel.lastMonthSummaryChartData },
lastMonthSummaryAnalysisModelProducer = remember { viewModel.lastMonthSummaryAnalysisModelProducer },
lastWeekSummaryChartData = lastWeekSummaryChartData,
lastMonthSummaryChartData = lastMonthSummaryChartData,
lastYearSummaryChartData = lastYearSummaryChartData,
todayStat = todayStat,
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
lastWeekAverageFocusTimes = lastWeekAnalysisValues,
lastMonthAverageFocusTimes = lastMonthAnalysisValues,
lastYearAverageFocusTimes = lastYearAnalysisValues,
modifier = modifier
)
}
@@ -95,12 +99,12 @@ fun StatsScreenRoot(
fun StatsScreen(
contentPadding: PaddingValues,
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastWeekSummaryAnalysisModelProducer: CartesianChartModelProducer,
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
lastMonthSummaryAnalysisModelProducer: CartesianChartModelProducer,
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
todayStat: Stat?,
lastWeekAverageFocusTimes: StatFocusTime?,
lastMonthAverageFocusTimes: StatFocusTime?,
lastWeekAverageFocusTimes: List<Int>,
lastMonthAverageFocusTimes: List<Int>,
lastYearAverageFocusTimes: List<Int>,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
@@ -108,6 +112,25 @@ fun StatsScreen(
var lastWeekStatExpanded 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(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
@@ -218,7 +241,11 @@ fun StatsScreen(
.padding(horizontal = 16.dp)
) {
Text(
millisecondsToHoursMinutes(lastWeekAverageFocusTimes?.total() ?: 0),
millisecondsToHoursMinutes(
remember(lastWeekAverageFocusTimes) {
lastWeekAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall,
fontFamily = openRundeClock
)
@@ -290,7 +317,11 @@ fun StatsScreen(
.padding(horizontal = 16.dp)
) {
Text(
millisecondsToHoursMinutes(lastMonthAverageFocusTimes?.total() ?: 0),
millisecondsToHoursMinutes(
remember(lastMonthAverageFocusTimes) {
lastMonthAverageFocusTimes.sum().toLong()
}
),
style = typography.displaySmall,
fontFamily = openRundeClock
)
@@ -343,8 +374,51 @@ fun StatsScreen(
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
fun StatsScreenPreview() {
val modelProducer = remember { CartesianChartModelProducer() }
val keys = remember { ExtraStore.Key<List<String>>() }
runBlocking {
LaunchedEffect(Unit) {
modelProducer.runTransaction {
columnSeries {
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(
PaddingValues(),
Pair(modelProducer, ExtraStore.Key()),
modelProducer,
Pair(modelProducer, ExtraStore.Key()),
modelProducer,
Pair(modelProducer, keys),
Pair(modelProducer, keys),
Pair(modelProducer, keys),
null,
null,
null
listOf(0, 0, 0, 0),
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.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.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
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.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.columnSeries
import com.patrykandpatrick.vico.core.cartesian.layer.ColumnCartesianLayer
import com.patrykandpatrick.vico.core.common.Fill
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.millisecondsToMinutes
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun TimeColumnChart(
fun TimeColumnChart(
modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier,
thickness: Dp = 40.dp,
@@ -80,7 +87,8 @@ internal fun TimeColumnChart(
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent),
valueFormatter = xValueFormatter
)
),
fadingEdges = FadingEdges()
),
modelProducer = modelProducer,
zoomState = rememberVicoZoomState(
@@ -92,4 +100,26 @@ internal fun TimeColumnChart(
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 com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
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.data.StatRepository
import java.time.format.DateTimeFormatter
import java.time.format.TextStyle
import java.util.Locale
@@ -29,83 +33,123 @@ class StatsViewModel(
) : ViewModel() {
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>>())
val lastWeekSummaryAnalysisModelProducer = CartesianChartModelProducer()
val lastMonthSummaryChartData =
private val lastMonthSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
private val lastYearSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
val lastMonthSummaryAnalysisModelProducer = CartesianChartModelProducer()
init {
viewModelScope.launch(Dispatchers.IO) {
lastWeekStatsSummary
.collect { list ->
// reversing is required because we need ascending order while the DB returns descending order
val reversed = list.reversed()
val keys = reversed.map {
it.date.dayOfWeek.getDisplayName(
TextStyle.NARROW,
Locale.getDefault()
)
}
val values = reversed.map { it.focusTime }
lastWeekSummaryChartData.first.runTransaction {
columnSeries { series(values) }
extras { it[lastWeekSummaryChartData.second] = keys }
}
private val yearDayFormatter = DateTimeFormatter.ofPattern("d MMM")
val lastWeekSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
statRepository.getLastNDaysStatsSummary(7)
.map { list ->
// reversing is required because we need ascending order while the DB returns descending order
val reversed = list.reversed()
val keys = reversed.map {
it.date.dayOfWeek.getDisplayName(
TextStyle.NARROW,
Locale.getDefault()
)
}
}
viewModelScope.launch(Dispatchers.IO) {
lastWeekAverageFocusTimes
.collect {
lastWeekSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0,
it?.focusTimeQ3 ?: 0,
it?.focusTimeQ4 ?: 0
)
}
}
val values = reversed.map { it.focusTime }
lastWeekSummary.first.runTransaction {
columnSeries { series(values) }
extras { it[lastWeekSummary.second] = keys }
}
}
viewModelScope.launch(Dispatchers.IO) {
lastMonthStatsSummary
.collect { list ->
val reversed = list.reversed()
val keys = reversed.map { it.date.dayOfMonth.toString() }
val values = reversed.map { it.focusTime }
lastMonthSummaryChartData.first.runTransaction {
columnSeries { series(values) }
extras { it[lastMonthSummaryChartData.second] = keys }
}
lastWeekSummary
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = lastWeekSummary
)
val lastWeekAverageFocusTimes: StateFlow<List<Int>> =
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 }
}
}
viewModelScope.launch(Dispatchers.IO) {
lastMonthAverageFocusTimes
.collect {
lastMonthSummaryAnalysisModelProducer.runTransaction {
columnSeries {
series(
it?.focusTimeQ1 ?: 0,
it?.focusTimeQ2 ?: 0,
it?.focusTimeQ3 ?: 0,
it?.focusTimeQ4 ?: 0
)
}
}
lastMonthSummary
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = lastMonthSummary
)
val lastMonthAverageFocusTimes: StateFlow<List<Int>> =
statRepository.getLastNDaysAverageFocusTimes(30)
.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 {
val Factory: ViewModelProvider.Factory = viewModelFactory {

View File

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