feat(internal): Add a basic UI for viewing stats, add Vico to gradle dependencies

This commit is contained in:
Nishant Mishra
2025-07-12 10:08:40 +05:30
parent 10d6e4e293
commit af0c1ee732
9 changed files with 251 additions and 28 deletions

View File

@@ -36,8 +36,8 @@ interface StatDao {
@Query("SELECT * FROM stat WHERE date = :date")
fun getStat(date: String): Flow<Stat?>
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat")
fun getAllStatsSummary(): Flow<List<StatSummary>>
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7")
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
@Query("SELECT AVG(focusTimeQ1) AS focusTimeQ1, AVG(focusTimeQ2) AS focusTimeQ2, AVG(focusTimeQ3) AS focusTimeQ3, AVG(focusTimeQ4) AS focusTimeQ4 FROM stat")
fun getAvgFocusTimes(): Flow<StatFocusTime?>

View File

@@ -26,7 +26,7 @@ interface StatRepository {
fun getTodayStat(): Flow<Stat?>
fun getAllStatsSummary(): Flow<List<StatSummary>>
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
fun getAverageFocusTimes(): Flow<StatFocusTime?>
}
@@ -95,7 +95,8 @@ class AppStatRepository(
return statDao.getStat(currentDate)
}
override fun getAllStatsSummary(): Flow<List<StatSummary>> = statDao.getAllStatsSummary()
override fun getLastWeekStatsSummary(): Flow<List<StatSummary>> =
statDao.getLastWeekStatsSummary()
override fun getAverageFocusTimes(): Flow<StatFocusTime?> = statDao.getAvgFocusTimes()
}

View File

@@ -45,7 +45,7 @@ import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass
import org.nsh07.pomodoro.MainActivity.Companion.screens
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreen
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@@ -157,7 +157,7 @@ fun AppScreen(
}
entry<Screen.Stats> {
StatsScreen()
StatsScreenRoot()
}
}
)

View File

@@ -1,26 +1,53 @@
/*
* 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.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.LoadingIndicator
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import kotlinx.coroutines.runBlocking
import org.nsh07.pomodoro.ui.statsScreen.viewModel.StatsViewModel
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTitle
@Composable
fun StatsScreenRoot(
modifier: Modifier = Modifier,
viewModel: StatsViewModel = viewModel(factory = StatsViewModel.Factory)
) {
val allStatsSummaryModelProducer = viewModel.allStatsSummaryModelProducer
StatsScreen(
allStatsSummaryModelProducer = allStatsSummaryModelProducer,
modifier = modifier
)
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun StatsScreen(modifier: Modifier = Modifier) {
fun StatsScreen(
allStatsSummaryModelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier
) {
Column(modifier) {
TopAppBar(
title = {
@@ -36,11 +63,26 @@ fun StatsScreen(modifier: Modifier = Modifier) {
subtitle = {},
titleHorizontalAlignment = Alignment.CenterHorizontally
)
Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize().background(colorScheme.surface)) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
LoadingIndicator()
Text("Coming Soon", style = typography.headlineSmall, fontFamily = robotoFlexTitle)
}
Text("This week", style = typography.headlineSmall, modifier = Modifier.padding(16.dp))
TimeColumnChart(allStatsSummaryModelProducer, modifier = Modifier.padding(start = 16.dp))
}
}
@Preview(
showSystemUi = true,
device = Devices.PIXEL_9_PRO
)
@Composable
fun StatsScreenPreview() {
val modelProducer = remember { CartesianChartModelProducer() }
runBlocking {
modelProducer.runTransaction {
columnSeries { series(5, 6) }
}
}
}
StatsScreen(
allStatsSummaryModelProducer = modelProducer
)
}

View File

@@ -0,0 +1,108 @@
/*
* 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 android.graphics.Path
import android.graphics.RectF
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
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.rememberColumnCartesianLayer
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.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.layer.ColumnCartesianLayer
import com.patrykandpatrick.vico.core.common.Fill
import org.nsh07.pomodoro.utils.millisecondsToHours
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
internal fun TimeColumnChart(
modelProducer: CartesianChartModelProducer,
modifier: Modifier = Modifier,
) {
val radius = with(LocalDensity.current) {
(48.dp / 2).toPx()
}
ProvideVicoTheme(rememberM3VicoTheme()) {
CartesianChartHost(
chart =
rememberCartesianChart(
rememberColumnCartesianLayer(
ColumnCartesianLayer.ColumnProvider.series(
vicoTheme.columnCartesianLayerColors.map { color ->
rememberLineComponent(
fill = fill(color),
thickness = 48.dp,
shape = { _, path, left, top, right, bottom ->
if (top + radius <= bottom - radius) {
path.arcTo(
RectF(left, top, right, top + 2 * radius),
180f,
180f
)
path.lineTo(right, bottom - radius)
path.arcTo(
RectF(left, bottom - 2 * radius, right, bottom),
0f,
180f
)
path.close()
} else {
path.addCircle(
left + radius,
bottom - radius,
radius,
Path.Direction.CW
)
}
}
)
}
),
columnCollectionSpacing = 4.dp
),
startAxis = VerticalAxis.rememberStart(
line = rememberLineComponent(Fill.Transparent),
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent),
valueFormatter = CartesianValueFormatter { measuringContext, value, _ ->
millisecondsToHours(value.toLong())
}
),
bottomAxis = HorizontalAxis.rememberBottom(
rememberLineComponent(Fill.Transparent),
tick = rememberLineComponent(Fill.Transparent),
guideline = rememberLineComponent(Fill.Transparent)
),
),
modelProducer = modelProducer,
zoomState = rememberVicoZoomState(
initialZoom = Zoom.fixed(),
minZoom = Zoom.min(Zoom.fixed(), Zoom.Content)
),
animationSpec = motionScheme.defaultSpatialSpec(),
modifier = modifier,
)
}
}

View File

@@ -0,0 +1,53 @@
/*
* 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.viewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
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 kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.StatRepository
class StatsViewModel(
statRepository: StatRepository
) : ViewModel() {
private val todayStat = statRepository.getTodayStat()
private val allStatsSummary = statRepository.getLastWeekStatsSummary()
private val averageFocusTimes = statRepository.getAverageFocusTimes()
val allStatsSummaryModelProducer = CartesianChartModelProducer()
init {
viewModelScope.launch(Dispatchers.IO) {
allStatsSummary
.collect { list ->
allStatsSummaryModelProducer.runTransaction {
columnSeries { series(list.reversed().map { it.focusTime }) }
}
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication)
val appStatRepository = application.container.appStatRepository
StatsViewModel(appStatRepository)
}
}
}
}

View File

@@ -8,10 +8,19 @@
package org.nsh07.pomodoro.utils
import java.util.Locale
import kotlin.math.ceil
import java.util.concurrent.TimeUnit
fun millisecondsToStr(t: Long): String {
val min = (ceil(t / 1000.0).toInt() / 60)
val sec = (ceil(t / 1000.0).toInt() % 60)
return String.format(locale = Locale.getDefault(), "%02d:%02d", min, sec)
}
fun millisecondsToStr(t: Long): String =
String.format(
Locale.getDefault(),
"%02d:%02d",
TimeUnit.MILLISECONDS.toMinutes(t),
TimeUnit.MILLISECONDS.toSeconds(t) % TimeUnit.MINUTES.toSeconds(1)
)
fun millisecondsToHours(t: Long): String =
String.format(
Locale.getDefault(),
"%dh",
TimeUnit.MILLISECONDS.toHours(t)
)