feat(internal): Add a basic UI for viewing stats, add Vico to gradle dependencies
This commit is contained in:
@@ -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?>
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
Reference in New Issue
Block a user