diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 7fe184d..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"
@@ -57,9 +57,9 @@ android {
release {
isMinifyEnabled = true
isShrinkResources = true
- proguardFiles(
- getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro"
- )
+ }
+ debug {
+ applicationIdSuffix = ".debug"
}
}
@@ -68,10 +68,16 @@ android {
create("foss") {
dimension = "version"
isDefault = true
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-foss.pro"
+ )
}
create("play") {
dimension = "version"
versionNameSuffix = "-play"
+ proguardFiles(
+ getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules-play.pro"
+ )
}
}
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
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/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/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
index 1917159..1b28db4 100644
--- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt
@@ -50,34 +50,22 @@ class MainActivity : ComponentActivity() {
}
setContent {
- val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
+ val settingsState by settingsViewModel.settingsState.collectAsStateWithLifecycle()
- val darkTheme = when (preferencesState.theme) {
+ val darkTheme = when (settingsState.theme) {
"dark" -> true
"light" -> false
else -> isSystemInDarkTheme()
}
- val seed = preferencesState.colorScheme.toColor()
+ val seed = settingsState.colorScheme.toColor()
val isPlus by settingsViewModel.isPlus.collectAsStateWithLifecycle()
- val isPurchaseStateLoaded by settingsViewModel.isPurchaseStateLoaded.collectAsStateWithLifecycle()
- val isSettingsLoaded by settingsViewModel.isSettingsLoaded.collectAsStateWithLifecycle()
-
- LaunchedEffect(isPurchaseStateLoaded, isPlus, isSettingsLoaded) {
- if (isPurchaseStateLoaded && isSettingsLoaded) {
- if (!isPlus) {
- settingsViewModel.resetPaywalledSettings()
- } else {
- settingsViewModel.reloadSettings()
- }
- }
- }
TomatoTheme(
darkTheme = darkTheme,
seedColor = seed,
- blackTheme = preferencesState.blackTheme
+ blackTheme = settingsState.blackTheme
) {
val colorScheme = colorScheme
LaunchedEffect(colorScheme) {
@@ -86,7 +74,7 @@ class MainActivity : ComponentActivity() {
AppScreen(
isPlus = isPlus,
- isAODEnabled = preferencesState.aodEnabled,
+ isAODEnabled = settingsState.aodEnabled,
setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it
}
@@ -105,6 +93,6 @@ class MainActivity : ComponentActivity() {
override fun onStart() {
super.onStart()
// Increase the timer loop frequency again when visible to make the progress smoother
- appContainer.appTimerRepository.timerFrequency = 10f
+ appContainer.appTimerRepository.timerFrequency = 60f
}
}
\ No newline at end of file
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/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
index bf445c8..12186a6 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt
@@ -23,11 +23,13 @@ import android.content.Context
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.toArgb
import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationCompat.VISIBILITY_PUBLIC
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.billing.BillingManagerProvider
+import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.service.addTimerActions
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import org.nsh07.pomodoro.utils.millisecondsToStr
@@ -40,6 +42,7 @@ interface AppContainer {
val notificationManager: NotificationManagerCompat
val notificationManagerService: NotificationManager
val notificationBuilder: NotificationCompat.Builder
+ val serviceHelper: ServiceHelper
val timerState: MutableStateFlow
val time: MutableStateFlow
var activityTurnScreenOn: (Boolean) -> Unit
@@ -83,6 +86,11 @@ class DefaultAppContainer(context: Context) : AppContainer {
.setSilent(true)
.setOngoing(true)
.setRequestPromotedOngoing(true)
+ .setVisibility(VISIBILITY_PUBLIC)
+ }
+
+ override val serviceHelper: ServiceHelper by lazy {
+ ServiceHelper(context)
}
override val timerState: MutableStateFlow by lazy {
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..ef462a6 100644
--- a/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt
+++ b/app/src/main/java/org/nsh07/pomodoro/data/AppDatabase.kt
@@ -34,7 +34,7 @@ abstract class AppDatabase : RoomDatabase() {
fun getDatabase(context: Context): AppDatabase {
return Instance ?: synchronized(this) {
- Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
+ Instance ?: Room.databaseBuilder(context, AppDatabase::class.java, "app_database")
.build()
.also { Instance = it }
}
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..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
}
/**
@@ -54,12 +55,12 @@ class AppTimerRepository : TimerRepository {
override var shortBreakTime = 5 * 60 * 1000L
override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4
- override var timerFrequency: Float = 10f
+ override var timerFrequency: Float = 60f
override var alarmEnabled = true
override var vibrateEnabled = true
override var dndEnabled: Boolean = false
override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
- override var serviceRunning = false
+ override var serviceRunning = MutableStateFlow(false)
}
\ No newline at end of file
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/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/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/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt
index 16a539b..a998da7 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
@@ -64,7 +63,6 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
-import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -79,9 +77,7 @@ fun AppScreen(
val context = LocalContext.current
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
- val remainingTime by timerViewModel.time.collectAsStateWithLifecycle()
-
- val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
+ val progress by timerViewModel.progress.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
val motionScheme = motionScheme
@@ -166,34 +162,7 @@ fun AppScreen(
timerState = uiState,
isPlus = isPlus,
progress = { progress },
- onAction = { action ->
- when (action) {
- TimerAction.ResetTimer ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.RESET.toString()
- context.startService(it)
- }
-
- is TimerAction.SkipTimer ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.SKIP.toString()
- context.startService(it)
- }
-
- TimerAction.StopAlarm ->
- Intent(context, TimerService::class.java).also {
- it.action =
- TimerService.Actions.STOP_ALARM.toString()
- context.startService(it)
- }
-
- TimerAction.ToggleTimer ->
- Intent(context, TimerService::class.java).also {
- it.action = TimerService.Actions.TOGGLE.toString()
- context.startService(it)
- }
- }
- },
+ onAction = timerViewModel::onAction,
modifier = modifier
.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
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..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
@@ -19,8 +19,6 @@ package org.nsh07.pomodoro.ui.settingsScreen
import android.annotation.SuppressLint
import android.app.LocaleManager
-import android.content.Intent
-import android.net.Uri
import android.os.Build
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -55,7 +53,6 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
@@ -68,7 +65,6 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import org.nsh07.pomodoro.R
-import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
@@ -77,7 +73,8 @@ import org.nsh07.pomodoro.ui.settingsScreen.components.PlusPromo
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
-import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -92,8 +89,6 @@ fun SettingsScreenRoot(
modifier: Modifier = Modifier,
viewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
- val context = LocalContext.current
-
val backStack = viewModel.backStack
DisposableEffect(Unit) {
@@ -106,12 +101,9 @@ fun SettingsScreenRoot(
val longBreakTimeInputFieldState = viewModel.longBreakTimeTextFieldState
val isPlus by viewModel.isPlus.collectAsStateWithLifecycle()
- val alarmEnabled by viewModel.alarmEnabled.collectAsStateWithLifecycle(true)
- val vibrateEnabled by viewModel.vibrateEnabled.collectAsStateWithLifecycle(true)
- val dndEnabled by viewModel.dndEnabled.collectAsStateWithLifecycle(false)
- val alarmSound by viewModel.alarmSound.collectAsStateWithLifecycle(viewModel.currentAlarmSound)
+ val serviceRunning by viewModel.serviceRunning.collectAsStateWithLifecycle()
- val preferencesState by viewModel.preferencesState.collectAsStateWithLifecycle()
+ val settingsState by viewModel.settingsState.collectAsStateWithLifecycle()
val sessionsSliderState = rememberSaveable(
saver = SliderState.Saver(
@@ -124,30 +116,14 @@ fun SettingsScreenRoot(
SettingsScreen(
isPlus = isPlus,
- preferencesState = preferencesState,
+ serviceRunning = serviceRunning,
+ settingsState = settingsState,
backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
- alarmEnabled = alarmEnabled,
- vibrateEnabled = vibrateEnabled,
- dndEnabled = dndEnabled,
- alarmSound = alarmSound,
- onAlarmEnabledChange = viewModel::saveAlarmEnabled,
- onVibrateEnabledChange = viewModel::saveVibrateEnabled,
- onBlackThemeChange = viewModel::saveBlackTheme,
- onAodEnabledChange = viewModel::saveAodEnabled,
- onDndEnabledChange = viewModel::saveDndEnabled,
- onAlarmSoundChanged = {
- viewModel.saveAlarmSound(it)
- Intent(context, TimerService::class.java).apply {
- action = TimerService.Actions.RESET.toString()
- context.startService(this)
- }
- },
- onThemeChange = viewModel::saveTheme,
- onColorSchemeChange = viewModel::saveColorScheme,
+ onAction = viewModel::onAction,
setShowPaywall = setShowPaywall,
modifier = modifier
)
@@ -158,24 +134,14 @@ fun SettingsScreenRoot(
@Composable
private fun SettingsScreen(
isPlus: Boolean,
- preferencesState: PreferencesState,
+ serviceRunning: Boolean,
+ settingsState: SettingsState,
backStack: SnapshotStateList,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
- alarmEnabled: Boolean,
- vibrateEnabled: Boolean,
- dndEnabled: Boolean,
- alarmSound: String,
- onAlarmEnabledChange: (Boolean) -> Unit,
- onVibrateEnabledChange: (Boolean) -> Unit,
- onBlackThemeChange: (Boolean) -> Unit,
- onAodEnabledChange: (Boolean) -> Unit,
- onDndEnabledChange: (Boolean) -> Unit,
- onAlarmSoundChanged: (Uri?) -> Unit,
- onThemeChange: (String) -> Unit,
- onColorSchemeChange: (Color) -> Unit,
+ onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit,
modifier: Modifier = Modifier
) {
@@ -312,23 +278,16 @@ private fun SettingsScreen(
entry {
AlarmSettings(
- preferencesState = preferencesState,
- alarmEnabled = alarmEnabled,
- vibrateEnabled = vibrateEnabled,
- alarmSound = alarmSound,
- onAlarmEnabledChange = onAlarmEnabledChange,
- onVibrateEnabledChange = onVibrateEnabledChange,
- onAlarmSoundChanged = onAlarmSoundChanged,
+ settingsState = settingsState,
+ onAction = onAction,
onBack = backStack::removeLastOrNull
)
}
entry {
AppearanceSettings(
- preferencesState = preferencesState,
+ settingsState = settingsState,
isPlus = isPlus,
- onBlackThemeChange = onBlackThemeChange,
- onThemeChange = onThemeChange,
- onColorSchemeChange = onColorSchemeChange,
+ onAction = onAction,
setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull
)
@@ -336,14 +295,13 @@ private fun SettingsScreen(
entry {
TimerSettings(
isPlus = isPlus,
- aodEnabled = preferencesState.aodEnabled,
- dndEnabled = dndEnabled,
+ serviceRunning = serviceRunning,
+ settingsState = settingsState,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
- onAodEnabledChange = onAodEnabledChange,
- onDndEnabledChange = onDndEnabledChange,
+ onAction = onAction,
setShowPaywall = setShowPaywall,
onBack = backStack::removeLastOrNull,
)
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/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/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/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt
index bc46e07..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
@@ -17,6 +17,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.screens
+import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager
@@ -64,7 +65,8 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
-import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -76,13 +78,8 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AlarmSettings(
- preferencesState: PreferencesState,
- alarmEnabled: Boolean,
- vibrateEnabled: Boolean,
- alarmSound: String,
- onAlarmEnabledChange: (Boolean) -> Unit,
- onVibrateEnabledChange: (Boolean) -> Unit,
- onAlarmSoundChanged: (Uri?) -> Unit,
+ settingsState: SettingsState,
+ onAction: (SettingsAction) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
@@ -91,10 +88,11 @@ fun AlarmSettings(
var alarmName by remember { mutableStateOf("...") }
- LaunchedEffect(alarmSound) {
+ LaunchedEffect(settingsState.alarmSound) {
withContext(Dispatchers.IO) {
alarmName =
- RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
+ RingtoneManager.getRingtone(context, settingsState.alarmSound.toUri())
+ ?.getTitle(context) ?: ""
}
}
@@ -112,38 +110,39 @@ fun AlarmSettings(
@Suppress("DEPRECATION")
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
- onAlarmSoundChanged(uri)
+ onAction(SettingsAction.SaveAlarmSound(uri))
}
}
- val ringtonePickerIntent = remember(alarmSound) {
+ @SuppressLint("LocalContextGetResourceValueCall")
+ val ringtonePickerIntent = remember(settingsState.alarmSound) {
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
- putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
+ putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, settingsState.alarmSound.toUri())
}
}
val switchItems = remember(
- preferencesState.blackTheme,
- preferencesState.aodEnabled,
- alarmEnabled,
- vibrateEnabled
+ settingsState.blackTheme,
+ settingsState.aodEnabled,
+ settingsState.alarmEnabled,
+ settingsState.vibrateEnabled
) {
listOf(
SettingsSwitchItem(
- checked = alarmEnabled,
+ checked = settingsState.alarmEnabled,
icon = R.drawable.alarm_on,
label = R.string.sound,
description = R.string.alarm_desc,
- onClick = onAlarmEnabledChange
+ onClick = { onAction(SettingsAction.SaveAlarmEnabled(it)) }
),
SettingsSwitchItem(
- checked = vibrateEnabled,
+ checked = settingsState.vibrateEnabled,
icon = R.drawable.mobile_vibrate,
label = R.string.vibrate,
description = R.string.vibrate_desc,
- onClick = onVibrateEnabledChange
+ onClick = { onAction(SettingsAction.SaveVibrateEnabled(it)) }
)
)
}
@@ -241,14 +240,10 @@ fun AlarmSettings(
@Preview
@Composable
fun AlarmSettingsPreview() {
- val preferencesState = PreferencesState()
+ val settingsState = SettingsState()
AlarmSettings(
- preferencesState = preferencesState,
- alarmEnabled = true,
- vibrateEnabled = false,
- alarmSound = "",
- onAlarmEnabledChange = {},
- onVibrateEnabledChange = {},
- onAlarmSoundChanged = {},
- onBack = {})
+ settingsState = settingsState,
+ onAction = {},
+ onBack = {}
+ )
}
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..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
@@ -39,7 +39,6 @@ import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -50,7 +49,8 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
-import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -62,11 +62,9 @@ import org.nsh07.pomodoro.utils.toColor
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppearanceSettings(
- preferencesState: PreferencesState,
+ settingsState: SettingsState,
isPlus: Boolean,
- onBlackThemeChange: (Boolean) -> Unit,
- onThemeChange: (String) -> Unit,
- onColorSchemeChange: (Color) -> Unit,
+ onAction: (SettingsAction) -> Unit,
setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
@@ -105,8 +103,8 @@ fun AppearanceSettings(
}
item {
ThemePickerListItem(
- theme = preferencesState.theme,
- onThemeChange = onThemeChange,
+ theme = settingsState.theme,
+ onThemeChange = { onAction(SettingsAction.SaveTheme(it)) },
items = if (isPlus) 3 else 1,
index = 0
)
@@ -118,20 +116,20 @@ fun AppearanceSettings(
item {
ColorSchemePickerListItem(
- color = preferencesState.colorScheme.toColor(),
+ color = settingsState.colorScheme.toColor(),
items = 3,
index = if (isPlus) 1 else 0,
isPlus = isPlus,
- onColorChange = onColorSchemeChange,
+ onColorChange = { onAction(SettingsAction.SaveColorScheme(it)) },
)
}
item {
val item = SettingsSwitchItem(
- checked = preferencesState.blackTheme,
+ checked = settingsState.blackTheme,
icon = R.drawable.contrast,
label = R.string.black_theme,
description = R.string.black_theme_desc,
- onClick = onBlackThemeChange
+ onClick = { onAction(SettingsAction.SaveBlackTheme(it)) }
)
ListItem(
leadingContent = {
@@ -175,14 +173,12 @@ fun AppearanceSettings(
@Preview
@Composable
fun AppearanceSettingsPreview() {
- val preferencesState = PreferencesState()
+ val settingsState = SettingsState()
TomatoTheme(dynamicColor = false) {
AppearanceSettings(
- preferencesState = preferencesState,
+ settingsState = settingsState,
isPlus = false,
- onBlackThemeChange = {},
- onThemeChange = {},
- onColorSchemeChange = {},
+ onAction = {},
setShowPaywall = {},
onBack = {}
)
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..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
@@ -36,10 +36,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
+import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton
@@ -48,6 +50,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem
+import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
@@ -56,8 +59,9 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
+import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@@ -76,6 +80,8 @@ import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
+import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -90,17 +96,16 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable
fun TimerSettings(
isPlus: Boolean,
- aodEnabled: Boolean,
- dndEnabled: Boolean,
+ serviceRunning: Boolean,
+ settingsState: SettingsState,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
- onAodEnabledChange: (Boolean) -> Unit,
- onDndEnabledChange: (Boolean) -> Unit,
+ onAction: (SettingsAction) -> Unit,
+ setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit,
- modifier: Modifier = Modifier,
- setShowPaywall: (Boolean) -> Unit
+ modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
@@ -108,14 +113,10 @@ fun TimerSettings(
val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
- LaunchedEffect(Unit) {
- if (!notificationManagerService.isNotificationPolicyAccessGranted())
- onDndEnabledChange(false)
- }
-
val switchItems = listOf(
SettingsSwitchItem(
- checked = dndEnabled,
+ checked = settingsState.dndEnabled,
+ enabled = !serviceRunning,
icon = R.drawable.dnd,
label = R.string.dnd,
description = R.string.dnd_desc,
@@ -128,15 +129,15 @@ fun TimerSettings(
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
}
- onDndEnabledChange(it)
+ onAction(SettingsAction.SaveDndEnabled(it))
}
),
SettingsSwitchItem(
- checked = aodEnabled,
+ checked = settingsState.aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
- onClick = onAodEnabledChange
+ onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
)
)
@@ -168,6 +169,20 @@ fun TimerSettings(
.padding(horizontal = 16.dp)
) {
item {
+ CompositionLocalProvider(LocalContentColor provides colorScheme.error) {
+ AnimatedVisibility(serviceRunning) {
+ Column {
+ Spacer(Modifier.height(8.dp))
+ Row(
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ Icon(painterResource(R.drawable.info), null)
+ Text(stringResource(R.string.timer_settings_reset_info))
+ }
+ }
+ }
+ }
Spacer(Modifier.height(14.dp))
}
item {
@@ -187,6 +202,7 @@ fun TimerSettings(
)
MinuteInputField(
state = focusTimeInputFieldState,
+ enabled = !serviceRunning,
shape = RoundedCornerShape(
topStart = topListItemShape.topStart,
bottomStart = topListItemShape.topStart,
@@ -207,6 +223,7 @@ fun TimerSettings(
)
MinuteInputField(
state = shortBreakTimeInputFieldState,
+ enabled = !serviceRunning,
shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next
)
@@ -222,6 +239,7 @@ fun TimerSettings(
)
MinuteInputField(
state = longBreakTimeInputFieldState,
+ enabled = !serviceRunning,
shape = RoundedCornerShape(
topStart = bottomListItemShape.topStart,
bottomStart = bottomListItemShape.topStart,
@@ -254,6 +272,7 @@ fun TimerSettings(
)
Slider(
state = sessionsSliderState,
+ enabled = !serviceRunning,
modifier = Modifier.padding(vertical = 4.dp)
)
}
@@ -278,6 +297,7 @@ fun TimerSettings(
trailingContent = {
Switch(
checked = item.checked,
+ enabled = item.enabled,
onCheckedChange = { item.onClick(it) },
thumbContent = {
if (item.checked) {
@@ -313,7 +333,7 @@ fun TimerSettings(
item {
PlusDivider(setShowPaywall)
}
- itemsIndexed(switchItems.drop(1)) { index, item ->
+ items(switchItems.drop(1)) { item ->
ListItem(
leadingContent = {
Icon(
@@ -392,24 +412,23 @@ fun TimerSettings(
@Preview
@Composable
private fun TimerSettingsPreview() {
- val focusTimeInputFieldState = TextFieldState("25")
- val shortBreakTimeInputFieldState = TextFieldState("5")
- val longBreakTimeInputFieldState = TextFieldState("15")
- val sessionsSliderState = SliderState(
+ val focusTimeInputFieldState = rememberTextFieldState("25")
+ val shortBreakTimeInputFieldState = rememberTextFieldState("5")
+ val longBreakTimeInputFieldState = rememberTextFieldState("15")
+ val sessionsSliderState = rememberSliderState(
value = 4f,
valueRange = 1f..8f,
steps = 6
)
TimerSettings(
isPlus = false,
- aodEnabled = true,
- dndEnabled = false,
+ serviceRunning = true,
+ settingsState = remember { SettingsState() },
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
- onAodEnabledChange = {},
- onDndEnabledChange = {},
+ onAction = {},
setShowPaywall = {},
onBack = {}
)
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/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/SettingsState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt
new file mode 100644
index 0000000..b9d42b4
--- /dev/null
+++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsState.kt
@@ -0,0 +1,33 @@
+/*
+ * Copyright (c) 2025 Nishant Mishra
+ *
+ * This file is part of Tomato - a minimalist pomodoro timer for Android.
+ *
+ * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
+ * General Public License as published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
+ * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
+ * Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along with Tomato.
+ * If not, see .
+ */
+
+package org.nsh07.pomodoro.ui.settingsScreen.viewModel
+
+import androidx.compose.runtime.Immutable
+import androidx.compose.ui.graphics.Color
+
+@Immutable
+data class SettingsState(
+ val theme: String = "auto",
+ val alarmSound: String = "",
+ val colorScheme: String = Color.White.toString(),
+ val blackTheme: Boolean = false,
+ val aodEnabled: Boolean = false,
+ val alarmEnabled: Boolean = true,
+ val vibrateEnabled: Boolean = true,
+ val dndEnabled: Boolean = false
+)
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..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
@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri
+import android.provider.Settings
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState
@@ -36,31 +37,35 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository
+import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.ui.Screen
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
+import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
+import org.nsh07.pomodoro.utils.millisecondsToStr
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel(
private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository,
+ private val serviceHelper: ServiceHelper,
+ private val time: MutableStateFlow,
private val timerRepository: TimerRepository,
+ private val timerState: MutableStateFlow
) : ViewModel() {
val backStack = mutableStateListOf(Screen.Settings.Main)
val isPlus = billingManager.isPlus
- val isPurchaseStateLoaded = billingManager.isLoaded
+ val serviceRunning = timerRepository.serviceRunning.asStateFlow()
- private val _isSettingsLoaded = MutableStateFlow(false)
- val isSettingsLoaded = _isSettingsLoaded.asStateFlow()
-
- private val _preferencesState = MutableStateFlow(PreferencesState())
- val preferencesState = _preferencesState.asStateFlow()
+ private val _settingsState = MutableStateFlow(SettingsState())
+ val settingsState = _settingsState.asStateFlow()
val focusTimeTextFieldState by lazy {
TextFieldState((timerRepository.focusTime / 60000).toString())
@@ -81,25 +86,26 @@ class SettingsViewModel(
)
}
- val currentAlarmSound = timerRepository.alarmSoundUri.toString()
-
private var focusFlowCollectionJob: Job? = null
private var shortBreakFlowCollectionJob: Job? = null
private var longBreakFlowCollectionJob: Job? = null
- val alarmSound =
- preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
- val alarmEnabled =
- preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
- val vibrateEnabled =
- preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
- val dndEnabled =
- preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
-
init {
viewModelScope.launch {
reloadSettings()
- _isSettingsLoaded.value = true
+ }
+ }
+
+ fun onAction(action: SettingsAction) {
+ when (action) {
+ is SettingsAction.SaveAlarmSound -> saveAlarmSound(action.uri)
+ is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled)
+ is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
+ is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
+ is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
+ is SettingsAction.SaveTheme -> saveTheme(action.theme)
+ is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
+ is SettingsAction.SaveAodEnabled -> saveAodEnabled(action.enabled)
}
}
@@ -109,6 +115,7 @@ class SettingsViewModel(
"session_length",
sessionsSliderState.value.toInt()
)
+ refreshTimer()
}
}
@@ -119,6 +126,7 @@ class SettingsViewModel(
.collect {
if (it.isNotEmpty()) {
timerRepository.focusTime = it.toString().toLong() * 60 * 1000
+ refreshTimer()
preferenceRepository.saveIntPreference(
"focus_time",
timerRepository.focusTime.toInt()
@@ -132,6 +140,7 @@ class SettingsViewModel(
.collect {
if (it.isNotEmpty()) {
timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000
+ refreshTimer()
preferenceRepository.saveIntPreference(
"short_break_time",
timerRepository.shortBreakTime.toInt()
@@ -145,6 +154,7 @@ class SettingsViewModel(
.collect {
if (it.isNotEmpty()) {
timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000
+ refreshTimer()
preferenceRepository.saveIntPreference(
"long_break_time",
timerRepository.longBreakTime.toInt()
@@ -155,85 +165,88 @@ class SettingsViewModel(
}
fun cancelTextFieldFlowCollection() {
+ if (!serviceRunning.value) serviceHelper.startService(TimerAction.ResetTimer)
focusFlowCollectionJob?.cancel()
shortBreakFlowCollectionJob?.cancel()
longBreakFlowCollectionJob?.cancel()
}
- fun saveAlarmEnabled(enabled: Boolean) {
+ private fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.alarmEnabled = enabled
+ _settingsState.update { currentState ->
+ currentState.copy(alarmEnabled = enabled)
+ }
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
}
}
- fun saveVibrateEnabled(enabled: Boolean) {
+ private fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.vibrateEnabled = enabled
+ _settingsState.update { currentState ->
+ currentState.copy(vibrateEnabled = enabled)
+ }
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
}
}
- fun saveDndEnabled(enabled: Boolean) {
+ private fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch {
timerRepository.dndEnabled = enabled
+ _settingsState.update { currentState ->
+ currentState.copy(dndEnabled = enabled)
+ }
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
}
}
- fun saveAlarmSound(uri: Uri?) {
+ private fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch {
timerRepository.alarmSoundUri = uri
+ _settingsState.update { currentState ->
+ currentState.copy(alarmSound = uri.toString())
+ }
preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
}
}
- fun saveColorScheme(colorScheme: Color) {
+ private fun saveColorScheme(colorScheme: Color) {
viewModelScope.launch {
- _preferencesState.update { currentState ->
+ _settingsState.update { currentState ->
currentState.copy(colorScheme = colorScheme.toString())
}
preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString())
}
}
- fun saveTheme(theme: String) {
+ private fun saveTheme(theme: String) {
viewModelScope.launch {
- _preferencesState.update { currentState ->
+ _settingsState.update { currentState ->
currentState.copy(theme = theme)
}
preferenceRepository.saveStringPreference("theme", theme)
}
}
- fun saveBlackTheme(blackTheme: Boolean) {
+ private fun saveBlackTheme(blackTheme: Boolean) {
viewModelScope.launch {
- _preferencesState.update { currentState ->
+ _settingsState.update { currentState ->
currentState.copy(blackTheme = blackTheme)
}
preferenceRepository.saveBooleanPreference("black_theme", blackTheme)
}
}
- fun saveAodEnabled(aodEnabled: Boolean) {
+ private fun saveAodEnabled(aodEnabled: Boolean) {
viewModelScope.launch {
- _preferencesState.update { currentState ->
+ _settingsState.update { currentState ->
currentState.copy(aodEnabled = aodEnabled)
}
preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
}
}
- fun resetPaywalledSettings() {
- _preferencesState.update { currentState ->
- currentState.copy(
- aodEnabled = false,
- blackTheme = false,
- colorScheme = Color.White.toString()
- )
- }
- }
-
suspend fun reloadSettings() {
val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto")
@@ -243,29 +256,69 @@ class SettingsViewModel(
?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
+ val alarmSound = preferenceRepository.getStringPreference("alarm_sound")
+ ?: preferenceRepository.saveStringPreference(
+ "alarm_sound",
+ (Settings.System.DEFAULT_ALARM_ALERT_URI
+ ?: Settings.System.DEFAULT_RINGTONE_URI).toString()
+ )
+ val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled")
+ ?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
+ val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled")
+ ?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
+ val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
+ ?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
- _preferencesState.update { currentState ->
+ _settingsState.update { currentState ->
currentState.copy(
theme = theme,
colorScheme = colorScheme,
+ alarmSound = alarmSound,
blackTheme = blackTheme,
- aodEnabled = aodEnabled
+ aodEnabled = aodEnabled,
+ alarmEnabled = alarmEnabled,
+ vibrateEnabled = vibrateEnabled,
+ dndEnabled = dndEnabled
)
}
}
+ private fun refreshTimer() {
+ if (!serviceRunning.value) {
+ time.update { timerRepository.focusTime }
+
+ timerState.update { currentState ->
+ currentState.copy(
+ timerMode = TimerMode.FOCUS,
+ timeStr = millisecondsToStr(time.value),
+ totalTime = time.value,
+ nextTimerMode = if (timerRepository.sessionLength > 1) TimerMode.SHORT_BREAK else TimerMode.LONG_BREAK,
+ nextTimeStr = millisecondsToStr(if (timerRepository.sessionLength > 1) timerRepository.shortBreakTime else timerRepository.longBreakTime),
+ currentFocusCount = 1,
+ totalFocusCount = timerRepository.sessionLength
+ )
+ }
+ }
+ }
+
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication)
+ val appBillingManager = application.container.billingManager
val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository
- val appBillingManager = application.container.billingManager
+ val serviceHelper = application.container.serviceHelper
+ val time = application.container.time
+ val timerState = application.container.timerState
SettingsViewModel(
billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository,
+ serviceHelper = serviceHelper,
+ time = time,
timerRepository = appTimerRepository,
+ timerState = timerState
)
}
}
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..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
@@ -30,8 +29,11 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
@@ -39,22 +41,28 @@ import org.nsh07.pomodoro.data.PreferenceRepository
import org.nsh07.pomodoro.data.Stat
import org.nsh07.pomodoro.data.StatRepository
import org.nsh07.pomodoro.data.TimerRepository
+import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.utils.millisecondsToStr
import java.time.LocalDate
import java.time.temporal.ChronoUnit
@OptIn(FlowPreview::class)
class TimerViewModel(
- application: Application,
private val preferenceRepository: PreferenceRepository,
+ private val serviceHelper: ServiceHelper,
private val statRepository: StatRepository,
private val timerRepository: TimerRepository,
private val _timerState: MutableStateFlow,
private val _time: MutableStateFlow
-) : AndroidViewModel(application) {
+) : ViewModel() {
val timerState: StateFlow = _timerState.asStateFlow()
val time: StateFlow = _time.asStateFlow()
+
+ val progress = _time.combine(_timerState) { remainingTime, uiState ->
+ (uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime
+ }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0f)
+
private var cycles = 0
private var startTime = 0L
@@ -62,7 +70,7 @@ class TimerViewModel(
private var pauseDuration = 0L
init {
- if (!timerRepository.serviceRunning)
+ if (!timerRepository.serviceRunning.value)
viewModelScope.launch(Dispatchers.IO) {
timerRepository.focusTime =
preferenceRepository.getIntPreference("focus_time")?.toLong()
@@ -108,9 +116,6 @@ class TimerViewModel(
)
).toUri()
- preferenceRepository.getBooleanPreference("aod_enabled")
- ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
-
_time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L
@@ -150,6 +155,10 @@ class TimerViewModel(
}
}
+ fun onAction(action: TimerAction) {
+ serviceHelper.startService(action)
+ }
+
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {
@@ -157,12 +166,13 @@ class TimerViewModel(
val appPreferenceRepository = application.container.appPreferenceRepository
val appStatRepository = application.container.appStatRepository
val appTimerRepository = application.container.appTimerRepository
+ val serviceHelper = application.container.serviceHelper
val timerState = application.container.timerState
val time = application.container.time
TimerViewModel(
- application = application,
preferenceRepository = appPreferenceRepository,
+ serviceHelper = serviceHelper,
statRepository = appStatRepository,
timerRepository = appTimerRepository,
_timerState = timerState,
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-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-in/strings.xml b/app/src/main/res/values-in/strings.xml
new file mode 100644
index 0000000..f1629a0
--- /dev/null
+++ b/app/src/main/res/values-in/strings.xml
@@ -0,0 +1,76 @@
+
+
+ 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/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/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 上評分
已選擇
+ 協助翻譯
+ 重置計時器以變更設定
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a608b21..433cd72 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -92,4 +92,6 @@
Rate on Google Play
BuyMeACoffee
Selected
+ Help with translation
+ Reset the timer to change settings
\ No newline at end of file
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
}
)
}
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
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/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
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
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" }