feat(ui): implement a navigation system in the stats screen
This commit is contained in:
@@ -297,7 +297,7 @@ fun AppScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.Stats> {
|
entry<Screen.Stats.Main> {
|
||||||
StatsScreenRoot(contentPadding = contentPadding)
|
StatsScreenRoot(contentPadding = contentPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ val mainScreens = listOf(
|
|||||||
R.string.timer
|
R.string.timer
|
||||||
),
|
),
|
||||||
NavItem(
|
NavItem(
|
||||||
Screen.Stats,
|
Screen.Stats.Main,
|
||||||
R.drawable.monitoring,
|
R.drawable.monitoring,
|
||||||
R.drawable.monitoring_filled,
|
R.drawable.monitoring_filled,
|
||||||
R.string.stats
|
R.string.stats
|
||||||
|
|||||||
@@ -48,7 +48,10 @@ sealed class Screen : NavKey {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
object Stats : Screen()
|
sealed class Stats : Screen() {
|
||||||
|
@Serializable
|
||||||
|
object Main : Stats()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
data class NavItem(
|
data class NavItem(
|
||||||
|
|||||||
@@ -18,67 +18,34 @@
|
|||||||
package org.nsh07.pomodoro.ui.statsScreen
|
package org.nsh07.pomodoro.ui.statsScreen
|
||||||
|
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.text.TextAutoSize
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
|
||||||
import androidx.compose.material3.MaterialTheme.shapes
|
|
||||||
import androidx.compose.material3.MaterialTheme.typography
|
|
||||||
import androidx.compose.material3.Scaffold
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TopAppBar
|
|
||||||
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.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|
||||||
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
import androidx.compose.ui.platform.LocalFontFamilyResolver
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.unit.sp
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
|
import androidx.navigation3.runtime.entryProvider
|
||||||
|
import androidx.navigation3.ui.NavDisplay
|
||||||
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.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 org.nsh07.pomodoro.BuildConfig
|
|
||||||
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.mergePaddingValues
|
import org.nsh07.pomodoro.ui.Screen
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.screens.StatsMainScreen
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400
|
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
|
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
|
||||||
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun StatsScreenRoot(
|
fun StatsScreenRoot(
|
||||||
@@ -86,6 +53,8 @@ fun StatsScreenRoot(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
|
||||||
) {
|
) {
|
||||||
|
val backStack = viewModel.backStack
|
||||||
|
|
||||||
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()
|
||||||
@@ -99,6 +68,7 @@ fun StatsScreenRoot(
|
|||||||
|
|
||||||
StatsScreen(
|
StatsScreen(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
backStack = backStack,
|
||||||
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||||
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||||
lastYearSummaryChartData = lastYearSummaryChartData,
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
@@ -115,6 +85,7 @@ fun StatsScreenRoot(
|
|||||||
@Composable
|
@Composable
|
||||||
fun StatsScreen(
|
fun StatsScreen(
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
|
backStack: SnapshotStateList<Screen.Stats>,
|
||||||
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
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>>>,
|
||||||
@@ -125,342 +96,49 @@ fun StatsScreen(
|
|||||||
generateSampleData: () -> Unit,
|
generateSampleData: () -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
|
||||||
|
|
||||||
val hoursFormat = stringResource(R.string.hours_format)
|
val hoursFormat = stringResource(R.string.hours_format)
|
||||||
val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format)
|
val hoursMinutesFormat = stringResource(R.string.hours_and_minutes_format)
|
||||||
val minutesFormat = stringResource(R.string.minutes_format)
|
val minutesFormat = stringResource(R.string.minutes_format)
|
||||||
|
|
||||||
val lastWeekSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
|
|
||||||
val lastMonthSummaryAnalysisModelProducer = remember { CartesianChartModelProducer() }
|
|
||||||
|
|
||||||
LaunchedEffect(lastWeekAverageFocusTimes) {
|
|
||||||
lastWeekSummaryAnalysisModelProducer.runTransaction {
|
|
||||||
columnSeries {
|
|
||||||
series(lastWeekAverageFocusTimes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(lastMonthAverageFocusTimes) {
|
|
||||||
lastMonthSummaryAnalysisModelProducer.runTransaction {
|
|
||||||
columnSeries {
|
|
||||||
series(lastMonthAverageFocusTimes)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val resolver = LocalFontFamilyResolver.current
|
val resolver = LocalFontFamilyResolver.current
|
||||||
val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface }
|
val axisTypeface = remember { resolver.resolve(googleFlex400).value as Typeface }
|
||||||
val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface }
|
val markerTypeface = remember { resolver.resolve(googleFlex600).value as Typeface }
|
||||||
|
|
||||||
Scaffold(
|
NavDisplay(
|
||||||
topBar = {
|
backStack = backStack,
|
||||||
TopAppBar(
|
onBack = backStack::removeLastOrNull,
|
||||||
title = {
|
transitionSpec = {
|
||||||
Text(
|
(slideInHorizontally(initialOffsetX = { it }))
|
||||||
stringResource(R.string.stats),
|
.togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
|
||||||
style = LocalTextStyle.current.copy(
|
|
||||||
fontFamily = robotoFlexTopBar,
|
|
||||||
fontSize = 32.sp,
|
|
||||||
lineHeight = 32.sp
|
|
||||||
)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
actions = if (BuildConfig.DEBUG) {
|
|
||||||
{
|
|
||||||
IconButton(
|
|
||||||
onClick = generateSampleData
|
|
||||||
) {
|
|
||||||
Spacer(Modifier.size(24.dp))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
{}
|
|
||||||
},
|
|
||||||
subtitle = {},
|
|
||||||
titleHorizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
scrollBehavior = scrollBehavior,
|
|
||||||
colors = topBarColors
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
popTransitionSpec = {
|
||||||
) { innerPadding ->
|
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||||
val insets = mergePaddingValues(innerPadding, contentPadding)
|
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||||
LazyColumn(
|
},
|
||||||
contentPadding = insets,
|
predictivePopTransitionSpec = {
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||||
modifier = Modifier
|
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||||
.background(topBarColors.containerColor)
|
},
|
||||||
.padding(horizontal = 16.dp)
|
entryProvider = entryProvider {
|
||||||
) {
|
entry<Screen.Stats.Main> {
|
||||||
item { Spacer(Modifier.height(14.dp)) }
|
StatsMainScreen(
|
||||||
|
contentPadding = contentPadding,
|
||||||
item {
|
lastWeekSummaryChartData = lastWeekSummaryChartData,
|
||||||
Text(
|
lastMonthSummaryChartData = lastMonthSummaryChartData,
|
||||||
stringResource(R.string.today),
|
lastYearSummaryChartData = lastYearSummaryChartData,
|
||||||
style = typography.headlineSmall,
|
todayStat = todayStat,
|
||||||
modifier = Modifier.padding(horizontal = 16.dp)
|
lastWeekAverageFocusTimes = lastWeekAverageFocusTimes,
|
||||||
|
lastMonthAverageFocusTimes = lastMonthAverageFocusTimes,
|
||||||
|
lastYearAverageFocusTimes = lastYearAverageFocusTimes,
|
||||||
|
generateSampleData = generateSampleData,
|
||||||
|
hoursFormat = hoursFormat,
|
||||||
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
|
minutesFormat = minutesFormat,
|
||||||
|
axisTypeface = axisTypeface,
|
||||||
|
markerTypeface = markerTypeface,
|
||||||
|
modifier = modifier
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
|
||||||
|
|
||||||
item {
|
|
||||||
Row {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
colorScheme.primaryContainer,
|
|
||||||
shapes.largeIncreased
|
|
||||||
)
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
Column(Modifier.padding(16.dp)) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.focus),
|
|
||||||
style = typography.titleMedium,
|
|
||||||
color = colorScheme.onPrimaryContainer
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
remember(todayStat) {
|
|
||||||
millisecondsToHoursMinutes(
|
|
||||||
todayStat?.totalFocusTime() ?: 0,
|
|
||||||
hoursMinutesFormat
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style = typography.displaySmall,
|
|
||||||
color = colorScheme.onPrimaryContainer,
|
|
||||||
maxLines = 1,
|
|
||||||
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(Modifier.width(8.dp))
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.background(
|
|
||||||
colorScheme.tertiaryContainer,
|
|
||||||
shapes.largeIncreased
|
|
||||||
)
|
|
||||||
.weight(1f)
|
|
||||||
) {
|
|
||||||
Column(Modifier.padding(16.dp)) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.break_),
|
|
||||||
style = typography.titleMedium,
|
|
||||||
color = colorScheme.onTertiaryContainer
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
remember(todayStat) {
|
|
||||||
millisecondsToHoursMinutes(
|
|
||||||
todayStat?.breakTime ?: 0,
|
|
||||||
hoursMinutesFormat
|
|
||||||
)
|
|
||||||
},
|
|
||||||
style = typography.displaySmall,
|
|
||||||
color = colorScheme.onTertiaryContainer,
|
|
||||||
maxLines = 1,
|
|
||||||
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
|
||||||
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(topListItemShape)
|
|
||||||
.background(listItemColors.containerColor)
|
|
||||||
.clickable {}
|
|
||||||
.padding(
|
|
||||||
start = 20.dp,
|
|
||||||
top = 20.dp,
|
|
||||||
bottom = 20.dp
|
|
||||||
) // end = 0 to let the chart touch the end
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.last_week),
|
|
||||||
style = typography.headlineSmall
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
millisecondsToHoursMinutes(
|
|
||||||
remember(lastWeekAverageFocusTimes) {
|
|
||||||
lastWeekAverageFocusTimes.sum().toLong()
|
|
||||||
},
|
|
||||||
hoursMinutesFormat
|
|
||||||
),
|
|
||||||
style = typography.displaySmall
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.focus_per_day_avg),
|
|
||||||
style = typography.titleSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 5.2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeColumnChart(
|
|
||||||
modelProducer = lastWeekSummaryChartData.first,
|
|
||||||
hoursFormat = hoursFormat,
|
|
||||||
hoursMinutesFormat = hoursMinutesFormat,
|
|
||||||
minutesFormat = minutesFormat,
|
|
||||||
axisTypeface = axisTypeface,
|
|
||||||
markerTypeface = markerTypeface,
|
|
||||||
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
|
||||||
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(middleListItemShape)
|
|
||||||
.background(listItemColors.containerColor)
|
|
||||||
.clickable {}
|
|
||||||
.padding(
|
|
||||||
start = 20.dp,
|
|
||||||
top = 20.dp,
|
|
||||||
bottom = 20.dp
|
|
||||||
) // end = 0 to let the chart touch the end
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.last_month),
|
|
||||||
style = typography.headlineSmall
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
millisecondsToHoursMinutes(
|
|
||||||
remember(lastMonthAverageFocusTimes) {
|
|
||||||
lastMonthAverageFocusTimes.sum().toLong()
|
|
||||||
},
|
|
||||||
hoursMinutesFormat
|
|
||||||
),
|
|
||||||
style = typography.displaySmall
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.focus_per_day_avg),
|
|
||||||
style = typography.titleSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 5.2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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()]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(bottomListItemShape)
|
|
||||||
.background(listItemColors.containerColor)
|
|
||||||
.clickable {}
|
|
||||||
.padding(
|
|
||||||
start = 20.dp,
|
|
||||||
top = 20.dp,
|
|
||||||
bottom = 20.dp
|
|
||||||
) // end = 0 to let the chart touch the end
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
stringResource(R.string.last_year),
|
|
||||||
style = typography.headlineSmall
|
|
||||||
)
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.Bottom,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
millisecondsToHoursMinutes(
|
|
||||||
remember(lastYearAverageFocusTimes) {
|
|
||||||
lastYearAverageFocusTimes.sum().toLong()
|
|
||||||
},
|
|
||||||
hoursMinutesFormat
|
|
||||||
),
|
|
||||||
style = typography.displaySmall
|
|
||||||
)
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.focus_per_day_avg),
|
|
||||||
style = typography.titleSmall,
|
|
||||||
modifier = Modifier.padding(bottom = 5.2.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
TimeLineChart(
|
|
||||||
modelProducer = lastYearSummaryChartData.first,
|
|
||||||
hoursFormat = hoursFormat,
|
|
||||||
hoursMinutesFormat = hoursMinutesFormat,
|
|
||||||
minutesFormat = minutesFormat,
|
|
||||||
axisTypeface = axisTypeface,
|
|
||||||
markerTypeface = markerTypeface,
|
|
||||||
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
|
||||||
context.model.extraStore[lastYearSummaryChartData.second][x.toInt()]
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(
|
|
||||||
widthDp = 400
|
|
||||||
)
|
|
||||||
@Composable
|
|
||||||
fun StatsScreenPreview() {
|
|
||||||
val modelProducer = remember { CartesianChartModelProducer() }
|
|
||||||
val keys = remember { ExtraStore.Key<List<String>>() }
|
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
modelProducer.runTransaction {
|
|
||||||
columnSeries {
|
|
||||||
series(5, 6, 5, 2, 11, 8, 5)
|
|
||||||
}
|
|
||||||
lineSeries {}
|
|
||||||
extras { it[keys] = listOf("M", "T", "W", "T", "F", "S", "S") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TomatoTheme {
|
|
||||||
Surface {
|
|
||||||
StatsScreen(
|
|
||||||
PaddingValues(),
|
|
||||||
Pair(modelProducer, keys),
|
|
||||||
Pair(modelProducer, keys),
|
|
||||||
Pair(modelProducer, keys),
|
|
||||||
null,
|
|
||||||
listOf(0, 0, 0, 0),
|
|
||||||
listOf(0, 0, 0, 0),
|
|
||||||
listOf(0, 0, 0, 0),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,367 @@
|
|||||||
|
/*
|
||||||
|
* 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.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
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.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.text.TextAutoSize
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.shapes
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||||
|
import com.patrykandpatrick.vico.core.common.data.ExtraStore
|
||||||
|
import org.nsh07.pomodoro.BuildConfig
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.data.Stat
|
||||||
|
import org.nsh07.pomodoro.ui.mergePaddingValues
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.TimeColumnChart
|
||||||
|
import org.nsh07.pomodoro.ui.statsScreen.TimeLineChart
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun StatsMainScreen(
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
lastWeekSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
|
lastMonthSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
|
lastYearSummaryChartData: Pair<CartesianChartModelProducer, ExtraStore.Key<List<String>>>,
|
||||||
|
todayStat: Stat?,
|
||||||
|
lastWeekAverageFocusTimes: List<Int>,
|
||||||
|
lastMonthAverageFocusTimes: List<Int>,
|
||||||
|
lastYearAverageFocusTimes: List<Int>,
|
||||||
|
generateSampleData: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
hoursMinutesFormat: String,
|
||||||
|
hoursFormat: String,
|
||||||
|
minutesFormat: String,
|
||||||
|
axisTypeface: Typeface,
|
||||||
|
markerTypeface: Typeface
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.stats),
|
||||||
|
style = LocalTextStyle.current.copy(
|
||||||
|
fontFamily = robotoFlexTopBar,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
lineHeight = 32.sp
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = if (BuildConfig.DEBUG) {
|
||||||
|
{
|
||||||
|
IconButton(
|
||||||
|
onClick = generateSampleData
|
||||||
|
) {
|
||||||
|
Spacer(Modifier.size(24.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{}
|
||||||
|
},
|
||||||
|
subtitle = {},
|
||||||
|
titleHorizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
colors = topBarColors
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
|
) { innerPadding ->
|
||||||
|
val insets = mergePaddingValues(innerPadding, contentPadding)
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = insets,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item { Spacer(Modifier.height(14.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.today),
|
||||||
|
style = typography.headlineSmall,
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Row {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
colorScheme.primaryContainer,
|
||||||
|
shapes.largeIncreased
|
||||||
|
)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus),
|
||||||
|
style = typography.titleMedium,
|
||||||
|
color = colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
remember(todayStat) {
|
||||||
|
millisecondsToHoursMinutes(
|
||||||
|
todayStat?.totalFocusTime() ?: 0,
|
||||||
|
hoursMinutesFormat
|
||||||
|
)
|
||||||
|
},
|
||||||
|
style = typography.displaySmall,
|
||||||
|
color = colorScheme.onPrimaryContainer,
|
||||||
|
maxLines = 1,
|
||||||
|
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
colorScheme.tertiaryContainer,
|
||||||
|
shapes.largeIncreased
|
||||||
|
)
|
||||||
|
.weight(1f)
|
||||||
|
) {
|
||||||
|
Column(Modifier.padding(16.dp)) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.break_),
|
||||||
|
style = typography.titleMedium,
|
||||||
|
color = colorScheme.onTertiaryContainer
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
remember(todayStat) {
|
||||||
|
millisecondsToHoursMinutes(
|
||||||
|
todayStat?.breakTime ?: 0,
|
||||||
|
hoursMinutesFormat
|
||||||
|
)
|
||||||
|
},
|
||||||
|
style = typography.displaySmall,
|
||||||
|
color = colorScheme.onTertiaryContainer,
|
||||||
|
maxLines = 1,
|
||||||
|
autoSize = TextAutoSize.StepBased(maxFontSize = typography.displaySmall.fontSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(topListItemShape)
|
||||||
|
.background(listItemColors.containerColor)
|
||||||
|
.clickable {}
|
||||||
|
.padding(
|
||||||
|
start = 20.dp,
|
||||||
|
top = 20.dp,
|
||||||
|
bottom = 20.dp
|
||||||
|
) // end = 0 to let the chart touch the end
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.last_week),
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
millisecondsToHoursMinutes(
|
||||||
|
remember(lastWeekAverageFocusTimes) {
|
||||||
|
lastWeekAverageFocusTimes.sum().toLong()
|
||||||
|
},
|
||||||
|
hoursMinutesFormat
|
||||||
|
),
|
||||||
|
style = typography.displaySmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus_per_day_avg),
|
||||||
|
style = typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(bottom = 5.2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeColumnChart(
|
||||||
|
modelProducer = lastWeekSummaryChartData.first,
|
||||||
|
hoursFormat = hoursFormat,
|
||||||
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
|
minutesFormat = minutesFormat,
|
||||||
|
axisTypeface = axisTypeface,
|
||||||
|
markerTypeface = markerTypeface,
|
||||||
|
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
||||||
|
context.model.extraStore[lastWeekSummaryChartData.second][x.toInt()]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(middleListItemShape)
|
||||||
|
.background(listItemColors.containerColor)
|
||||||
|
.clickable {}
|
||||||
|
.padding(
|
||||||
|
start = 20.dp,
|
||||||
|
top = 20.dp,
|
||||||
|
bottom = 20.dp
|
||||||
|
) // end = 0 to let the chart touch the end
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.last_month),
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
millisecondsToHoursMinutes(
|
||||||
|
remember(lastMonthAverageFocusTimes) {
|
||||||
|
lastMonthAverageFocusTimes.sum().toLong()
|
||||||
|
},
|
||||||
|
hoursMinutesFormat
|
||||||
|
),
|
||||||
|
style = typography.displaySmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.focus_per_day_avg),
|
||||||
|
style = typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(bottom = 5.2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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()]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(bottomListItemShape)
|
||||||
|
.background(listItemColors.containerColor)
|
||||||
|
.clickable {}
|
||||||
|
.padding(
|
||||||
|
start = 20.dp,
|
||||||
|
top = 20.dp,
|
||||||
|
bottom = 20.dp
|
||||||
|
) // end = 0 to let the chart touch the end
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.last_year),
|
||||||
|
style = typography.headlineSmall
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
millisecondsToHoursMinutes(
|
||||||
|
remember(lastYearAverageFocusTimes) {
|
||||||
|
lastYearAverageFocusTimes.sum().toLong()
|
||||||
|
},
|
||||||
|
hoursMinutesFormat
|
||||||
|
),
|
||||||
|
style = typography.displaySmall
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.focus_per_day_avg),
|
||||||
|
style = typography.titleSmall,
|
||||||
|
modifier = Modifier.padding(bottom = 5.2.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
TimeLineChart(
|
||||||
|
modelProducer = lastYearSummaryChartData.first,
|
||||||
|
hoursFormat = hoursFormat,
|
||||||
|
hoursMinutesFormat = hoursMinutesFormat,
|
||||||
|
minutesFormat = minutesFormat,
|
||||||
|
axisTypeface = axisTypeface,
|
||||||
|
markerTypeface = markerTypeface,
|
||||||
|
xValueFormatter = CartesianValueFormatter { context, x, _ ->
|
||||||
|
context.model.extraStore[lastYearSummaryChartData.second][x.toInt()]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.ui.statsScreen.viewModel
|
package org.nsh07.pomodoro.ui.statsScreen.viewModel
|
||||||
|
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
|
||||||
@@ -38,6 +39,7 @@ import org.nsh07.pomodoro.BuildConfig
|
|||||||
import org.nsh07.pomodoro.TomatoApplication
|
import org.nsh07.pomodoro.TomatoApplication
|
||||||
import org.nsh07.pomodoro.data.Stat
|
import org.nsh07.pomodoro.data.Stat
|
||||||
import org.nsh07.pomodoro.data.StatRepository
|
import org.nsh07.pomodoro.data.StatRepository
|
||||||
|
import org.nsh07.pomodoro.ui.Screen
|
||||||
import java.time.LocalDate
|
import java.time.LocalDate
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
import java.time.format.TextStyle
|
import java.time.format.TextStyle
|
||||||
@@ -46,6 +48,7 @@ import java.util.Locale
|
|||||||
class StatsViewModel(
|
class StatsViewModel(
|
||||||
private val statRepository: StatRepository
|
private val statRepository: StatRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
val backStack = mutableStateListOf<Screen.Stats>(Screen.Stats.Main)
|
||||||
|
|
||||||
val todayStat = statRepository
|
val todayStat = statRepository
|
||||||
.getTodayStat()
|
.getTodayStat()
|
||||||
|
|||||||
Reference in New Issue
Block a user