Merge branch 'dev'

This commit is contained in:
Nishant Mishra
2025-11-10 00:14:19 +05:30
41 changed files with 800 additions and 314 deletions

View File

@@ -43,8 +43,8 @@ android {
applicationId = "org.nsh07.pomodoro"
minSdk = 27
targetSdk = 36
versionCode = 19
versionName = "1.6.4"
versionCode = 20
versionName = "1.6.5"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -57,9 +57,9 @@ android {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
)
}
debug {
applicationIdSuffix = ".debug"
}
}
@@ -68,10 +68,16 @@ android {
create("foss") {
dimension = "version"
isDefault = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-foss.pro"
)
}
create("play") {
dimension = "version"
versionNameSuffix = "-play"
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-play.pro"
)
}
}

View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

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,11 +23,13 @@ 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
import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.billing.BillingManagerProvider
import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.service.addTimerActions
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
@@ -40,6 +42,7 @@ interface AppContainer {
val notificationManager: NotificationManagerCompat
val notificationManagerService: NotificationManager
val notificationBuilder: NotificationCompat.Builder
val serviceHelper: ServiceHelper
val timerState: MutableStateFlow<TimerState>
val time: MutableStateFlow<Long>
var activityTurnScreenOn: (Boolean) -> Unit
@@ -83,6 +86,11 @@ class DefaultAppContainer(context: Context) : AppContainer {
.setSilent(true)
.setOngoing(true)
.setRequestPromotedOngoing(true)
.setVisibility(VISIBILITY_PUBLIC)
}
override val serviceHelper: ServiceHelper by lazy {
ServiceHelper(context)
}
override val timerState: MutableStateFlow<TimerState> by lazy {

View File

@@ -34,7 +34,7 @@ abstract class AppDatabase : RoomDatabase() {
fun getDatabase(context: Context): AppDatabase {
return Instance ?: synchronized(this) {
Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
Instance ?: Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.build()
.also { Instance = it }
}

View File

@@ -21,6 +21,7 @@ import android.net.Uri
import android.provider.Settings
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.lightColorScheme
import kotlinx.coroutines.flow.MutableStateFlow
/**
* Interface that holds the timer durations for each timer type. This repository maintains a single
@@ -43,7 +44,7 @@ interface TimerRepository {
var alarmSoundUri: Uri?
var serviceRunning: Boolean
var serviceRunning: MutableStateFlow<Boolean>
}
/**
@@ -54,12 +55,12 @@ 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
override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
override var serviceRunning = false
override var serviceRunning = MutableStateFlow(false)
}

View File

@@ -0,0 +1,58 @@
/*
* 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.service
import android.content.Context
import android.content.Intent
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
/**
* Helper class that holds a reference to [Context] and helps call [Context.startService] in
* [androidx.lifecycle.ViewModel]s. This class must be managed by an [android.app.Application] class
* to scope it to the Activity's lifecycle and prevent leaks.
*/
class ServiceHelper(private val context: Context) {
fun startService(action: TimerAction) {
when (action) {
TimerAction.ResetTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.RESET.toString()
context.startService(it)
}
is TimerAction.SkipTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.SKIP.toString()
context.startService(it)
}
TimerAction.StopAlarm ->
Intent(context, TimerService::class.java).also {
it.action =
TimerService.Actions.STOP_ALARM.toString()
context.startService(it)
}
TimerAction.ToggleTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.TOGGLE.toString()
context.startService(it)
}
}
}
}

View File

@@ -98,12 +98,12 @@ class TimerService : Service() {
override fun onCreate() {
super.onCreate()
timerRepository.serviceRunning = true
timerRepository.serviceRunning.update { true }
alarm = initializeMediaPlayer()
}
override fun onDestroy() {
timerRepository.serviceRunning = false
timerRepository.serviceRunning.update { false }
runBlocking {
job.cancel()
saveTimeToDb()

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
@@ -64,7 +63,6 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -79,9 +77,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
@@ -166,34 +162,7 @@ fun AppScreen(
timerState = uiState,
isPlus = isPlus,
progress = { progress },
onAction = { action ->
when (action) {
TimerAction.ResetTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.RESET.toString()
context.startService(it)
}
is TimerAction.SkipTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.SKIP.toString()
context.startService(it)
}
TimerAction.StopAlarm ->
Intent(context, TimerService::class.java).also {
it.action =
TimerService.Actions.STOP_ALARM.toString()
context.startService(it)
}
TimerAction.ToggleTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.TOGGLE.toString()
context.startService(it)
}
}
},
onAction = timerViewModel::onAction,
modifier = modifier
.padding(
start = contentPadding.calculateStartPadding(layoutDirection),

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,9 @@ 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 serviceRunning by viewModel.serviceRunning.collectAsStateWithLifecycle()
val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
val settingsState by viewModel.settingsState.collectAsStateWithLifecycle()
val sessionsSliderState = rememberSaveable(
saver = SliderState.Saver(
@@ -124,30 +116,14 @@ fun SettingsScreenRoot(
SettingsScreen(
isPlus = isPlus,
preferencesState = preferencesState,
serviceRunning = serviceRunning,
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 +134,14 @@ fun SettingsScreenRoot(
@Composable
private fun SettingsScreen(
isPlus: Boolean,
preferencesState: PreferencesState,
serviceRunning: Boolean,
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 +278,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 +295,13 @@ private fun SettingsScreen(
entry<Screen.Settings.Timer> {
TimerSettings(
isPlus = isPlus,
aodEnabled = preferencesState.aodEnabled,
dndEnabled = dndEnabled,
serviceRunning = serviceRunning,
settingsState = settingsState,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange,
onDndEnabledChange = onDndEnabledChange,
onAction = onAction,
setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull,
)

View File

@@ -22,6 +22,7 @@ import androidx.annotation.StringRes
data class SettingsSwitchItem(
val checked: Boolean,
val enabled: Boolean = true,
@param:DrawableRes val icon: Int,
@param:StringRes val label: Int,
@param:StringRes val description: Int,

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

@@ -46,12 +46,14 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@Composable
fun MinuteInputField(
state: TextFieldState,
enabled: Boolean,
shape: Shape,
modifier: Modifier = Modifier,
imeAction: ImeAction = ImeAction.Next
) {
BasicTextField(
state = state,
enabled = enabled,
lineLimits = TextFieldLineLimits.SingleLine,
inputTransformation = MinutesInputTransformation,
// outputTransformation = MinutesOutputTransformation,
@@ -63,7 +65,7 @@ fun MinuteInputField(
fontFamily = interClock,
fontSize = 57.sp,
letterSpacing = (-2).sp,
color = colorScheme.onSurfaceVariant,
color = if (enabled) colorScheme.onSurfaceVariant else colorScheme.outlineVariant,
textAlign = TextAlign.Center
),
cursorBrush = SolidColor(colorScheme.onSurface),

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
@@ -48,6 +50,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
@@ -56,8 +59,9 @@ 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.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -76,6 +80,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 +96,16 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable
fun TimerSettings(
isPlus: Boolean,
aodEnabled: Boolean,
dndEnabled: Boolean,
serviceRunning: 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
@@ -108,14 +113,10 @@ fun TimerSettings(
val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
LaunchedEffect(Unit) {
if (!notificationManagerService.isNotificationPolicyAccessGranted())
onDndEnabledChange(false)
}
val switchItems = listOf(
SettingsSwitchItem(
checked = dndEnabled,
checked = settingsState.dndEnabled,
enabled = !serviceRunning,
icon = R.drawable.dnd,
label = R.string.dnd,
description = R.string.dnd_desc,
@@ -128,15 +129,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)) }
)
)
@@ -168,6 +169,20 @@ fun TimerSettings(
.padding(horizontal = 16.dp)
) {
item {
CompositionLocalProvider(LocalContentColor provides colorScheme.error) {
AnimatedVisibility(serviceRunning) {
Column {
Spacer(Modifier.height(8.dp))
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Icon(painterResource(R.drawable.info), null)
Text(stringResource(R.string.timer_settings_reset_info))
}
}
}
}
Spacer(Modifier.height(14.dp))
}
item {
@@ -187,6 +202,7 @@ fun TimerSettings(
)
MinuteInputField(
state = focusTimeInputFieldState,
enabled = !serviceRunning,
shape = RoundedCornerShape(
topStart = topListItemShape.topStart,
bottomStart = topListItemShape.topStart,
@@ -207,6 +223,7 @@ fun TimerSettings(
)
MinuteInputField(
state = shortBreakTimeInputFieldState,
enabled = !serviceRunning,
shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next
)
@@ -222,6 +239,7 @@ fun TimerSettings(
)
MinuteInputField(
state = longBreakTimeInputFieldState,
enabled = !serviceRunning,
shape = RoundedCornerShape(
topStart = bottomListItemShape.topStart,
bottomStart = bottomListItemShape.topStart,
@@ -254,6 +272,7 @@ fun TimerSettings(
)
Slider(
state = sessionsSliderState,
enabled = !serviceRunning,
modifier = Modifier.padding(vertical = 4.dp)
)
}
@@ -278,6 +297,7 @@ fun TimerSettings(
trailingContent = {
Switch(
checked = item.checked,
enabled = item.enabled,
onCheckedChange = { item.onClick(it) },
thumbContent = {
if (item.checked) {
@@ -313,7 +333,7 @@ fun TimerSettings(
item {
PlusDivider(setShowPaywall)
}
itemsIndexed(switchItems.drop(1)) { index, item ->
items(switchItems.drop(1)) { item ->
ListItem(
leadingContent = {
Icon(
@@ -392,24 +412,23 @@ 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,
serviceRunning = true,
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,31 +37,35 @@ 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
import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel(
private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository,
private val serviceHelper: ServiceHelper,
private val time: MutableStateFlow<Long>,
private val timerRepository: TimerRepository,
private val timerState: MutableStateFlow<TimerState>
) : ViewModel() {
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus
val isPurchaseStateLoaded = billingManager.isLoaded
val serviceRunning = timerRepository.serviceRunning.asStateFlow()
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 +86,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)
}
}
@@ -109,6 +115,7 @@ class SettingsViewModel(
"session_length",
sessionsSliderState.value.toInt()
)
refreshTimer()
}
}
@@ -119,6 +126,7 @@ class SettingsViewModel(
.collect {
if (it.isNotEmpty()) {
timerRepository.focusTime = it.toString().toLong() * 60 * 1000
refreshTimer()
preferenceRepository.saveIntPreference(
"focus_time",
timerRepository.focusTime.toInt()
@@ -132,6 +140,7 @@ class SettingsViewModel(
.collect {
if (it.isNotEmpty()) {
timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000
refreshTimer()
preferenceRepository.saveIntPreference(
"short_break_time",
timerRepository.shortBreakTime.toInt()
@@ -145,6 +154,7 @@ class SettingsViewModel(
.collect {
if (it.isNotEmpty()) {
timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000
refreshTimer()
preferenceRepository.saveIntPreference(
"long_break_time",
timerRepository.longBreakTime.toInt()
@@ -155,85 +165,88 @@ class SettingsViewModel(
}
fun cancelTextFieldFlowCollection() {
if (!serviceRunning.value) serviceHelper.startService(TimerAction.ResetTimer)
focusFlowCollectionJob?.cancel()
shortBreakFlowCollectionJob?.cancel()
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,29 +256,69 @@ 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
)
}
}
private fun refreshTimer() {
if (!serviceRunning.value) {
time.update { timerRepository.focusTime }
timerState.update { currentState ->
currentState.copy(
timerMode = TimerMode.FOCUS,
timeStr = millisecondsToStr(time.value),
totalTime = time.value,
nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime),
currentFocusCount = 1,
totalFocusCount = timerRepository.sessionLength
)
}
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication)
val appBillingManager = application.container.billingManager
val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository
val appBillingManager = application.container.billingManager
val serviceHelper = application.container.serviceHelper
val time = application.container.time
val timerState = application.container.timerState
SettingsViewModel(
billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository,
serviceHelper = serviceHelper,
time = time,
timerRepository = appTimerRepository,
timerState = timerState
)
}
}

View File

@@ -17,10 +17,9 @@
package org.nsh07.pomodoro.ui.timerScreen.viewModel
import android.app.Application
import android.provider.Settings
import androidx.core.net.toUri
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.ViewModelProvider.AndroidViewModelFactory.Companion.APPLICATION_KEY
import androidx.lifecycle.viewModelScope
@@ -30,8 +29,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
@@ -39,22 +41,28 @@ import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.utils.millisecondsToStr
import java.time.LocalDate
import java.time.temporal.ChronoUnit
@OptIn(FlowPreview::class)
class TimerViewModel(
application: Application,
private val preferenceRepository: PreferenceRepository,
private val serviceHelper: ServiceHelper,
private val statRepository: StatRepository,
private val timerRepository: TimerRepository,
private val _timerState: MutableStateFlow<TimerState>,
private val _time: MutableStateFlow<Long>
) : AndroidViewModel(application) {
) : ViewModel() {
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
@@ -62,7 +70,7 @@ class TimerViewModel(
private var pauseDuration = 0L
init {
if (!timerRepository.serviceRunning)
if (!timerRepository.serviceRunning.value)
viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime =
preferenceRepository.getIntPreference("focus_time")?.toLong()
@@ -108,9 +116,6 @@ class TimerViewModel(
)
).toUri()
preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L
@@ -150,6 +155,10 @@ class TimerViewModel(
}
}
fun onAction(action: TimerAction) {
serviceHelper.startService(action)
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
@@ -157,12 +166,13 @@ class TimerViewModel(
val appPreferenceRepository = application.container.appPreferenceRepository
val appStatRepository = application.container.appStatRepository
val appTimerRepository = application.container.appTimerRepository
val serviceHelper = application.container.serviceHelper
val timerState = application.container.timerState
val time = application.container.time
TimerViewModel(
application = application,
preferenceRepository = appPreferenceRepository,
serviceHelper = serviceHelper,
statRepository = appStatRepository,
timerRepository = appTimerRepository,
_timerState = timerState,

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

@@ -62,4 +62,16 @@
<string name="weekly_productivity_analysis">साप्ताहिक उत्पादकता विश्लेषण</string>
<string name="appearance">दिखावट</string>
<string name="durations">अवधियां</string>
<string name="dnd">परेशान न करें</string>
<string name="dnd_desc">फ़ोकस टाइमर चलाते समय \'परेशान न करें\' मोड चालू करें</string>
<string name="get_plus">Tomato+ प्राप्त करें</string>
<string name="dynamic_color">डायनामिक रंग</string>
<string name="dynamic_color_desc">अपने वॉलपेपर से थीम रंग अनुकूलित करें</string>
<string name="tomato_foss_desc">इस संस्करण में सभी सुविधाएँ अनलॉक हैं। अगर मेरे ऐप ने आपके जीवन में कोई बदलाव लाया है, तो कृपया %1$s पर दान करके मेरी मदद करें।</string>
<string name="language">भाषा</string>
<string name="choose_language">भाषा चुनें</string>
<string name="rate_on_google_play">Google Play पर रेटिंग दें</string>
<string name="selected">चयनित</string>
<string name="help_with_translation">अनुवाद में सहायता करें</string>
<string name="timer_settings_reset_info">सेटिंग्स बदलने के लिए टाइमर रीसेट करें</string>
</resources>

View File

@@ -0,0 +1,76 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="dynamic">Dinamis</string>
<string name="alarm">Alarm</string>
<string name="alarm_desc">Bunyikan alarm ketika timer selesai</string>
<string name="alarm_sound">Suara alarm</string>
<string name="black_theme">Tema hitam</string>
<string name="black_theme_desc">Gunakan tema hitam</string>
<string name="break_">Rehat</string>
<string name="choose_color_scheme">Pilih skema warna</string>
<string name="choose_theme">Pilih tema</string>
<string name="color">Warna</string>
<string name="color_scheme">Skema warna</string>
<string name="completed">Selesai</string>
<string name="dark">Gelap</string>
<string name="exit">Keluar</string>
<string name="focus">Fokus</string>
<string name="focus_per_day_avg">fokus per hari (rerata)</string>
<string name="last_month">Bulan lalu</string>
<string name="last_week">Minggu lalu</string>
<string name="last_year">Tahun lalu</string>
<string name="light">Terang</string>
<string name="long_break">Rehat panjang</string>
<string name="min_remaining_notification">%1$s menit tersisa</string>
<string name="monthly_productivity_analysis">Analisis produktivitas bulanan</string>
<string name="more">Lebih banyak</string>
<string name="more_info">Info lebih</string>
<string name="ok">Oke</string>
<string name="pause">Jeda</string>
<string name="paused">Dijeda</string>
<string name="play">Mulai</string>
<string name="pomodoro_info">\"Sesi\" adalah rangkaian interval pomodoro yang berisi interval fokus, interval rehat pendek, dan interval rehat panjang. Rehat terakhir dalam suatu sesi selalu merupakan rehat panjang.</string>
<string name="productivity_analysis">Analisis produktivitas</string>
<string name="productivity_analysis_desc">Durasi fokus berdasarkan segmen waktu dalam sehari</string>
<string name="restart">Mulai ulang</string>
<string name="session_length">Lama sesi</string>
<string name="session_length_desc">Interval fokus dalam satu sesi: %1$d</string>
<string name="settings">Pengaturan</string>
<string name="short_break">Rehat pendek</string>
<string name="skip">Lewati</string>
<string name="skip_to_next">Lewati ke berikutnya</string>
<string name="start">Mulai</string>
<string name="start_next">Mulai selanjutnya</string>
<string name="stats">Statistik</string>
<string name="stop">Hentikan</string>
<string name="stop_alarm">Hentikan alarm</string>
<string name="stop_alarm_dialog_text">Sesi ini sudah selesai. Ketuk di mana saja untuk hentikan alarm.</string>
<string name="stop_alarm_question">Hentikan Alarm?</string>
<string name="system_default">Sistem</string>
<string name="theme">Tema</string>
<string name="timer">Timer</string>
<string name="timer_progress">Progres timer</string>
<string name="timer_session_count">%1$d dari %2$d</string>
<string name="today">Hari ini</string>
<string name="up_next">Berikutnya</string>
<string name="up_next_notification">Berikutnya: %1$s (%2$s)</string>
<string name="vibrate">Getar</string>
<string name="vibrate_desc">Getar ketika timer selesai</string>
<string name="weekly_productivity_analysis">Analisis produktivitas mingguan</string>
<string name="appearance">Tampilan</string>
<string name="durations">Durasi</string>
<string name="sound">Suara</string>
<string name="dnd">Jangan Ganggu</string>
<string name="dnd_desc">Aktifkan mode Jangan Ganggu ketika timer Fokus berjalan</string>
<string name="get_plus">Dapatkan Tomato+</string>
<string name="dynamic_color">Warna dinamis</string>
<string name="dynamic_color_desc">Sesuaikan warna tema dari wallpaper perangkat</string>
<string name="tomato_foss">Tomato FOSS</string>
<string name="tomato_foss_desc">Semua fitur tidak terkunci di versi sumber terbuka ini. Jika aplikasi ini bermanfaat , mohon pertimbangkan untuk dukung saya dengan berdonasi di %1$s.</string>
<string name="language">Bahasa</string>
<string name="choose_language">Pilih bahasa</string>
<string name="rate_on_google_play">Beri nilai di Google Play</string>
<string name="selected">Dipilih</string>
<string name="always_on_display_desc">Ketuk di mana saja saat melihat pengatur waktu untuk beralih ke mode AOD</string>
<string name="always_on_display">Always On Display (AOD)</string>
</resources>

View File

@@ -65,4 +65,12 @@
<string name="dynamic_color_desc">Adatta i colori del tema dal tuo sfondo</string>
<string name="language">Lingua</string>
<string name="choose_language">Scegli la lingua</string>
<string name="durations">Durate</string>
<string name="dnd_desc">Attiva la modalità non disturbare quando avvii un timer di concentrazione.</string>
<string name="tomato_foss">Tomato FOSS</string>
<string name="rate_on_google_play">Valuta su Google Play</string>
<string name="selected">Selezionato</string>
<string name="dynamic_color">Colore dinamico</string>
<string name="tomato_foss_desc">Tutte le funzionalità sono sbloccate in questa versione. Se la mia app ha fatto la differenza nella tua vita, considera di supportarmi con una donazione su %1$s.</string>
<string name="always_on_display">Schermo sempre attivo</string>
</resources>

View File

@@ -1,3 +1,65 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>
<string name="alarm">Alarm</string>
<string name="alarm_desc">Laat alarm afgaan wanneer de timer afloopt</string>
<string name="alarm_sound">Alarm geluid</string>
<string name="always_on_display">Always On Display</string>
<string name="always_on_display_desc">Tik ergens terwijl u de timer bekijkt om over te schakelen naar de AOD-modus</string>
<string name="black_theme">Donker thema</string>
<string name="black_theme_desc">Gebruik een puur-zwart donker thema</string>
<string name="break_">Pauze</string>
<string name="choose_color_scheme">Kies een kleurthema</string>
<string name="choose_theme">Kies een thema</string>
<string name="color">Kleur</string>
<string name="color_scheme">Kleurthema</string>
<string name="completed">Voltooid</string>
<string name="dark">Donker</string>
<string name="dynamic">Dynamisch</string>
<string name="exit">Verlaat</string>
<string name="focus">Focus</string>
<string name="focus_per_day_avg">focus per dag (gem.)</string>
<string name="last_month">Voorbije maand</string>
<string name="last_week">Voorbije week</string>
<string name="last_year">Voorbije jaar</string>
<string name="light">Licht</string>
<string name="long_break">Lange pauze</string>
<string name="min_remaining_notification">%1$s min resterend</string>
<string name="monthly_productivity_analysis">Maandelijkse productiviteitsanalyse</string>
<string name="more">Meer</string>
<string name="more_info">Meer info</string>
<string name="ok">OK</string>
<string name="pause">Pauzeer</string>
<string name="paused">Gepauzeerd</string>
<string name="play">Start</string>
<string name="pomodoro_info">Een \"sessie\" is een reeks pomodoro-intervallen die bestaan uit focusintervallen, korte pauzes en een lange pauze. De laatste pauze van een sessie is altijd een lange pauze.</string>
<string name="productivity_analysis">Productiviteitsanalyse</string>
<string name="productivity_analysis_desc">Focustijden op verschillende tijdstippen van de dag</string>
<string name="restart">Herstart</string>
<string name="session_length">Sessielengte</string>
<string name="session_length_desc">Focusintervallen in één sessie: %1$d</string>
<string name="settings">Instellingen</string>
<string name="short_break">Korte pauze</string>
<string name="skip">Skip</string>
<string name="skip_to_next">Skip naar de volgende</string>
<string name="start">Start</string>
<string name="start_next">Start de volgende</string>
<string name="stats">Statistieken</string>
<string name="stop">Stop</string>
<string name="stop_alarm">Stop alarm</string>
<string name="stop_alarm_dialog_text">De huidige sessie is voltooid. Tik ergens om het alarm te stoppen.</string>
<string name="stop_alarm_question">Alarm stoppen?</string>
<string name="system_default">Systeem</string>
<string name="theme">Thema</string>
<string name="timer">Timer</string>
<string name="timer_progress">Timervoortgang</string>
<string name="timer_session_count">%1$d van %2$d</string>
<string name="today">Vandaag</string>
<string name="up_next">Volgende</string>
<string name="up_next_notification">Volgende: %1$s (%2$s)</string>
<string name="vibrate">Trilling</string>
<string name="vibrate_desc">Tril wanneer een timer afgaat</string>
<string name="weekly_productivity_analysis">Wekelijkse productiviteitsanalyse</string>
<string name="appearance">Uiterlijk</string>
<string name="durations">Duur</string>
<string name="sound">Geluid</string>
</resources>

View File

@@ -75,4 +75,6 @@
<string name="choose_language">選擇語言</string>
<string name="rate_on_google_play">在 Google Play 上評分</string>
<string name="selected">已選擇</string>
<string name="help_with_translation">協助翻譯</string>
<string name="timer_settings_reset_info">重置計時器以變更設定</string>
</resources>

View File

@@ -92,4 +92,6 @@
<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>
<string name="timer_settings_reset_info">Reset the timer to change settings</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))
}
}
}

View File

@@ -0,0 +1,10 @@
New features:
- Timer durations now refresh automatically when changed in Settings
- As a consequence of the above feature and to improve stability, it is no longer possible to change the timer durations while the timer is running
Fixes:
- Significantly improved timer screen performance (by ~90%)
- Progress circle in timer screen is now much smoother
- Notification is now visible on lockscreen by default
- Updated translations

View File

@@ -0,0 +1,12 @@
<i> Tomat</i> adalah timer Pomodoro minimalis untuk Android berdasarkan Material 3 Expressive.
Tomat sepenuhnya gratis dan bersumber terbuka selamanya. Anda dapat menemukan kode sumber, melaporkan bug, atau menyarankan fitur di https://github.com/nsh07/Tomato
<b>Fitur: </b>
- UI minimalis sederhana berdasarka Material 3 Expressive terbaru
- Statistik terperinci dari waktu kerja/belajar yang mudah dipahami
- Statistik untuk harian terlihat sekilas
- Statistik untuk minggu lalu dan bulan lalu ditampilkan dalam grafik yang bersih dan mudah dibaca
- Statistik tambahan untuk minggu lalu dan bulan yang menampilkan hari apa yang paling produktif
- Parameter timer yang dapat disesuaikan
- Dukungan untuk Live Updates Android 16

View File

@@ -0,0 +1 @@
Timer Podomoro minimalis

View File

@@ -0,0 +1,12 @@
<i>Tomato</i> is een minimalistische Pomodoro timer voor Android gebaseerd op Material 3 Expressive.
Tomato is volledig gratis and open-source, voor altijd. Je kan de broncode vinden of fouten rapporteren op https://github.com/nsh07/Tomato
<b>Mogelijkheden van deze app:</b>
- Simpele, minimalistische UI gebaseerd op de laatste Material 3 expressive richtlijnen
- Gedetailleerde statistieken van werk/studietijd in een gemakkelijk begrijpbare manier weergegeven
- Statistieken van de huidige dag in één oogopslag zichtbaar
- Statistieken van de voorbije week en maand weergegeven in een makkelijk leesbare grafiek
- Extra statistieken van de voorbije week en maand die tonen op welk moment van de dag je het meest productief bent
- Aanpasbare timer parameters
- Ondersteuning voor Android 16 Live-updates

View File

@@ -0,0 +1 @@
Minimalistische Pomodoro timer

View File

@@ -12,9 +12,9 @@ ksp = "2.3.1"
lifecycleRuntimeKtx = "2.9.4"
materialKolor = "4.0.3"
navigation3 = "1.0.0-rc01"
revenuecat = "9.12.1"
revenuecat = "9.13.0"
room = "2.8.3"
vico = "2.2.1"
vico = "2.3.1"
[libraries]
androidx-activity-compose = { group = "androidx.activity", name = "activity-compose", version.ref = "activityCompose" }