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"
)
}
debug {
applicationIdSuffix = ".debug"
}
}
flavorDimensions += "version"

View File

@@ -25,7 +25,6 @@ import kotlinx.coroutines.flow.asStateFlow
*/
class FossBillingManager : BillingManager {
override val isPlus = MutableStateFlow(true).asStateFlow()
override val isLoaded = MutableStateFlow(true).asStateFlow()
}
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 {
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
val settingsState by settingsViewModel.settingsState.collectAsStateWithLifecycle()
val darkTheme = when (preferencesState.theme) {
val darkTheme = when (settingsState.theme) {
"dark" -> true
"light" -> false
else -> isSystemInDarkTheme()
}
val seed = preferencesState.colorScheme.toColor()
val seed = settingsState.colorScheme.toColor()
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(
darkTheme = darkTheme,
seedColor = seed,
blackTheme = preferencesState.blackTheme
blackTheme = settingsState.blackTheme
) {
val colorScheme = colorScheme
LaunchedEffect(colorScheme) {
@@ -86,7 +74,7 @@ class MainActivity : ComponentActivity() {
AppScreen(
isPlus = isPlus,
isAODEnabled = preferencesState.aodEnabled,
isAODEnabled = settingsState.aodEnabled,
setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it
}
@@ -105,6 +93,6 @@ class MainActivity : ComponentActivity() {
override fun onStart() {
super.onStart()
// 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 {
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.toArgb
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.R
@@ -83,6 +84,7 @@ class DefaultAppContainer(context: Context) : AppContainer {
.setSilent(true)
.setOngoing(true)
.setRequestPromotedOngoing(true)
.setVisibility(VISIBILITY_PUBLIC)
}
override val timerState: MutableStateFlow<TimerState> by lazy {

View File

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

View File

@@ -113,7 +113,7 @@ fun SharedTransitionScope.AlwaysOnDisplay(
}
onDispose {
setTimerFrequency(10f)
setTimerFrequency(60f)
window.clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
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.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
@@ -79,9 +78,7 @@ fun AppScreen(
val context = LocalContext.current
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
val remainingTime by timerViewModel.time.collectAsStateWithLifecycle()
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
val progress by timerViewModel.progress.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
val motionScheme = motionScheme

View File

@@ -19,8 +19,6 @@ package org.nsh07.pomodoro.ui.settingsScreen
import android.annotation.SuppressLint
import android.app.LocaleManager
import android.content.Intent
import android.net.Uri
import android.os.Build
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -55,7 +53,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -68,7 +65,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
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.AppearanceSettings
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.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -92,8 +89,6 @@ fun SettingsScreenRoot(
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
val context = LocalContext.current
val backStack = viewModel.backStack
DisposableEffect(Unit) {
@@ -106,12 +101,8 @@ fun SettingsScreenRoot(
val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState
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(
saver = SliderState.Saver(
@@ -124,30 +115,13 @@ fun SettingsScreenRoot(
SettingsScreen(
isPlus = isPlus,
preferencesState = preferencesState,
settingsState = settingsState,
backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
alarmEnabled = alarmEnabled,
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,
onAction = viewModel::onAction,
setShowPaywall = setShowPaywall,
modifier = modifier
)
@@ -158,24 +132,13 @@ fun SettingsScreenRoot(
@Composable
private fun SettingsScreen(
isPlus: Boolean,
preferencesState: PreferencesState,
settingsState: SettingsState,
backStack: SnapshotStateList<Screen.Settings>,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
alarmEnabled: Boolean,
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,
onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
@@ -312,23 +275,16 @@ private fun SettingsScreen(
entry<Screen.Settings.Alarm> {
AlarmSettings(
preferencesState = preferencesState,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
alarmSound = alarmSound,
onAlarmEnabledChange = onAlarmEnabledChange,
onVibrateEnabledChange = onVibrateEnabledChange,
onAlarmSoundChanged = onAlarmSoundChanged,
settingsState = settingsState,
onAction = onAction,
onBack = backStack::removeLastOrNull
)
}
entry<Screen.Settings.Appearance> {
AppearanceSettings(
preferencesState = preferencesState,
settingsState = settingsState,
isPlus = isPlus,
onBlackThemeChange = onBlackThemeChange,
onThemeChange = onThemeChange,
onColorSchemeChange = onColorSchemeChange,
onAction = onAction,
setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull
)
@@ -336,14 +292,12 @@ private fun SettingsScreen(
entry<Screen.Settings.Timer> {
TimerSettings(
isPlus = isPlus,
aodEnabled = preferencesState.aodEnabled,
dndEnabled = dndEnabled,
settingsState = settingsState,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange,
onDndEnabledChange = onDndEnabledChange,
onAction = onAction,
setShowPaywall = setShowPaywall,
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
@@ -119,41 +117,8 @@ fun AboutCard(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
colors = 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))
}
}
TopButton(buttonColors)
BottomButton(buttonColors)
}
}
}

View File

@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.screens
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager
@@ -64,7 +65,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.R
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.CustomColors.listItemColors
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)
@Composable
fun AlarmSettings(
preferencesState: PreferencesState,
alarmEnabled: Boolean,
vibrateEnabled: Boolean,
alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
settingsState: SettingsState,
onAction: (SettingsAction) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -91,10 +88,11 @@ fun AlarmSettings(
var alarmName by remember { mutableStateOf("...") }
LaunchedEffect(alarmSound) {
LaunchedEffect(settingsState.alarmSound) {
withContext(Dispatchers.IO) {
alarmName =
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
RingtoneManager.getRingtone(context, settingsState.alarmSound.toUri())
?.getTitle(context) ?: ""
}
}
@@ -112,38 +110,39 @@ fun AlarmSettings(
@Suppress("DEPRECATION")
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 {
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, alarmSound.toUri())
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSound.toUri())
}
}
val switchItems = remember(
preferencesState.blackTheme,
preferencesState.aodEnabled,
alarmEnabled,
vibrateEnabled
settingsState.blackTheme,
settingsState.aodEnabled,
settingsState.alarmEnabled,
settingsState.vibrateEnabled
) {
listOf(
SettingsSwitchItem(
checked = alarmEnabled,
checked = settingsState.alarmEnabled,
icon = R.drawable.alarm_on,
label = R.string.sound,
description = R.string.alarm_desc,
onClick = onAlarmEnabledChange
onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
),
SettingsSwitchItem(
checked = vibrateEnabled,
checked = settingsState.vibrateEnabled,
icon = R.drawable.mobile_vibrate,
label = R.string.vibrate,
description = R.string.vibrate_desc,
onClick = onVibrateEnabledChange
onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) }
)
)
}
@@ -241,14 +240,10 @@ fun AlarmSettings(
@Preview
@Composable
fun AlarmSettingsPreview() {
val preferencesState = PreferencesState()
val settingsState = SettingsState()
AlarmSettings(
preferencesState = preferencesState,
alarmEnabled = true,
vibrateEnabled = false,
alarmSound = "",
onAlarmEnabledChange = {},
onVibrateEnabledChange = {},
onAlarmSoundChanged = {},
onBack = {})
settingsState = settingsState,
onAction = {},
onBack = {}
)
}

View File

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

View File

@@ -36,10 +36,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton
@@ -56,6 +58,7 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.components.MinuteInputField
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.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -90,17 +95,15 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable
fun TimerSettings(
isPlus: Boolean,
aodEnabled: Boolean,
dndEnabled: Boolean,
settingsState: SettingsState,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit,
onDndEnabledChange: (Boolean) -> Unit,
onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier,
setShowPaywall: (Boolean) -> Unit
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
@@ -110,12 +113,12 @@ fun TimerSettings(
LaunchedEffect(Unit) {
if (!notificationManagerService.isNotificationPolicyAccessGranted())
onDndEnabledChange(false)
onAction(SettingsAction.SaveDndEnabled(false))
}
val switchItems = listOf(
SettingsSwitchItem(
checked = dndEnabled,
checked = settingsState.dndEnabled,
icon = R.drawable.dnd,
label = R.string.dnd,
description = R.string.dnd_desc,
@@ -128,15 +131,15 @@ fun TimerSettings(
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
}
onDndEnabledChange(it)
onAction(SettingsAction.SaveDndEnabled(it))
}
),
SettingsSwitchItem(
checked = aodEnabled,
checked = settingsState.aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = onAodEnabledChange
onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
)
)
@@ -313,7 +316,7 @@ fun TimerSettings(
item {
PlusDivider(setShowPaywall)
}
itemsIndexed(switchItems.drop(1)) { index, item ->
items(switchItems.drop(1)) { item ->
ListItem(
leadingContent = {
Icon(
@@ -392,24 +395,22 @@ fun TimerSettings(
@Preview
@Composable
private fun TimerSettingsPreview() {
val focusTimeInputFieldState = TextFieldState("25")
val shortBreakTimeInputFieldState = TextFieldState("5")
val longBreakTimeInputFieldState = TextFieldState("15")
val sessionsSliderState = SliderState(
val focusTimeInputFieldState = rememberTextFieldState("25")
val shortBreakTimeInputFieldState = rememberTextFieldState("5")
val longBreakTimeInputFieldState = rememberTextFieldState("15")
val sessionsSliderState = rememberSliderState(
value = 4f,
valueRange = 1f..8f,
steps = 6
)
TimerSettings(
isPlus = false,
aodEnabled = true,
dndEnabled = false,
settingsState = remember { SettingsState() },
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
onAodEnabledChange = {},
onDndEnabledChange = {},
onAction = {},
setShowPaywall = {},
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
import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState
@@ -36,7 +37,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
@@ -54,13 +54,9 @@ class SettingsViewModel(
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus
val isPurchaseStateLoaded = billingManager.isLoaded
private val _isSettingsLoaded = MutableStateFlow(false)
val isSettingsLoaded = _isSettingsLoaded.asStateFlow()
private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow()
private val _settingsState = MutableStateFlow(SettingsState())
val settingsState = _settingsState.asStateFlow()
val focusTimeTextFieldState by lazy {
TextFieldState((timerRepository.focusTime / 60000).toString())
@@ -81,25 +77,26 @@ class SettingsViewModel(
)
}
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
private var focusFlowCollectionJob: Job? = null
private var shortBreakFlowCollectionJob: 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 {
viewModelScope.launch {
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()
}
fun saveAlarmEnabled(enabled: Boolean) {
private fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.alarmEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(alarmEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
}
}
fun saveVibrateEnabled(enabled: Boolean) {
private fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.vibrateEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(vibrateEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
}
}
fun saveDndEnabled(enabled: Boolean) {
private fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.dndEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(dndEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
}
}
fun saveAlarmSound(uri: Uri?) {
private fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch {
timerRepository.alarmSoundUri = uri
_settingsState.update { currentState ->
currentState.copy(alarmSound = uri.toString())
}
preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
}
}
fun saveColorScheme(colorScheme: Color) {
private fun saveColorScheme(colorScheme: Color) {
viewModelScope.launch {
_preferencesState.update { currentState ->
_settingsState.update { currentState ->
currentState.copy(colorScheme = colorScheme.toString())
}
preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString())
}
}
fun saveTheme(theme: String) {
private fun saveTheme(theme: String) {
viewModelScope.launch {
_preferencesState.update { currentState ->
_settingsState.update { currentState ->
currentState.copy(theme = theme)
}
preferenceRepository.saveStringPreference("theme", theme)
}
}
fun saveBlackTheme(blackTheme: Boolean) {
private fun saveBlackTheme(blackTheme: Boolean) {
viewModelScope.launch {
_preferencesState.update { currentState ->
_settingsState.update { currentState ->
currentState.copy(blackTheme = blackTheme)
}
preferenceRepository.saveBooleanPreference("black_theme", blackTheme)
}
}
fun saveAodEnabled(aodEnabled: Boolean) {
private fun saveAodEnabled(aodEnabled: Boolean) {
viewModelScope.launch {
_preferencesState.update { currentState ->
_settingsState.update { currentState ->
currentState.copy(aodEnabled = aodEnabled)
}
preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
}
}
fun resetPaywalledSettings() {
_preferencesState.update { currentState ->
currentState.copy(
aodEnabled = false,
blackTheme = false,
colorScheme = Color.White.toString()
)
}
}
suspend fun reloadSettings() {
val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto")
@@ -243,13 +242,29 @@ class SettingsViewModel(
?: preferenceRepository.saveBooleanPreference("black_theme", false)
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()
)
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(
theme = theme,
colorScheme = colorScheme,
alarmSound = alarmSound,
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.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
@@ -55,6 +58,11 @@ class TimerViewModel(
val timerState: StateFlow<TimerState> = _timerState.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 startTime = 0L
@@ -108,9 +116,6 @@ class TimerViewModel(
)
).toUri()
preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_time.update { timerRepository.focusTime }
cycles = 0
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="bmc">BuyMeACoffee</string>
<string name="selected">Selected</string>
<string name="help_with_translation">Help with translation</string>
</resources>

View File

@@ -33,9 +33,6 @@ class PlayBillingManager : BillingManager {
private val _isPlus = MutableStateFlow(false)
override val isPlus = _isPlus.asStateFlow()
private val _isLoaded = MutableStateFlow(false)
override val isLoaded = _isLoaded.asStateFlow()
private val purchases by lazy { Purchases.sharedInstance }
init {
@@ -48,11 +45,9 @@ class PlayBillingManager : BillingManager {
purchases.getCustomerInfoWith(
onSuccess = { customerInfo ->
_isPlus.value = customerInfo.entitlements[ENTITLEMENT_ID]?.isActive == true
_isLoaded.value = true
},
onError = { 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))
}
}
}