From 5d0e648c0774cd77d54f4b5d59707bd82775f8f6 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 4 Nov 2025 19:56:40 +0530 Subject: [PATCH 01/19] chore(deps): update dependencies --- app/build.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7fe184d..6f39b02 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -61,6 +61,9 @@ android { getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" ) } + debug { + applicationIdSuffix = ".debug" + } } flavorDimensions += "version" From 41e8618d1450f273aad363df68ac0fb3dd160aed Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 4 Nov 2025 20:01:12 +0530 Subject: [PATCH 02/19] fix(notification): make notification visible on lockscreen --- app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index bf445c8..3df1ce1 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -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 by lazy { From ee5bf647c6ddccffafe9f1ba1cbbab0a3cf7968f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 5 Nov 2025 08:51:58 +0530 Subject: [PATCH 03/19] fix(timer): significantly reduce recomposition count (by ~90%) This improves the performance and perceived smoothness of the timer screen by a lot --- app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt | 5 +---- .../pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt | 9 +++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 16a539b..e581922 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -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 diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 2c4e493..64fce02 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -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.map +import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import org.nsh07.pomodoro.TomatoApplication @@ -55,6 +58,12 @@ class TimerViewModel( val timerState: StateFlow = _timerState.asStateFlow() val time: StateFlow = _time.asStateFlow() + + val progress = _time.map { + val uiState = timerState.value + (uiState.totalTime.toFloat() - it) / uiState.totalTime + }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f) + private var cycles = 0 private var startTime = 0L From d441289e72831cdd5c4c814edc29a7442ef61113 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 5 Nov 2025 12:52:18 +0530 Subject: [PATCH 04/19] fix(timer): combine flows to fix a bug with the progress indicator --- .../pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 64fce02..9052bcb 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -33,7 +33,7 @@ import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch @@ -59,9 +59,8 @@ class TimerViewModel( val time: StateFlow = _time.asStateFlow() - val progress = _time.map { - val uiState = timerState.value - (uiState.totalTime.toFloat() - it) / uiState.totalTime + val progress = _time.combine(_timerState) { remainingTime, uiState -> + (uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f) private var cycles = 0 From 42582545bfd88d990b6099f4ec0d98af316a53ee Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Fri, 7 Nov 2025 17:50:07 +0530 Subject: [PATCH 05/19] refactor(settings): use MVI intents instead of lambdas #117 --- .../ui/settingsScreen/SettingsScreen.kt | 42 +++---------------- .../settingsScreen/screens/AlarmSettings.kt | 17 ++++---- .../screens/AppearanceSettings.kt | 16 +++---- .../settingsScreen/screens/TimerSettings.kt | 20 ++++----- .../viewModel/SettingsAction.kt | 32 ++++++++++++++ .../viewModel/SettingsViewModel.kt | 29 +++++++++---- 6 files changed, 83 insertions(+), 73 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index c8ae71f..0f48913 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -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 @@ -78,6 +74,7 @@ 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.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) { @@ -134,20 +129,7 @@ fun SettingsScreenRoot( 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 ) @@ -168,14 +150,7 @@ private fun SettingsScreen( 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 ) { @@ -316,9 +291,7 @@ private fun SettingsScreen( alarmEnabled = alarmEnabled, vibrateEnabled = vibrateEnabled, alarmSound = alarmSound, - onAlarmEnabledChange = onAlarmEnabledChange, - onVibrateEnabledChange = onVibrateEnabledChange, - onAlarmSoundChanged = onAlarmSoundChanged, + onAction = onAction, onBack = backStack::removeLastOrNull ) } @@ -326,9 +299,7 @@ private fun SettingsScreen( AppearanceSettings( preferencesState = preferencesState, isPlus = isPlus, - onBlackThemeChange = onBlackThemeChange, - onThemeChange = onThemeChange, - onColorSchemeChange = onColorSchemeChange, + onAction = onAction, setShowPaywall = setShowPaywall, onBack = backStack::removeLastOrNull ) @@ -342,8 +313,7 @@ private fun SettingsScreen( shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, sessionsSliderState = sessionsSliderState, - onAodEnabledChange = onAodEnabledChange, - onDndEnabledChange = onDndEnabledChange, + onAction = onAction, setShowPaywall = setShowPaywall, onBack = backStack::removeLastOrNull, ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index bc46e07..b8ebdb4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -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 @@ -65,6 +66,7 @@ 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.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors @@ -80,9 +82,7 @@ fun AlarmSettings( alarmEnabled: Boolean, vibrateEnabled: Boolean, alarmSound: String, - onAlarmEnabledChange: (Boolean) -> Unit, - onVibrateEnabledChange: (Boolean) -> Unit, - onAlarmSoundChanged: (Uri?) -> Unit, + onAction: (SettingsAction) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier ) { @@ -112,10 +112,11 @@ fun AlarmSettings( @Suppress("DEPRECATION") result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) } - onAlarmSoundChanged(uri) + onAction(SettingsAction.SaveAlarmSound(uri)) } } + @SuppressLint("LocalContextGetResourceValueCall") val ringtonePickerIntent = remember(alarmSound) { Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) @@ -136,14 +137,14 @@ fun AlarmSettings( icon = R.drawable.alarm_on, label = R.string.sound, description = R.string.alarm_desc, - onClick = onAlarmEnabledChange + onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) } ), SettingsSwitchItem( checked = vibrateEnabled, icon = R.drawable.mobile_vibrate, label = R.string.vibrate, description = R.string.vibrate_desc, - onClick = onVibrateEnabledChange + onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) } ) ) } @@ -247,8 +248,6 @@ fun AlarmSettingsPreview() { alarmEnabled = true, vibrateEnabled = false, alarmSound = "", - onAlarmEnabledChange = {}, - onVibrateEnabledChange = {}, - onAlarmSoundChanged = {}, + onAction = {}, onBack = {}) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index d5a12ff..755e02d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -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 @@ -51,6 +50,7 @@ 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.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors @@ -64,9 +64,7 @@ import org.nsh07.pomodoro.utils.toColor fun AppearanceSettings( preferencesState: PreferencesState, isPlus: Boolean, - onBlackThemeChange: (Boolean) -> Unit, - onThemeChange: (String) -> Unit, - onColorSchemeChange: (Color) -> Unit, + onAction: (SettingsAction) -> Unit, setShowPaywall: (Boolean) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier @@ -106,7 +104,7 @@ fun AppearanceSettings( item { ThemePickerListItem( theme = preferencesState.theme, - onThemeChange = onThemeChange, + onThemeChange = { onAction(SettingsAction.SaveTheme(it)) }, items = if (isPlus) 3 else 1, index = 0 ) @@ -122,7 +120,7 @@ fun AppearanceSettings( items = 3, index = if (isPlus) 1 else 0, isPlus = isPlus, - onColorChange = onColorSchemeChange, + onColorChange = { onAction(SettingsAction.SaveColorScheme(it)) }, ) } item { @@ -131,7 +129,7 @@ fun AppearanceSettings( icon = R.drawable.contrast, label = R.string.black_theme, description = R.string.black_theme_desc, - onClick = onBlackThemeChange + onClick = { onAction(SettingsAction.SaveBlackTheme(it)) } ) ListItem( leadingContent = { @@ -180,9 +178,7 @@ fun AppearanceSettingsPreview() { AppearanceSettings( preferencesState = preferencesState, isPlus = false, - onBlackThemeChange = {}, - onThemeChange = {}, - onColorSchemeChange = {}, + onAction = {}, setShowPaywall = {}, onBack = {} ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index b0c7ea3..74504f0 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -36,6 +36,7 @@ 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 @@ -76,6 +77,7 @@ 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.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors @@ -96,11 +98,10 @@ fun TimerSettings( shortBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState, sessionsSliderState: SliderState, - onAodEnabledChange: (Boolean) -> Unit, - onDndEnabledChange: (Boolean) -> Unit, + setShowPaywall: (Boolean) -> Unit, + onAction: (SettingsAction) -> Unit, onBack: () -> Unit, - modifier: Modifier = Modifier, - setShowPaywall: (Boolean) -> Unit + modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val context = LocalContext.current @@ -110,7 +111,7 @@ fun TimerSettings( LaunchedEffect(Unit) { if (!notificationManagerService.isNotificationPolicyAccessGranted()) - onDndEnabledChange(false) + onAction(SettingsAction.SaveDndEnabled(false)) } val switchItems = listOf( @@ -128,7 +129,7 @@ fun TimerSettings( } else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) { notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) } - onDndEnabledChange(it) + onAction(SettingsAction.SaveDndEnabled(it)) } ), SettingsSwitchItem( @@ -136,7 +137,7 @@ fun TimerSettings( 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 +314,7 @@ fun TimerSettings( item { PlusDivider(setShowPaywall) } - itemsIndexed(switchItems.drop(1)) { index, item -> + items(switchItems.drop(1)) { item -> ListItem( leadingContent = { Icon( @@ -408,8 +409,7 @@ private fun TimerSettingsPreview() { shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, sessionsSliderState = sessionsSliderState, - onAodEnabledChange = {}, - onDndEnabledChange = {}, + onAction = {}, setShowPaywall = {}, onBack = {} ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt new file mode 100644 index 0000000..ae5541a --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsAction.kt @@ -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 . + */ + +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 +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 94ba2c6..07aa448 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -103,6 +103,19 @@ class SettingsViewModel( } } + 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) + } + } + private fun updateSessionLength() { viewModelScope.launch { timerRepository.sessionLength = preferenceRepository.saveIntPreference( @@ -160,35 +173,35 @@ class SettingsViewModel( longBreakFlowCollectionJob?.cancel() } - fun saveAlarmEnabled(enabled: Boolean) { + private fun saveAlarmEnabled(enabled: Boolean) { viewModelScope.launch { timerRepository.alarmEnabled = enabled preferenceRepository.saveBooleanPreference("alarm_enabled", enabled) } } - fun saveVibrateEnabled(enabled: Boolean) { + private fun saveVibrateEnabled(enabled: Boolean) { viewModelScope.launch { timerRepository.vibrateEnabled = enabled preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled) } } - fun saveDndEnabled(enabled: Boolean) { + private fun saveDndEnabled(enabled: Boolean) { viewModelScope.launch { timerRepository.dndEnabled = enabled preferenceRepository.saveBooleanPreference("dnd_enabled", enabled) } } - fun saveAlarmSound(uri: Uri?) { + private fun saveAlarmSound(uri: Uri?) { viewModelScope.launch { timerRepository.alarmSoundUri = uri preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) } } - fun saveColorScheme(colorScheme: Color) { + private fun saveColorScheme(colorScheme: Color) { viewModelScope.launch { _preferencesState.update { currentState -> currentState.copy(colorScheme = colorScheme.toString()) @@ -197,7 +210,7 @@ class SettingsViewModel( } } - fun saveTheme(theme: String) { + private fun saveTheme(theme: String) { viewModelScope.launch { _preferencesState.update { currentState -> currentState.copy(theme = theme) @@ -206,7 +219,7 @@ class SettingsViewModel( } } - fun saveBlackTheme(blackTheme: Boolean) { + private fun saveBlackTheme(blackTheme: Boolean) { viewModelScope.launch { _preferencesState.update { currentState -> currentState.copy(blackTheme = blackTheme) @@ -215,7 +228,7 @@ class SettingsViewModel( } } - fun saveAodEnabled(aodEnabled: Boolean) { + private fun saveAodEnabled(aodEnabled: Boolean) { viewModelScope.launch { _preferencesState.update { currentState -> currentState.copy(aodEnabled = aodEnabled) From f4fa16b5df3df52a65d7b1985bcf2f7898dfa3eb Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Fri, 7 Nov 2025 22:49:44 +0530 Subject: [PATCH 06/19] feat(settings): remove BMC link in Play Store version --- .../settingsScreen/components/AboutButtons.kt | 87 +++++++++++++++++++ .../ui/settingsScreen/components/AboutCard.kt | 39 +-------- app/src/main/res/drawable/weblate.xml | 26 ++++++ app/src/main/res/values/strings.xml | 1 + .../settingsScreen/components/AboutButtons.kt | 86 ++++++++++++++++++ 5 files changed, 202 insertions(+), 37 deletions(-) create mode 100644 app/src/foss/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt create mode 100644 app/src/main/res/drawable/weblate.xml create mode 100644 app/src/play/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt diff --git a/app/src/foss/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt b/app/src/foss/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt new file mode 100644 index 0000000..3ce2377 --- /dev/null +++ b/app/src/foss/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt @@ -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 . + */ + +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)) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt index 14028fe..3ce8e3a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt @@ -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) } } } \ No newline at end of file diff --git a/app/src/main/res/drawable/weblate.xml b/app/src/main/res/drawable/weblate.xml new file mode 100644 index 0000000..22126d0 --- /dev/null +++ b/app/src/main/res/drawable/weblate.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a608b21..078f5d7 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,4 +92,5 @@ Rate on Google Play BuyMeACoffee Selected + Help with translation \ No newline at end of file diff --git a/app/src/play/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt b/app/src/play/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt new file mode 100644 index 0000000..aebde4a --- /dev/null +++ b/app/src/play/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutButtons.kt @@ -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 . + */ + +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)) + } + } +} \ No newline at end of file From 831171da51902d3cd9658cf9ee59d4780067a8c2 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 7 Nov 2025 22:51:32 +0100 Subject: [PATCH 07/19] Translated using Weblate (Dutch) Currently translated at 84.2% (64 of 76 strings) Translated using Weblate (Dutch) Currently translated at 100.0% (18 of 18 strings) Translated using Weblate (Italian) Currently translated at 100.0% (76 of 76 strings) Co-authored-by: Extner32 Co-authored-by: Hosted Weblate Co-authored-by: Leonardo Taiti Translate-URL: https://hosted.weblate.org/projects/tomato/store-descriptions/nl/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/it/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/nl/ Translation: Tomato/Store descriptions Translation: Tomato/Strings --- app/src/main/res/values-it/strings.xml | 8 +++ app/src/main/res/values-nl/strings.xml | 64 ++++++++++++++++++- .../android/nl-NL/full_description.txt | 12 ++++ .../android/nl-NL/short_description.txt | 1 + 4 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/nl-NL/full_description.txt create mode 100644 fastlane/metadata/android/nl-NL/short_description.txt diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 092043f..3ceaf35 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -65,4 +65,12 @@ Adatta i colori del tema dal tuo sfondo Lingua Scegli la lingua + Durate + Attiva la modalità non disturbare quando avvii un timer di concentrazione. + Tomato FOSS + Valuta su Google Play + Selezionato + Colore dinamico + 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. + Schermo sempre attivo diff --git a/app/src/main/res/values-nl/strings.xml b/app/src/main/res/values-nl/strings.xml index 55344e5..3b5f32d 100644 --- a/app/src/main/res/values-nl/strings.xml +++ b/app/src/main/res/values-nl/strings.xml @@ -1,3 +1,65 @@ - \ No newline at end of file + Alarm + Laat alarm afgaan wanneer de timer afloopt + Alarm geluid + Always On Display + Tik ergens terwijl u de timer bekijkt om over te schakelen naar de AOD-modus + Donker thema + Gebruik een puur-zwart donker thema + Pauze + Kies een kleurthema + Kies een thema + Kleur + Kleurthema + Voltooid + Donker + Dynamisch + Verlaat + Focus + focus per dag (gem.) + Voorbije maand + Voorbije week + Voorbije jaar + Licht + Lange pauze + %1$s min resterend + Maandelijkse productiviteitsanalyse + Meer + Meer info + OK + Pauzeer + Gepauzeerd + Start + 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. + Productiviteitsanalyse + Focustijden op verschillende tijdstippen van de dag + Herstart + Sessielengte + Focusintervallen in één sessie: %1$d + Instellingen + Korte pauze + Skip + Skip naar de volgende + Start + Start de volgende + Statistieken + Stop + Stop alarm + De huidige sessie is voltooid. Tik ergens om het alarm te stoppen. + Alarm stoppen? + Systeem + Thema + Timer + Timervoortgang + %1$d van %2$d + Vandaag + Volgende + Volgende: %1$s (%2$s) + Trilling + Tril wanneer een timer afgaat + Wekelijkse productiviteitsanalyse + Uiterlijk + Duur + Geluid + diff --git a/fastlane/metadata/android/nl-NL/full_description.txt b/fastlane/metadata/android/nl-NL/full_description.txt new file mode 100644 index 0000000..3123fd0 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/full_description.txt @@ -0,0 +1,12 @@ +Tomato 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 + +Mogelijkheden van deze app: +- 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 diff --git a/fastlane/metadata/android/nl-NL/short_description.txt b/fastlane/metadata/android/nl-NL/short_description.txt new file mode 100644 index 0000000..b704468 --- /dev/null +++ b/fastlane/metadata/android/nl-NL/short_description.txt @@ -0,0 +1 @@ +Minimalistische Pomodoro timer From 57a9243a1921f0df54abe50505e9b22e334351fb Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 9 Nov 2025 02:47:05 +0100 Subject: [PATCH 08/19] Added translation using Weblate (Indonesian) Co-authored-by: Mikail Muzakki --- app/src/main/res/values-in/strings.xml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 app/src/main/res/values-in/strings.xml diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml new file mode 100644 index 0000000..55344e5 --- /dev/null +++ b/app/src/main/res/values-in/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 538c984d40c553d025dcba06d4461cb4f35674d3 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 09:17:55 +0530 Subject: [PATCH 09/19] refactor: rename "preferences" to "settings" to maintain consistency --- .../java/org/nsh07/pomodoro/MainActivity.kt | 10 +++---- .../ui/settingsScreen/SettingsScreen.kt | 14 ++++----- .../settingsScreen/screens/AlarmSettings.kt | 12 ++++---- .../screens/AppearanceSettings.kt | 14 ++++----- .../viewModel/PreferencesState.kt | 19 ------------ .../settingsScreen/viewModel/SettingsState.kt | 29 +++++++++++++++++++ .../viewModel/SettingsViewModel.kt | 16 +++++----- 7 files changed, 62 insertions(+), 52 deletions(-) delete mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 1917159..a36bb6d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -50,15 +50,15 @@ 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() @@ -77,7 +77,7 @@ class MainActivity : ComponentActivity() { TomatoTheme( darkTheme = darkTheme, seedColor = seed, - blackTheme = preferencesState.blackTheme + blackTheme = settingsState.blackTheme ) { val colorScheme = colorScheme LaunchedEffect(colorScheme) { @@ -86,7 +86,7 @@ class MainActivity : ComponentActivity() { AppScreen( isPlus = isPlus, - isAODEnabled = preferencesState.aodEnabled, + isAODEnabled = settingsState.aodEnabled, setTimerFrequency = { appContainer.appTimerRepository.timerFrequency = it } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 0f48913..2827eab 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -73,8 +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 @@ -106,7 +106,7 @@ fun SettingsScreenRoot( 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( @@ -119,7 +119,7 @@ fun SettingsScreenRoot( SettingsScreen( isPlus = isPlus, - preferencesState = preferencesState, + settingsState = settingsState, backStack = backStack, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, @@ -140,7 +140,7 @@ fun SettingsScreenRoot( @Composable private fun SettingsScreen( isPlus: Boolean, - preferencesState: PreferencesState, + settingsState: SettingsState, backStack: SnapshotStateList, focusTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState, @@ -287,7 +287,7 @@ private fun SettingsScreen( entry { AlarmSettings( - preferencesState = preferencesState, + settingsState = settingsState, alarmEnabled = alarmEnabled, vibrateEnabled = vibrateEnabled, alarmSound = alarmSound, @@ -297,7 +297,7 @@ private fun SettingsScreen( } entry { AppearanceSettings( - preferencesState = preferencesState, + settingsState = settingsState, isPlus = isPlus, onAction = onAction, setShowPaywall = setShowPaywall, @@ -307,7 +307,7 @@ private fun SettingsScreen( entry { TimerSettings( isPlus = isPlus, - aodEnabled = preferencesState.aodEnabled, + aodEnabled = settingsState.aodEnabled, dndEnabled = dndEnabled, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index b8ebdb4..186fba8 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -65,8 +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 @@ -78,7 +78,7 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun AlarmSettings( - preferencesState: PreferencesState, + settingsState: SettingsState, alarmEnabled: Boolean, vibrateEnabled: Boolean, alarmSound: String, @@ -126,8 +126,8 @@ fun AlarmSettings( } val switchItems = remember( - preferencesState.blackTheme, - preferencesState.aodEnabled, + settingsState.blackTheme, + settingsState.aodEnabled, alarmEnabled, vibrateEnabled ) { @@ -242,9 +242,9 @@ fun AlarmSettings( @Preview @Composable fun AlarmSettingsPreview() { - val preferencesState = PreferencesState() + val settingsState = SettingsState() AlarmSettings( - preferencesState = preferencesState, + settingsState = settingsState, alarmEnabled = true, vibrateEnabled = false, alarmSound = "", diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index 755e02d..9b838bf 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -49,8 +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,7 +62,7 @@ import org.nsh07.pomodoro.utils.toColor @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun AppearanceSettings( - preferencesState: PreferencesState, + settingsState: SettingsState, isPlus: Boolean, onAction: (SettingsAction) -> Unit, setShowPaywall: (Boolean) -> Unit, @@ -103,7 +103,7 @@ fun AppearanceSettings( } item { ThemePickerListItem( - theme = preferencesState.theme, + theme = settingsState.theme, onThemeChange = { onAction(SettingsAction.SaveTheme(it)) }, items = if (isPlus) 3 else 1, index = 0 @@ -116,7 +116,7 @@ fun AppearanceSettings( item { ColorSchemePickerListItem( - color = preferencesState.colorScheme.toColor(), + color = settingsState.colorScheme.toColor(), items = 3, index = if (isPlus) 1 else 0, isPlus = isPlus, @@ -125,7 +125,7 @@ fun AppearanceSettings( } 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, @@ -173,10 +173,10 @@ fun AppearanceSettings( @Preview @Composable fun AppearanceSettingsPreview() { - val preferencesState = PreferencesState() + val settingsState = SettingsState() TomatoTheme(dynamicColor = false) { AppearanceSettings( - preferencesState = preferencesState, + settingsState = settingsState, isPlus = false, onAction = {}, setShowPaywall = {}, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt deleted file mode 100644 index 7231852..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt +++ /dev/null @@ -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 . - */ - -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 -) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt new file mode 100644 index 0000000..bb43365 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.viewModel + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color + +@Immutable +data class SettingsState( + val theme: String = "auto", + val colorScheme: String = Color.White.toString(), + val blackTheme: Boolean = false, + val aodEnabled: Boolean = false +) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 07aa448..c86bf3f 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -59,8 +59,8 @@ class SettingsViewModel( 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()) @@ -203,7 +203,7 @@ class SettingsViewModel( private fun saveColorScheme(colorScheme: Color) { viewModelScope.launch { - _preferencesState.update { currentState -> + _settingsState.update { currentState -> currentState.copy(colorScheme = colorScheme.toString()) } preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString()) @@ -212,7 +212,7 @@ class SettingsViewModel( private fun saveTheme(theme: String) { viewModelScope.launch { - _preferencesState.update { currentState -> + _settingsState.update { currentState -> currentState.copy(theme = theme) } preferenceRepository.saveStringPreference("theme", theme) @@ -221,7 +221,7 @@ class SettingsViewModel( private fun saveBlackTheme(blackTheme: Boolean) { viewModelScope.launch { - _preferencesState.update { currentState -> + _settingsState.update { currentState -> currentState.copy(blackTheme = blackTheme) } preferenceRepository.saveBooleanPreference("black_theme", blackTheme) @@ -230,7 +230,7 @@ class SettingsViewModel( private fun saveAodEnabled(aodEnabled: Boolean) { viewModelScope.launch { - _preferencesState.update { currentState -> + _settingsState.update { currentState -> currentState.copy(aodEnabled = aodEnabled) } preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled) @@ -238,7 +238,7 @@ class SettingsViewModel( } fun resetPaywalledSettings() { - _preferencesState.update { currentState -> + _settingsState.update { currentState -> currentState.copy( aodEnabled = false, blackTheme = false, @@ -257,7 +257,7 @@ class SettingsViewModel( val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") ?: preferenceRepository.saveBooleanPreference("aod_enabled", false) - _preferencesState.update { currentState -> + _settingsState.update { currentState -> currentState.copy( theme = theme, colorScheme = colorScheme, From e1fa6c28b9cd76b6e294b0ef3e97e318e208363f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 11:47:18 +0530 Subject: [PATCH 10/19] refactor(settings): move state variables to single state class #117 --- .../pomodoro/billing/FossBillingManager.kt | 1 - .../java/org/nsh07/pomodoro/MainActivity.kt | 12 ---- .../nsh07/pomodoro/billing/BillingManager.kt | 1 - .../ui/settingsScreen/SettingsScreen.kt | 18 +----- .../settingsScreen/screens/AlarmSettings.kt | 26 ++++----- .../settingsScreen/screens/TimerSettings.kt | 23 ++++---- .../settingsScreen/viewModel/SettingsState.kt | 6 +- .../viewModel/SettingsViewModel.kt | 58 ++++++++++--------- .../pomodoro/billing/PlayBillingManager.kt | 5 -- 9 files changed, 59 insertions(+), 91 deletions(-) diff --git a/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt index 529b58c..4a155a3 100644 --- a/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt +++ b/app/src/foss/java/org/nsh07/pomodoro/billing/FossBillingManager.kt @@ -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 { diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index a36bb6d..95e9502 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -61,18 +61,6 @@ class MainActivity : ComponentActivity() { 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, diff --git a/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt index 4cdc106..66b1659 100644 --- a/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt +++ b/app/src/main/java/org/nsh07/pomodoro/billing/BillingManager.kt @@ -21,5 +21,4 @@ import kotlinx.coroutines.flow.StateFlow interface BillingManager { val isPlus: StateFlow - val isLoaded: StateFlow } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 2827eab..f7c2c44 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -101,10 +101,6 @@ 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 settingsState by viewModel.settingsState.collectAsStateWithLifecycle() @@ -125,10 +121,6 @@ fun SettingsScreenRoot( shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, sessionsSliderState = sessionsSliderState, - alarmEnabled = alarmEnabled, - vibrateEnabled = vibrateEnabled, - dndEnabled = dndEnabled, - alarmSound = alarmSound, onAction = viewModel::onAction, setShowPaywall = setShowPaywall, modifier = modifier @@ -146,10 +138,6 @@ private fun SettingsScreen( shortBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState, sessionsSliderState: SliderState, - alarmEnabled: Boolean, - vibrateEnabled: Boolean, - dndEnabled: Boolean, - alarmSound: String, onAction: (SettingsAction) -> Unit, setShowPaywall: (Boolean) -> Unit, modifier: Modifier = Modifier @@ -288,9 +276,6 @@ private fun SettingsScreen( entry { AlarmSettings( settingsState = settingsState, - alarmEnabled = alarmEnabled, - vibrateEnabled = vibrateEnabled, - alarmSound = alarmSound, onAction = onAction, onBack = backStack::removeLastOrNull ) @@ -307,8 +292,7 @@ private fun SettingsScreen( entry { TimerSettings( isPlus = isPlus, - aodEnabled = settingsState.aodEnabled, - dndEnabled = dndEnabled, + settingsState = settingsState, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index 186fba8..7b3fcdd 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -79,9 +79,6 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape @Composable fun AlarmSettings( settingsState: SettingsState, - alarmEnabled: Boolean, - vibrateEnabled: Boolean, - alarmSound: String, 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) ?: "" } } @@ -117,30 +115,30 @@ fun AlarmSettings( } @SuppressLint("LocalContextGetResourceValueCall") - val ringtonePickerIntent = remember(alarmSound) { + 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( settingsState.blackTheme, settingsState.aodEnabled, - alarmEnabled, - vibrateEnabled + 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 = { onAction(SettingsAction.SaveAlarmEnabled(it)) } ), SettingsSwitchItem( - checked = vibrateEnabled, + checked = settingsState.vibrateEnabled, icon = R.drawable.mobile_vibrate, label = R.string.vibrate, description = R.string.vibrate_desc, @@ -245,9 +243,7 @@ fun AlarmSettingsPreview() { val settingsState = SettingsState() AlarmSettings( settingsState = settingsState, - alarmEnabled = true, - vibrateEnabled = false, - alarmSound = "", onAction = {}, - onBack = {}) + onBack = {} + ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index 74504f0..a7cb380 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -41,6 +41,7 @@ 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 @@ -57,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 @@ -78,6 +80,7 @@ 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 @@ -92,14 +95,13 @@ 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, - setShowPaywall: (Boolean) -> Unit, onAction: (SettingsAction) -> Unit, + setShowPaywall: (Boolean) -> Unit, onBack: () -> Unit, modifier: Modifier = Modifier ) { @@ -116,7 +118,7 @@ fun TimerSettings( val switchItems = listOf( SettingsSwitchItem( - checked = dndEnabled, + checked = settingsState.dndEnabled, icon = R.drawable.dnd, label = R.string.dnd, description = R.string.dnd_desc, @@ -133,7 +135,7 @@ fun TimerSettings( } ), SettingsSwitchItem( - checked = aodEnabled, + checked = settingsState.aodEnabled, icon = R.drawable.aod, label = R.string.always_on_display, description = R.string.always_on_display_desc, @@ -393,18 +395,17 @@ 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, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt index bb43365..b9d42b4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt @@ -23,7 +23,11 @@ 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 aodEnabled: Boolean = false, + val alarmEnabled: Boolean = true, + val vibrateEnabled: Boolean = true, + val dndEnabled: Boolean = false ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index c86bf3f..d412bdd 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -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,10 +54,6 @@ class SettingsViewModel( val backStack = mutableStateListOf(Screen.Settings.Main) val isPlus = billingManager.isPlus - val isPurchaseStateLoaded = billingManager.isLoaded - - private val _isSettingsLoaded = MutableStateFlow(false) - val isSettingsLoaded = _isSettingsLoaded.asStateFlow() private val _settingsState = MutableStateFlow(SettingsState()) val settingsState = _settingsState.asStateFlow() @@ -81,25 +77,13 @@ 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 } } @@ -176,6 +160,9 @@ class SettingsViewModel( private fun saveAlarmEnabled(enabled: Boolean) { viewModelScope.launch { timerRepository.alarmEnabled = enabled + _settingsState.update { currentState -> + currentState.copy(alarmEnabled = enabled) + } preferenceRepository.saveBooleanPreference("alarm_enabled", enabled) } } @@ -183,6 +170,9 @@ class SettingsViewModel( private fun saveVibrateEnabled(enabled: Boolean) { viewModelScope.launch { timerRepository.vibrateEnabled = enabled + _settingsState.update { currentState -> + currentState.copy(vibrateEnabled = enabled) + } preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled) } } @@ -190,6 +180,9 @@ class SettingsViewModel( private fun saveDndEnabled(enabled: Boolean) { viewModelScope.launch { timerRepository.dndEnabled = enabled + _settingsState.update { currentState -> + currentState.copy(dndEnabled = enabled) + } preferenceRepository.saveBooleanPreference("dnd_enabled", enabled) } } @@ -197,6 +190,9 @@ class SettingsViewModel( private fun saveAlarmSound(uri: Uri?) { viewModelScope.launch { timerRepository.alarmSoundUri = uri + _settingsState.update { currentState -> + currentState.copy(alarmSound = uri.toString()) + } preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) } } @@ -237,16 +233,6 @@ class SettingsViewModel( } } - fun resetPaywalledSettings() { - _settingsState.update { currentState -> - currentState.copy( - aodEnabled = false, - blackTheme = false, - colorScheme = Color.White.toString() - ) - } - } - suspend fun reloadSettings() { val theme = preferenceRepository.getStringPreference("theme") ?: preferenceRepository.saveStringPreference("theme", "auto") @@ -256,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) _settingsState.update { currentState -> currentState.copy( theme = theme, colorScheme = colorScheme, + alarmSound = alarmSound, blackTheme = blackTheme, - aodEnabled = aodEnabled + aodEnabled = aodEnabled, + alarmEnabled = alarmEnabled, + vibrateEnabled = vibrateEnabled, + dndEnabled = dndEnabled ) } } diff --git a/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt index a658884..94bfcb2 100644 --- a/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt +++ b/app/src/play/java/org/nsh07/pomodoro/billing/PlayBillingManager.kt @@ -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 } ) } From 8f2245269135bd22d8db6a30ae54eaf4ebe68931 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 19:44:47 +0530 Subject: [PATCH 11/19] feat(ui): smoothen timer progress by increasing update rate --- app/src/main/java/org/nsh07/pomodoro/MainActivity.kt | 2 +- app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt | 2 +- app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 2 +- .../nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt | 3 --- 4 files changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 95e9502..1b28db4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -93,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 } } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt index 788d1af..d7aea1a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt @@ -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 diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index 8ed8dcb..ed82236 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -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 diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 9052bcb..84c6ed4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -116,9 +116,6 @@ class TimerViewModel( ) ).toUri() - preferenceRepository.getBooleanPreference("aod_enabled") - ?: preferenceRepository.saveBooleanPreference("aod_enabled", false) - _time.update { timerRepository.focusTime } cycles = 0 startTime = 0L From 5ad7f5396e4c45441c9e2f9e930411b26274383f Mon Sep 17 00:00:00 2001 From: Himanshu-Kandwal Date: Sun, 9 Nov 2025 10:12:07 +0530 Subject: [PATCH 12/19] fix: ensuring room DB instance is created only once by double-checked locking to AppDatabase singleton initialization --- app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt index 83fa1ab..0694d6f 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt @@ -17,9 +17,7 @@ import androidx.room.TypeConverters @Database( entities = [IntPreference::class, BooleanPreference::class, StringPreference::class, Stat::class], version = 2, - autoMigrations = [ - AutoMigration(from = 1, to = 2) - ] + autoMigrations = [AutoMigration(from = 1, to = 2)] ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -34,9 +32,8 @@ abstract class AppDatabase : RoomDatabase() { fun getDatabase(context: Context): AppDatabase { return Instance ?: synchronized(this) { - Room.databaseBuilder(context, AppDatabase::class.java, "app_database") - .build() - .also { Instance = it } + Instance ?: Room.databaseBuilder(context, AppDatabase::class.java, "app_database") + .build().also { Instance = it } } } } From af899c06b4647d78dd6bf6b66452e8a69b050af0 Mon Sep 17 00:00:00 2001 From: Himanshu-Kandwal Date: Sun, 9 Nov 2025 17:14:35 +0530 Subject: [PATCH 13/19] refactor: align AppDatabase code style with project conventions --- app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt index 0694d6f..ef462a6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt @@ -17,7 +17,9 @@ import androidx.room.TypeConverters @Database( entities = [IntPreference::class, BooleanPreference::class, StringPreference::class, Stat::class], version = 2, - autoMigrations = [AutoMigration(from = 1, to = 2)] + autoMigrations = [ + AutoMigration(from = 1, to = 2) + ] ) @TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { @@ -33,7 +35,8 @@ abstract class AppDatabase : RoomDatabase() { fun getDatabase(context: Context): AppDatabase { return Instance ?: synchronized(this) { Instance ?: Room.databaseBuilder(context, AppDatabase::class.java, "app_database") - .build().also { Instance = it } + .build() + .also { Instance = it } } } } From 9ec3e6851f246760ef62e22a907886aec2d41e2f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 22:08:47 +0530 Subject: [PATCH 14/19] feat(settings): disable editing time when service is running, auto reload when navigating to timer screen --- .../nsh07/pomodoro/data/TimerRepository.kt | 5 ++- .../nsh07/pomodoro/service/TimerService.kt | 4 +- .../ui/settingsScreen/SettingsScreen.kt | 4 ++ .../ui/settingsScreen/SettingsSwitchItem.kt | 1 + .../components/MinuteInputField.kt | 4 +- .../settingsScreen/screens/TimerSettings.kt | 39 +++++++++++++++++-- .../viewModel/SettingsViewModel.kt | 5 ++- .../timerScreen/viewModel/TimerViewModel.kt | 2 +- 8 files changed, 52 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt index d7aea1a..89f27af 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/TimerRepository.kt @@ -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 } /** @@ -61,5 +62,5 @@ class AppTimerRepository : TimerRepository { 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) } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 01552a2..e87e2a7 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -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() diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index f7c2c44..559142e 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -101,6 +101,7 @@ fun SettingsScreenRoot( val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState val isPlus by viewModel.isPlus.collectAsStateWithLifecycle() + val serviceRunning by viewModel.serviceRunning.collectAsStateWithLifecycle() val settingsState by viewModel.settingsState.collectAsStateWithLifecycle() @@ -115,6 +116,7 @@ fun SettingsScreenRoot( SettingsScreen( isPlus = isPlus, + serviceRunning = serviceRunning, settingsState = settingsState, backStack = backStack, focusTimeInputFieldState = focusTimeInputFieldState, @@ -132,6 +134,7 @@ fun SettingsScreenRoot( @Composable private fun SettingsScreen( isPlus: Boolean, + serviceRunning: Boolean, settingsState: SettingsState, backStack: SnapshotStateList, focusTimeInputFieldState: TextFieldState, @@ -292,6 +295,7 @@ private fun SettingsScreen( entry { TimerSettings( isPlus = isPlus, + serviceRunning = serviceRunning, settingsState = settingsState, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt index 75638e3..f16c2f4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt @@ -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, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt index f5f0c23..f530eea 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt @@ -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), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index a7cb380..0d28931 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -50,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 @@ -60,7 +61,8 @@ 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.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -76,6 +78,7 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider @@ -95,6 +98,7 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape @Composable fun TimerSettings( isPlus: Boolean, + serviceRunning: Boolean, settingsState: SettingsState, focusTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState, @@ -111,14 +115,21 @@ fun TimerSettings( val notificationManagerService = remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - LaunchedEffect(Unit) { - if (!notificationManagerService.isNotificationPolicyAccessGranted()) - onAction(SettingsAction.SaveDndEnabled(false)) + DisposableEffect(Unit) { + onDispose { + if (!serviceRunning) { + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.RESET.toString() + context.startService(it) + } + } + } } val switchItems = listOf( SettingsSwitchItem( checked = settingsState.dndEnabled, + enabled = !serviceRunning, icon = R.drawable.dnd, label = R.string.dnd, description = R.string.dnd_desc, @@ -171,6 +182,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("Reset the timer to change settings") + } + } + } + } Spacer(Modifier.height(14.dp)) } item { @@ -190,6 +215,7 @@ fun TimerSettings( ) MinuteInputField( state = focusTimeInputFieldState, + enabled = !serviceRunning, shape = RoundedCornerShape( topStart = topListItemShape.topStart, bottomStart = topListItemShape.topStart, @@ -210,6 +236,7 @@ fun TimerSettings( ) MinuteInputField( state = shortBreakTimeInputFieldState, + enabled = !serviceRunning, shape = RoundedCornerShape(middleListItemShape.topStart), imeAction = ImeAction.Next ) @@ -225,6 +252,7 @@ fun TimerSettings( ) MinuteInputField( state = longBreakTimeInputFieldState, + enabled = !serviceRunning, shape = RoundedCornerShape( topStart = bottomListItemShape.topStart, bottomStart = bottomListItemShape.topStart, @@ -257,6 +285,7 @@ fun TimerSettings( ) Slider( state = sessionsSliderState, + enabled = !serviceRunning, modifier = Modifier.padding(vertical = 4.dp) ) } @@ -281,6 +310,7 @@ fun TimerSettings( trailingContent = { Switch( checked = item.checked, + enabled = item.enabled, onCheckedChange = { item.onClick(it) }, thumbContent = { if (item.checked) { @@ -405,6 +435,7 @@ private fun TimerSettingsPreview() { ) TimerSettings( isPlus = false, + serviceRunning = true, settingsState = remember { SettingsState() }, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index d412bdd..a61146f 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -54,6 +54,7 @@ class SettingsViewModel( val backStack = mutableStateListOf(Screen.Settings.Main) val isPlus = billingManager.isPlus + val serviceRunning = timerRepository.serviceRunning.asStateFlow() private val _settingsState = MutableStateFlow(SettingsState()) val settingsState = _settingsState.asStateFlow() @@ -273,14 +274,14 @@ class SettingsViewModel( 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 SettingsViewModel( billingManager = appBillingManager, preferenceRepository = appPreferenceRepository, - timerRepository = appTimerRepository, + timerRepository = appTimerRepository ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 84c6ed4..6fefa40 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -70,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() From 5cb864f084d9c1f3dce4e940eab1298325e2d527 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 22:44:45 +0530 Subject: [PATCH 15/19] feat(settings): improve auto-reload behaviour --- .../org/nsh07/pomodoro/data/AppContainer.kt | 6 ++ .../nsh07/pomodoro/service/ServiceHelper.kt | 58 +++++++++++++++++++ .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 30 +--------- .../settingsScreen/screens/TimerSettings.kt | 15 +---- .../viewModel/SettingsViewModel.kt | 39 ++++++++++++- .../timerScreen/viewModel/TimerViewModel.kt | 15 +++-- app/src/main/res/values/strings.xml | 1 + 7 files changed, 115 insertions(+), 49 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index 3df1ce1..12186a6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt @@ -29,6 +29,7 @@ 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 @@ -41,6 +42,7 @@ interface AppContainer { val notificationManager: NotificationManagerCompat val notificationManagerService: NotificationManager val notificationBuilder: NotificationCompat.Builder + val serviceHelper: ServiceHelper val timerState: MutableStateFlow val time: MutableStateFlow var activityTurnScreenOn: (Boolean) -> Unit @@ -87,6 +89,10 @@ class DefaultAppContainer(context: Context) : AppContainer { .setVisibility(VISIBILITY_PUBLIC) } + override val serviceHelper: ServiceHelper by lazy { + ServiceHelper(context) + } + override val timerState: MutableStateFlow by lazy { MutableStateFlow( TimerState( diff --git a/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt b/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt new file mode 100644 index 0000000..bb7bc89 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/service/ServiceHelper.kt @@ -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 . + */ + +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) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index e581922..a998da7 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -63,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) @@ -163,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), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index 0d28931..da31588 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -62,7 +62,6 @@ import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberSliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -78,7 +77,6 @@ import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider @@ -115,17 +113,6 @@ fun TimerSettings( val notificationManagerService = remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } - DisposableEffect(Unit) { - onDispose { - if (!serviceRunning) { - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.RESET.toString() - context.startService(it) - } - } - } - } - val switchItems = listOf( SettingsSwitchItem( checked = settingsState.dndEnabled, @@ -191,7 +178,7 @@ fun TimerSettings( horizontalArrangement = Arrangement.spacedBy(8.dp) ) { Icon(painterResource(R.drawable.info), null) - Text("Reset the timer to change settings") + Text(stringResource(R.string.timer_settings_reset_info)) } } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index a61146f..896eb28 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -43,13 +43,21 @@ 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, private val timerRepository: TimerRepository, + private val timerState: MutableStateFlow ) : ViewModel() { val backStack = mutableStateListOf(Screen.Settings.Main) @@ -107,6 +115,7 @@ class SettingsViewModel( "session_length", sessionsSliderState.value.toInt() ) + refreshTimer() } } @@ -117,6 +126,7 @@ class SettingsViewModel( .collect { if (it.isNotEmpty()) { timerRepository.focusTime = it.toString().toLong() * 60 * 1000 + refreshTimer() preferenceRepository.saveIntPreference( "focus_time", timerRepository.focusTime.toInt() @@ -130,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() @@ -143,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() @@ -153,6 +165,7 @@ class SettingsViewModel( } fun cancelTextFieldFlowCollection() { + if (!serviceRunning.value) serviceHelper.startService(TimerAction.ResetTimer) focusFlowCollectionJob?.cancel() shortBreakFlowCollectionJob?.cancel() longBreakFlowCollectionJob?.cancel() @@ -270,6 +283,24 @@ class SettingsViewModel( } } + 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 { @@ -277,11 +308,17 @@ class SettingsViewModel( val appBillingManager = application.container.billingManager val appPreferenceRepository = application.container.appPreferenceRepository val appTimerRepository = application.container.appTimerRepository + val serviceHelper = application.container.serviceHelper + val time = application.container.time + val timerState = application.container.timerState SettingsViewModel( billingManager = appBillingManager, preferenceRepository = appPreferenceRepository, - timerRepository = appTimerRepository + serviceHelper = serviceHelper, + time = time, + timerRepository = appTimerRepository, + timerState = timerState ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 6fefa40..318a862 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -17,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 @@ -42,19 +41,20 @@ 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, private val _time: MutableStateFlow -) : AndroidViewModel(application) { +) : ViewModel() { val timerState: StateFlow = _timerState.asStateFlow() val time: StateFlow = _time.asStateFlow() @@ -155,6 +155,10 @@ class TimerViewModel( } } + fun onAction(action: TimerAction) { + serviceHelper.startService(action) + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { @@ -162,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, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 078f5d7..433cd72 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -93,4 +93,5 @@ BuyMeACoffee Selected Help with translation + Reset the timer to change settings \ No newline at end of file From 395969f73dfac41db1441e0cded33092de08e36b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 9 Nov 2025 15:27:28 +0100 Subject: [PATCH 16/19] Translated using Weblate (Indonesian) Currently translated at 100.0% (76 of 76 strings) Translated using Weblate (Indonesian) Currently translated at 100.0% (18 of 18 strings) Co-authored-by: Hosted Weblate Co-authored-by: Mikail Muzakki Translate-URL: https://hosted.weblate.org/projects/tomato/store-descriptions/id/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/id/ Translation: Tomato/Store descriptions Translation: Tomato/Strings --- app/src/main/res/values-in/strings.xml | 75 ++++++++++++++++++- .../metadata/android/id/full_description.txt | 12 +++ .../metadata/android/id/short_description.txt | 1 + 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/id/full_description.txt create mode 100644 fastlane/metadata/android/id/short_description.txt diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 55344e5..f1629a0 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,3 +1,76 @@ - \ No newline at end of file + Dinamis + Alarm + Bunyikan alarm ketika timer selesai + Suara alarm + Tema hitam + Gunakan tema hitam + Rehat + Pilih skema warna + Pilih tema + Warna + Skema warna + Selesai + Gelap + Keluar + Fokus + fokus per hari (rerata) + Bulan lalu + Minggu lalu + Tahun lalu + Terang + Rehat panjang + %1$s menit tersisa + Analisis produktivitas bulanan + Lebih banyak + Info lebih + Oke + Jeda + Dijeda + Mulai + \"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. + Analisis produktivitas + Durasi fokus berdasarkan segmen waktu dalam sehari + Mulai ulang + Lama sesi + Interval fokus dalam satu sesi: %1$d + Pengaturan + Rehat pendek + Lewati + Lewati ke berikutnya + Mulai + Mulai selanjutnya + Statistik + Hentikan + Hentikan alarm + Sesi ini sudah selesai. Ketuk di mana saja untuk hentikan alarm. + Hentikan Alarm? + Sistem + Tema + Timer + Progres timer + %1$d dari %2$d + Hari ini + Berikutnya + Berikutnya: %1$s (%2$s) + Getar + Getar ketika timer selesai + Analisis produktivitas mingguan + Tampilan + Durasi + Suara + Jangan Ganggu + Aktifkan mode Jangan Ganggu ketika timer Fokus berjalan + Dapatkan Tomato+ + Warna dinamis + Sesuaikan warna tema dari wallpaper perangkat + Tomato FOSS + Semua fitur tidak terkunci di versi sumber terbuka ini. Jika aplikasi ini bermanfaat , mohon pertimbangkan untuk dukung saya dengan berdonasi di %1$s. + Bahasa + Pilih bahasa + Beri nilai di Google Play + Dipilih + Ketuk di mana saja saat melihat pengatur waktu untuk beralih ke mode AOD + Always On Display (AOD) + diff --git a/fastlane/metadata/android/id/full_description.txt b/fastlane/metadata/android/id/full_description.txt new file mode 100644 index 0000000..dc0cc2f --- /dev/null +++ b/fastlane/metadata/android/id/full_description.txt @@ -0,0 +1,12 @@ + Tomat 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 + +Fitur: +- 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 diff --git a/fastlane/metadata/android/id/short_description.txt b/fastlane/metadata/android/id/short_description.txt new file mode 100644 index 0000000..0d99f50 --- /dev/null +++ b/fastlane/metadata/android/id/short_description.txt @@ -0,0 +1 @@ +Timer Podomoro minimalis From 0626f0bbac5a0934dc5cbc28a2369a859bf307b3 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sun, 9 Nov 2025 23:51:14 +0530 Subject: [PATCH 17/19] build: allow obfuscation in play store release to further optimize app --- app/build.gradle.kts | 9 +++++--- ...uard-rules.pro => proguard-rules-foss.pro} | 0 app/proguard-rules-play.pro | 21 +++++++++++++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) rename app/{proguard-rules.pro => proguard-rules-foss.pro} (100%) create mode 100644 app/proguard-rules-play.pro diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6f39b02..0934fed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -57,9 +57,6 @@ android { release { isMinifyEnabled = true isShrinkResources = true - proguardFiles( - getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro" - ) } debug { applicationIdSuffix = ".debug" @@ -71,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" + ) } } diff --git a/app/proguard-rules.pro b/app/proguard-rules-foss.pro similarity index 100% rename from app/proguard-rules.pro rename to app/proguard-rules-foss.pro diff --git a/app/proguard-rules-play.pro b/app/proguard-rules-play.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules-play.pro @@ -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 From 88d6a1b3008ddd171bd335cfaa82e9687b2e580d Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 9 Nov 2025 19:41:08 +0100 Subject: [PATCH 18/19] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (78 of 78 strings) Translated using Weblate (Hindi) Currently translated at 100.0% (78 of 78 strings) Co-authored-by: Nishant Mishra <66912344+nsh07@users.noreply.github.com> Co-authored-by: ezn24 Translate-URL: https://hosted.weblate.org/projects/tomato/strings/hi/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/zh_Hant/ Translation: Tomato/Strings --- app/src/main/res/values-hi/strings.xml | 12 ++++++++++++ app/src/main/res/values-zh-rTW/strings.xml | 2 ++ 2 files changed, 14 insertions(+) diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index a9b9992..877508a 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -62,4 +62,16 @@ साप्ताहिक उत्पादकता विश्लेषण दिखावट अवधियां + परेशान न करें + फ़ोकस टाइमर चलाते समय \'परेशान न करें\' मोड चालू करें + Tomato+ प्राप्त करें + डायनामिक रंग + अपने वॉलपेपर से थीम रंग अनुकूलित करें + इस संस्करण में सभी सुविधाएँ अनलॉक हैं। अगर मेरे ऐप ने आपके जीवन में कोई बदलाव लाया है, तो कृपया %1$s पर दान करके मेरी मदद करें। + भाषा + भाषा चुनें + Google Play पर रेटिंग दें + चयनित + अनुवाद में सहायता करें + सेटिंग्स बदलने के लिए टाइमर रीसेट करें diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index 098513d..a290702 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -75,4 +75,6 @@ 選擇語言 在 Google Play 上評分 已選擇 + 協助翻譯 + 重置計時器以變更設定 From 7837fd8a9f20a99a500e9f96d75ded8d3d47bf5f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 10 Nov 2025 00:13:00 +0530 Subject: [PATCH 19/19] chore(release): bump version, update changelog --- app/build.gradle.kts | 4 ++-- fastlane/metadata/android/en-US/changelogs/20.txt | 10 ++++++++++ gradle/libs.versions.toml | 4 ++-- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/20.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0934fed..9bfce7d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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" diff --git a/fastlane/metadata/android/en-US/changelogs/20.txt b/fastlane/metadata/android/en-US/changelogs/20.txt new file mode 100644 index 0000000..2387a8d --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/20.txt @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ae2a776..725f3db 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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" }