From af0c1ee7323cddb42bea0ed7a8d7506306217657 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sat, 12 Jul 2025 10:08:40 +0530 Subject: [PATCH] feat(internal): Add a basic UI for viewing stats, add Vico to gradle dependencies --- app/build.gradle.kts | 12 +- .../java/org/nsh07/pomodoro/data/StatDao.kt | 4 +- .../org/nsh07/pomodoro/data/StatRepository.kt | 5 +- .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 4 +- .../pomodoro/ui/statsScreen/StatsScreen.kt | 66 +++++++++-- .../ui/statsScreen/TimeColumnChart.kt | 108 ++++++++++++++++++ .../statsScreen/viewModel/StatsViewModel.kt | 53 +++++++++ .../java/org/nsh07/pomodoro/utils/Utils.kt | 21 +++- gradle/libs.versions.toml | 6 +- 9 files changed, 251 insertions(+), 28 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 087330c..0425b1d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,10 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + import org.jetbrains.kotlin.gradle.dsl.JvmTarget plugins { @@ -26,8 +33,7 @@ android { release { isMinifyEnabled = false proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), - "proguard-rules.pro" + getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } } @@ -64,6 +70,8 @@ dependencies { implementation(libs.androidx.navigation3.runtime) implementation(libs.androidx.navigation3.ui) + implementation(libs.vico.compose.m3) + implementation(libs.androidx.room.runtime) implementation(libs.androidx.room.ktx) ksp(libs.androidx.room.compiler) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt b/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt index d8131b5..a2610ea 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/StatDao.kt @@ -36,8 +36,8 @@ interface StatDao { @Query("SELECT * FROM stat WHERE date = :date") fun getStat(date: String): Flow - @Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat") - fun getAllStatsSummary(): Flow> + @Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7") + fun getLastWeekStatsSummary(): Flow> @Query("SELECT AVG(focusTimeQ1) AS focusTimeQ1, AVG(focusTimeQ2) AS focusTimeQ2, AVG(focusTimeQ3) AS focusTimeQ3, AVG(focusTimeQ4) AS focusTimeQ4 FROM stat") fun getAvgFocusTimes(): Flow diff --git a/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt index 8b7dc98..0453d3d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/StatRepository.kt @@ -26,7 +26,7 @@ interface StatRepository { fun getTodayStat(): Flow - fun getAllStatsSummary(): Flow> + fun getLastWeekStatsSummary(): Flow> fun getAverageFocusTimes(): Flow } @@ -95,7 +95,8 @@ class AppStatRepository( return statDao.getStat(currentDate) } - override fun getAllStatsSummary(): Flow> = statDao.getAllStatsSummary() + override fun getLastWeekStatsSummary(): Flow> = + statDao.getLastWeekStatsSummary() override fun getAverageFocusTimes(): Flow = statDao.getAvgFocusTimes() } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 50241bf..ce00f64 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -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 { - StatsScreen() + StatsScreenRoot() } } ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt index 6720c33..7317e54 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/StatsScreen.kt @@ -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 . + */ + 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) } } } -} \ No newline at end of file + + StatsScreen( + allStatsSummaryModelProducer = modelProducer + ) +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt new file mode 100644 index 0000000..c49be99 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/TimeColumnChart.kt @@ -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 . + */ + +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, + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt new file mode 100644 index 0000000..99257cf --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/statsScreen/viewModel/StatsViewModel.kt @@ -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 . + */ + +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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt index eccac42..9b6dfd7 100644 --- a/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt +++ b/app/src/main/java/org/nsh07/pomodoro/utils/Utils.kt @@ -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) -} \ No newline at end of file +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) + ) \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 2696859..21ea12e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,7 +1,7 @@ [versions] activityCompose = "1.10.1" adaptive = "1.1.0" -agp = "8.11.0" +agp = "8.11.1" composeBom = "2025.06.02" coreKtx = "1.16.0" espressoCore = "3.6.1" @@ -12,6 +12,7 @@ ksp = "2.2.0-2.0.2" lifecycleRuntimeKtx = "2.9.1" navigation3Runtime = "1.0.0-alpha05" room = "2.7.2" +vico = "2.2.0-alpha.1" [libraries] androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" } @@ -36,10 +37,11 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } junit = { group = "junit", name = "junit", version.ref = "junit" } +vico-compose-m3 = { group = "com.patrykandpatrick.vico", name = "compose-m3", version.ref = "vico" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } -kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin"} +kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }