refactor(internal): Add TypeConvertor for LocalDate to directly handle LocalDate in database
This commit is contained in:
@@ -11,11 +11,13 @@ import android.content.Context
|
|||||||
import androidx.room.Database
|
import androidx.room.Database
|
||||||
import androidx.room.Room
|
import androidx.room.Room
|
||||||
import androidx.room.RoomDatabase
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
entities = [IntPreference::class, Stat::class],
|
entities = [IntPreference::class, Stat::class],
|
||||||
version = 1
|
version = 1
|
||||||
)
|
)
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|
||||||
abstract fun preferenceDao(): PreferenceDao
|
abstract fun preferenceDao(): PreferenceDao
|
||||||
|
|||||||
23
app/src/main/java/org/nsh07/pomodoro/data/Converters.kt
Normal file
23
app/src/main/java/org/nsh07/pomodoro/data/Converters.kt
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* 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.data
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
@TypeConverter
|
||||||
|
fun localDateToString(localDate: LocalDate?): String? {
|
||||||
|
return localDate?.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun stringToLocalDate(date: String?): LocalDate? {
|
||||||
|
return if (date != null) LocalDate.parse(date) else null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ package org.nsh07.pomodoro.data
|
|||||||
|
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data class for storing the user's statistics in the app's database. This class stores the focus
|
* Data class for storing the user's statistics in the app's database. This class stores the focus
|
||||||
@@ -18,7 +19,7 @@ import androidx.room.PrimaryKey
|
|||||||
@Entity(tableName = "stat")
|
@Entity(tableName = "stat")
|
||||||
data class Stat(
|
data class Stat(
|
||||||
@PrimaryKey
|
@PrimaryKey
|
||||||
val date: String,
|
val date: LocalDate,
|
||||||
val focusTimeQ1: Long,
|
val focusTimeQ1: Long,
|
||||||
val focusTimeQ2: Long,
|
val focusTimeQ2: Long,
|
||||||
val focusTimeQ3: Long,
|
val focusTimeQ3: Long,
|
||||||
@@ -27,7 +28,7 @@ data class Stat(
|
|||||||
)
|
)
|
||||||
|
|
||||||
data class StatSummary(
|
data class StatSummary(
|
||||||
val date: String,
|
val date: LocalDate,
|
||||||
val focusTime: Long,
|
val focusTime: Long,
|
||||||
val breakTime: Long
|
val breakTime: Long
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.room.Insert
|
|||||||
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
import androidx.room.OnConflictStrategy.Companion.REPLACE
|
||||||
import androidx.room.Query
|
import androidx.room.Query
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import java.time.LocalDate
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface StatDao {
|
interface StatDao {
|
||||||
@@ -19,22 +20,22 @@ interface StatDao {
|
|||||||
suspend fun insertStat(stat: Stat)
|
suspend fun insertStat(stat: Stat)
|
||||||
|
|
||||||
@Query("UPDATE stat SET focusTimeQ1 = focusTimeQ1 + :focusTime WHERE date = :date")
|
@Query("UPDATE stat SET focusTimeQ1 = focusTimeQ1 + :focusTime WHERE date = :date")
|
||||||
suspend fun addFocusTimeQ1(date: String, focusTime: Long)
|
suspend fun addFocusTimeQ1(date: LocalDate, focusTime: Long)
|
||||||
|
|
||||||
@Query("UPDATE stat SET focusTimeQ2 = focusTimeQ2 + :focusTime WHERE date = :date")
|
@Query("UPDATE stat SET focusTimeQ2 = focusTimeQ2 + :focusTime WHERE date = :date")
|
||||||
suspend fun addFocusTimeQ2(date: String, focusTime: Long)
|
suspend fun addFocusTimeQ2(date: LocalDate, focusTime: Long)
|
||||||
|
|
||||||
@Query("UPDATE stat SET focusTimeQ3 = focusTimeQ3 + :focusTime WHERE date = :date")
|
@Query("UPDATE stat SET focusTimeQ3 = focusTimeQ3 + :focusTime WHERE date = :date")
|
||||||
suspend fun addFocusTimeQ3(date: String, focusTime: Long)
|
suspend fun addFocusTimeQ3(date: LocalDate, focusTime: Long)
|
||||||
|
|
||||||
@Query("UPDATE stat SET focusTimeQ4 = focusTimeQ4 + :focusTime WHERE date = :date")
|
@Query("UPDATE stat SET focusTimeQ4 = focusTimeQ4 + :focusTime WHERE date = :date")
|
||||||
suspend fun addFocusTimeQ4(date: String, focusTime: Long)
|
suspend fun addFocusTimeQ4(date: LocalDate, focusTime: Long)
|
||||||
|
|
||||||
@Query("UPDATE stat SET breakTime = breakTime + :breakTime WHERE date = :date")
|
@Query("UPDATE stat SET breakTime = breakTime + :breakTime WHERE date = :date")
|
||||||
suspend fun addBreakTime(date: String, breakTime: Long)
|
suspend fun addBreakTime(date: LocalDate, breakTime: Long)
|
||||||
|
|
||||||
@Query("SELECT * FROM stat WHERE date = :date")
|
@Query("SELECT * FROM stat WHERE date = :date")
|
||||||
fun getStat(date: String): Flow<Stat?>
|
fun getStat(date: LocalDate): Flow<Stat?>
|
||||||
|
|
||||||
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7")
|
@Query("SELECT date, focusTimeQ1 + focusTimeQ2 + focusTimeQ3 + focusTimeQ4 as focusTime, breakTime FROM stat ORDER BY date DESC LIMIT 7")
|
||||||
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
|
fun getLastWeekStatsSummary(): Flow<List<StatSummary>>
|
||||||
@@ -43,8 +44,8 @@ interface StatDao {
|
|||||||
fun getAvgFocusTimes(): Flow<StatFocusTime?>
|
fun getAvgFocusTimes(): Flow<StatFocusTime?>
|
||||||
|
|
||||||
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
@Query("SELECT EXISTS (SELECT * FROM stat WHERE date = :date)")
|
||||||
suspend fun statExists(date: String): Boolean
|
suspend fun statExists(date: LocalDate): Boolean
|
||||||
|
|
||||||
@Query("SELECT date FROM stat ORDER BY date DESC LIMIT 1")
|
@Query("SELECT date FROM stat ORDER BY date DESC LIMIT 1")
|
||||||
suspend fun getLastDate(): String?
|
suspend fun getLastDate(): LocalDate?
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ interface StatRepository {
|
|||||||
|
|
||||||
fun getAverageFocusTimes(): Flow<StatFocusTime?>
|
fun getAverageFocusTimes(): Flow<StatFocusTime?>
|
||||||
|
|
||||||
suspend fun getLastDate(): String?
|
suspend fun getLastDate(): LocalDate?
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -45,7 +45,7 @@ class AppStatRepository(
|
|||||||
override suspend fun insertStat(stat: Stat) = statDao.insertStat(stat)
|
override suspend fun insertStat(stat: Stat) = statDao.insertStat(stat)
|
||||||
|
|
||||||
override suspend fun addFocusTime(focusTime: Long) = withContext(ioDispatcher) {
|
override suspend fun addFocusTime(focusTime: Long) = withContext(ioDispatcher) {
|
||||||
val currentDate = LocalDate.now().toString()
|
val currentDate = LocalDate.now()
|
||||||
val currentTime = LocalTime.now().toSecondOfDay()
|
val currentTime = LocalTime.now().toSecondOfDay()
|
||||||
val secondsInDay = 24 * 60 * 60
|
val secondsInDay = 24 * 60 * 60
|
||||||
|
|
||||||
@@ -88,7 +88,7 @@ class AppStatRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun addBreakTime(breakTime: Long) = withContext(ioDispatcher) {
|
override suspend fun addBreakTime(breakTime: Long) = withContext(ioDispatcher) {
|
||||||
val currentDate = LocalDate.now().toString()
|
val currentDate = LocalDate.now()
|
||||||
if (statDao.statExists(currentDate)) {
|
if (statDao.statExists(currentDate)) {
|
||||||
statDao.addBreakTime(currentDate, breakTime)
|
statDao.addBreakTime(currentDate, breakTime)
|
||||||
} else {
|
} else {
|
||||||
@@ -97,7 +97,7 @@ class AppStatRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun getTodayStat(): Flow<Stat?> {
|
override fun getTodayStat(): Flow<Stat?> {
|
||||||
val currentDate = LocalDate.now().toString()
|
val currentDate = LocalDate.now()
|
||||||
return statDao.getStat(currentDate)
|
return statDao.getStat(currentDate)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,5 +106,5 @@ class AppStatRepository(
|
|||||||
|
|
||||||
override fun getAverageFocusTimes(): Flow<StatFocusTime?> = statDao.getAvgFocusTimes()
|
override fun getAverageFocusTimes(): Flow<StatFocusTime?> = statDao.getAvgFocusTimes()
|
||||||
|
|
||||||
override suspend fun getLastDate(): String? = statDao.getLastDate()
|
override suspend fun getLastDate(): LocalDate? = statDao.getLastDate()
|
||||||
}
|
}
|
||||||
@@ -157,7 +157,13 @@ fun AppScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry<Screen.Stats> {
|
entry<Screen.Stats> {
|
||||||
StatsScreenRoot()
|
StatsScreenRoot(
|
||||||
|
modifier = modifier.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* 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.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
|
||||||
|
import com.patrykandpatrick.vico.core.cartesian.data.CartesianValueFormatter
|
||||||
|
import org.nsh07.pomodoro.utils.millisecondsToHoursMinutes
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColumnScope.ProductivityGraph(
|
||||||
|
expanded: Boolean,
|
||||||
|
modelProducer: CartesianChartModelProducer,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(expanded) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Text("Productivity analysis", style = typography.titleMedium)
|
||||||
|
Text("Time of day versus focus hours", style = typography.bodySmall)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
TimeColumnChart(
|
||||||
|
modelProducer,
|
||||||
|
yValueFormatter = CartesianValueFormatter { _, value, _ ->
|
||||||
|
millisecondsToHoursMinutes(value.toLong())
|
||||||
|
},
|
||||||
|
xValueFormatter = CartesianValueFormatter { _, value, _ ->
|
||||||
|
when (value) {
|
||||||
|
0.0 -> "0 - 6"
|
||||||
|
1.0 -> "6 - 12"
|
||||||
|
2.0 -> "12 - 18"
|
||||||
|
3.0 -> "18 - 24"
|
||||||
|
else -> ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,6 @@
|
|||||||
|
|
||||||
package org.nsh07.pomodoro.ui.statsScreen
|
package org.nsh07.pomodoro.ui.statsScreen
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -31,6 +30,7 @@ import androidx.compose.material3.MaterialTheme.motionScheme
|
|||||||
import androidx.compose.material3.MaterialTheme.typography
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -41,6 +41,7 @@ import androidx.compose.runtime.setValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.rotate
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.tooling.preview.Devices
|
import androidx.compose.ui.tooling.preview.Devices
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
@@ -64,7 +65,7 @@ fun StatsScreenRoot(
|
|||||||
) {
|
) {
|
||||||
val todayStat by viewModel.todayStat.collectAsState(null)
|
val todayStat by viewModel.todayStat.collectAsState(null)
|
||||||
StatsScreen(
|
StatsScreen(
|
||||||
allStatsSummaryModelProducer = viewModel.allStatsSummaryModelProducer,
|
lastWeekSummaryModelProducer = viewModel.lastWeekSummaryChartModelProducer,
|
||||||
todayStatModelProducer = viewModel.todayStatModelProducer,
|
todayStatModelProducer = viewModel.todayStatModelProducer,
|
||||||
todayStat = todayStat,
|
todayStat = todayStat,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -74,150 +75,150 @@ fun StatsScreenRoot(
|
|||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun StatsScreen(
|
fun StatsScreen(
|
||||||
allStatsSummaryModelProducer: CartesianChartModelProducer,
|
lastWeekSummaryModelProducer: CartesianChartModelProducer,
|
||||||
todayStatModelProducer: CartesianChartModelProducer,
|
todayStatModelProducer: CartesianChartModelProducer,
|
||||||
todayStat: Stat?,
|
todayStat: Stat?,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||||
|
|
||||||
var todayStatExpanded by rememberSaveable { mutableStateOf(false) }
|
var todayStatExpanded by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
LazyColumn(
|
Column(
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
modifier = modifier
|
modifier = modifier.nestedScroll(scrollBehavior.nestedScrollConnection)
|
||||||
) {
|
) {
|
||||||
item {
|
TopAppBar(
|
||||||
TopAppBar(
|
title = {
|
||||||
title = {
|
Text(
|
||||||
Text(
|
"Stats",
|
||||||
"Stats",
|
style = LocalTextStyle.current.copy(
|
||||||
style = LocalTextStyle.current.copy(
|
fontFamily = robotoFlexTopBar,
|
||||||
fontFamily = robotoFlexTopBar,
|
fontSize = 32.sp,
|
||||||
fontSize = 32.sp,
|
lineHeight = 32.sp
|
||||||
lineHeight = 32.sp
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
},
|
)
|
||||||
subtitle = {},
|
},
|
||||||
titleHorizontalAlignment = Alignment.CenterHorizontally
|
subtitle = {},
|
||||||
)
|
titleHorizontalAlignment = Alignment.CenterHorizontally,
|
||||||
}
|
scrollBehavior = scrollBehavior
|
||||||
item {
|
)
|
||||||
Text(
|
|
||||||
"Today",
|
LazyColumn(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||||
style = typography.headlineSmall,
|
item {
|
||||||
modifier = Modifier
|
Text(
|
||||||
.fillMaxWidth()
|
"Today",
|
||||||
.padding(horizontal = 16.dp)
|
style = typography.headlineSmall,
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.fillMaxWidth()
|
||||||
colorScheme.primaryContainer,
|
.padding(16.dp)
|
||||||
MaterialTheme.shapes.largeIncreased
|
)
|
||||||
)
|
}
|
||||||
.weight(1f)
|
item {
|
||||||
) {
|
Row(modifier = Modifier.padding(horizontal = 16.dp)) {
|
||||||
Column(Modifier.padding(16.dp)) {
|
Box(
|
||||||
Text(
|
modifier = Modifier
|
||||||
"Focus",
|
.background(
|
||||||
style = typography.titleMedium,
|
colorScheme.primaryContainer,
|
||||||
color = colorScheme.onPrimaryContainer
|
MaterialTheme.shapes.largeIncreased
|
||||||
)
|
)
|
||||||
Text(
|
.weight(1f)
|
||||||
if (todayStat != null) remember(todayStat) {
|
) {
|
||||||
millisecondsToHoursMinutes(
|
Column(Modifier.padding(16.dp)) {
|
||||||
todayStat.focusTimeQ1 +
|
Text(
|
||||||
todayStat.focusTimeQ2 +
|
"Focus",
|
||||||
todayStat.focusTimeQ3 +
|
style = typography.titleMedium,
|
||||||
todayStat.focusTimeQ4
|
color = colorScheme.onPrimaryContainer
|
||||||
)
|
)
|
||||||
} else "0h 0m",
|
Text(
|
||||||
style = typography.displaySmall,
|
if (todayStat != null) remember(todayStat) {
|
||||||
fontFamily = openRundeClock,
|
millisecondsToHoursMinutes(
|
||||||
color = colorScheme.onPrimaryContainer
|
todayStat.focusTimeQ1 +
|
||||||
)
|
todayStat.focusTimeQ2 +
|
||||||
|
todayStat.focusTimeQ3 +
|
||||||
|
todayStat.focusTimeQ4
|
||||||
|
)
|
||||||
|
} else "0h 0m",
|
||||||
|
style = typography.displaySmall,
|
||||||
|
fontFamily = openRundeClock,
|
||||||
|
color = colorScheme.onPrimaryContainer
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
Spacer(Modifier.width(8.dp))
|
||||||
Spacer(Modifier.width(8.dp))
|
Box(
|
||||||
Box(
|
modifier = Modifier
|
||||||
modifier = Modifier
|
.background(
|
||||||
.background(
|
colorScheme.tertiaryContainer,
|
||||||
colorScheme.tertiaryContainer,
|
MaterialTheme.shapes.largeIncreased
|
||||||
MaterialTheme.shapes.largeIncreased
|
)
|
||||||
)
|
.weight(1f)
|
||||||
.weight(1f)
|
) {
|
||||||
) {
|
Column(Modifier.padding(16.dp)) {
|
||||||
Column(Modifier.padding(16.dp)) {
|
Text(
|
||||||
Text(
|
"Break",
|
||||||
"Break",
|
style = typography.titleMedium,
|
||||||
style = typography.titleMedium,
|
color = colorScheme.onTertiaryContainer
|
||||||
color = colorScheme.onTertiaryContainer
|
)
|
||||||
)
|
Text(
|
||||||
Text(
|
if (todayStat != null) remember(todayStat) {
|
||||||
if (todayStat != null) remember(todayStat) {
|
millisecondsToHoursMinutes(todayStat.breakTime)
|
||||||
millisecondsToHoursMinutes(todayStat.breakTime)
|
} else "0h 0m",
|
||||||
} else "0h 0m",
|
style = typography.displaySmall,
|
||||||
style = typography.displaySmall,
|
fontFamily = openRundeClock,
|
||||||
fontFamily = openRundeClock,
|
color = colorScheme.onTertiaryContainer
|
||||||
color = colorScheme.onTertiaryContainer
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
item {
|
||||||
item {
|
val iconRotation by animateFloatAsState(
|
||||||
val iconRotation by animateFloatAsState(
|
if (todayStatExpanded) 180f else 0f,
|
||||||
if (todayStatExpanded) 180f else 0f,
|
animationSpec = motionScheme.defaultSpatialSpec()
|
||||||
animationSpec = motionScheme.defaultSpatialSpec()
|
)
|
||||||
)
|
Column(
|
||||||
Column(
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
modifier = Modifier.fillMaxWidth()
|
||||||
modifier = Modifier.fillMaxWidth()
|
|
||||||
) {
|
|
||||||
FilledTonalIconToggleButton(
|
|
||||||
checked = todayStatExpanded,
|
|
||||||
onCheckedChange = { todayStatExpanded = it },
|
|
||||||
shapes = IconButtonDefaults.toggleableShapes(),
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
.width(52.dp)
|
|
||||||
.align(Alignment.End)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
FilledTonalIconToggleButton(
|
||||||
painterResource(R.drawable.arrow_down),
|
checked = todayStatExpanded,
|
||||||
"More info",
|
onCheckedChange = { todayStatExpanded = it },
|
||||||
modifier = Modifier.rotate(iconRotation)
|
shapes = IconButtonDefaults.toggleableShapes(),
|
||||||
)
|
modifier = Modifier
|
||||||
}
|
.padding(horizontal = 16.dp)
|
||||||
AnimatedVisibility(todayStatExpanded) {
|
.width(52.dp)
|
||||||
TimeColumnChart(
|
.align(Alignment.End)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_down),
|
||||||
|
"More info",
|
||||||
|
modifier = Modifier.rotate(iconRotation)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ProductivityGraph(
|
||||||
|
todayStatExpanded,
|
||||||
todayStatModelProducer,
|
todayStatModelProducer,
|
||||||
timeConverter = ::millisecondsToHoursMinutes,
|
Modifier.padding(horizontal = 32.dp)
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
"This week",
|
||||||
|
style = typography.headlineSmall,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TimeColumnChart(
|
||||||
|
lastWeekSummaryModelProducer,
|
||||||
|
modifier = Modifier.padding(start = 16.dp)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
Text(
|
|
||||||
"This week",
|
|
||||||
style = typography.headlineSmall,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(16.dp))
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
TimeColumnChart(
|
|
||||||
allStatsSummaryModelProducer,
|
|
||||||
modifier = Modifier.padding(start = 16.dp)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,11 @@ internal fun TimeColumnChart(
|
|||||||
modelProducer: CartesianChartModelProducer,
|
modelProducer: CartesianChartModelProducer,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
thickness: Dp = 40.dp,
|
thickness: Dp = 40.dp,
|
||||||
timeConverter: (Long) -> String = ::millisecondsToHours
|
columnCollectionSpacing: Dp = 4.dp,
|
||||||
|
xValueFormatter: CartesianValueFormatter = CartesianValueFormatter.Default,
|
||||||
|
yValueFormatter: CartesianValueFormatter = CartesianValueFormatter { measuringContext, value, _ ->
|
||||||
|
millisecondsToHours(value.toLong())
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
val radius = with(LocalDensity.current) {
|
val radius = with(LocalDensity.current) {
|
||||||
(thickness / 2).toPx()
|
(thickness / 2).toPx()
|
||||||
@@ -82,20 +86,19 @@ internal fun TimeColumnChart(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
columnCollectionSpacing = 4.dp
|
columnCollectionSpacing = columnCollectionSpacing
|
||||||
),
|
),
|
||||||
startAxis = VerticalAxis.rememberStart(
|
startAxis = VerticalAxis.rememberStart(
|
||||||
line = rememberLineComponent(Fill.Transparent),
|
line = rememberLineComponent(Fill.Transparent),
|
||||||
tick = rememberLineComponent(Fill.Transparent),
|
tick = rememberLineComponent(Fill.Transparent),
|
||||||
guideline = rememberLineComponent(Fill.Transparent),
|
guideline = rememberLineComponent(Fill.Transparent),
|
||||||
valueFormatter = CartesianValueFormatter { measuringContext, value, _ ->
|
valueFormatter = yValueFormatter
|
||||||
timeConverter(value.toLong())
|
|
||||||
}
|
|
||||||
),
|
),
|
||||||
bottomAxis = HorizontalAxis.rememberBottom(
|
bottomAxis = HorizontalAxis.rememberBottom(
|
||||||
rememberLineComponent(Fill.Transparent),
|
rememberLineComponent(Fill.Transparent),
|
||||||
tick = rememberLineComponent(Fill.Transparent),
|
tick = rememberLineComponent(Fill.Transparent),
|
||||||
guideline = rememberLineComponent(Fill.Transparent)
|
guideline = rememberLineComponent(Fill.Transparent),
|
||||||
|
valueFormatter = xValueFormatter
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
modelProducer = modelProducer,
|
modelProducer = modelProducer,
|
||||||
|
|||||||
@@ -28,15 +28,18 @@ class StatsViewModel(
|
|||||||
private val allStatsSummary = statRepository.getLastWeekStatsSummary()
|
private val allStatsSummary = statRepository.getLastWeekStatsSummary()
|
||||||
private val averageFocusTimes = statRepository.getAverageFocusTimes()
|
private val averageFocusTimes = statRepository.getAverageFocusTimes()
|
||||||
|
|
||||||
val allStatsSummaryModelProducer = CartesianChartModelProducer()
|
val lastWeekSummaryChartModelProducer = CartesianChartModelProducer()
|
||||||
val todayStatModelProducer = CartesianChartModelProducer()
|
val todayStatModelProducer = CartesianChartModelProducer()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
viewModelScope.launch(Dispatchers.IO) {
|
viewModelScope.launch(Dispatchers.IO) {
|
||||||
allStatsSummary
|
allStatsSummary
|
||||||
.collect { list ->
|
.collect { list ->
|
||||||
allStatsSummaryModelProducer.runTransaction {
|
lastWeekSummaryChartModelProducer.runTransaction {
|
||||||
columnSeries { series(list.reversed().map { it.focusTime }) }
|
columnSeries {
|
||||||
|
// reversing is required because we need ascending order while the DB returns descending order
|
||||||
|
series(list.reversed().map { it.focusTime })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,13 +85,15 @@ class TimerViewModel(
|
|||||||
|
|
||||||
resetTimer()
|
resetTimer()
|
||||||
|
|
||||||
var lastDate = LocalDate.parse(statRepository.getLastDate())
|
var lastDate = statRepository.getLastDate()
|
||||||
val today = LocalDate.now()
|
val today = LocalDate.now()
|
||||||
|
|
||||||
// Fills dates between today and lastDate with 0s to ensure continuous history
|
// Fills dates between today and lastDate with 0s to ensure continuous history
|
||||||
while (lastDate.until(today).days > 0) {
|
lastDate?.until(today)?.days?.let {
|
||||||
lastDate = lastDate.plusDays(1)
|
while (it > 0) {
|
||||||
statRepository.insertStat(Stat(lastDate.toString(), 0, 0, 0, 0, 0))
|
lastDate = lastDate?.plusDays(1)
|
||||||
|
statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
delay(1500)
|
delay(1500)
|
||||||
|
|||||||
Reference in New Issue
Block a user