feat: Save time to database when timer state changes
This commit is contained in:
@@ -10,14 +10,14 @@ package org.nsh07.pomodoro.data
|
||||
import android.content.Context
|
||||
|
||||
interface AppContainer {
|
||||
val appPreferencesRepository: AppPreferenceRepository
|
||||
val appPreferenceRepository: AppPreferenceRepository
|
||||
val appStatRepository: AppStatRepository
|
||||
val appTimerRepository: AppTimerRepository
|
||||
}
|
||||
|
||||
class DefaultAppContainer(context: Context) : AppContainer {
|
||||
|
||||
override val appPreferencesRepository: AppPreferenceRepository by lazy {
|
||||
override val appPreferenceRepository: AppPreferenceRepository by lazy {
|
||||
AppPreferenceRepository(AppDatabase.getDatabase(context).preferenceDao())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface PreferencesRepository {
|
||||
interface PreferenceRepository {
|
||||
suspend fun saveIntPreference(key: String, value: Int): Int
|
||||
|
||||
suspend fun getIntPreference(key: String): Int?
|
||||
@@ -15,7 +15,7 @@ interface PreferencesRepository {
|
||||
class AppPreferenceRepository(
|
||||
private val preferenceDao: PreferenceDao,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : PreferencesRepository {
|
||||
) : PreferenceRepository {
|
||||
override suspend fun saveIntPreference(key: String, value: Int): Int =
|
||||
withContext(ioDispatcher) {
|
||||
preferenceDao.insertIntPreference(IntPreference(key, value))
|
||||
|
||||
@@ -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
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* 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
|
||||
@@ -14,6 +14,9 @@ import androidx.room.PrimaryKey
|
||||
data class Stat(
|
||||
@PrimaryKey
|
||||
val date: String,
|
||||
val focusTime: Int,
|
||||
val breakTime: Int
|
||||
val focusTimeQ1: Long,
|
||||
val focusTimeQ2: Long,
|
||||
val focusTimeQ3: Long,
|
||||
val focusTimeQ4: Long,
|
||||
val breakTime: Long
|
||||
)
|
||||
|
||||
@@ -18,11 +18,20 @@ interface StatDao {
|
||||
@Insert(onConflict = REPLACE)
|
||||
suspend fun insertStat(stat: Stat)
|
||||
|
||||
@Query("UPDATE stat SET focusTime = focusTime + :focusTime WHERE date = :date")
|
||||
suspend fun addFocusTime(date: String, focusTime: Int)
|
||||
@Query("UPDATE stat SET focusTimeQ1 = focusTimeQ1 + :focusTime WHERE date = :date")
|
||||
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")
|
||||
suspend fun addBreakTime(date: String, breakTime: Int)
|
||||
suspend fun addBreakTime(date: String, breakTime: Long)
|
||||
|
||||
@Query("SELECT * FROM stat WHERE date = :date")
|
||||
fun getStat(date: String): Flow<Stat?>
|
||||
|
||||
@@ -12,11 +12,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalTime
|
||||
|
||||
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?>
|
||||
|
||||
@@ -27,21 +28,55 @@ class AppStatRepository(
|
||||
private val statDao: StatDao,
|
||||
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
|
||||
) : StatRepository {
|
||||
override suspend fun addFocusTime(focusTime: Int) = withContext(ioDispatcher) {
|
||||
override suspend fun addFocusTime(focusTime: Long) = withContext(ioDispatcher) {
|
||||
val currentDate = LocalDate.now().toString()
|
||||
val currentTime = LocalTime.now().toSecondOfDay()
|
||||
val secondsInDay = 24 * 60 * 60
|
||||
|
||||
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 {
|
||||
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()
|
||||
if (statDao.statExists(currentDate)) {
|
||||
statDao.addBreakTime(currentDate, breakTime)
|
||||
} else {
|
||||
statDao.insertStat(Stat(currentDate, 0, breakTime))
|
||||
statDao.insertStat(Stat(currentDate, 0, 0, 0, 0, breakTime))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
interface TimerRepository {
|
||||
var focusTime: Int
|
||||
var shortBreakTime: Int
|
||||
var longBreakTime: Int
|
||||
var focusTime: Long
|
||||
var shortBreakTime: Long
|
||||
var longBreakTime: Long
|
||||
var sessionLength: Int
|
||||
}
|
||||
|
||||
class AppTimerRepository : TimerRepository {
|
||||
override var focusTime = 25 * 60 * 1000
|
||||
override var shortBreakTime = 5 * 60 * 1000
|
||||
override var longBreakTime = 15 * 60 * 1000
|
||||
override var focusTime = 25 * 60 * 1000L
|
||||
override var shortBreakTime = 5 * 60 * 1000L
|
||||
override var longBreakTime = 15 * 60 * 1000L
|
||||
override var sessionLength = 4
|
||||
}
|
||||
@@ -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
|
||||
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
@@ -46,7 +53,7 @@ class SettingsViewModel(
|
||||
timerRepository.focusTime = preferenceRepository.saveIntPreference(
|
||||
"focus_time",
|
||||
it.toString().toInt() * 60 * 1000
|
||||
)
|
||||
).toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,7 +65,7 @@ class SettingsViewModel(
|
||||
timerRepository.shortBreakTime = preferenceRepository.saveIntPreference(
|
||||
"short_break_time",
|
||||
it.toString().toInt() * 60 * 1000
|
||||
)
|
||||
).toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -70,7 +77,7 @@ class SettingsViewModel(
|
||||
timerRepository.longBreakTime = preferenceRepository.saveIntPreference(
|
||||
"long_break_time",
|
||||
it.toString().toInt() * 60 * 1000
|
||||
)
|
||||
).toLong()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -89,7 +96,7 @@ class SettingsViewModel(
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
val application = (this[APPLICATION_KEY] as TomatoApplication)
|
||||
val appPreferenceRepository = application.container.appPreferencesRepository
|
||||
val appPreferenceRepository = application.container.appPreferenceRepository
|
||||
val appTimerRepository = application.container.appTimerRepository
|
||||
|
||||
SettingsViewModel(
|
||||
|
||||
@@ -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
|
||||
|
||||
data class TimerState(
|
||||
val timerMode: TimerMode = TimerMode.FOCUS,
|
||||
val timeStr: String = "25:00",
|
||||
val totalTime: Int = 25 * 60,
|
||||
val totalTime: Long = 25 * 60,
|
||||
val timerRunning: Boolean = false,
|
||||
val nextTimerMode: TimerMode = TimerMode.SHORT_BREAK,
|
||||
val nextTimeStr: String = "5:00"
|
||||
|
||||
@@ -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
|
||||
|
||||
import android.os.SystemClock
|
||||
@@ -17,30 +24,18 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
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.utils.millisecondsToStr
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
class TimerViewModel(
|
||||
private val preferenceRepository: AppPreferenceRepository,
|
||||
private val preferenceRepository: PreferenceRepository,
|
||||
private val statRepository: StatRepository,
|
||||
private val timerRepository: TimerRepository
|
||||
) : ViewModel() {
|
||||
init {
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Document code
|
||||
private val _timerState = MutableStateFlow(
|
||||
TimerState(
|
||||
totalTime = timerRepository.focusTime,
|
||||
@@ -48,68 +43,106 @@ class TimerViewModel(
|
||||
nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
|
||||
)
|
||||
)
|
||||
|
||||
val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
|
||||
var timerJob: Job? = null
|
||||
|
||||
private val _time = MutableStateFlow(timerRepository.focusTime)
|
||||
val time: StateFlow<Int> = _time.asStateFlow()
|
||||
|
||||
val time: StateFlow<Long> = _time.asStateFlow()
|
||||
private var cycles = 0
|
||||
|
||||
private var startTime = 0L
|
||||
private var pauseTime = 0L
|
||||
private var pauseDuration = 0L
|
||||
|
||||
fun resetTimer() {
|
||||
_time.update { timerRepository.focusTime }
|
||||
cycles = 0
|
||||
startTime = 0L
|
||||
pauseTime = 0L
|
||||
pauseDuration = 0L
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
timerRepository.focusTime =
|
||||
preferenceRepository.getIntPreference("focus_time")?.toLong()
|
||||
?: preferenceRepository.saveIntPreference(
|
||||
"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 ->
|
||||
currentState.copy(
|
||||
timerMode = TimerMode.FOCUS,
|
||||
timeStr = millisecondsToStr(time.value),
|
||||
totalTime = time.value,
|
||||
nextTimerMode = TimerMode.SHORT_BREAK,
|
||||
nextTimeStr = millisecondsToStr(timerRepository.shortBreakTime)
|
||||
)
|
||||
resetTimer()
|
||||
}
|
||||
}
|
||||
|
||||
fun skipTimer() {
|
||||
startTime = 0L
|
||||
pauseTime = 0L
|
||||
pauseDuration = 0L
|
||||
cycles = (cycles + 1) % (timerRepository.sessionLength * 2)
|
||||
|
||||
if (cycles % 2 == 0) {
|
||||
fun resetTimer() {
|
||||
viewModelScope.launch {
|
||||
saveTimeToDb()
|
||||
_time.update { timerRepository.focusTime }
|
||||
cycles = 0
|
||||
startTime = 0L
|
||||
pauseTime = 0L
|
||||
pauseDuration = 0L
|
||||
|
||||
_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
|
||||
)
|
||||
nextTimerMode = TimerMode.SHORT_BREAK,
|
||||
nextTimeStr = 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)
|
||||
)
|
||||
fun skipTimer() {
|
||||
viewModelScope.launch {
|
||||
saveTimeToDb()
|
||||
startTime = 0L
|
||||
pauseTime = 0L
|
||||
pauseDuration = 0L
|
||||
|
||||
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 {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
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
|
||||
|
||||
TimerViewModel(
|
||||
preferenceRepository = appPreferenceRepository,
|
||||
statRepository = appStatRepository,
|
||||
timerRepository = appTimerRepository
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
import java.util.Locale
|
||||
import kotlin.math.ceil
|
||||
|
||||
fun millisecondsToStr(t: Int): String {
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user