Merge branch 'dev'

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,87 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
@Composable
fun TopButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://coff.ee/nsh07") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.bmc),
contentDescription = null,
modifier = Modifier.height(24.dp)
)
Text(text = stringResource(R.string.bmc))
}
}
}
@Composable
fun BottomButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://hosted.weblate.org/engage/tomato/") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.weblate),
contentDescription = null,
modifier = Modifier.size(20.dp)
)
Text(text = stringResource(R.string.help_with_translation))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -21,6 +21,7 @@ import android.net.Uri
import android.provider.Settings import android.provider.Settings
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.lightColorScheme 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 * 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 alarmSoundUri: Uri?
var serviceRunning: Boolean var serviceRunning: MutableStateFlow<Boolean>
} }
/** /**
@@ -54,12 +55,12 @@ class AppTimerRepository : TimerRepository {
override var shortBreakTime = 5 * 60 * 1000L override var shortBreakTime = 5 * 60 * 1000L
override var longBreakTime = 15 * 60 * 1000L override var longBreakTime = 15 * 60 * 1000L
override var sessionLength = 4 override var sessionLength = 4
override var timerFrequency: Float = 10f override var timerFrequency: Float = 60f
override var alarmEnabled = true override var alarmEnabled = true
override var vibrateEnabled = true override var vibrateEnabled = true
override var dndEnabled: Boolean = false override var dndEnabled: Boolean = false
override var colorScheme = lightColorScheme() override var colorScheme = lightColorScheme()
override var alarmSoundUri: Uri? = override var alarmSoundUri: Uri? =
Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI Settings.System.DEFAULT_ALARM_ALERT_URI ?: Settings.System.DEFAULT_RINGTONE_URI
override var serviceRunning = false override var serviceRunning = MutableStateFlow(false)
} }

View File

@@ -0,0 +1,58 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.service
import android.content.Context
import android.content.Intent
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
/**
* Helper class that holds a reference to [Context] and helps call [Context.startService] in
* [androidx.lifecycle.ViewModel]s. This class must be managed by an [android.app.Application] class
* to scope it to the Activity's lifecycle and prevent leaks.
*/
class ServiceHelper(private val context: Context) {
fun startService(action: TimerAction) {
when (action) {
TimerAction.ResetTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.RESET.toString()
context.startService(it)
}
is TimerAction.SkipTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.SKIP.toString()
context.startService(it)
}
TimerAction.StopAlarm ->
Intent(context, TimerService::class.java).also {
it.action =
TimerService.Actions.STOP_ALARM.toString()
context.startService(it)
}
TimerAction.ToggleTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.TOGGLE.toString()
context.startService(it)
}
}
}
}

View File

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

View File

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

View File

@@ -45,7 +45,6 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
@@ -64,7 +63,6 @@ import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@@ -79,9 +77,7 @@ fun AppScreen(
val context = LocalContext.current val context = LocalContext.current
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle() val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
val remainingTime by timerViewModel.time.collectAsStateWithLifecycle() val progress by timerViewModel.progress.collectAsStateWithLifecycle()
val progress by rememberUpdatedState((uiState.totalTime.toFloat() - remainingTime) / uiState.totalTime)
val layoutDirection = LocalLayoutDirection.current val layoutDirection = LocalLayoutDirection.current
val motionScheme = motionScheme val motionScheme = motionScheme
@@ -166,34 +162,7 @@ fun AppScreen(
timerState = uiState, timerState = uiState,
isPlus = isPlus, isPlus = isPlus,
progress = { progress }, progress = { progress },
onAction = { action -> onAction = timerViewModel::onAction,
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)
}
}
},
modifier = modifier modifier = modifier
.padding( .padding(
start = contentPadding.calculateStartPadding(layoutDirection), start = contentPadding.calculateStartPadding(layoutDirection),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -36,10 +36,12 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.FilledTonalIconToggleButton
@@ -48,6 +50,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LargeFlexibleTopAppBar import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider import androidx.compose.material3.Slider
@@ -56,8 +59,9 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@@ -76,6 +80,8 @@ import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider import org.nsh07.pomodoro.ui.settingsScreen.components.PlusDivider
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsAction
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
@@ -90,17 +96,16 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable @Composable
fun TimerSettings( fun TimerSettings(
isPlus: Boolean, isPlus: Boolean,
aodEnabled: Boolean, serviceRunning: Boolean,
dndEnabled: Boolean, settingsState: SettingsState,
focusTimeInputFieldState: TextFieldState, focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState, sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit, onAction: (SettingsAction) -> Unit,
onDndEnabledChange: (Boolean) -> Unit, setShowPaywall: (Boolean) -> Unit,
onBack: () -> Unit, onBack: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier
setShowPaywall: (Boolean) -> Unit
) { ) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current val context = LocalContext.current
@@ -108,14 +113,10 @@ fun TimerSettings(
val notificationManagerService = val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager } remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
LaunchedEffect(Unit) {
if (!notificationManagerService.isNotificationPolicyAccessGranted())
onDndEnabledChange(false)
}
val switchItems = listOf( val switchItems = listOf(
SettingsSwitchItem( SettingsSwitchItem(
checked = dndEnabled, checked = settingsState.dndEnabled,
enabled = !serviceRunning,
icon = R.drawable.dnd, icon = R.drawable.dnd,
label = R.string.dnd, label = R.string.dnd,
description = R.string.dnd_desc, description = R.string.dnd_desc,
@@ -128,15 +129,15 @@ fun TimerSettings(
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) { } else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL) notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
} }
onDndEnabledChange(it) onAction(SettingsAction.SaveDndEnabled(it))
} }
), ),
SettingsSwitchItem( SettingsSwitchItem(
checked = aodEnabled, checked = settingsState.aodEnabled,
icon = R.drawable.aod, icon = R.drawable.aod,
label = R.string.always_on_display, label = R.string.always_on_display,
description = R.string.always_on_display_desc, description = R.string.always_on_display_desc,
onClick = onAodEnabledChange onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
) )
) )
@@ -168,6 +169,20 @@ fun TimerSettings(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
) { ) {
item { 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)) Spacer(Modifier.height(14.dp))
} }
item { item {
@@ -187,6 +202,7 @@ fun TimerSettings(
) )
MinuteInputField( MinuteInputField(
state = focusTimeInputFieldState, state = focusTimeInputFieldState,
enabled = !serviceRunning,
shape = RoundedCornerShape( shape = RoundedCornerShape(
topStart = topListItemShape.topStart, topStart = topListItemShape.topStart,
bottomStart = topListItemShape.topStart, bottomStart = topListItemShape.topStart,
@@ -207,6 +223,7 @@ fun TimerSettings(
) )
MinuteInputField( MinuteInputField(
state = shortBreakTimeInputFieldState, state = shortBreakTimeInputFieldState,
enabled = !serviceRunning,
shape = RoundedCornerShape(middleListItemShape.topStart), shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next imeAction = ImeAction.Next
) )
@@ -222,6 +239,7 @@ fun TimerSettings(
) )
MinuteInputField( MinuteInputField(
state = longBreakTimeInputFieldState, state = longBreakTimeInputFieldState,
enabled = !serviceRunning,
shape = RoundedCornerShape( shape = RoundedCornerShape(
topStart = bottomListItemShape.topStart, topStart = bottomListItemShape.topStart,
bottomStart = bottomListItemShape.topStart, bottomStart = bottomListItemShape.topStart,
@@ -254,6 +272,7 @@ fun TimerSettings(
) )
Slider( Slider(
state = sessionsSliderState, state = sessionsSliderState,
enabled = !serviceRunning,
modifier = Modifier.padding(vertical = 4.dp) modifier = Modifier.padding(vertical = 4.dp)
) )
} }
@@ -278,6 +297,7 @@ fun TimerSettings(
trailingContent = { trailingContent = {
Switch( Switch(
checked = item.checked, checked = item.checked,
enabled = item.enabled,
onCheckedChange = { item.onClick(it) }, onCheckedChange = { item.onClick(it) },
thumbContent = { thumbContent = {
if (item.checked) { if (item.checked) {
@@ -313,7 +333,7 @@ fun TimerSettings(
item { item {
PlusDivider(setShowPaywall) PlusDivider(setShowPaywall)
} }
itemsIndexed(switchItems.drop(1)) { index, item -> items(switchItems.drop(1)) { item ->
ListItem( ListItem(
leadingContent = { leadingContent = {
Icon( Icon(
@@ -392,24 +412,23 @@ fun TimerSettings(
@Preview @Preview
@Composable @Composable
private fun TimerSettingsPreview() { private fun TimerSettingsPreview() {
val focusTimeInputFieldState = TextFieldState("25") val focusTimeInputFieldState = rememberTextFieldState("25")
val shortBreakTimeInputFieldState = TextFieldState("5") val shortBreakTimeInputFieldState = rememberTextFieldState("5")
val longBreakTimeInputFieldState = TextFieldState("15") val longBreakTimeInputFieldState = rememberTextFieldState("15")
val sessionsSliderState = SliderState( val sessionsSliderState = rememberSliderState(
value = 4f, value = 4f,
valueRange = 1f..8f, valueRange = 1f..8f,
steps = 6 steps = 6
) )
TimerSettings( TimerSettings(
isPlus = false, isPlus = false,
aodEnabled = true, serviceRunning = true,
dndEnabled = false, settingsState = remember { SettingsState() },
focusTimeInputFieldState = focusTimeInputFieldState, focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState, sessionsSliderState = sessionsSliderState,
onAodEnabledChange = {}, onAction = {},
onDndEnabledChange = {},
setShowPaywall = {}, setShowPaywall = {},
onBack = {} onBack = {}
) )

View File

@@ -1,19 +0,0 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
data class PreferencesState(
val theme: String = "auto",
val colorScheme: String = Color.White.toString(),
val blackTheme: Boolean = false,
val aodEnabled: Boolean = false
)

View File

@@ -0,0 +1,32 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri
import androidx.compose.ui.graphics.Color
sealed interface SettingsAction {
data class SaveAlarmEnabled(val enabled: Boolean) : SettingsAction
data class SaveVibrateEnabled(val enabled: Boolean) : SettingsAction
data class SaveBlackTheme(val enabled: Boolean) : SettingsAction
data class SaveAodEnabled(val enabled: Boolean) : SettingsAction
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
data class SaveAlarmSound(val uri: Uri?) : SettingsAction
data class SaveTheme(val theme: String) : SettingsAction
data class SaveColorScheme(val color: Color) : SettingsAction
}

View File

@@ -0,0 +1,33 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
@Immutable
data class SettingsState(
val theme: String = "auto",
val alarmSound: String = "",
val colorScheme: String = Color.White.toString(),
val blackTheme: Boolean = false,
val aodEnabled: Boolean = false,
val alarmEnabled: Boolean = true,
val vibrateEnabled: Boolean = true,
val dndEnabled: Boolean = false
)

View File

@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.ui.settingsScreen.viewModel package org.nsh07.pomodoro.ui.settingsScreen.viewModel
import android.net.Uri import android.net.Uri
import android.provider.Settings
import androidx.compose.foundation.text.input.TextFieldState import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState import androidx.compose.material3.SliderState
@@ -36,31 +37,35 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.billing.BillingManager import org.nsh07.pomodoro.billing.BillingManager
import org.nsh07.pomodoro.data.AppPreferenceRepository import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.service.ServiceHelper
import org.nsh07.pomodoro.ui.Screen 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) @OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel( class SettingsViewModel(
private val billingManager: BillingManager, private val billingManager: BillingManager,
private val preferenceRepository: AppPreferenceRepository, private val preferenceRepository: AppPreferenceRepository,
private val serviceHelper: ServiceHelper,
private val time: MutableStateFlow<Long>,
private val timerRepository: TimerRepository, private val timerRepository: TimerRepository,
private val timerState: MutableStateFlow<TimerState>
) : ViewModel() { ) : ViewModel() {
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main) val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus val isPlus = billingManager.isPlus
val isPurchaseStateLoaded = billingManager.isLoaded val serviceRunning = timerRepository.serviceRunning.asStateFlow()
private val _isSettingsLoaded = MutableStateFlow(false) private val _settingsState = MutableStateFlow(SettingsState())
val isSettingsLoaded = _isSettingsLoaded.asStateFlow() val settingsState = _settingsState.asStateFlow()
private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow()
val focusTimeTextFieldState by lazy { val focusTimeTextFieldState by lazy {
TextFieldState((timerRepository.focusTime / 60000).toString()) TextFieldState((timerRepository.focusTime / 60000).toString())
@@ -81,25 +86,26 @@ class SettingsViewModel(
) )
} }
val currentAlarmSound = timerRepository.alarmSoundUri.toString()
private var focusFlowCollectionJob: Job? = null private var focusFlowCollectionJob: Job? = null
private var shortBreakFlowCollectionJob: Job? = null private var shortBreakFlowCollectionJob: Job? = null
private var longBreakFlowCollectionJob: Job? = null private var longBreakFlowCollectionJob: Job? = null
val alarmSound =
preferenceRepository.getStringPreferenceFlow("alarm_sound").distinctUntilChanged()
val alarmEnabled =
preferenceRepository.getBooleanPreferenceFlow("alarm_enabled").distinctUntilChanged()
val vibrateEnabled =
preferenceRepository.getBooleanPreferenceFlow("vibrate_enabled").distinctUntilChanged()
val dndEnabled =
preferenceRepository.getBooleanPreferenceFlow("dnd_enabled").distinctUntilChanged()
init { init {
viewModelScope.launch { viewModelScope.launch {
reloadSettings() reloadSettings()
_isSettingsLoaded.value = true }
}
fun onAction(action: SettingsAction) {
when (action) {
is SettingsAction.SaveAlarmSound -> saveAlarmSound(action.uri)
is SettingsAction.SaveAlarmEnabled -> saveAlarmEnabled(action.enabled)
is SettingsAction.SaveVibrateEnabled -> saveVibrateEnabled(action.enabled)
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
is SettingsAction.SaveTheme -> saveTheme(action.theme)
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
is SettingsAction.SaveAodEnabled -> saveAodEnabled(action.enabled)
} }
} }
@@ -109,6 +115,7 @@ class SettingsViewModel(
"session_length", "session_length",
sessionsSliderState.value.toInt() sessionsSliderState.value.toInt()
) )
refreshTimer()
} }
} }
@@ -119,6 +126,7 @@ class SettingsViewModel(
.collect { .collect {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
timerRepository.focusTime = it.toString().toLong() * 60 * 1000 timerRepository.focusTime = it.toString().toLong() * 60 * 1000
refreshTimer()
preferenceRepository.saveIntPreference( preferenceRepository.saveIntPreference(
"focus_time", "focus_time",
timerRepository.focusTime.toInt() timerRepository.focusTime.toInt()
@@ -132,6 +140,7 @@ class SettingsViewModel(
.collect { .collect {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000 timerRepository.shortBreakTime = it.toString().toLong() * 60 * 1000
refreshTimer()
preferenceRepository.saveIntPreference( preferenceRepository.saveIntPreference(
"short_break_time", "short_break_time",
timerRepository.shortBreakTime.toInt() timerRepository.shortBreakTime.toInt()
@@ -145,6 +154,7 @@ class SettingsViewModel(
.collect { .collect {
if (it.isNotEmpty()) { if (it.isNotEmpty()) {
timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000 timerRepository.longBreakTime = it.toString().toLong() * 60 * 1000
refreshTimer()
preferenceRepository.saveIntPreference( preferenceRepository.saveIntPreference(
"long_break_time", "long_break_time",
timerRepository.longBreakTime.toInt() timerRepository.longBreakTime.toInt()
@@ -155,85 +165,88 @@ class SettingsViewModel(
} }
fun cancelTextFieldFlowCollection() { fun cancelTextFieldFlowCollection() {
if (!serviceRunning.value) serviceHelper.startService(TimerAction.ResetTimer)
focusFlowCollectionJob?.cancel() focusFlowCollectionJob?.cancel()
shortBreakFlowCollectionJob?.cancel() shortBreakFlowCollectionJob?.cancel()
longBreakFlowCollectionJob?.cancel() longBreakFlowCollectionJob?.cancel()
} }
fun saveAlarmEnabled(enabled: Boolean) { private fun saveAlarmEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmEnabled = enabled timerRepository.alarmEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(alarmEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("alarm_enabled", enabled) preferenceRepository.saveBooleanPreference("alarm_enabled", enabled)
} }
} }
fun saveVibrateEnabled(enabled: Boolean) { private fun saveVibrateEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.vibrateEnabled = enabled timerRepository.vibrateEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(vibrateEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled) preferenceRepository.saveBooleanPreference("vibrate_enabled", enabled)
} }
} }
fun saveDndEnabled(enabled: Boolean) { private fun saveDndEnabled(enabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.dndEnabled = enabled timerRepository.dndEnabled = enabled
_settingsState.update { currentState ->
currentState.copy(dndEnabled = enabled)
}
preferenceRepository.saveBooleanPreference("dnd_enabled", enabled) preferenceRepository.saveBooleanPreference("dnd_enabled", enabled)
} }
} }
fun saveAlarmSound(uri: Uri?) { private fun saveAlarmSound(uri: Uri?) {
viewModelScope.launch { viewModelScope.launch {
timerRepository.alarmSoundUri = uri timerRepository.alarmSoundUri = uri
_settingsState.update { currentState ->
currentState.copy(alarmSound = uri.toString())
}
preferenceRepository.saveStringPreference("alarm_sound", uri.toString()) preferenceRepository.saveStringPreference("alarm_sound", uri.toString())
} }
} }
fun saveColorScheme(colorScheme: Color) { private fun saveColorScheme(colorScheme: Color) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(colorScheme = colorScheme.toString()) currentState.copy(colorScheme = colorScheme.toString())
} }
preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString()) preferenceRepository.saveStringPreference("color_scheme", colorScheme.toString())
} }
} }
fun saveTheme(theme: String) { private fun saveTheme(theme: String) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(theme = theme) currentState.copy(theme = theme)
} }
preferenceRepository.saveStringPreference("theme", theme) preferenceRepository.saveStringPreference("theme", theme)
} }
} }
fun saveBlackTheme(blackTheme: Boolean) { private fun saveBlackTheme(blackTheme: Boolean) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(blackTheme = blackTheme) currentState.copy(blackTheme = blackTheme)
} }
preferenceRepository.saveBooleanPreference("black_theme", blackTheme) preferenceRepository.saveBooleanPreference("black_theme", blackTheme)
} }
} }
fun saveAodEnabled(aodEnabled: Boolean) { private fun saveAodEnabled(aodEnabled: Boolean) {
viewModelScope.launch { viewModelScope.launch {
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy(aodEnabled = aodEnabled) currentState.copy(aodEnabled = aodEnabled)
} }
preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled) preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
} }
} }
fun resetPaywalledSettings() {
_preferencesState.update { currentState ->
currentState.copy(
aodEnabled = false,
blackTheme = false,
colorScheme = Color.White.toString()
)
}
}
suspend fun reloadSettings() { suspend fun reloadSettings() {
val theme = preferenceRepository.getStringPreference("theme") val theme = preferenceRepository.getStringPreference("theme")
?: preferenceRepository.saveStringPreference("theme", "auto") ?: preferenceRepository.saveStringPreference("theme", "auto")
@@ -243,29 +256,69 @@ class SettingsViewModel(
?: preferenceRepository.saveBooleanPreference("black_theme", false) ?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false) ?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
val alarmSound = preferenceRepository.getStringPreference("alarm_sound")
?: preferenceRepository.saveStringPreference(
"alarm_sound",
(Settings.System.DEFAULT_ALARM_ALERT_URI
?: Settings.System.DEFAULT_RINGTONE_URI).toString()
)
val alarmEnabled = preferenceRepository.getBooleanPreference("alarm_enabled")
?: preferenceRepository.saveBooleanPreference("alarm_enabled", true)
val vibrateEnabled = preferenceRepository.getBooleanPreference("vibrate_enabled")
?: preferenceRepository.saveBooleanPreference("vibrate_enabled", true)
val dndEnabled = preferenceRepository.getBooleanPreference("dnd_enabled")
?: preferenceRepository.saveBooleanPreference("dnd_enabled", false)
_preferencesState.update { currentState -> _settingsState.update { currentState ->
currentState.copy( currentState.copy(
theme = theme, theme = theme,
colorScheme = colorScheme, colorScheme = colorScheme,
alarmSound = alarmSound,
blackTheme = blackTheme, blackTheme = blackTheme,
aodEnabled = aodEnabled aodEnabled = aodEnabled,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled
) )
} }
} }
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 { companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory { val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer { initializer {
val application = (this[APPLICATION_KEY] as TomatoApplication) val application = (this[APPLICATION_KEY] as TomatoApplication)
val appBillingManager = application.container.billingManager
val appPreferenceRepository = application.container.appPreferenceRepository val appPreferenceRepository = application.container.appPreferenceRepository
val appTimerRepository = application.container.appTimerRepository 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( SettingsViewModel(
billingManager = appBillingManager, billingManager = appBillingManager,
preferenceRepository = appPreferenceRepository, preferenceRepository = appPreferenceRepository,
serviceHelper = serviceHelper,
time = time,
timerRepository = appTimerRepository, timerRepository = appTimerRepository,
timerState = timerState
) )
} }
} }

View File

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

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2025 Nishant Mishra
~
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
~
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
~ General Public License as published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Tomato.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M9.662,3.809c-1.875,1.19 -2.81,3.515 -2.83,5.795 -0.014,2.628 0.666,5.258 1.988,7.305 0.936,1.46 2.238,2.715 3.836,3.412a6.942,6.942 0,0 0,5.647 -0.07c1.997,-0.927 3.523,-2.73 4.463,-4.785 1.606,-3.518 1.643,-7.724 0.12,-11.295 -1.146,0.458 -2.166,-0.271 -2.166,-0.271s0.003,1.122 -1.083,1.685c1.115,2.612 1.088,5.717 -0.03,8.263 -0.538,1.225 -1.358,2.365 -2.498,3.01 -0.917,0.52 -2.04,0.625 -3.052,0.184 -1.342,-0.585 -2.293,-1.864 -2.89,-3.254 -0.466,-1.067 -0.782,-2.447 -0.802,-3.878 -0.037,-1.724 0.728,-3.193 1.635,-3.218 0.622,-0.024 1.427,0.918 1.598,2.435 0.158,1.543 -0.177,3.72 -1.174,5.49 0.677,1.085 1.77,1.98 2.951,1.974 1.386,-2.338 1.827,-4.911 1.793,-6.987 -0.02,-2.28 -0.955,-4.603 -2.83,-5.795 -1.437,-0.907 -3.173,-0.948 -4.676,0zM3.278,3.9s-1.018,0.73 -2.163,0.27c-1.524,3.573 -1.488,7.778 0.12,11.296 0.94,2.056 2.465,3.858 4.462,4.785a6.95,6.95 0,0 0,5.523 0.124,9.12 9.12,0 0,1 -1.75,-1.455 11.18,11.18 0,0 1,-1.267 -1.628c-0.768,-0.08 -1.498,-0.482 -2.003,-0.913 -1.447,-1.213 -2.453,-3.478 -2.632,-5.9 -0.12,-1.635 0.14,-3.354 0.795,-4.894C3.276,5.022 3.278,3.9 3.278,3.9z" />
</vector>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -92,4 +92,6 @@
<string name="rate_on_google_play">Rate on Google Play</string> <string name="rate_on_google_play">Rate on Google Play</string>
<string name="bmc">BuyMeACoffee</string> <string name="bmc">BuyMeACoffee</string>
<string name="selected">Selected</string> <string name="selected">Selected</string>
<string name="help_with_translation">Help with translation</string>
<string name="timer_settings_reset_info">Reset the timer to change settings</string>
</resources> </resources>

View File

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

View File

@@ -0,0 +1,86 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.components
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonColors
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
@Composable
fun TopButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://hosted.weblate.org/engage/tomato/") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.weblate),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(text = stringResource(R.string.help_with_translation))
}
}
}
@Composable
fun BottomButton(
buttonColors: ButtonColors,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=org.nsh07.pomodoro") },
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.play_store),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(text = stringResource(R.string.rate_on_google_play))
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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