feat(stats): fully implement last week screen
This commit is contained in:
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* 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.data
|
package org.nsh07.pomodoro.data
|
||||||
@@ -29,17 +39,10 @@ data class Stat(
|
|||||||
fun totalFocusTime() = focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4
|
fun totalFocusTime() = focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4
|
||||||
}
|
}
|
||||||
|
|
||||||
data class StatSummary(
|
data class StatTime(
|
||||||
val date: LocalDate,
|
|
||||||
val focusTime: Long,
|
|
||||||
val breakTime: Long
|
|
||||||
)
|
|
||||||
|
|
||||||
data class StatFocusTime(
|
|
||||||
val focusTimeQ1: Long,
|
val focusTimeQ1: Long,
|
||||||
val focusTimeQ2: Long,
|
val focusTimeQ2: Long,
|
||||||
val focusTimeQ3: Long,
|
val focusTimeQ3: Long,
|
||||||
val focusTimeQ4: Long
|
val focusTimeQ4: Long,
|
||||||
) {
|
val breakTime: Long,
|
||||||
fun total() = focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4
|
)
|
||||||
}
|
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* 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.data
|
package org.nsh07.pomodoro.data
|
||||||
@@ -37,23 +47,24 @@ interface StatDao {
|
|||||||
@Query("SELECT * FROM stat WHERE date = :date")
|
@Query("SELECT * FROM stat WHERE date = :date")
|
||||||
fun getStat(date: LocalDate): Flow<Stat?>
|
fun getStat(date: LocalDate): Flow<Stat?>
|
||||||
|
|
||||||
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT :n")
|
@Query("SELECT date, focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4, breakTime FROM stat ORDER BY date DESC LIMIT :n")
|
||||||
fun getLastNDaysStatsSummary(n: Int): Flow<List<StatSummary>>
|
fun getLastNDaysStats(n: Int): Flow<List<Stat>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT " +
|
"SELECT " +
|
||||||
"AVG(focusTimeQ1) AS focusTimeQ1, " +
|
"AVG(focusTimeQ1) AS focusTimeQ1, " +
|
||||||
"AVG(focusTimeQ2) AS focusTimeQ2, " +
|
"AVG(focusTimeQ2) AS focusTimeQ2, " +
|
||||||
"AVG(focusTimeQ3) AS focusTimeQ3, " +
|
"AVG(focusTimeQ3) AS focusTimeQ3, " +
|
||||||
"AVG(focusTimeQ4) AS focusTimeQ4 " +
|
"AVG(focusTimeQ4) AS focusTimeQ4, " +
|
||||||
|
"AVG(breakTime) AS breakTime " +
|
||||||
"FROM (" +
|
"FROM (" +
|
||||||
"SELECT * FROM (" +
|
"SELECT * FROM (" +
|
||||||
"SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4 FROM stat ORDER BY date DESC LIMIT :n" +
|
"SELECT focusTimeQ1, focusTimeQ2, focusTimeQ3, focusTimeQ4, breakTime FROM stat ORDER BY date DESC LIMIT :n" +
|
||||||
") " +
|
") " +
|
||||||
"WHERE focusTimeQ1 != 0 OR focusTimeQ2 != 0 OR focusTimeQ3 != 0 OR focusTimeQ4 != 0 " +
|
"WHERE focusTimeQ1 != 0 OR focusTimeQ2 != 0 OR focusTimeQ3 != 0 OR focusTimeQ4 != 0 " +
|
||||||
")"
|
")"
|
||||||
)
|
)
|
||||||
fun getLastNDaysAvgFocusTimes(n: Int): Flow<StatFocusTime?>
|
fun getLastNDaysAvgStats(n: Int): Flow<StatTime?>
|
||||||
|
|
||||||
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
||||||
suspend fun statExists(date: LocalDate): Boolean
|
suspend fun statExists(date: LocalDate): Boolean
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* 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.data
|
package org.nsh07.pomodoro.data
|
||||||
@@ -28,9 +38,9 @@ interface StatRepository {
|
|||||||
|
|
||||||
fun getTodayStat(): Flow<Stat?>
|
fun getTodayStat(): Flow<Stat?>
|
||||||
|
|
||||||
fun getLastNDaysStatsSummary(n: Int): Flow<List<StatSummary>>
|
fun getLastNDaysStats(n: Int): Flow<List<Stat>>
|
||||||
|
|
||||||
fun getLastNDaysAverageFocusTimes(n: Int): Flow<StatFocusTime?>
|
fun getLastNDaysAverageFocusTimes(n: Int): Flow<StatTime?>
|
||||||
|
|
||||||
suspend fun getLastDate(): LocalDate?
|
suspend fun getLastDate(): LocalDate?
|
||||||
}
|
}
|
||||||
@@ -101,11 +111,11 @@ class AppStatRepository(
|
|||||||
return statDao.getStat(currentDate)
|
return statDao.getStat(currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getLastNDaysStatsSummary(n: Int): Flow<List<StatSummary>> =
|
override fun getLastNDaysStats(n: Int): Flow<List<Stat>> =
|
||||||
statDao.getLastNDaysStatsSummary(n)
|
statDao.getLastNDaysStats(n)
|
||||||
|
|
||||||
override fun getLastNDaysAverageFocusTimes(n: Int): Flow<StatFocusTime?> =
|
override fun getLastNDaysAverageFocusTimes(n: Int): Flow<StatTime?> =
|
||||||
statDao.getLastNDaysAvgFocusTimes(n)
|
statDao.getLastNDaysAvgStats(n)
|
||||||
|
|
||||||
override suspend fun getLastDate(): LocalDate? = statDao.getLastDate()
|
override suspend fun getLastDate(): LocalDate? = statDao.getLastDate()
|
||||||
}
|
}
|
||||||
@@ -62,6 +62,7 @@ fun StatsScreenRoot(
|
|||||||
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
|
val todayStat by viewModel.todayStat.collectAsStateWithLifecycle(null)
|
||||||
|
|
||||||
val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
|
val lastWeekSummaryChartData by viewModel.lastWeekSummaryChartData.collectAsStateWithLifecycle()
|
||||||
|
val lastWeekSummaryValues by viewModel.lastWeekStats.collectAsStateWithLifecycle()
|
||||||
val lastWeekAnalysisValues by viewModel.lastWeekAverageFocusTimes.collectAsStateWithLifecycle()
|
val lastWeekAnalysisValues by viewModel.lastWeekAverageFocusTimes.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val lastMonthSummaryChartData by viewModel.lastMonthSummaryChartData.collectAsStateWithLifecycle()
|
val lastMonthSummaryChartData by viewModel.lastMonthSummaryChartData.collectAsStateWithLifecycle()
|
||||||
@@ -74,10 +75,11 @@ fun StatsScreenRoot(
|
|||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
backStack = backStack,
|
backStack = backStack,
|
||||||
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||||
|
lastWeekSummaryValues = lastWeekSummaryValues,
|
||||||
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
lastWeekAverageFocusTimes = lastWeekAnalysisValues,
|
lastWeekAnalysisValues = lastWeekAnalysisValues,
|
||||||
lastMonthAverageFocusTimes = lastMonthAnalysisValues,
|
lastMonthAverageFocusTimes = lastMonthAnalysisValues,
|
||||||
lastYearAverageFocusTimes = lastYearAnalysisValues,
|
lastYearAverageFocusTimes = lastYearAnalysisValues,
|
||||||
generateSampleData = viewModel::generateSampleData,
|
generateSampleData = viewModel::generateSampleData,
|
||||||
@@ -94,10 +96,11 @@ fun StatsScreen(
|
|||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
backStack: SnapshotStateList<Screen.Stats>,
|
backStack: SnapshotStateList<Screen.Stats>,
|
||||||
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
|
lastWeekSummaryValues: List<Pair<String, List<Long>>>,
|
||||||
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
lastWeekAverageFocusTimes: List<Int>,
|
lastWeekAnalysisValues: Pair<List<Long>, Long>,
|
||||||
lastMonthAverageFocusTimes: List<Int>,
|
lastMonthAverageFocusTimes: List<Int>,
|
||||||
lastYearAverageFocusTimes: List<Int>,
|
lastYearAverageFocusTimes: List<Int>,
|
||||||
generateSampleData: () -> Unit,
|
generateSampleData: () -> Unit,
|
||||||
@@ -134,7 +137,7 @@ fun StatsScreen(
|
|||||||
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
|
lastWeekAverageFocusTimes = lastWeekAnalysisValues.first,
|
||||||
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
|
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
|
||||||
lastYearAverageFocusTimes = lastYearAverageFocusTimes,
|
lastYearAverageFocusTimes = lastYearAverageFocusTimes,
|
||||||
generateSampleData = generateSampleData,
|
generateSampleData = generateSampleData,
|
||||||
@@ -154,10 +157,11 @@ fun StatsScreen(
|
|||||||
entry<Screen.Stats.LastWeek> {
|
entry<Screen.Stats.LastWeek> {
|
||||||
LastWeekScreen(
|
LastWeekScreen(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
|
lastWeekAnalysisValues = lastWeekAnalysisValues,
|
||||||
|
lastWeekSummaryValues = lastWeekSummaryValues,
|
||||||
|
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||||
onBack = backStack::removeLastOrNull,
|
onBack = backStack::removeLastOrNull,
|
||||||
hoursMinutesFormat = hoursMinutesFormat,
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
|
||||||
hoursFormat = hoursFormat,
|
hoursFormat = hoursFormat,
|
||||||
minutesFormat = minutesFormat,
|
minutesFormat = minutesFormat,
|
||||||
axisTypeface = axisTypeface,
|
axisTypeface = axisTypeface,
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ import androidx.compose.animation.EnterTransition
|
|||||||
import androidx.compose.animation.ExitTransition
|
import androidx.compose.animation.ExitTransition
|
||||||
import androidx.compose.animation.SharedTransitionDefaults
|
import androidx.compose.animation.SharedTransitionDefaults
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -37,8 +39,8 @@ fun Modifier.sharedBoundsReveal(
|
|||||||
sharedTransitionScope: SharedTransitionScope,
|
sharedTransitionScope: SharedTransitionScope,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
animatedVisibilityScope: AnimatedVisibilityScope = LocalNavAnimatedContentScope.current,
|
||||||
boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform,
|
boundsTransform: BoundsTransform = SharedTransitionDefaults.BoundsTransform,
|
||||||
enter: EnterTransition = EnterTransition.None,
|
enter: EnterTransition = fadeIn(),
|
||||||
exit: ExitTransition = ExitTransition.None,
|
exit: ExitTransition = fadeOut(),
|
||||||
resizeMode: SharedTransitionScope.ResizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
|
resizeMode: SharedTransitionScope.ResizeMode = SharedTransitionScope.ResizeMode.RemeasureToBounds,
|
||||||
clipShape: Shape = MaterialTheme.shapes.largeIncreased,
|
clipShape: Shape = MaterialTheme.shapes.largeIncreased,
|
||||||
renderInOverlayDuringTransition: Boolean = true,
|
renderInOverlayDuringTransition: Boolean = true,
|
||||||
@@ -55,7 +57,7 @@ fun Modifier.sharedBoundsReveal(
|
|||||||
clipInOverlayDuringTransition = OverlayClip(clipShape),
|
clipInOverlayDuringTransition = OverlayClip(clipShape),
|
||||||
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
|
renderInOverlayDuringTransition = renderInOverlayDuringTransition,
|
||||||
)
|
)
|
||||||
.skipToLookaheadSize()
|
// .skipToLookaheadSize()
|
||||||
.skipToLookaheadPosition()
|
// .skipToLookaheadPosition()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,9 +25,12 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
import androidx.compose.material3.MaterialTheme.shapes
|
import androidx.compose.material3.MaterialTheme.shapes
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -35,6 +38,7 @@ import androidx.compose.ui.unit.Dp
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.util.fastForEachIndexed
|
import androidx.compose.ui.util.fastForEachIndexed
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A custom implementation of the 1-Dimensional heatmap plot that varies the width of the cells
|
* A custom implementation of the 1-Dimensional heatmap plot that varies the width of the cells
|
||||||
@@ -98,6 +102,60 @@ fun VariableWidth1DHeatmap(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FocusBreakRatioVisualization(
|
||||||
|
focusDuration: Long,
|
||||||
|
breakDuration: Long,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
height: Dp = 40.dp,
|
||||||
|
gap: Dp = 2.dp
|
||||||
|
) {
|
||||||
|
val focusPercentage = ((focusDuration / (focusDuration.toFloat() + breakDuration)) * 100)
|
||||||
|
val breakPercentage = ((breakDuration / (focusDuration.toFloat() + breakDuration)) * 100)
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(gap),
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = focusPercentage.roundToInt().toString() + '%',
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
color = colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(end = 6.dp)
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
Modifier
|
||||||
|
.weight(focusPercentage)
|
||||||
|
.height(height)
|
||||||
|
.background(
|
||||||
|
colorScheme.primary,
|
||||||
|
shapes.large.copy(
|
||||||
|
topEnd = shapes.extraSmall.topEnd,
|
||||||
|
bottomEnd = shapes.extraSmall.bottomEnd
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Spacer(
|
||||||
|
Modifier
|
||||||
|
.weight(breakPercentage)
|
||||||
|
.height(height)
|
||||||
|
.background(
|
||||||
|
colorScheme.tertiary,
|
||||||
|
shapes.large.copy(
|
||||||
|
topStart = shapes.extraSmall.topStart,
|
||||||
|
bottomStart = shapes.extraSmall.bottomStart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = breakPercentage.roundToInt().toString() + '%',
|
||||||
|
style = typography.bodyLarge,
|
||||||
|
color = colorScheme.tertiary,
|
||||||
|
modifier = Modifier.padding(start = 6.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun VariableWidth1DHeatmapPreview() {
|
fun VariableWidth1DHeatmapPreview() {
|
||||||
@@ -19,14 +19,15 @@ package org.nsh07.pomodoro.ui.statsScreen.screens
|
|||||||
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
@@ -46,28 +47,34 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
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.common.data.ExtraStore
|
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.ui.mergePaddingValues
|
import org.nsh07.pomodoro.ui.mergePaddingValues
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.FocusBreakRatioVisualization
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.components.TimeColumnChart
|
import org.nsh07.pomodoro.ui.statsScreen.components.TimeColumnChart
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.components.VariableWidth1DHeatmap
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.components.sharedBoundsReveal
|
import org.nsh07.pomodoro.ui.statsScreen.components.sharedBoundsReveal
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToMinutes
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun SharedTransitionScope.LastWeekScreen(
|
fun SharedTransitionScope.LastWeekScreen(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastWeekAverageFocusTimes: List<Int>,
|
lastWeekAnalysisValues: Pair<List<Long>, Long>,
|
||||||
|
lastWeekSummaryValues: List<Pair<String, List<Long>>>,
|
||||||
|
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
hoursMinutesFormat: String,
|
hoursMinutesFormat: String,
|
||||||
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
|
||||||
hoursFormat: String,
|
hoursFormat: String,
|
||||||
minutesFormat: String,
|
minutesFormat: String,
|
||||||
axisTypeface: Typeface,
|
axisTypeface: Typeface,
|
||||||
@@ -75,10 +82,10 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
val rankList = remember(lastWeekAverageFocusTimes) {
|
val rankList = remember(lastWeekAnalysisValues) {
|
||||||
val sortedIndices =
|
val sortedIndices =
|
||||||
lastWeekAverageFocusTimes.indices.sortedByDescending { lastWeekAverageFocusTimes[it] }
|
lastWeekAnalysisValues.first.indices.sortedByDescending { lastWeekAnalysisValues.first[it] }
|
||||||
val ranks = MutableList(lastWeekAverageFocusTimes.size) { 0 }
|
val ranks = MutableList(lastWeekAnalysisValues.first.size) { 0 }
|
||||||
|
|
||||||
sortedIndices.forEachIndexed { rank, originalIndex ->
|
sortedIndices.forEachIndexed { rank, originalIndex ->
|
||||||
ranks[originalIndex] = rank
|
ranks[originalIndex] = rank
|
||||||
@@ -87,6 +94,10 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
ranks
|
ranks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val focusDuration = remember(lastWeekAnalysisValues) {
|
||||||
|
lastWeekAnalysisValues.first.sum()
|
||||||
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
TopAppBar(
|
TopAppBar(
|
||||||
@@ -115,11 +126,7 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior
|
||||||
colors = TopAppBarDefaults.topAppBarColors(
|
|
||||||
containerColor = colorScheme.surfaceBright,
|
|
||||||
scrolledContainerColor = colorScheme.surfaceBright
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -135,11 +142,10 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
) { innerPadding ->
|
) { innerPadding ->
|
||||||
val insets = mergePaddingValues(innerPadding, contentPadding)
|
val insets = mergePaddingValues(innerPadding, contentPadding)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
contentPadding = insets,
|
contentPadding = insets,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(colorScheme.surfaceBright)
|
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
item {
|
item {
|
||||||
@@ -150,9 +156,7 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
millisecondsToHoursMinutes(
|
millisecondsToHoursMinutes(
|
||||||
remember(lastWeekAverageFocusTimes) {
|
focusDuration,
|
||||||
lastWeekAverageFocusTimes.sum().toLong()
|
|
||||||
},
|
|
||||||
hoursMinutesFormat
|
hoursMinutesFormat
|
||||||
),
|
),
|
||||||
style = typography.displaySmall,
|
style = typography.displaySmall,
|
||||||
@@ -175,7 +179,8 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
}
|
||||||
|
item {
|
||||||
TimeColumnChart(
|
TimeColumnChart(
|
||||||
modelProducer = lastWeekSummaryChartData.first,
|
modelProducer = lastWeekSummaryChartData.first,
|
||||||
hoursFormat = hoursFormat,
|
hoursFormat = hoursFormat,
|
||||||
@@ -194,6 +199,79 @@ fun SharedTransitionScope.LastWeekScreen(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(8.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Focus overview",
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Average focus durations at different times of the day",
|
||||||
|
style = typography.bodySmall,
|
||||||
|
color = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { VariableWidth1DHeatmap(lastWeekAnalysisValues.first, rankList = rankList) }
|
||||||
|
item {
|
||||||
|
Row {
|
||||||
|
lastWeekAnalysisValues.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 { Spacer(Modifier.height(8.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus_break_ratio),
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
FocusBreakRatioVisualization(
|
||||||
|
focusDuration = focusDuration,
|
||||||
|
breakDuration = lastWeekAnalysisValues.second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(8.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"Focus insights",
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
"Focus overview of each day of the past week",
|
||||||
|
style = typography.bodySmall,
|
||||||
|
color = colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||||
|
lastWeekSummaryValues.fastForEach {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Text(
|
||||||
|
it.first,
|
||||||
|
style = typography.labelSmall,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
VariableWidth1DHeatmap(it.second, rankList = rankList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,7 +82,7 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
lastWeekAverageFocusTimes: List<Int>,
|
lastWeekAverageFocusTimes: List<Long>,
|
||||||
lastMonthAverageFocusTimes: List<Int>,
|
lastMonthAverageFocusTimes: List<Int>,
|
||||||
lastYearAverageFocusTimes: List<Int>,
|
lastYearAverageFocusTimes: List<Int>,
|
||||||
generateSampleData: () -> Unit,
|
generateSampleData: () -> Unit,
|
||||||
@@ -254,7 +254,7 @@ fun SharedTransitionScope.StatsMainScreen(
|
|||||||
Text(
|
Text(
|
||||||
millisecondsToHoursMinutes(
|
millisecondsToHoursMinutes(
|
||||||
remember(lastWeekAverageFocusTimes) {
|
remember(lastWeekAverageFocusTimes) {
|
||||||
lastWeekAverageFocusTimes.sum().toLong()
|
lastWeekAverageFocusTimes.sum()
|
||||||
},
|
},
|
||||||
hoursMinutesFormat
|
hoursMinutesFormat
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -67,8 +67,10 @@ class StatsViewModel(
|
|||||||
|
|
||||||
private val yearDayFormatter = DateTimeFormatter.ofPattern("d MMM")
|
private val yearDayFormatter = DateTimeFormatter.ofPattern("d MMM")
|
||||||
|
|
||||||
|
private val lastWeekStatsFlow = statRepository.getLastNDaysStats(7)
|
||||||
|
|
||||||
val lastWeekSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
val lastWeekSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
statRepository.getLastNDaysStatsSummary(7)
|
lastWeekStatsFlow
|
||||||
.map { list ->
|
.map { list ->
|
||||||
// reversing is required because we need ascending order while the DB returns descending order
|
// reversing is required because we need ascending order while the DB returns descending order
|
||||||
val reversed = list.reversed()
|
val reversed = list.reversed()
|
||||||
@@ -78,7 +80,7 @@ class StatsViewModel(
|
|||||||
Locale.getDefault()
|
Locale.getDefault()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
val values = reversed.map { it.focusTime }
|
val values = reversed.map { it.totalFocusTime() }
|
||||||
lastWeekSummary.first.runTransaction {
|
lastWeekSummary.first.runTransaction {
|
||||||
columnSeries { series(values) }
|
columnSeries { series(values) }
|
||||||
extras { it[lastWeekSummary.second] = keys }
|
extras { it[lastWeekSummary.second] = keys }
|
||||||
@@ -92,29 +94,57 @@ class StatsViewModel(
|
|||||||
initialValue = lastWeekSummary
|
initialValue = lastWeekSummary
|
||||||
)
|
)
|
||||||
|
|
||||||
val lastWeekAverageFocusTimes: StateFlow<List<Int>> =
|
val lastWeekStats: StateFlow<List<Pair<String, List<Long>>>> =
|
||||||
|
lastWeekStatsFlow
|
||||||
|
.map { value ->
|
||||||
|
value.reversed().map {
|
||||||
|
Pair(
|
||||||
|
it.date.dayOfWeek.getDisplayName(
|
||||||
|
TextStyle.NARROW,
|
||||||
|
Locale.getDefault()
|
||||||
|
),
|
||||||
|
listOf(
|
||||||
|
it.focusTimeQ1,
|
||||||
|
it.focusTimeQ2,
|
||||||
|
it.focusTimeQ3,
|
||||||
|
it.focusTimeQ4
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(5000),
|
||||||
|
initialValue = emptyList()
|
||||||
|
)
|
||||||
|
|
||||||
|
val lastWeekAverageFocusTimes: StateFlow<Pair<List<Long>, Long>> =
|
||||||
statRepository.getLastNDaysAverageFocusTimes(7)
|
statRepository.getLastNDaysAverageFocusTimes(7)
|
||||||
.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 lastMonthSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
val lastMonthSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
statRepository.getLastNDaysStatsSummary(30)
|
statRepository.getLastNDaysStats(30)
|
||||||
.map { list ->
|
.map { list ->
|
||||||
val reversed = list.reversed()
|
val reversed = list.reversed()
|
||||||
val keys = reversed.map { it.date.dayOfMonth.toString() }
|
val keys = reversed.map { it.date.dayOfMonth.toString() }
|
||||||
val values = reversed.map { it.focusTime }
|
val values = reversed.map { it.totalFocusTime() }
|
||||||
lastMonthSummary.first.runTransaction {
|
lastMonthSummary.first.runTransaction {
|
||||||
columnSeries { series(values) }
|
columnSeries { series(values) }
|
||||||
extras { it[lastMonthSummary.second] = keys }
|
extras { it[lastMonthSummary.second] = keys }
|
||||||
@@ -146,11 +176,11 @@ class StatsViewModel(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val lastYearSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
val lastYearSummaryChartData: StateFlow<Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>> =
|
||||||
statRepository.getLastNDaysStatsSummary(365)
|
statRepository.getLastNDaysStats(365)
|
||||||
.map { list ->
|
.map { list ->
|
||||||
val reversed = list.reversed()
|
val reversed = list.reversed()
|
||||||
val keys = reversed.map { it.date.format(yearDayFormatter) }
|
val keys = reversed.map { it.date.format(yearDayFormatter) }
|
||||||
val values = reversed.map { it.focusTime }
|
val values = reversed.map { it.totalFocusTime() }
|
||||||
lastYearSummary.first.runTransaction {
|
lastYearSummary.first.runTransaction {
|
||||||
lineSeries { series(values) }
|
lineSeries { series(values) }
|
||||||
extras { it[lastYearSummary.second] = keys }
|
extras { it[lastYearSummary.second] = keys }
|
||||||
@@ -195,13 +225,11 @@ class StatsViewModel(
|
|||||||
(1 * 60 * 60 * 1000L..3 * 60 * 60 * 1000L).random(),
|
(1 * 60 * 60 * 1000L..3 * 60 * 60 * 1000L).random(),
|
||||||
(0..3 * 60 * 60 * 1000L).random(),
|
(0..3 * 60 * 60 * 1000L).random(),
|
||||||
(0..1 * 60 * 60 * 1000L).random(),
|
(0..1 * 60 * 60 * 1000L).random(),
|
||||||
0
|
(0..100 * 60 * 1000L).random()
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
it = it.plusDays(1)
|
it = it.plusDays(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
statRepository.addBreakTime((0..30 * 60 * 1000L).random())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,4 +113,5 @@
|
|||||||
<string name="secure_aod_desc">Automatically lock your device after a timeout, while keeping the AOD visible</string>
|
<string name="secure_aod_desc">Automatically lock your device after a timeout, while keeping the AOD visible</string>
|
||||||
<string name="timer_reset_message">Timer reset</string>
|
<string name="timer_reset_message">Timer reset</string>
|
||||||
<string name="undo">Undo</string>
|
<string name="undo">Undo</string>
|
||||||
|
<string name="focus_break_ratio">Focus-break ratio</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user