feat: Save time to database when timer state changes

This commit is contained in:
Nishant Mishra
2025-07-10 10:21:49 +05:30
parent 023f127181
commit e17b50f54e
10 changed files with 209 additions and 89 deletions

View File

@@ -10,14 +10,14 @@ package org.nsh07.pomodoro.data
import android.content.Context import android.content.Context
interface AppContainer { interface AppContainer {
val appPreferencesRepository: AppPreferenceRepository val appPreferenceRepository: AppPreferenceRepository
val appStatRepository: AppStatRepository val appStatRepository: AppStatRepository
val appTimerRepository: AppTimerRepository val appTimerRepository: AppTimerRepository
} }
class DefaultAppContainer(context: Context) : AppContainer { class DefaultAppContainer(context: Context) : AppContainer {
override val appPreferencesRepository: AppPreferenceRepository by lazy { override val appPreferenceRepository: AppPreferenceRepository by lazy {
AppPreferenceRepository(AppDatabase.getDatabase(context).preferenceDao()) AppPreferenceRepository(AppDatabase.getDatabase(context).preferenceDao())
} }

View File

@@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
interface PreferencesRepository { interface PreferenceRepository {
suspend fun saveIntPreference(key: String, value: Int): Int suspend fun saveIntPreference(key: String, value: Int): Int
suspend fun getIntPreference(key: String): Int? suspend fun getIntPreference(key: String): Int?
@@ -15,7 +15,7 @@ interface PreferencesRepository {
class AppPreferenceRepository( class AppPreferenceRepository(
private val preferenceDao: PreferenceDao, private val preferenceDao: PreferenceDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : PreferencesRepository { ) : PreferenceRepository {
override suspend fun saveIntPreference(key: String, value: Int): Int = override suspend fun saveIntPreference(key: String, value: Int): Int =
withContext(ioDispatcher) { withContext(ioDispatcher) {
preferenceDao.insertIntPreference(IntPreference(key, value)) preferenceDao.insertIntPreference(IntPreference(key, value))

View File

@@ -1,8 +1,8 @@
/* /*
* Copyright (c) 2025 Nishant Mishra * Copyright (c) 2025 Nishant Mishra
* *
* You should have received a copy of the GNU General Public License * You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
package org.nsh07.pomodoro.data package org.nsh07.pomodoro.data
@@ -14,6 +14,9 @@ import androidx.room.PrimaryKey
data class Stat( data class Stat(
@PrimaryKey @PrimaryKey
val date: String, val date: String,
val focusTime: Int, val focusTimeQ1: Long,
val breakTime: Int val focusTimeQ2: Long,
val focusTimeQ3: Long,
val focusTimeQ4: Long,
val breakTime: Long
) )

View File

@@ -18,11 +18,20 @@ interface StatDao {
@Insert(onConflict = REPLACE) @Insert(onConflict = REPLACE)
suspend fun insertStat(stat: Stat) suspend fun insertStat(stat: Stat)
@Query("UPDATE stat SET focusTime = focusTime + :focusTime WHERE date = :date") @Query("UPDATE stat SET focusTimeQ1 = focusTimeQ1 + :focusTime WHERE date = :date")
suspend fun addFocusTime(date: String, focusTime: Int) suspend fun addFocusTimeQ1(date: String, focusTime: Long)
@Query("UPDATE stat SET focusTimeQ2 = focusTimeQ2 + :focusTime WHERE date = :date")
suspend fun addFocusTimeQ2(date: String, focusTime: Long)
@Query("UPDATE stat SET focusTimeQ3 = focusTimeQ3 + :focusTime WHERE date = :date")
suspend fun addFocusTimeQ3(date: String, focusTime: Long)
@Query("UPDATE stat SET focusTimeQ4 = focusTimeQ4 + :focusTime WHERE date = :date")
suspend fun addFocusTimeQ4(date: String, 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: Int) suspend fun addBreakTime(date: String, 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: String): Flow<Stat?>

View File

@@ -12,11 +12,12 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.time.LocalDate import java.time.LocalDate
import java.time.LocalTime
interface StatRepository { interface StatRepository {
suspend fun addFocusTime(focusTime: Int) suspend fun addFocusTime(focusTime: Long)
suspend fun addBreakTime(breakTime: Int) suspend fun addBreakTime(breakTime: Long)
fun getTodayStat(): Flow<Stat?> fun getTodayStat(): Flow<Stat?>
@@ -27,21 +28,55 @@ class AppStatRepository(
private val statDao: StatDao, private val statDao: StatDao,
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
) : StatRepository { ) : StatRepository {
override suspend fun addFocusTime(focusTime: Int) = withContext(ioDispatcher) { override suspend fun addFocusTime(focusTime: Long) = withContext(ioDispatcher) {
val currentDate = LocalDate.now().toString() val currentDate = LocalDate.now().toString()
val currentTime = LocalTime.now().toSecondOfDay()
val secondsInDay = 24 * 60 * 60
if (statDao.statExists(currentDate)) { if (statDao.statExists(currentDate)) {
statDao.addFocusTime(currentDate, focusTime) when (currentTime) {
in 0..(secondsInDay / 4) ->
statDao.addFocusTimeQ1(currentDate, focusTime)
in (secondsInDay / 4)..(secondsInDay / 2) ->
statDao.addFocusTimeQ2(currentDate, focusTime)
in (secondsInDay / 2)..(3 * secondsInDay / 4) ->
statDao.addFocusTimeQ3(currentDate, focusTime)
else -> statDao.addFocusTimeQ4(currentDate, focusTime)
}
} else { } else {
statDao.insertStat(Stat(currentDate, focusTime, 0)) when (currentTime) {
in 0..(secondsInDay / 4) ->
statDao.insertStat(
Stat(currentDate, focusTime, 0, 0, 0, 0)
)
in (secondsInDay / 4)..(secondsInDay / 2) ->
statDao.insertStat(
Stat(currentDate, 0, focusTime, 0, 0, 0)
)
in (secondsInDay / 2)..(3 * secondsInDay / 4) ->
statDao.insertStat(
Stat(currentDate, 0, 0, focusTime, 0, 0)
)
else ->
statDao.insertStat(
Stat(currentDate, 0, 0, 0, focusTime, 0)
)
}
} }
} }
override suspend fun addBreakTime(breakTime: Int) = withContext(ioDispatcher) { override suspend fun addBreakTime(breakTime: Long) = withContext(ioDispatcher) {
val currentDate = LocalDate.now().toString() val currentDate = LocalDate.now().toString()
if (statDao.statExists(currentDate)) { if (statDao.statExists(currentDate)) {
statDao.addBreakTime(currentDate, breakTime) statDao.addBreakTime(currentDate, breakTime)
} else { } else {
statDao.insertStat(Stat(currentDate, 0, breakTime)) statDao.insertStat(Stat(currentDate, 0, 0, 0, 0, breakTime))
} }
} }

View File

@@ -1,15 +1,22 @@
/*
* 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 package org.nsh07.pomodoro.data
interface TimerRepository { interface TimerRepository {
var focusTime: Int var focusTime: Long
var shortBreakTime: Int var shortBreakTime: Long
var longBreakTime: Int var longBreakTime: Long
var sessionLength: Int var sessionLength: Int
} }
class AppTimerRepository : TimerRepository { class AppTimerRepository : TimerRepository {
override var focusTime = 25 * 60 * 1000 override var focusTime = 25 * 60 * 1000L
override var shortBreakTime = 5 * 60 * 1000 override var shortBreakTime = 5 * 60 * 1000L
override var longBreakTime = 15 * 60 * 1000 override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4 override var sessionLength = 4
} }

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.viewModel package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
@@ -46,7 +53,7 @@ class SettingsViewModel(
timerRepository.focusTime = preferenceRepository.saveIntPreference( timerRepository.focusTime = preferenceRepository.saveIntPreference(
"focus_time", "focus_time",
it.toString().toInt() * 60 * 1000 it.toString().toInt() * 60 * 1000
) ).toLong()
} }
} }
} }
@@ -58,7 +65,7 @@ class SettingsViewModel(
timerRepository.shortBreakTime = preferenceRepository.saveIntPreference( timerRepository.shortBreakTime = preferenceRepository.saveIntPreference(
"short_break_time", "short_break_time",
it.toString().toInt() * 60 * 1000 it.toString().toInt() * 60 * 1000
) ).toLong()
} }
} }
} }
@@ -70,7 +77,7 @@ class SettingsViewModel(
timerRepository.longBreakTime = preferenceRepository.saveIntPreference( timerRepository.longBreakTime = preferenceRepository.saveIntPreference(
"long_break_time", "long_break_time",
it.toString().toInt() * 60 * 1000 it.toString().toInt() * 60 * 1000
) ).toLong()
} }
} }
} }
@@ -89,7 +96,7 @@ class SettingsViewModel(
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication) val application = (this[APPLICATION_KEY] as TomatoApplication)
val appPreferenceRepository = application.container.appPreferencesRepository val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository val appTimerRepository = application.container.appTimerRepository
SettingsViewModel( SettingsViewModel(

View File

@@ -1,9 +1,16 @@
/*
* 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.timerScreen.viewModel package org.nsh07.pomodoro.ui.timerScreen.viewModel
data class TimerState( data class TimerState(
val timerMode: TimerMode = TimerMode.FOCUS, val timerMode: TimerMode = TimerMode.FOCUS,
val timeStr: String = "25:00", val timeStr: String = "25:00",
val totalTime: Int = 25 * 60, val totalTime: Long = 25 * 60,
val timerRunning: Boolean = false, val timerRunning: Boolean = false,
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK, val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
val nextTimeStr: String = "5:00" val nextTimeStr: String = "5:00"

View File

@@ -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 <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.timerScreen.viewModel package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.os.SystemClock import android.os.SystemClock
@@ -17,30 +24,18 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.AppPreferenceRepository import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.utils.millisecondsToStr import org.nsh07.pomodoro.utils.millisecondsToStr
@OptIn(FlowPreview::class) @OptIn(FlowPreview::class)
class TimerViewModel( class TimerViewModel(
private val preferenceRepository: AppPreferenceRepository, private val preferenceRepository: PreferenceRepository,
private val statRepository: StatRepository,
private val timerRepository: TimerRepository private val timerRepository: TimerRepository
) : ViewModel() { ) : ViewModel() {
init { // TODO: Document code
viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime = preferenceRepository.getIntPreference("focus_time")
?: preferenceRepository.saveIntPreference("focus_time", timerRepository.focusTime)
timerRepository.shortBreakTime = preferenceRepository.getIntPreference("short_break_time")
?: preferenceRepository.saveIntPreference("short_break_time", timerRepository.shortBreakTime)
timerRepository.longBreakTime = preferenceRepository.getIntPreference("long_break_time")
?: preferenceRepository.saveIntPreference("long_break_time", timerRepository.longBreakTime)
timerRepository.sessionLength = preferenceRepository.getIntPreference("session_length")
?: preferenceRepository.saveIntPreference("session_length", timerRepository.sessionLength)
resetTimer()
}
}
private val _timerState = MutableStateFlow( private val _timerState = MutableStateFlow(
TimerState( TimerState(
totalTime = timerRepository.focusTime, totalTime = timerRepository.focusTime,
@@ -48,68 +43,106 @@ class TimerViewModel(
nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime) nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
) )
) )
val timerState: StateFlow<TimerState> = _timerState.asStateFlow() val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
var timerJob: Job? = null var timerJob: Job? = null
private val _time = MutableStateFlow(timerRepository.focusTime) private val _time = MutableStateFlow(timerRepository.focusTime)
val time: StateFlow<Int> = _time.asStateFlow()
val time: StateFlow<Long> = _time.asStateFlow()
private var cycles = 0 private var cycles = 0
private var startTime = 0L private var startTime = 0L
private var pauseTime = 0L private var pauseTime = 0L
private var pauseDuration = 0L private var pauseDuration = 0L
fun resetTimer() { init {
_time.update { timerRepository.focusTime } viewModelScope.launch(Dispatchers.IO) {
cycles = 0 timerRepository.focusTime =
startTime = 0L preferenceRepository.getIntPreference("focus_time")?.toLong()
pauseTime = 0L ?: preferenceRepository.saveIntPreference(
pauseDuration = 0L "focus_time",
timerRepository.focusTime.toInt()
).toLong()
timerRepository.shortBreakTime =
preferenceRepository.getIntPreference("short_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"short_break_time",
timerRepository.shortBreakTime.toInt()
).toLong()
timerRepository.longBreakTime =
preferenceRepository.getIntPreference("long_break_time")?.toLong()
?: preferenceRepository.saveIntPreference(
"long_break_time",
timerRepository.longBreakTime.toInt()
).toLong()
timerRepository.sessionLength = preferenceRepository.getIntPreference("session_length")
?: preferenceRepository.saveIntPreference(
"session_length",
timerRepository.sessionLength
)
_timerState.update { currentState -> resetTimer()
currentState.copy(
timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value),
totalTime = time.value,
nextTimerMode = TimerMode.SHORT_BREAK,
nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
)
} }
} }
fun skipTimer() { fun resetTimer() {
startTime = 0L viewModelScope.launch {
pauseTime = 0L saveTimeToDb()
pauseDuration = 0L
cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
if (cycles % 2 == 0) {
_time.update { timerRepository.focusTime } _time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L
pauseTime = 0L
pauseDuration = 0L
_timerState.update { currentState -> _timerState.update { currentState ->
currentState.copy( currentState.copy(
timerMode = TimerMode.FOCUS, timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value), timeStr = millisecondsToStr(time.value),
totalTime = time.value, totalTime = time.value,
nextTimerMode = if (cycles == 6) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, nextTimerMode = TimerMode.SHORT_BREAK,
nextTimeStr = if (cycles == 6) millisecondsToStr( nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
timerRepository.longBreakTime
) else millisecondsToStr(
timerRepository.shortBreakTime
)
) )
} }
} else { }
val long = cycles == (timerRepository.sessionLength * 2) - 1 }
_time.update { if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime }
_timerState.update { currentState -> fun skipTimer() {
currentState.copy( viewModelScope.launch {
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, saveTimeToDb()
timeStr = millisecondsToStr(time.value), startTime = 0L
totalTime = time.value, pauseTime = 0L
nextTimerMode = TimerMode.FOCUS, pauseDuration = 0L
nextTimeStr = millisecondsToStr(timerRepository.focusTime)
) cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
if (cycles % 2 == 0) {
_time.update { timerRepository.focusTime }
_timerState.update { currentState ->
currentState.copy(
timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value),
totalTime = time.value,
nextTimerMode = if (cycles == 6) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
nextTimeStr = if (cycles == 6) millisecondsToStr(
timerRepository.longBreakTime
) else millisecondsToStr(
timerRepository.shortBreakTime
)
)
}
} else {
val long = cycles == (timerRepository.sessionLength * 2) - 1
_time.update { if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime }
_timerState.update { currentState ->
currentState.copy(
timerMode = if (long) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK,
timeStr = millisecondsToStr(time.value),
totalTime = time.value,
nextTimerMode = TimerMode.FOCUS,
nextTimeStr = millisecondsToStr(timerRepository.focusTime)
)
}
} }
} }
} }
@@ -164,15 +197,27 @@ class TimerViewModel(
} }
} }
suspend fun saveTimeToDb() {
when (timerState.value.timerMode) {
TimerMode.FOCUS -> statRepository
.addFocusTime((timerState.value.totalTime - time.value))
else -> statRepository
.addBreakTime((timerState.value.totalTime - time.value))
}
}
companion object { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication) val application = (this[APPLICATION_KEY] as TomatoApplication)
val appPreferenceRepository = application.container.appPreferencesRepository val appPreferenceRepository = application.container.appPreferenceRepository
val appStatRepository = application.container.appStatRepository
val appTimerRepository = application.container.appTimerRepository val appTimerRepository = application.container.appTimerRepository
TimerViewModel( TimerViewModel(
preferenceRepository = appPreferenceRepository, preferenceRepository = appPreferenceRepository,
statRepository = appStatRepository,
timerRepository = appTimerRepository timerRepository = appTimerRepository
) )
} }

View File

@@ -1,9 +1,16 @@
/*
* 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.utils package org.nsh07.pomodoro.utils
import java.util.Locale import java.util.Locale
import kotlin.math.ceil import kotlin.math.ceil
fun millisecondsToStr(t: Int): String { fun millisecondsToStr(t: Long): String {
val min = (ceil(t / 1000.0).toInt() / 60) val min = (ceil(t / 1000.0).toInt() / 60)
val sec = (ceil(t / 1000.0).toInt() % 60) val sec = (ceil(t / 1000.0).toInt() % 60)
return String.format(locale = Locale.getDefault(), "%02d:%02d", min, sec) return String.format(locale = Locale.getDefault(), "%02d:%02d", min, sec)