diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 1b28db4..215fdc9 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -69,14 +69,14 @@ class MainActivity : ComponentActivity() { ) { val colorScheme = colorScheme LaunchedEffect(colorScheme) { - appContainer.appTimerRepository.colorScheme = colorScheme + appContainer.stateRepository.colorScheme = colorScheme } AppScreen( isPlus = isPlus, isAODEnabled = settingsState.aodEnabled, setTimerFrequency = { - appContainer.appTimerRepository.timerFrequency = it + appContainer.stateRepository.timerFrequency = it } ) } @@ -86,13 +86,13 @@ class MainActivity : ComponentActivity() { override fun onStop() { super.onStop() - // Reduce the timer loop frequency when not visible to save battery power - appContainer.appTimerRepository.timerFrequency = 1f + // Reduce the timer loop frequency when not visible to save battery + appContainer.stateRepository.timerFrequency = 1f } override fun onStart() { super.onStart() // Increase the timer loop frequency again when visible to make the progress smoother - appContainer.appTimerRepository.timerFrequency = 60f + appContainer.stateRepository.timerFrequency = 60f } } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index 12186a6..37c9f8a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -31,19 +31,16 @@ import org.nsh07.pomodoro.billing.BillingManager import org.nsh07.pomodoro.billing.BillingManagerProvider import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.service.addTimerActions -import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState -import org.nsh07.pomodoro.utils.millisecondsToStr interface AppContainer { val appPreferenceRepository: AppPreferenceRepository val appStatRepository: AppStatRepository - val appTimerRepository: AppTimerRepository + val stateRepository: StateRepository val billingManager: BillingManager val notificationManager: NotificationManagerCompat val notificationManagerService: NotificationManager val notificationBuilder: NotificationCompat.Builder val serviceHelper: ServiceHelper - val timerState: MutableStateFlow val time: MutableStateFlow var activityTurnScreenOn: (Boolean) -> Unit } @@ -58,7 +55,9 @@ class DefaultAppContainer(context: Context) : AppContainer { AppStatRepository(AppDatabase.getDatabase(context).statDao()) } - override val appTimerRepository: AppTimerRepository by lazy { AppTimerRepository() } + override val stateRepository: StateRepository by lazy { + StateRepository() + } override val billingManager: BillingManager by lazy { BillingManagerProvider.manager } @@ -93,20 +92,9 @@ class DefaultAppContainer(context: Context) : AppContainer { ServiceHelper(context) } - override val timerState: MutableStateFlow by lazy { - MutableStateFlow( - TimerState( - totalTime = appTimerRepository.focusTime, - timeStr = millisecondsToStr(appTimerRepository.focusTime), - nextTimeStr = millisecondsToStr(appTimerRepository.shortBreakTime) - ) - ) - } - override val time: MutableStateFlow by lazy { - MutableStateFlow(appTimerRepository.focusTime) + MutableStateFlow(stateRepository.settingsState.value.focusTime) } override var activityTurnScreenOn: (Boolean) -> Unit = {} - } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt new file mode 100644 index 0000000..410e711 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/data/StateRepository.kt @@ -0,0 +1,31 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.data + +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.lightColorScheme +import kotlinx.coroutines.flow.MutableStateFlow +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState + +class StateRepository { + val timerState = MutableStateFlow(TimerState()) + val settingsState = MutableStateFlow(SettingsState()) + var timerFrequency: Float = 60f + var colorScheme: ColorScheme = lightColorScheme() +} diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt deleted file mode 100644 index 89f27af..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) 2025 Nishant Mishra - * - * This file is part of Tomato - a minimalist pomodoro timer for Android. - * - * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU - * General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tomato. - * If not, see . - */ - -package org.nsh07.pomodoro.data - -import android.net.Uri -import android.provider.Settings -import androidx.compose.material3.ColorScheme -import androidx.compose.material3.lightColorScheme -import kotlinx.coroutines.flow.MutableStateFlow - -/** - * Interface that holds the timer durations for each timer type. This repository maintains a single - * source of truth for the timer durations for the various ViewModels in the app. - */ -interface TimerRepository { - var focusTime: Long - var shortBreakTime: Long - var longBreakTime: Long - - var sessionLength: Int - - var timerFrequency: Float - - var alarmEnabled: Boolean - var vibrateEnabled: Boolean - var dndEnabled: Boolean - - var colorScheme: ColorScheme - - var alarmSoundUri: Uri? - - var serviceRunning: MutableStateFlow -} - -/** - * See [TimerRepository] for more details - */ -class AppTimerRepository : TimerRepository { - override var focusTime = 25 * 60 * 1000L - override var shortBreakTime = 5 * 60 * 1000L - override var longBreakTime = 15 * 60 * 1000L - override var sessionLength = 4 - override var timerFrequency: Float = 60f - override var alarmEnabled = true - override var vibrateEnabled = true - override var dndEnabled: Boolean = false - override var colorScheme = lightColorScheme() - override var alarmSoundUri: Uri? = - Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI - override var serviceRunning = MutableStateFlow(false) -} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 1ee266e..bffa6cd 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -36,7 +36,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay -import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking @@ -53,12 +52,13 @@ class TimerService : Service() { (application as TomatoApplication).container } - private val timerRepository by lazy { appContainer.appTimerRepository } + private val stateRepository by lazy { appContainer.stateRepository } private val statRepository by lazy { appContainer.appStatRepository } private val notificationManager by lazy { appContainer.notificationManager } private val notificationManagerService by lazy { appContainer.notificationManagerService } private val notificationBuilder by lazy { appContainer.notificationBuilder } - private val _timerState by lazy { appContainer.timerState } + private val _timerState by lazy { stateRepository.timerState } + private val _settingsState by lazy { stateRepository.settingsState } private val _time by lazy { appContainer.time } /** @@ -68,8 +68,6 @@ class TimerService : Service() { get() = _time.value set(value) = _time.update { value } - private val timerState by lazy { _timerState.asStateFlow() } - private var cycles = 0 private var startTime = 0L private var pauseTime = 0L @@ -94,7 +92,7 @@ class TimerService : Service() { } } - private val cs by lazy { timerRepository.colorScheme } + private val cs by lazy { stateRepository.colorScheme } private lateinit var notificationStyle: NotificationCompat.ProgressStyle @@ -104,12 +102,12 @@ class TimerService : Service() { override fun onCreate() { super.onCreate() - timerRepository.serviceRunning.update { true } + stateRepository.timerState.update { it.copy(serviceRunning = true) } alarm = initializeMediaPlayer() } override fun onDestroy() { - timerRepository.serviceRunning.update { false } + stateRepository.timerState.update { it.copy(serviceRunning = false) } runBlocking { job.cancel() saveTimeToDb() @@ -129,7 +127,7 @@ class TimerService : Service() { } Actions.RESET.toString() -> { - if (timerState.value.timerRunning) toggleTimer() + if (_timerState.value.timerRunning) toggleTimer() skipScope.launch { resetTimer() stopForegroundService() @@ -148,7 +146,7 @@ class TimerService : Service() { private fun toggleTimer() { updateProgressSegments() - if (timerState.value.timerRunning) { + if (_timerState.value.timerRunning) { setDoNotDisturb(false) notificationBuilder.clearActions().addTimerActions( this, R.drawable.play, getString(R.string.start) @@ -159,7 +157,7 @@ class TimerService : Service() { } pauseTime = SystemClock.elapsedRealtime() } else { - if (timerState.value.timerMode == TimerMode.FOCUS) setDoNotDisturb(true) + if (_timerState.value.timerMode == TimerMode.FOCUS) setDoNotDisturb(true) else setDoNotDisturb(false) notificationBuilder.clearActions().addTimerActions( this, R.drawable.pause, getString(R.string.stop) @@ -171,19 +169,20 @@ class TimerService : Service() { timerScope.launch { while (true) { - if (!timerState.value.timerRunning) break + if (!_timerState.value.timerRunning) break if (startTime == 0L) startTime = SystemClock.elapsedRealtime() - time = when (timerState.value.timerMode) { - TimerMode.FOCUS -> timerRepository.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) + val settingsState = _settingsState.value + time = when (_timerState.value.timerMode) { + TimerMode.FOCUS -> settingsState.focusTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) - TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) + TimerMode.SHORT_BREAK -> settingsState.shortBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) - else -> timerRepository.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) + else -> settingsState.longBreakTime - (SystemClock.elapsedRealtime() - startTime - pauseDuration) } iterations = - (iterations + 1) % timerRepository.timerFrequency.toInt().coerceAtLeast(1) + (iterations + 1) % stateRepository.timerFrequency.toInt().coerceAtLeast(1) if (iterations == 0) showTimerNotification(time.toInt()) @@ -199,7 +198,7 @@ class TimerService : Service() { timeStr = millisecondsToStr(time) ) } - val totalTime = timerState.value.totalTime + val totalTime = _timerState.value.totalTime if (totalTime - time < lastSavedDuration) lastSavedDuration = @@ -208,7 +207,7 @@ class TimerService : Service() { saveTimeToDb() } - delay((1000f / timerRepository.timerFrequency).toLong()) + delay((1000f / stateRepository.timerFrequency).toLong()) } } } @@ -221,21 +220,23 @@ class TimerService : Service() { fun showTimerNotification( remainingTime: Int, paused: Boolean = false, complete: Boolean = false ) { + val settingsState = _settingsState.value + if (complete) notificationBuilder.clearActions().addStopAlarmAction(this) - val totalTime = when (timerState.value.timerMode) { - TimerMode.FOCUS -> timerRepository.focusTime.toInt() - TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() - else -> timerRepository.longBreakTime.toInt() + val totalTime = when (_timerState.value.timerMode) { + TimerMode.FOCUS -> settingsState.focusTime.toInt() + TimerMode.SHORT_BREAK -> settingsState.shortBreakTime.toInt() + else -> settingsState.longBreakTime.toInt() } - val currentTimer = when (timerState.value.timerMode) { + val currentTimer = when (_timerState.value.timerMode) { TimerMode.FOCUS -> getString(R.string.focus) TimerMode.SHORT_BREAK -> getString(R.string.short_break) else -> getString(R.string.long_break) } - val nextTimer = when (timerState.value.nextTimerMode) { + val nextTimer = when (_timerState.value.nextTimerMode) { TimerMode.FOCUS -> getString(R.string.focus) TimerMode.SHORT_BREAK -> getString(R.string.short_break) else -> getString(R.string.long_break) @@ -258,14 +259,14 @@ class TimerService : Service() { getString( R.string.up_next_notification, nextTimer, - timerState.value.nextTimeStr + _timerState.value.nextTimeStr ) ) .setStyle( notificationStyle .setProgress( // Set the current progress by filling the previous intervals and part of the current interval if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { - (totalTime - remainingTime) + ((cycles + 1) / 2) * timerRepository.focusTime.toInt() + (cycles / 2) * timerRepository.shortBreakTime.toInt() + (totalTime - remainingTime) + ((cycles + 1) / 2) * settingsState.focusTime.toInt() + (cycles / 2) * settingsState.shortBreakTime.toInt() } else (totalTime - remainingTime) ) ) @@ -283,37 +284,38 @@ class TimerService : Service() { } private fun updateProgressSegments() { + val settingsState = _settingsState.value notificationStyle = NotificationCompat.ProgressStyle() .also { // Add all the Focus, Short break and long break intervals in order if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) { // Android 16 and later supports live updates // Set progress bar sections if on Baklava or later - for (i in 0.. timerRepository.focusTime.toInt() - TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() - else -> timerRepository.longBreakTime.toInt() + when (_timerState.value.timerMode) { + TimerMode.FOCUS -> settingsState.focusTime.toInt() + TimerMode.SHORT_BREAK -> settingsState.shortBreakTime.toInt() + else -> settingsState.longBreakTime.toInt() } ) ) @@ -322,10 +324,12 @@ class TimerService : Service() { } private suspend fun resetTimer() { + val settingsState = _settingsState.value + updateProgressSegments() saveTimeToDb() lastSavedDuration = 0 - time = timerRepository.focusTime + time = settingsState.focusTime cycles = 0 startTime = 0L pauseTime = 0L @@ -336,15 +340,16 @@ class TimerService : Service() { timerMode = TimerMode.FOCUS, timeStr = millisecondsToStr(time), totalTime = time, - nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, - nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime), + nextTimerMode = if (settingsState.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, + nextTimeStr = millisecondsToStr(if (settingsState.sessionLength > 1) settingsState.shortBreakTime else settingsState.longBreakTime), currentFocusCount = 1, - totalFocusCount = timerRepository.sessionLength + totalFocusCount = settingsState.sessionLength ) } } private suspend fun skipTimer(fromButton: Boolean = false) { + val settingsState = _settingsState.value updateProgressSegments() saveTimeToDb() updateProgressSegments() @@ -354,30 +359,30 @@ class TimerService : Service() { pauseTime = 0L pauseDuration = 0L - cycles = (cycles + 1) % (timerRepository.sessionLength * 2) + cycles = (cycles + 1) % (settingsState.sessionLength * 2) if (cycles % 2 == 0) { - if (timerState.value.timerRunning) setDoNotDisturb(true) - time = timerRepository.focusTime + if (_timerState.value.timerRunning) setDoNotDisturb(true) + time = settingsState.focusTime _timerState.update { currentState -> currentState.copy( timerMode = TimerMode.FOCUS, timeStr = millisecondsToStr(time), totalTime = time, - nextTimerMode = if (cycles == (timerRepository.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, - nextTimeStr = if (cycles == (timerRepository.sessionLength - 1) * 2) millisecondsToStr( - timerRepository.longBreakTime + nextTimerMode = if (cycles == (settingsState.sessionLength - 1) * 2) TimerMode.LONG_BREAK else TimerMode.SHORT_BREAK, + nextTimeStr = if (cycles == (settingsState.sessionLength - 1) * 2) millisecondsToStr( + settingsState.longBreakTime ) else millisecondsToStr( - timerRepository.shortBreakTime + settingsState.shortBreakTime ), currentFocusCount = cycles / 2 + 1, - totalFocusCount = timerRepository.sessionLength + totalFocusCount = settingsState.sessionLength ) } } else { - if (timerState.value.timerRunning) setDoNotDisturb(false) - val long = cycles == (timerRepository.sessionLength * 2) - 1 - time = if (long) timerRepository.longBreakTime else timerRepository.shortBreakTime + if (_timerState.value.timerRunning) setDoNotDisturb(false) + val long = cycles == (settingsState.sessionLength * 2) - 1 + time = if (long) settingsState.longBreakTime else settingsState.shortBreakTime _timerState.update { currentState -> currentState.copy( @@ -385,14 +390,15 @@ class TimerService : Service() { timeStr = millisecondsToStr(time), totalTime = time, nextTimerMode = TimerMode.FOCUS, - nextTimeStr = millisecondsToStr(timerRepository.focusTime) + nextTimeStr = millisecondsToStr(settingsState.focusTime) ) } } } fun startAlarm() { - if (timerRepository.alarmEnabled) alarm?.start() + val settingsState = _settingsState.value + if (settingsState.alarmEnabled) alarm?.start() appContainer.activityTurnScreenOn(true) @@ -401,7 +407,7 @@ class TimerService : Service() { stopAlarm() } - if (timerRepository.vibrateEnabled) { + if (settingsState.vibrateEnabled) { if (!vibrator.hasVibrator()) { return } @@ -413,14 +419,15 @@ class TimerService : Service() { } fun stopAlarm() { + val settingsState = _settingsState.value autoAlarmStopScope?.cancel() - if (timerRepository.alarmEnabled) { + if (settingsState.alarmEnabled) { alarm?.pause() alarm?.seekTo(0) } - if (timerRepository.vibrateEnabled) { + if (settingsState.vibrateEnabled) { vibrator.cancel() } @@ -434,10 +441,10 @@ class TimerService : Service() { getString(R.string.start_next) ) showTimerNotification( - when (timerState.value.timerMode) { - TimerMode.FOCUS -> timerRepository.focusTime.toInt() - TimerMode.SHORT_BREAK -> timerRepository.shortBreakTime.toInt() - else -> timerRepository.longBreakTime.toInt() + when (_timerState.value.timerMode) { + TimerMode.FOCUS -> settingsState.focusTime.toInt() + TimerMode.SHORT_BREAK -> settingsState.shortBreakTime.toInt() + else -> settingsState.longBreakTime.toInt() }, paused = true, complete = false ) } @@ -451,7 +458,7 @@ class TimerService : Service() { .setUsage(AudioAttributes.USAGE_ALARM) .build() ) - timerRepository.alarmSoundUri?.let { + _settingsState.value.alarmSoundUri?.let { setDataSource(applicationContext, it) prepare() } @@ -463,7 +470,7 @@ class TimerService : Service() { } private fun setDoNotDisturb(doNotDisturb: Boolean) { - if (timerRepository.dndEnabled && notificationManagerService.isNotificationPolicyAccessGranted()) { + if (_settingsState.value.dndEnabled && notificationManagerService.isNotificationPolicyAccessGranted()) { if (doNotDisturb) { notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALARMS) } else notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) @@ -477,8 +484,8 @@ class TimerService : Service() { suspend fun saveTimeToDb() { saveLock.withLock { - val elapsedTime = timerState.value.totalTime - time - when (timerState.value.timerMode) { + val elapsedTime = _timerState.value.totalTime - time + when (_timerState.value.timerMode) { TimerMode.FOCUS -> statRepository.addFocusTime( (elapsedTime - lastSavedDuration).coerceAtLeast(0L) ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index 15051e7..e0e2fd9 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -62,7 +62,6 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import androidx.core.net.toUri import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.nsh07.pomodoro.R @@ -92,10 +91,10 @@ fun AlarmSettings( var alarmName by remember { mutableStateOf("...") } - LaunchedEffect(settingsState.alarmSound) { + LaunchedEffect(settingsState.alarmSoundUri) { withContext(Dispatchers.IO) { alarmName = - RingtoneManager.getRingtone(context, settingsState.alarmSound.toUri()) + RingtoneManager.getRingtone(context, settingsState.alarmSoundUri) ?.getTitle(context) ?: "" } } @@ -119,11 +118,11 @@ fun AlarmSettings( } @SuppressLint("LocalContextGetResourceValueCall") - val ringtonePickerIntent = remember(settingsState.alarmSound) { + val ringtonePickerIntent = remember(settingsState.alarmSoundUri) { Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound)) - putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSound.toUri()) + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSoundUri) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt index b9d42b4..31ddc7c 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt @@ -17,17 +17,27 @@ package org.nsh07.pomodoro.ui.settingsScreen.viewModel +import android.net.Uri +import android.provider.Settings import androidx.compose.runtime.Immutable import androidx.compose.ui.graphics.Color @Immutable data class SettingsState( val theme: String = "auto", - val alarmSound: String = "", val colorScheme: String = Color.White.toString(), val blackTheme: Boolean = false, val aodEnabled: Boolean = false, val alarmEnabled: Boolean = true, val vibrateEnabled: Boolean = true, - val dndEnabled: Boolean = false + val dndEnabled: Boolean = false, + + val focusTime: Long = 25 * 60 * 1000L, + val shortBreakTime: Long = 5 * 60 * 1000L, + val longBreakTime: Long = 15 * 60 * 1000L, + + val sessionLength: Int = 4, + + val alarmSoundUri: Uri? = + Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 896eb28..abca69d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -25,6 +25,7 @@ import androidx.compose.material3.SliderState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.graphics.Color +import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY @@ -35,51 +36,57 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.debounce +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.billing.BillingManager -import org.nsh07.pomodoro.data.AppPreferenceRepository -import org.nsh07.pomodoro.data.TimerRepository +import org.nsh07.pomodoro.data.PreferenceRepository +import org.nsh07.pomodoro.data.StateRepository import org.nsh07.pomodoro.service.ServiceHelper import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode -import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState import org.nsh07.pomodoro.utils.millisecondsToStr @OptIn(FlowPreview::class, ExperimentalMaterial3Api::class) class SettingsViewModel( private val billingManager: BillingManager, - private val preferenceRepository: AppPreferenceRepository, + private val preferenceRepository: PreferenceRepository, + private val stateRepository: StateRepository, private val serviceHelper: ServiceHelper, - private val time: MutableStateFlow, - private val timerRepository: TimerRepository, - private val timerState: MutableStateFlow + private val time: MutableStateFlow ) : ViewModel() { val backStack = mutableStateListOf(Screen.Settings.Main) val isPlus = billingManager.isPlus - val serviceRunning = timerRepository.serviceRunning.asStateFlow() + val serviceRunning = stateRepository.timerState.map { it.serviceRunning } + .stateIn( + viewModelScope, + SharingStarted.WhileSubscribed(5000), + false + ) - private val _settingsState = MutableStateFlow(SettingsState()) + private val _settingsState = stateRepository.settingsState val settingsState = _settingsState.asStateFlow() val focusTimeTextFieldState by lazy { - TextFieldState((timerRepository.focusTime / 60000).toString()) + TextFieldState((_settingsState.value.focusTime / 60000).toString()) } val shortBreakTimeTextFieldState by lazy { - TextFieldState((timerRepository.shortBreakTime / 60000).toString()) + TextFieldState((_settingsState.value.shortBreakTime / 60000).toString()) } val longBreakTimeTextFieldState by lazy { - TextFieldState((timerRepository.longBreakTime / 60000).toString()) + TextFieldState((_settingsState.value.longBreakTime / 60000).toString()) } val sessionsSliderState by lazy { SliderState( - value = timerRepository.sessionLength.toFloat(), + value = _settingsState.value.sessionLength.toFloat(), steps = 4, valueRange = 1f..6f, onValueChangeFinished = ::updateSessionLength @@ -110,11 +117,15 @@ class SettingsViewModel( } private fun updateSessionLength() { - viewModelScope.launch { - timerRepository.sessionLength = preferenceRepository.saveIntPreference( - "session_length", - sessionsSliderState.value.toInt() - ) + viewModelScope.launch(Dispatchers.IO) { + _settingsState.update { currentState -> + currentState.copy( + sessionLength = preferenceRepository.saveIntPreference( + "session_length", + sessionsSliderState.value.toInt() + ) + ) + } refreshTimer() } } @@ -125,11 +136,13 @@ class SettingsViewModel( .debounce(500) .collect { if (it.isNotEmpty()) { - timerRepository.focusTime = it.toString().toLong() * 60 * 1000 + _settingsState.update { currentState -> + currentState.copy(focusTime = it.toString().toLong() * 60 * 1000) + } refreshTimer() preferenceRepository.saveIntPreference( "focus_time", - timerRepository.focusTime.toInt() + _settingsState.value.focusTime.toInt() ) } } @@ -139,11 +152,13 @@ class SettingsViewModel( .debounce(500) .collect { if (it.isNotEmpty()) { - timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000 + _settingsState.update { currentState -> + currentState.copy(shortBreakTime = it.toString().toLong() * 60 * 1000) + } refreshTimer() preferenceRepository.saveIntPreference( "short_break_time", - timerRepository.shortBreakTime.toInt() + _settingsState.value.shortBreakTime.toInt() ) } } @@ -153,11 +168,13 @@ class SettingsViewModel( .debounce(500) .collect { if (it.isNotEmpty()) { - timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000 + _settingsState.update { currentState -> + currentState.copy(longBreakTime = it.toString().toLong() * 60 * 1000) + } refreshTimer() preferenceRepository.saveIntPreference( "long_break_time", - timerRepository.longBreakTime.toInt() + _settingsState.value.longBreakTime.toInt() ) } } @@ -173,7 +190,6 @@ class SettingsViewModel( private fun saveAlarmEnabled(enabled: Boolean) { viewModelScope.launch { - timerRepository.alarmEnabled = enabled _settingsState.update { currentState -> currentState.copy(alarmEnabled = enabled) } @@ -183,7 +199,6 @@ class SettingsViewModel( private fun saveVibrateEnabled(enabled: Boolean) { viewModelScope.launch { - timerRepository.vibrateEnabled = enabled _settingsState.update { currentState -> currentState.copy(vibrateEnabled = enabled) } @@ -193,7 +208,6 @@ class SettingsViewModel( private fun saveDndEnabled(enabled: Boolean) { viewModelScope.launch { - timerRepository.dndEnabled = enabled _settingsState.update { currentState -> currentState.copy(dndEnabled = enabled) } @@ -203,9 +217,8 @@ class SettingsViewModel( private fun saveAlarmSound(uri: Uri?) { viewModelScope.launch { - timerRepository.alarmSoundUri = uri _settingsState.update { currentState -> - currentState.copy(alarmSound = uri.toString()) + currentState.copy(alarmSoundUri = uri) } preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) } @@ -248,32 +261,71 @@ class SettingsViewModel( } suspend fun reloadSettings() { + var settingsState = _settingsState.value + val focusTime = + preferenceRepository.getIntPreference("focus_time")?.toLong() + ?: preferenceRepository.saveIntPreference( + "focus_time", + settingsState.focusTime.toInt() + ).toLong() + val shortBreakTime = + preferenceRepository.getIntPreference("short_break_time")?.toLong() + ?: preferenceRepository.saveIntPreference( + "short_break_time", + settingsState.shortBreakTime.toInt() + ).toLong() + val longBreakTime = + preferenceRepository.getIntPreference("long_break_time")?.toLong() + ?: preferenceRepository.saveIntPreference( + "long_break_time", + settingsState.longBreakTime.toInt() + ).toLong() + val sessionLength = + preferenceRepository.getIntPreference("session_length") + ?: preferenceRepository.saveIntPreference( + "session_length", + settingsState.sessionLength + ) + + val alarmSoundUri = ( + preferenceRepository.getStringPreference("alarm_sound") + ?: preferenceRepository.saveStringPreference( + "alarm_sound", + (Settings.System.DEFAULT_ALARM_ALERT_URI + ?: Settings.System.DEFAULT_RINGTONE_URI).toString() + ) + ).toUri() + val theme = preferenceRepository.getStringPreference("theme") - ?: preferenceRepository.saveStringPreference("theme", "auto") + ?: preferenceRepository.saveStringPreference("theme", settingsState.theme) val colorScheme = preferenceRepository.getStringPreference("color_scheme") - ?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString()) + ?: preferenceRepository.saveStringPreference("color_scheme", settingsState.colorScheme) val blackTheme = preferenceRepository.getBooleanPreference("black_theme") - ?: preferenceRepository.saveBooleanPreference("black_theme", false) + ?: preferenceRepository.saveBooleanPreference("black_theme", settingsState.blackTheme) val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") - ?: preferenceRepository.saveBooleanPreference("aod_enabled", false) - val alarmSound = preferenceRepository.getStringPreference("alarm_sound") - ?: preferenceRepository.saveStringPreference( - "alarm_sound", - (Settings.System.DEFAULT_ALARM_ALERT_URI - ?: Settings.System.DEFAULT_RINGTONE_URI).toString() - ) + ?: preferenceRepository.saveBooleanPreference("aod_enabled", settingsState.aodEnabled) val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled") - ?: preferenceRepository.saveBooleanPreference("alarm_enabled", true) + ?: preferenceRepository.saveBooleanPreference( + "alarm_enabled", + settingsState.alarmEnabled + ) val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled") - ?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true) + ?: preferenceRepository.saveBooleanPreference( + "vibrate_enabled", + settingsState.vibrateEnabled + ) val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled") - ?: preferenceRepository.saveBooleanPreference("dnd_enabled", false) + ?: preferenceRepository.saveBooleanPreference("dnd_enabled", settingsState.dndEnabled) _settingsState.update { currentState -> currentState.copy( + focusTime = focusTime, + shortBreakTime = shortBreakTime, + longBreakTime = longBreakTime, + sessionLength = sessionLength, theme = theme, colorScheme = colorScheme, - alarmSound = alarmSound, + alarmSoundUri = alarmSoundUri, blackTheme = blackTheme, aodEnabled = aodEnabled, alarmEnabled = alarmEnabled, @@ -281,21 +333,40 @@ class SettingsViewModel( dndEnabled = dndEnabled ) } - } - private fun refreshTimer() { - if (!serviceRunning.value) { - time.update { timerRepository.focusTime } + settingsState = _settingsState.value - timerState.update { currentState -> + time.update { settingsState.focusTime } + + if (!stateRepository.timerState.value.serviceRunning) + stateRepository.timerState.update { currentState -> currentState.copy( timerMode = TimerMode.FOCUS, timeStr = millisecondsToStr(time.value), totalTime = time.value, - nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, - nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime), + nextTimerMode = if (settingsState.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, + nextTimeStr = millisecondsToStr(if (settingsState.sessionLength > 1) settingsState.shortBreakTime else settingsState.longBreakTime), currentFocusCount = 1, - totalFocusCount = timerRepository.sessionLength + totalFocusCount = settingsState.sessionLength + ) + } + } + + private fun refreshTimer() { + if (!serviceRunning.value) { + val settingsState = _settingsState.value + + time.update { settingsState.focusTime } + + stateRepository.timerState.update { currentState -> + currentState.copy( + timerMode = TimerMode.FOCUS, + timeStr = millisecondsToStr(time.value), + totalTime = time.value, + nextTimerMode = if (settingsState.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, + nextTimeStr = millisecondsToStr(if (settingsState.sessionLength > 1) settingsState.shortBreakTime else settingsState.longBreakTime), + currentFocusCount = 1, + totalFocusCount = settingsState.sessionLength ) } } @@ -307,18 +378,16 @@ class SettingsViewModel( val application = (this[APPLICATION_KEY] as TomatoApplication) val appBillingManager = application.container.billingManager val appPreferenceRepository = application.container.appPreferenceRepository - val appTimerRepository = application.container.appTimerRepository val serviceHelper = application.container.serviceHelper + val stateRepository = application.container.stateRepository val time = application.container.time - val timerState = application.container.timerState SettingsViewModel( billingManager = appBillingManager, preferenceRepository = appPreferenceRepository, serviceHelper = serviceHelper, - time = time, - timerRepository = appTimerRepository, - timerState = timerState + stateRepository = stateRepository, + time = time ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt index 3a3cd3b..5c742d6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerState.kt @@ -1,8 +1,18 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui.timerScreen.viewModel @@ -17,7 +27,8 @@ data class TimerState( val showBrandTitle: Boolean = true, val currentFocusCount: Int = 1, val totalFocusCount: Int = 4, - val alarmRinging: Boolean = false + val alarmRinging: Boolean = false, + val serviceRunning: Boolean = false ) enum class TimerMode { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 318a862..a670b70 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -17,8 +17,6 @@ package org.nsh07.pomodoro.ui.timerScreen.viewModel -import android.provider.Settings -import androidx.core.net.toUri import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY @@ -37,122 +35,49 @@ import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.nsh07.pomodoro.TomatoApplication -import org.nsh07.pomodoro.data.PreferenceRepository import org.nsh07.pomodoro.data.Stat import org.nsh07.pomodoro.data.StatRepository -import org.nsh07.pomodoro.data.TimerRepository +import org.nsh07.pomodoro.data.StateRepository import org.nsh07.pomodoro.service.ServiceHelper -import org.nsh07.pomodoro.utils.millisecondsToStr import java.time.LocalDate import java.time.temporal.ChronoUnit @OptIn(FlowPreview::class) class TimerViewModel( - private val preferenceRepository: PreferenceRepository, private val serviceHelper: ServiceHelper, + private val stateRepository: StateRepository, private val statRepository: StatRepository, - private val timerRepository: TimerRepository, - private val _timerState: MutableStateFlow, private val _time: MutableStateFlow ) : ViewModel() { - val timerState: StateFlow = _timerState.asStateFlow() + val timerState: StateFlow = stateRepository.timerState.asStateFlow() val time: StateFlow = _time.asStateFlow() - val progress = _time.combine(_timerState) { remainingTime, uiState -> + val progress = _time.combine(stateRepository.timerState) { remainingTime, uiState -> (uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f) - private var cycles = 0 - - private var startTime = 0L - private var pauseTime = 0L - private var pauseDuration = 0L - init { - if (!timerRepository.serviceRunning.value) - 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 - ) + viewModelScope.launch(Dispatchers.IO) { + var lastDate = statRepository.getLastDate() + val today = LocalDate.now() - timerRepository.alarmEnabled = - preferenceRepository.getBooleanPreference("alarm_enabled") - ?: preferenceRepository.saveBooleanPreference("alarm_enabled", true) - timerRepository.vibrateEnabled = - preferenceRepository.getBooleanPreference("vibrate_enabled") - ?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true) - timerRepository.dndEnabled = - preferenceRepository.getBooleanPreference("dnd_enabled") - ?: preferenceRepository.saveBooleanPreference("dnd_enabled", false) - - timerRepository.alarmSoundUri = ( - preferenceRepository.getStringPreference("alarm_sound") - ?: preferenceRepository.saveStringPreference( - "alarm_sound", - (Settings.System.DEFAULT_ALARM_ALERT_URI - ?: Settings.System.DEFAULT_RINGTONE_URI).toString() - ) - ).toUri() - - _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 (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK, - nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime), - currentFocusCount = 1, - totalFocusCount = timerRepository.sessionLength - ) - } - - var lastDate = statRepository.getLastDate() - val today = LocalDate.now() - - // Fills dates between today and lastDate with 0s to ensure continuous history - if (lastDate != null) { - while (ChronoUnit.DAYS.between(lastDate, today) > 0) { - lastDate = lastDate?.plusDays(1) - statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0)) - } - } else { - statRepository.insertStat(Stat(today, 0, 0, 0, 0, 0)) - } - - delay(1500) - - _timerState.update { currentState -> - currentState.copy(showBrandTitle = false) + // Fills dates between today and lastDate with 0s to ensure continuous history + if (lastDate != null) { + while (ChronoUnit.DAYS.between(lastDate, today) > 0) { + lastDate = lastDate?.plusDays(1) + statRepository.insertStat(Stat(lastDate!!, 0, 0, 0, 0, 0)) } + } else { + statRepository.insertStat(Stat(today, 0, 0, 0, 0, 0)) } + + delay(1500) + + stateRepository.timerState.update { currentState -> + currentState.copy(showBrandTitle = false) + } + } } fun onAction(action: TimerAction) { @@ -163,19 +88,15 @@ class TimerViewModel( val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { val application = (this[APPLICATION_KEY] as TomatoApplication) - val appPreferenceRepository = application.container.appPreferenceRepository val appStatRepository = application.container.appStatRepository - val appTimerRepository = application.container.appTimerRepository + val stateRepository = application.container.stateRepository val serviceHelper = application.container.serviceHelper - val timerState = application.container.timerState val time = application.container.time TimerViewModel( - preferenceRepository = appPreferenceRepository, serviceHelper = serviceHelper, + stateRepository = stateRepository, statRepository = appStatRepository, - timerRepository = appTimerRepository, - _timerState = timerState, _time = time ) }