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" }