Merge pull request #132 from nsh07/architecture-cleanup

Architecture cleanup (part 1)
This commit is contained in:
Nishant Mishra
2025-11-09 19:57:15 +05:30
committed by GitHub
23 changed files with 419 additions and 259 deletions

View File

@@ -61,6 +61,9 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
) )
} }
debug {
applicationIdSuffix = ".debug"
}
} }
flavorDimensions += "version" flavorDimensions += "version"

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow
*/ */
class FossBillingManager : BillingManager { class FossBillingManager : BillingManager {
override val isPlus = MutableStateFlow(true).asStateFlow() override val isPlus = MutableStateFlow(true).asStateFlow()
override val isLoaded = MutableStateFlow(true).asStateFlow()
} }
object BillingManagerProvider { object BillingManagerProvider {

View File

@@ -0,0 +1,87 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
@Composable
fun TopButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://coff.ee/nsh07") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.bmc),
contentDescription = null,
modifier = Modifier.height(24.dp)
)
Text(text = stringResource(R.string.bmc))
}
}
}
@Composable
fun BottomButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://hosted.weblate.org/engage/tomato/") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.weblate),
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Text(text = stringResource(R.string.help_with_translation))
}
}
}

View File

@@ -50,34 +50,22 @@ class MainActivity : ComponentActivity() {
} }
setContent { setContent {
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle() val settingsState by settingsViewModel.settingsState.collectAsStateWithLifecycle()
val darkTheme = when (preferencesState.theme) { val darkTheme = when (settingsState.theme) {
"dark" -> true "dark" -> true
"light" -> false "light" -> false
else -> isSystemInDarkTheme() else -> isSystemInDarkTheme()
} }
val seed = preferencesState.colorScheme.toColor() val seed = settingsState.colorScheme.toColor()
val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle() val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
val isPurchaseStateLoaded by settingsViewModel.isPurchaseStateLoaded.collectAsStateWithLifecycle()
val isSettingsLoaded by settingsViewModel.isSettingsLoaded.collectAsStateWithLifecycle()
LaunchedEffect(isPurchaseStateLoaded, isPlus, isSettingsLoaded) {
if (isPurchaseStateLoaded && isSettingsLoaded) {
if (!isPlus) {
settingsViewModel.resetPaywalledSettings()
} else {
settingsViewModel.reloadSettings()
}
}
}
TomatoTheme( TomatoTheme(
darkTheme = darkTheme, darkTheme = darkTheme,
seedColor = seed, seedColor = seed,
blackTheme = preferencesState.blackTheme blackTheme = settingsState.blackTheme
) { ) {
val colorScheme = colorScheme val colorScheme = colorScheme
LaunchedEffect(colorScheme) { LaunchedEffect(colorScheme) {
@@ -86,7 +74,7 @@ class MainActivity : ComponentActivity() {
AppScreen( AppScreen(
isPlus = isPlus, isPlus = isPlus,
isAODEnabled = preferencesState.aodEnabled, isAODEnabled = settingsState.aodEnabled,
setTimerFrequency = { setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it appContainer.appTimerRepository.timerFrequency = it
} }
@@ -105,6 +93,6 @@ class MainActivity : ComponentActivity() {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
// Increase the timer loop frequency again when visible to make the progress smoother // Increase the timer loop frequency again when visible to make the progress smoother
appContainer.appTimerRepository.timerFrequency = 10f appContainer.appTimerRepository.timerFrequency = 60f
} }
} }

View File

@@ -21,5 +21,4 @@ import kotlinx.coroutines.flow.StateFlow
interface BillingManager { interface BillingManager {
val isPlus: StateFlow<Boolean> val isPlus: StateFlow<Boolean>
val isLoaded: StateFlow<Boolean>
} }

View File

@@ -23,6 +23,7 @@ import android.content.Context
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.graphics.toArgb
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
@@ -83,6 +84,7 @@ class DefaultAppContainer(context: Context) : AppContainer {
.setSilent(true) .setSilent(true)
.setOngoing(true) .setOngoing(true)
.setRequestPromotedOngoing(true) .setRequestPromotedOngoing(true)
.setVisibility(VISIBILITY_PUBLIC)
} }
override val timerState: MutableStateFlow<TimerState> by lazy { override val timerState: MutableStateFlow<TimerState> by lazy {

View File

@@ -54,7 +54,7 @@ class AppTimerRepository : TimerRepository {
override var shortBreakTime = 5 * 60 * 1000L override var shortBreakTime = 5 * 60 * 1000L
override var longBreakTime = 15 * 60 * 1000L override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4 override var sessionLength = 4
override var timerFrequency: Float = 10f override var timerFrequency: Float = 60f
override var alarmEnabled = true override var alarmEnabled = true
override var vibrateEnabled = true override var vibrateEnabled = true
override var dndEnabled: Boolean = false override var dndEnabled: Boolean = false

View File

@@ -113,7 +113,7 @@ fun SharedTransitionScope.AlwaysOnDisplay(
} }
onDispose { onDispose {
setTimerFrequency(10f) setTimerFrequency(60f)
window.clearFlags( window.clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON

View File

@@ -45,7 +45,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -79,9 +78,7 @@ fun AppScreen(
val context = LocalContext.current val context = LocalContext.current
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle() val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
val remainingTime by timerViewModel.time.collectAsStateWithLifecycle() val progress by timerViewModel.progress.collectAsStateWithLifecycle()
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val motionScheme = motionScheme val motionScheme = motionScheme

View File

@@ -19,8 +19,6 @@ package org.nsh07.pomodoro.ui.settingsScreen
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.LocaleManager import android.app.LocaleManager
import android.content.Intent
import android.net.Uri
import android.os.Build import android.os.Build
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
@@ -55,7 +53,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
@@ -68,7 +65,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.NavDisplay
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
@@ -77,7 +73,8 @@ import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.settingsScreens import org.nsh07.pomodoro.ui.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -92,8 +89,6 @@ fun SettingsScreenRoot(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory) viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) { ) {
val context = LocalContext.current
val backStack = viewModel.backStack val backStack = viewModel.backStack
DisposableEffect(Unit) { DisposableEffect(Unit) {
@@ -106,12 +101,8 @@ fun SettingsScreenRoot(
val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState
val isPlus by viewModel.isPlus.collectAsStateWithLifecycle() val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle() val settingsState by viewModel.settingsState.collectAsStateWithLifecycle()
val sessionsSliderState = rememberSaveable( val sessionsSliderState = rememberSaveable(
saver = SliderState.Saver( saver = SliderState.Saver(
@@ -124,30 +115,13 @@ fun SettingsScreenRoot(
SettingsScreen( SettingsScreen(
isPlus = isPlus, isPlus = isPlus,
preferencesState = preferencesState, settingsState = settingsState,
backStack = backStack, backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled, onAction = viewModel::onAction,
vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled,
alarmSound = alarmSound,
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
onBlackThemeChange = viewModel::saveBlackTheme,
onAodEnabledChange = viewModel::saveAodEnabled,
onDndEnabledChange = viewModel::saveDndEnabled,
onAlarmSoundChanged = {
viewModel.saveAlarmSound(it)
Intent(context, TimerService::class.java).apply {
action = TimerService.Actions.RESET.toString()
context.startService(this)
}
},
onThemeChange = viewModel::saveTheme,
onColorSchemeChange = viewModel::saveColorScheme,
setShowPaywall = setShowPaywall, setShowPaywall = setShowPaywall,
modifier = modifier modifier = modifier
) )
@@ -158,24 +132,13 @@ fun SettingsScreenRoot(
@Composable @Composable
private fun SettingsScreen( private fun SettingsScreen(
isPlus: Boolean, isPlus: Boolean,
preferencesState: PreferencesState, settingsState: SettingsState,
backStack: SnapshotStateList<Screen.Settings>, backStack: SnapshotStateList<Screen.Settings>,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
alarmEnabled: Boolean, onAction: (SettingsAction) -> Unit,
vibrateEnabled: Boolean,
dndEnabled: Boolean,
alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onBlackThemeChange: (Boolean) -> Unit,
onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
setShowPaywall: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -312,23 +275,16 @@ private fun SettingsScreen(
entry<Screen.Settings.Alarm> { entry<Screen.Settings.Alarm> {
AlarmSettings( AlarmSettings(
preferencesState = preferencesState, settingsState = settingsState,
alarmEnabled = alarmEnabled, onAction = onAction,
vibrateEnabled = vibrateEnabled,
alarmSound = alarmSound,
onAlarmEnabledChange = onAlarmEnabledChange,
onVibrateEnabledChange = onVibrateEnabledChange,
onAlarmSoundChanged = onAlarmSoundChanged,
onBack = backStack::removeLastOrNull onBack = backStack::removeLastOrNull
) )
} }
entry<Screen.Settings.Appearance> { entry<Screen.Settings.Appearance> {
AppearanceSettings( AppearanceSettings(
preferencesState = preferencesState, settingsState = settingsState,
isPlus = isPlus, isPlus = isPlus,
onBlackThemeChange = onBlackThemeChange, onAction = onAction,
onThemeChange = onThemeChange,
onColorSchemeChange = onColorSchemeChange,
setShowPaywall = setShowPaywall, setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull onBack = backStack::removeLastOrNull
) )
@@ -336,14 +292,12 @@ private fun SettingsScreen(
entry<Screen.Settings.Timer> { entry<Screen.Settings.Timer> {
TimerSettings( TimerSettings(
isPlus = isPlus, isPlus = isPlus,
aodEnabled = preferencesState.aodEnabled, settingsState = settingsState,
dndEnabled = dndEnabled,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange, onAction = onAction,
onDndEnabledChange = onDndEnabledChange,
setShowPaywall = setShowPaywall, setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull, onBack = backStack::removeLastOrNull,
) )

View File

@@ -24,10 +24,8 @@ import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
@@ -119,41 +117,8 @@ fun AboutCard(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp) horizontalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Button( TopButton(buttonColors)
colors = buttonColors, BottomButton(buttonColors)
onClick = { uriHandler.openUri("https://coff.ee/nsh07") }
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.bmc),
contentDescription = null,
modifier = Modifier.height(24.dp)
)
Text(text = stringResource(R.string.bmc))
}
}
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=org.nsh07.pomodoro") }
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.play_store),
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Text(text = stringResource(R.string.rate_on_google_play))
}
}
} }
} }
} }

View File

@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.screens package org.nsh07.pomodoro.ui.settingsScreen.screens
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.media.RingtoneManager import android.media.RingtoneManager
@@ -64,7 +65,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.R import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -76,13 +78,8 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun AlarmSettings( fun AlarmSettings(
preferencesState: PreferencesState, settingsState: SettingsState,
alarmEnabled: Boolean, onAction: (SettingsAction) -> Unit,
vibrateEnabled: Boolean,
alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
@@ -91,10 +88,11 @@ fun AlarmSettings(
var alarmName by remember { mutableStateOf("...") } var alarmName by remember { mutableStateOf("...") }
LaunchedEffect(alarmSound) { LaunchedEffect(settingsState.alarmSound) {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
alarmName = alarmName =
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: "" RingtoneManager.getRingtone(context, settingsState.alarmSound.toUri())
?.getTitle(context) ?: ""
} }
} }
@@ -112,38 +110,39 @@ fun AlarmSettings(
@Suppress("DEPRECATION") @Suppress("DEPRECATION")
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
} }
onAlarmSoundChanged(uri) onAction(SettingsAction.SaveAlarmSound(uri))
} }
} }
val ringtonePickerIntent = remember(alarmSound) { @SuppressLint("LocalContextGetResourceValueCall")
val ringtonePickerIntent = remember(settingsState.alarmSound) {
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound)) putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri()) putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSound.toUri())
} }
} }
val switchItems = remember( val switchItems = remember(
preferencesState.blackTheme, settingsState.blackTheme,
preferencesState.aodEnabled, settingsState.aodEnabled,
alarmEnabled, settingsState.alarmEnabled,
vibrateEnabled settingsState.vibrateEnabled
) { ) {
listOf( listOf(
SettingsSwitchItem( SettingsSwitchItem(
checked = alarmEnabled, checked = settingsState.alarmEnabled,
icon = R.drawable.alarm_on, icon = R.drawable.alarm_on,
label = R.string.sound, label = R.string.sound,
description = R.string.alarm_desc, description = R.string.alarm_desc,
onClick = onAlarmEnabledChange onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = vibrateEnabled, checked = settingsState.vibrateEnabled,
icon = R.drawable.mobile_vibrate, icon = R.drawable.mobile_vibrate,
label = R.string.vibrate, label = R.string.vibrate,
description = R.string.vibrate_desc, description = R.string.vibrate_desc,
onClick = onVibrateEnabledChange onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) }
) )
) )
} }
@@ -241,14 +240,10 @@ fun AlarmSettings(
@Preview @Preview
@Composable @Composable
fun AlarmSettingsPreview() { fun AlarmSettingsPreview() {
val preferencesState = PreferencesState() val settingsState = SettingsState()
AlarmSettings( AlarmSettings(
preferencesState = preferencesState, settingsState = settingsState,
alarmEnabled = true, onAction = {},
vibrateEnabled = false, onBack = {}
alarmSound = "", )
onAlarmEnabledChange = {},
onVibrateEnabledChange = {},
onAlarmSoundChanged = {},
onBack = {})
} }

View File

@@ -39,7 +39,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@@ -50,7 +49,8 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -62,11 +62,9 @@ import org.nsh07.pomodoro.utils.toColor
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable @Composable
fun AppearanceSettings( fun AppearanceSettings(
preferencesState: PreferencesState, settingsState: SettingsState,
isPlus: Boolean, isPlus: Boolean,
onBlackThemeChange: (Boolean) -> Unit, onAction: (SettingsAction) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
setShowPaywall: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
@@ -105,8 +103,8 @@ fun AppearanceSettings(
} }
item { item {
ThemePickerListItem( ThemePickerListItem(
theme = preferencesState.theme, theme = settingsState.theme,
onThemeChange = onThemeChange, onThemeChange = { onAction(SettingsAction.SaveTheme(it)) },
items = if (isPlus) 3 else 1, items = if (isPlus) 3 else 1,
index = 0 index = 0
) )
@@ -118,20 +116,20 @@ fun AppearanceSettings(
item { item {
ColorSchemePickerListItem( ColorSchemePickerListItem(
color = preferencesState.colorScheme.toColor(), color = settingsState.colorScheme.toColor(),
items = 3, items = 3,
index = if (isPlus) 1 else 0, index = if (isPlus) 1 else 0,
isPlus = isPlus, isPlus = isPlus,
onColorChange = onColorSchemeChange, onColorChange = { onAction(SettingsAction.SaveColorScheme(it)) },
) )
} }
item { item {
val item = SettingsSwitchItem( val item = SettingsSwitchItem(
checked = preferencesState.blackTheme, checked = settingsState.blackTheme,
icon = R.drawable.contrast, icon = R.drawable.contrast,
label = R.string.black_theme, label = R.string.black_theme,
description = R.string.black_theme_desc, description = R.string.black_theme_desc,
onClick = onBlackThemeChange onClick = { onAction(SettingsAction.SaveBlackTheme(it)) }
) )
ListItem( ListItem(
leadingContent = { leadingContent = {
@@ -175,14 +173,12 @@ fun AppearanceSettings(
@Preview @Preview
@Composable @Composable
fun AppearanceSettingsPreview() { fun AppearanceSettingsPreview() {
val preferencesState = PreferencesState() val settingsState = SettingsState()
TomatoTheme(dynamicColor = false) { TomatoTheme(dynamicColor = false) {
AppearanceSettings( AppearanceSettings(
preferencesState = preferencesState, settingsState = settingsState,
isPlus = false, isPlus = false,
onBlackThemeChange = {}, onAction = {},
onThemeChange = {},
onColorSchemeChange = {},
setShowPaywall = {}, setShowPaywall = {},
onBack = {} onBack = {}
) )

View File

@@ -36,10 +36,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.FilledTonalIconToggleButton
@@ -56,6 +58,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
@@ -76,6 +79,8 @@ import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -90,17 +95,15 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable @Composable
fun TimerSettings( fun TimerSettings(
isPlus: Boolean, isPlus: Boolean,
aodEnabled: Boolean, settingsState: SettingsState,
dndEnabled: Boolean,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit, onAction: (SettingsAction) -> Unit,
onDndEnabledChange: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier
setShowPaywall: (Boolean) -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current val context = LocalContext.current
@@ -110,12 +113,12 @@ fun TimerSettings(
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
if (!notificationManagerService.isNotificationPolicyAccessGranted()) if (!notificationManagerService.isNotificationPolicyAccessGranted())
onDndEnabledChange(false) onAction(SettingsAction.SaveDndEnabled(false))
} }
val switchItems = listOf( val switchItems = listOf(
SettingsSwitchItem( SettingsSwitchItem(
checked = dndEnabled, checked = settingsState.dndEnabled,
icon = R.drawable.dnd, icon = R.drawable.dnd,
label = R.string.dnd, label = R.string.dnd,
description = R.string.dnd_desc, description = R.string.dnd_desc,
@@ -128,15 +131,15 @@ fun TimerSettings(
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) { } else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
} }
onDndEnabledChange(it) onAction(SettingsAction.SaveDndEnabled(it))
} }
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = aodEnabled, checked = settingsState.aodEnabled,
icon = R.drawable.aod, icon = R.drawable.aod,
label = R.string.always_on_display, label = R.string.always_on_display,
description = R.string.always_on_display_desc, description = R.string.always_on_display_desc,
onClick = onAodEnabledChange onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
) )
) )
@@ -313,7 +316,7 @@ fun TimerSettings(
item { item {
PlusDivider(setShowPaywall) PlusDivider(setShowPaywall)
} }
itemsIndexed(switchItems.drop(1)) { index, item -> items(switchItems.drop(1)) { item ->
ListItem( ListItem(
leadingContent = { leadingContent = {
Icon( Icon(
@@ -392,24 +395,22 @@ fun TimerSettings(
@Preview @Preview
@Composable @Composable
private fun TimerSettingsPreview() { private fun TimerSettingsPreview() {
val focusTimeInputFieldState = TextFieldState("25") val focusTimeInputFieldState = rememberTextFieldState("25")
val shortBreakTimeInputFieldState = TextFieldState("5") val shortBreakTimeInputFieldState = rememberTextFieldState("5")
val longBreakTimeInputFieldState = TextFieldState("15") val longBreakTimeInputFieldState = rememberTextFieldState("15")
val sessionsSliderState = SliderState( val sessionsSliderState = rememberSliderState(
value = 4f, value = 4f,
valueRange = 1f..8f, valueRange = 1f..8f,
steps = 6 steps = 6
) )
TimerSettings( TimerSettings(
isPlus = false, isPlus = false,
aodEnabled = true, settingsState = remember { SettingsState() },
dndEnabled = false,
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
onAodEnabledChange = {}, onAction = {},
onDndEnabledChange = {},
setShowPaywall = {}, setShowPaywall = {},
onBack = {} onBack = {}
) )

View File

@@ -1,19 +0,0 @@
/*
* 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.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
data class PreferencesState(
val theme: String = "auto",
val colorScheme: String = Color.White.toString(),
val blackTheme: Boolean = false,
val aodEnabled: Boolean = false
)

View File

@@ -0,0 +1,32 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri
import androidx.compose.ui.graphics.Color
sealed interface SettingsAction {
data class SaveAlarmEnabled(val enabled: Boolean) : SettingsAction
data class SaveVibrateEnabled(val enabled: Boolean) : SettingsAction
data class SaveBlackTheme(val enabled: Boolean) : SettingsAction
data class SaveAodEnabled(val enabled: Boolean) : SettingsAction
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
data class SaveAlarmSound(val uri: Uri?) : SettingsAction
data class SaveTheme(val theme: String) : SettingsAction
data class SaveColorScheme(val color: Color) : SettingsAction
}

View File

@@ -0,0 +1,33 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
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
)

View File

@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.viewModel package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState import androidx.compose.material3.SliderState
@@ -36,7 +37,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
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
@@ -54,13 +54,9 @@ class SettingsViewModel(
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main) val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus val isPlus = billingManager.isPlus
val isPurchaseStateLoaded = billingManager.isLoaded
private val _isSettingsLoaded = MutableStateFlow(false) private val _settingsState = MutableStateFlow(SettingsState())
val isSettingsLoaded = _isSettingsLoaded.asStateFlow() val settingsState = _settingsState.asStateFlow()
private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow()
val focusTimeTextFieldState by lazy { val focusTimeTextFieldState by lazy {
TextFieldState((timerRepository.focusTime / 60000).toString()) TextFieldState((timerRepository.focusTime / 60000).toString())
@@ -81,25 +77,26 @@ class SettingsViewModel(
) )
} }
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
private var focusFlowCollectionJob: Job? = null private var focusFlowCollectionJob: Job? = null
private var shortBreakFlowCollectionJob: Job? = null private var shortBreakFlowCollectionJob: Job? = null
private var longBreakFlowCollectionJob: Job? = null private var longBreakFlowCollectionJob: Job? = null
val alarmSound =
preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
val alarmEnabled =
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
val vibrateEnabled =
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
val dndEnabled =
preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
init { init {
viewModelScope.launch { viewModelScope.launch {
reloadSettings() reloadSettings()
_isSettingsLoaded.value = true }
}
fun onAction(action: SettingsAction) {
when (action) {
is SettingsAction.SaveAlarmSound -> saveAlarmSound(action.uri)
is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled)
is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
is SettingsAction.SaveTheme -> saveTheme(action.theme)
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
is SettingsAction.SaveAodEnabled -> saveAodEnabled(action.enabled)
} }
} }
@@ -160,80 +157,82 @@ class SettingsViewModel(
longBreakFlowCollectionJob?.cancel() longBreakFlowCollectionJob?.cancel()
} }
fun saveAlarmEnabled(enabled: Boolean) { private fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmEnabled = enabled timerRepository.alarmEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(alarmEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled) preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
} }
} }
fun saveVibrateEnabled(enabled: Boolean) { private fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.vibrateEnabled = enabled timerRepository.vibrateEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(vibrateEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled) preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
} }
} }
fun saveDndEnabled(enabled: Boolean) { private fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.dndEnabled = enabled timerRepository.dndEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(dndEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled) preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
} }
} }
fun saveAlarmSound(uri: Uri?) { private fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmSoundUri = uri timerRepository.alarmSoundUri = uri
_settingsState.update { currentState ->
currentState.copy(alarmSound = uri.toString())
}
preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
} }
} }
fun saveColorScheme(colorScheme: Color) { private fun saveColorScheme(colorScheme: Color) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(colorScheme = colorScheme.toString()) currentState.copy(colorScheme = colorScheme.toString())
} }
preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString()) preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString())
} }
} }
fun saveTheme(theme: String) { private fun saveTheme(theme: String) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(theme = theme) currentState.copy(theme = theme)
} }
preferenceRepository.saveStringPreference("theme", theme) preferenceRepository.saveStringPreference("theme", theme)
} }
} }
fun saveBlackTheme(blackTheme: Boolean) { private fun saveBlackTheme(blackTheme: Boolean) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(blackTheme = blackTheme) currentState.copy(blackTheme = blackTheme)
} }
preferenceRepository.saveBooleanPreference("black_theme", blackTheme) preferenceRepository.saveBooleanPreference("black_theme", blackTheme)
} }
} }
fun saveAodEnabled(aodEnabled: Boolean) { private fun saveAodEnabled(aodEnabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(aodEnabled = aodEnabled) currentState.copy(aodEnabled = aodEnabled)
} }
preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled) preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
} }
} }
fun resetPaywalledSettings() {
_preferencesState.update { currentState ->
currentState.copy(
aodEnabled = false,
blackTheme = false,
colorScheme = Color.White.toString()
)
}
}
suspend fun reloadSettings() { suspend fun reloadSettings() {
val theme = preferenceRepository.getStringPreference("theme") val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto") ?: preferenceRepository.saveStringPreference("theme", "auto")
@@ -243,13 +242,29 @@ class SettingsViewModel(
?: preferenceRepository.saveBooleanPreference("black_theme", false) ?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false) ?: 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()
)
val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled")
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy( currentState.copy(
theme = theme, theme = theme,
colorScheme = colorScheme, colorScheme = colorScheme,
alarmSound = alarmSound,
blackTheme = blackTheme, blackTheme = blackTheme,
aodEnabled = aodEnabled aodEnabled = aodEnabled,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled
) )
} }
} }

View File

@@ -30,8 +30,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
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
@@ -55,6 +58,11 @@ class TimerViewModel(
val timerState: StateFlow<TimerState> = _timerState.asStateFlow() val timerState: StateFlow<TimerState> = _timerState.asStateFlow()
val time: StateFlow<Long> = _time.asStateFlow() val time: StateFlow<Long> = _time.asStateFlow()
val progress = _time.combine(_timerState) { remainingTime, uiState ->
(uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f)
private var cycles = 0 private var cycles = 0
private var startTime = 0L private var startTime = 0L
@@ -108,9 +116,6 @@ class TimerViewModel(
) )
).toUri() ).toUri()
preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_time.update { timerRepository.focusTime } _time.update { timerRepository.focusTime }
cycles = 0 cycles = 0
startTime = 0L startTime = 0L

View File

@@ -0,0 +1,26 @@
<!--
~ 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 <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.662,3.809c-1.875,1.19 -2.81,3.515 -2.83,5.795 -0.014,2.628 0.666,5.258 1.988,7.305 0.936,1.46 2.238,2.715 3.836,3.412a6.942,6.942 0,0 0,5.647 -0.07c1.997,-0.927 3.523,-2.73 4.463,-4.785 1.606,-3.518 1.643,-7.724 0.12,-11.295 -1.146,0.458 -2.166,-0.271 -2.166,-0.271s0.003,1.122 -1.083,1.685c1.115,2.612 1.088,5.717 -0.03,8.263 -0.538,1.225 -1.358,2.365 -2.498,3.01 -0.917,0.52 -2.04,0.625 -3.052,0.184 -1.342,-0.585 -2.293,-1.864 -2.89,-3.254 -0.466,-1.067 -0.782,-2.447 -0.802,-3.878 -0.037,-1.724 0.728,-3.193 1.635,-3.218 0.622,-0.024 1.427,0.918 1.598,2.435 0.158,1.543 -0.177,3.72 -1.174,5.49 0.677,1.085 1.77,1.98 2.951,1.974 1.386,-2.338 1.827,-4.911 1.793,-6.987 -0.02,-2.28 -0.955,-4.603 -2.83,-5.795 -1.437,-0.907 -3.173,-0.948 -4.676,0zM3.278,3.9s-1.018,0.73 -2.163,0.27c-1.524,3.573 -1.488,7.778 0.12,11.296 0.94,2.056 2.465,3.858 4.462,4.785a6.95,6.95 0,0 0,5.523 0.124,9.12 9.12,0 0,1 -1.75,-1.455 11.18,11.18 0,0 1,-1.267 -1.628c-0.768,-0.08 -1.498,-0.482 -2.003,-0.913 -1.447,-1.213 -2.453,-3.478 -2.632,-5.9 -0.12,-1.635 0.14,-3.354 0.795,-4.894C3.276,5.022 3.278,3.9 3.278,3.9z" />
</vector>

View File

@@ -92,4 +92,5 @@
<string name="rate_on_google_play">Rate on Google Play</string> <string name="rate_on_google_play">Rate on Google Play</string>
<string name="bmc">BuyMeACoffee</string> <string name="bmc">BuyMeACoffee</string>
<string name="selected">Selected</string> <string name="selected">Selected</string>
<string name="help_with_translation">Help with translation</string>
</resources> </resources>

View File

@@ -33,9 +33,6 @@ class PlayBillingManager : BillingManager {
private val _isPlus = MutableStateFlow(false) private val _isPlus = MutableStateFlow(false)
override val isPlus = _isPlus.asStateFlow() override val isPlus = _isPlus.asStateFlow()
private val _isLoaded = MutableStateFlow(false)
override val isLoaded = _isLoaded.asStateFlow()
private val purchases by lazy { Purchases.sharedInstance } private val purchases by lazy { Purchases.sharedInstance }
init { init {
@@ -48,11 +45,9 @@ class PlayBillingManager : BillingManager {
purchases.getCustomerInfoWith( purchases.getCustomerInfoWith(
onSuccess = { customerInfo -> onSuccess = { customerInfo ->
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true _isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
_isLoaded.value = true
}, },
onError = { error -> onError = { error ->
Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error") Log.e("GooglePlayPaywallManager", "Error fetching customer info: $error")
_isLoaded.value = true
} }
) )
} }

View File

@@ -0,0 +1,86 @@
/*
* 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 <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
@Composable
fun TopButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://hosted.weblate.org/engage/tomato/") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.weblate),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(text = stringResource(R.string.help_with_translation))
}
}
}
@Composable
fun BottomButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=org.nsh07.pomodoro") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.play_store),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(text = stringResource(R.string.rate_on_google_play))
}
}
}