Merge branch 'dev'

This commit is contained in:
Nishant Mishra
2025-12-09 19:49:49 +05:30
32 changed files with 565 additions and 131 deletions

View File

@@ -2,7 +2,7 @@
name: Bug report
about: Create a report of a bug or crash
title: "[BUG] <enter your title here>"
labels: bug, priority-unassigned
labels: bug, needs-triage
assignees: nsh07
---
@@ -12,6 +12,7 @@ A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@@ -24,10 +25,11 @@ A clear and concise description of what you expected to happen.
If applicable, add screenshots to help explain your problem.
**Device (please complete the following information):**
- Device model: [e.g. Google Pixel 10]
- Android version: [e.g. Android 16]
- App version: [e.g. v1.4.3]
- Installed from: [e.g. Play Store]
- Device model: [e.g. Google Pixel 10]
- Android version: [e.g. Android 16]
- App version: [e.g. v1.4.3]
- Installed from: [e.g. Play Store]
**Additional context**
Add any other context about the problem here.

View File

@@ -2,7 +2,7 @@
name: Feature request
about: Suggest a new feature
title: "[FEATURE] <enter your title here>"
labels: enhancement, priority-unassigned
labels: enhancement, needs-triage
assignees: nsh07
---

View File

@@ -43,8 +43,8 @@ android {
applicationId = "org.nsh07.pomodoro"
minSdk = 27
targetSdk = 36
versionCode = 22
versionName = "1.7.0"
versionCode = 23
versionName = "1.7.1"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -20,6 +20,7 @@ package org.nsh07.pomodoro.data
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.lightColorScheme
import kotlinx.coroutines.flow.MutableStateFlow
import org.nsh07.pomodoro.service.TimerStateSnapshot
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsState
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@@ -28,4 +29,6 @@ class StateRepository {
val settingsState = MutableStateFlow(SettingsState())
var timerFrequency: Float = 60f
var colorScheme: ColorScheme = lightColorScheme()
var timerStateSnapshot: TimerStateSnapshot =
TimerStateSnapshot(time = 0, timerState = TimerState())
}

View File

@@ -35,6 +35,12 @@ class ServiceHelper(private val context: Context) {
context.startService(it)
}
TimerAction.UndoReset ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.UNDO_RESET.toString()
context.startService(it)
}
is TimerAction.SkipTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.SKIP.toString()

View File

@@ -75,6 +75,8 @@ class TimerService : Service() {
private var lastSavedDuration = 0L
private val timerStateSnapshot by lazy { stateRepository.timerStateSnapshot }
private val saveLock = Mutex()
private var job = SupervisorJob()
private val timerScope = CoroutineScope(Dispatchers.IO + job)
@@ -134,6 +136,8 @@ class TimerService : Service() {
}
}
Actions.UNDO_RESET.toString() -> undoReset()
Actions.SKIP.toString() -> skipScope.launch { skipTimer(true) }
Actions.STOP_ALARM.toString() -> stopAlarm()
@@ -326,6 +330,16 @@ class TimerService : Service() {
private suspend fun resetTimer() {
val settingsState = _settingsState.value
timerStateSnapshot.save(
lastSavedDuration,
time,
cycles,
startTime,
pauseTime,
pauseDuration,
_timerState.value
)
saveTimeToDb()
lastSavedDuration = 0
time = settingsState.focusTime
@@ -349,6 +363,16 @@ class TimerService : Service() {
updateProgressSegments()
}
private fun undoReset() {
lastSavedDuration = timerStateSnapshot.lastSavedDuration
time = timerStateSnapshot.time
cycles = timerStateSnapshot.cycles
startTime = timerStateSnapshot.startTime
pauseTime = timerStateSnapshot.pauseTime
pauseDuration = timerStateSnapshot.pauseDuration
_timerState.update { timerStateSnapshot.timerState }
}
private suspend fun skipTimer(fromButton: Boolean = false) {
val settingsState = _settingsState.value
saveTimeToDb()
@@ -406,7 +430,7 @@ class TimerService : Service() {
autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch {
delay(1 * 60 * 1000)
stopAlarm()
stopAlarm(fromAutoStop = true)
}
if (settingsState.vibrateEnabled) {
@@ -420,7 +444,13 @@ class TimerService : Service() {
}
}
fun stopAlarm() {
/**
* Stops ringing the alarm and vibration, and performs related necessary actions
*
* @param fromAutoStop Whether the function was triggered automatically by the program instead of
* intentionally by the user
*/
fun stopAlarm(fromAutoStop: Boolean = false) {
val settingsState = _settingsState.value
autoAlarmStopScope?.cancel()
@@ -449,6 +479,9 @@ class TimerService : Service() {
else -> settingsState.longBreakTime.toInt()
}, paused = true, complete = false
)
if (settingsState.autostartNextSession && !fromAutoStop) // auto start next session
toggleTimer()
}
private fun initializeMediaPlayer(): MediaPlayer? {
@@ -515,6 +548,6 @@ class TimerService : Service() {
}
enum class Actions {
TOGGLE, SKIP, RESET, STOP_ALARM, UPDATE_ALARM_TONE
TOGGLE, SKIP, RESET, UNDO_RESET, STOP_ALARM, UPDATE_ALARM_TONE
}
}

View File

@@ -0,0 +1,48 @@
/*
* 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 org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
data class TimerStateSnapshot(
var lastSavedDuration: Long = 0L,
var time: Long,
var cycles: Int = 0,
var startTime: Long = 0L,
var pauseTime: Long = 0L,
var pauseDuration: Long = 0L,
var timerState: TimerState
) {
fun save(
lastSavedDuration: Long,
time: Long,
cycles: Int,
startTime: Long,
pauseTime: Long,
pauseDuration: Long,
timerState: TimerState
) {
this.lastSavedDuration = lastSavedDuration
this.time = time
this.cycles = cycles
this.startTime = startTime
this.pauseTime = pauseTime
this.pauseDuration = pauseDuration
this.timerState = timerState
}
}

View File

@@ -63,7 +63,7 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import kotlinx.coroutines.delay
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex400
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
@@ -83,6 +83,7 @@ import kotlin.random.Random
@Composable
fun SharedTransitionScope.AlwaysOnDisplay(
timerState: TimerState,
secureAod: Boolean,
progress: () -> Float,
setTimerFrequency: (Float) -> Unit,
modifier: Modifier = Modifier
@@ -100,8 +101,10 @@ fun SharedTransitionScope.AlwaysOnDisplay(
DisposableEffect(Unit) {
setTimerFrequency(1f)
window.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
if (secureAod) {
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
} else WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
)
activity?.setShowWhenLocked(true)
insetsController.apply {
@@ -246,7 +249,7 @@ fun SharedTransitionScope.AlwaysOnDisplay(
Text(
text = timerState.timeStr,
style = TextStyle(
fontFamily = googleFlex600,
fontFamily = googleFlex400,
fontSize = 56.sp,
letterSpacing = (-2).sp,
fontFeatureSettings = "tnum"
@@ -273,6 +276,7 @@ private fun AlwaysOnDisplayPreview() {
SharedTransitionLayout {
AlwaysOnDisplay(
timerState = timerState,
secureAod = true,
progress = progress,
setTimerFrequency = {}
)

View File

@@ -88,6 +88,7 @@ import androidx.window.core.layout.WindowSizeClass
import org.nsh07.pomodoro.billing.TomatoPlusPaywallDialog
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
import org.nsh07.pomodoro.ui.timerScreen.AlarmDialog
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
@@ -101,11 +102,13 @@ fun AppScreen(
isPlus: Boolean,
setTimerFrequency: (Float) -> Unit,
modifier: Modifier = Modifier,
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
settingsViewModel: SettingsViewModel = viewModel(factory = SettingsViewModel.Factory)
) {
val context = LocalContext.current
val uiState by timerViewModel.timerState.collectAsStateWithLifecycle()
val settingsState by settingsViewModel.settingsState.collectAsStateWithLifecycle()
val progress by timerViewModel.progress.collectAsStateWithLifecycle()
val layoutDirection = LocalLayoutDirection.current
@@ -212,7 +215,7 @@ fun AppScreen(
),
modifier = Modifier.height(56.dp)
) {
Row {
Row(verticalAlignment = Alignment.CenterVertically) {
Crossfade(selected) {
if (it) Icon(
painterResource(item.selectedIcon),
@@ -278,6 +281,7 @@ fun AppScreen(
entry<Screen.AOD> {
AlwaysOnDisplay(
timerState = uiState,
secureAod = settingsState.secureAod,
progress = { progress },
setTimerFrequency = setTimerFrequency,
modifier = if (isAODEnabled) Modifier.clickable {

View File

@@ -35,7 +35,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
@@ -128,8 +127,7 @@ fun ColorSchemePickerListItem(
leadingContent = {
Icon(
painter = painterResource(R.drawable.palette),
contentDescription = null,
tint = colorScheme.primary
contentDescription = null
)
},
headlineContent = { Text(stringResource(R.string.color_scheme)) },

View File

@@ -24,7 +24,9 @@ import android.os.Build
import android.provider.Settings
import android.widget.Toast
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -38,7 +40,6 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -54,6 +55,7 @@ import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Slider
@@ -74,9 +76,11 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
@@ -114,46 +118,72 @@ fun TimerSettings(
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
val inspectionMode = LocalInspectionMode.current
val appName = stringResource(R.string.app_name)
val notificationManagerService =
remember { context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager }
val notificationManagerService = remember {
if (!inspectionMode)
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
else
null
}
val switchItems = remember(
settingsState.dndEnabled,
settingsState.aodEnabled,
settingsState.autostartNextSession,
settingsState.secureAod,
isPlus,
serviceRunning
) {
listOf(
SettingsSwitchItem(
checked = settingsState.dndEnabled,
enabled = !serviceRunning,
icon = R.drawable.dnd,
label = R.string.dnd,
description = R.string.dnd_desc,
onClick = {
if (it && !notificationManagerService.isNotificationPolicyAccessGranted()) {
val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)
Toast.makeText(
context,
"Enable permission for \"$appName\"",
Toast.LENGTH_LONG
)
.show()
context.startActivity(intent)
} else if (!it && notificationManagerService.isNotificationPolicyAccessGranted()) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
listOf(
SettingsSwitchItem(
checked = settingsState.autostartNextSession,
icon = R.drawable.autoplay,
label = R.string.auto_start_next_timer,
description = R.string.auto_start_next_timer_desc,
onClick = { onAction(SettingsAction.SaveAutostartNextSession(it)) }
),
SettingsSwitchItem(
checked = settingsState.dndEnabled,
enabled = !serviceRunning,
icon = R.drawable.dnd,
label = R.string.dnd,
description = R.string.dnd_desc,
onClick = {
if (it && notificationManagerService?.isNotificationPolicyAccessGranted() == false) {
val intent = Intent(Settings.ACTION_NOTIFICATION_POLICY_ACCESS_SETTINGS)
Toast.makeText(
context,
"Enable permission for \"$appName\"",
Toast.LENGTH_LONG
)
.show()
context.startActivity(intent)
} else if (!it && notificationManagerService?.isNotificationPolicyAccessGranted() == true) {
notificationManagerService.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_ALL)
}
onAction(SettingsAction.SaveDndEnabled(it))
}
onAction(SettingsAction.SaveDndEnabled(it))
}
)
),
SettingsSwitchItem(
checked = settingsState.aodEnabled,
enabled = isPlus,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
listOf(
SettingsSwitchItem(
checked = settingsState.aodEnabled,
enabled = isPlus,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = { onAction(SettingsAction.SaveAodEnabled(it)) }
),
SettingsSwitchItem(
checked = settingsState.secureAod && isPlus,
enabled = isPlus,
icon = R.drawable.mobile_lock_portrait,
label = R.string.secure_aod,
description = R.string.secure_aod_desc,
onClick = { onAction(SettingsAction.SaveSecureAod(it)) }
)
)
)
}
@@ -310,7 +340,7 @@ fun TimerSettings(
}
item { Spacer(Modifier.height(12.dp)) }
itemsIndexed(if (isPlus) switchItems else switchItems.take(1)) { index, item ->
itemsIndexed(switchItems[0]) { index, item ->
ListItem(
leadingContent = {
Icon(
@@ -346,59 +376,18 @@ fun TimerSettings(
},
colors = listItemColors,
modifier = Modifier.clip(
if (isPlus) when (index) {
when (index) {
0 -> topListItemShape
switchItems.size - 1 -> bottomListItemShape
switchItems[0].size - 1 -> bottomListItemShape
else -> middleListItemShape
}
else cardShape
)
)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
if (isPlus) {
item { Spacer(Modifier.height(12.dp)) }
item {
ListItem(
leadingContent = {
Icon(painterResource(R.drawable.view_day), null)
},
headlineContent = { Text(stringResource(R.string.session_only_progress)) },
supportingContent = { Text(stringResource(R.string.session_only_progress_desc)) },
trailingContent = {
Switch(
checked = settingsState.singleProgressBar,
enabled = !serviceRunning,
onCheckedChange = { onAction(SettingsAction.SaveSingleProgressBar(it)) },
thumbContent = {
if (settingsState.singleProgressBar) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
} else {
Icon(
painter = painterResource(R.drawable.clear),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
}
},
colors = switchColors
)
},
colors = listItemColors,
modifier = Modifier.clip(cardShape)
)
}
}
if (!isPlus) {
item {
PlusDivider(setShowPaywall)
}
items(switchItems.drop(1)) { item ->
itemsIndexed(switchItems[1]) { index, item ->
ListItem(
leadingContent = {
Icon(
@@ -433,11 +422,115 @@ fun TimerSettings(
)
},
colors = listItemColors,
modifier = Modifier.clip(
when (index) {
0 -> topListItemShape
switchItems[1].size - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
)
}
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.BAKLAVA) {
item { Spacer(Modifier.height(12.dp)) }
item {
ListItem(
leadingContent = {
Icon(painterResource(R.drawable.view_day), null)
},
headlineContent = { Text(stringResource(R.string.session_only_progress)) },
supportingContent = {
var expanded by remember { mutableStateOf(false) }
Text(
stringResource(R.string.session_only_progress_desc),
maxLines = if (expanded) Int.MAX_VALUE else 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.clickable { expanded = !expanded }
.animateContentSize(motionScheme.defaultSpatialSpec())
)
},
trailingContent = {
Switch(
checked = settingsState.singleProgressBar,
enabled = !serviceRunning,
onCheckedChange = { onAction(SettingsAction.SaveSingleProgressBar(it)) },
thumbContent = {
if (settingsState.singleProgressBar) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
} else {
Icon(
painter = painterResource(R.drawable.clear),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
}
},
colors = switchColors
)
},
colors = listItemColors,
modifier = Modifier.clip(cardShape)
)
}
}
if (!isPlus) {
item {
PlusDivider(setShowPaywall)
}
itemsIndexed(switchItems[1]) { index, item ->
ListItem(
leadingContent = {
Icon(
painterResource(item.icon),
contentDescription = null,
modifier = Modifier.padding(top = 4.dp)
)
},
headlineContent = { Text(stringResource(item.label)) },
supportingContent = { Text(stringResource(item.description)) },
trailingContent = {
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
enabled = item.enabled,
thumbContent = {
if (item.checked) {
Icon(
painter = painterResource(R.drawable.check),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
} else {
Icon(
painter = painterResource(R.drawable.clear),
contentDescription = null,
modifier = Modifier.size(SwitchDefaults.IconSize),
)
}
},
colors = switchColors
)
},
colors = listItemColors,
modifier = Modifier.clip(
when (index) {
0 -> topListItemShape
switchItems[1].size - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
)
}
}
item {
var expanded by remember { mutableStateOf(false) }
Column(
@@ -487,7 +580,7 @@ private fun TimerSettingsPreview() {
)
TimerSettings(
isPlus = false,
serviceRunning = true,
serviceRunning = false,
settingsState = remember { SettingsState() },
contentPadding = PaddingValues(),
focusTimeInputFieldState = focusTimeInputFieldState,

View File

@@ -28,6 +28,8 @@ sealed interface SettingsAction {
data class SaveDndEnabled(val enabled: Boolean) : SettingsAction
data class SaveMediaVolumeForAlarm(val enabled: Boolean) : SettingsAction
data class SaveSingleProgressBar(val enabled: Boolean) : SettingsAction
data class SaveAutostartNextSession(val enabled: Boolean) : SettingsAction
data class SaveSecureAod(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

@@ -33,6 +33,8 @@ data class SettingsState(
val dndEnabled: Boolean = false,
val mediaVolumeForAlarm: Boolean = false,
val singleProgressBar: Boolean = false,
val autostartNextSession: Boolean = false,
val secureAod: Boolean = true,
val focusTime: Long = 25 * 60 * 1000L,
val shortBreakTime: Long = 5 * 60 * 1000L,

View File

@@ -39,6 +39,7 @@ import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
@@ -64,7 +65,9 @@ class SettingsViewModel(
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
val isPlus = billingManager.isPlus
val serviceRunning = stateRepository.timerState.map { it.serviceRunning }
val serviceRunning = stateRepository.timerState
.map { it.serviceRunning }
.flowOn(Dispatchers.IO)
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
@@ -111,6 +114,8 @@ class SettingsViewModel(
is SettingsAction.SaveDndEnabled -> saveDndEnabled(action.enabled)
is SettingsAction.SaveMediaVolumeForAlarm -> saveMediaVolumeForAlarm(action.enabled)
is SettingsAction.SaveSingleProgressBar -> saveSingleProgressBar(action.enabled)
is SettingsAction.SaveAutostartNextSession -> saveAutostartNextSession(action.enabled)
is SettingsAction.SaveSecureAod -> saveSecureAod(action.enabled)
is SettingsAction.SaveColorScheme -> saveColorScheme(action.color)
is SettingsAction.SaveTheme -> saveTheme(action.theme)
is SettingsAction.SaveBlackTheme -> saveBlackTheme(action.enabled)
@@ -286,6 +291,30 @@ class SettingsViewModel(
}
}
private fun saveAutostartNextSession(autostartNextSession: Boolean) {
viewModelScope.launch {
_settingsState.update { currentState ->
currentState.copy(autostartNextSession = autostartNextSession)
}
preferenceRepository.saveBooleanPreference(
"autostart_next_session",
autostartNextSession
)
}
}
private fun saveSecureAod(secureAod: Boolean) {
viewModelScope.launch {
_settingsState.update { currentState ->
currentState.copy(secureAod = secureAod)
}
preferenceRepository.saveBooleanPreference(
"secure_aod",
secureAod
)
}
}
suspend fun reloadSettings() {
var settingsState = _settingsState.value
val focusTime =
@@ -353,6 +382,14 @@ class SettingsViewModel(
"single_progress_bar",
settingsState.singleProgressBar
)
val autostartNextSession =
preferenceRepository.getBooleanPreference("autostart_next_session")
?: preferenceRepository.saveBooleanPreference(
"autostart_next_session",
settingsState.autostartNextSession
)
val secureAod = preferenceRepository.getBooleanPreference("secure_aod")
?: preferenceRepository.saveBooleanPreference("secure_aod", true)
_settingsState.update { currentState ->
currentState.copy(
@@ -369,7 +406,9 @@ class SettingsViewModel(
vibrateEnabled = vibrateEnabled,
dndEnabled = dndEnabled,
mediaVolumeForAlarm = mediaVolumeForAlarm,
singleProgressBar = singleProgressBar
singleProgressBar = singleProgressBar,
autostartNextSession = autostartNextSession,
secureAod = secureAod
)
}

View File

@@ -27,9 +27,10 @@ import com.patrykandpatrick.vico.core.cartesian.data.CartesianChartModelProducer
import com.patrykandpatrick.vico.core.cartesian.data.columnSeries
import com.patrykandpatrick.vico.core.cartesian.data.lineSeries
import com.patrykandpatrick.vico.core.common.data.ExtraStore
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
@@ -46,7 +47,13 @@ class StatsViewModel(
private val statRepository: StatRepository
) : ViewModel() {
val todayStat = statRepository.getTodayStat().distinctUntilChanged()
val todayStat = statRepository
.getTodayStat()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
initialValue = null
)
private val lastWeekSummary =
Pair(CartesianChartModelProducer(), ExtraStore.Key<List<String>>())
@@ -75,6 +82,7 @@ class StatsViewModel(
}
lastWeekSummary
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
@@ -91,6 +99,7 @@ class StatsViewModel(
it?.focusTimeQ4?.toInt() ?: 0
)
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
@@ -109,6 +118,7 @@ class StatsViewModel(
}
lastMonthSummary
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
@@ -125,6 +135,7 @@ class StatsViewModel(
it?.focusTimeQ4?.toInt() ?: 0
)
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
@@ -143,6 +154,7 @@ class StatsViewModel(
}
lastYearSummary
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),
@@ -159,6 +171,7 @@ class StatsViewModel(
it?.focusTimeQ4?.toInt() ?: 0
)
}
.flowOn(Dispatchers.IO)
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000),

View File

@@ -18,6 +18,7 @@
package org.nsh07.pomodoro.ui.timerScreen
import android.Manifest
import android.annotation.SuppressLint
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
@@ -65,6 +66,10 @@ import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarDuration
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
@@ -74,6 +79,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
@@ -83,6 +89,7 @@ import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
@@ -94,8 +101,8 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import kotlinx.coroutines.launch
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.mergePaddingValues
import org.nsh07.pomodoro.ui.theme.AppFonts.googleFlex600
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@@ -103,7 +110,6 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerAction
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SharedTransitionScope.TimerScreen(
@@ -115,7 +121,9 @@ fun SharedTransitionScope.TimerScreen(
modifier: Modifier = Modifier
) {
val motionScheme = motionScheme
val scope = rememberCoroutineScope()
val haptic = LocalHapticFeedback.current
val context = LocalContext.current
val color by animateColorAsState(
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
@@ -139,6 +147,7 @@ fun SharedTransitionScope.TimerScreen(
)
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val snackbarHostState = remember { SnackbarHostState() }
Scaffold(
topBar = {
@@ -215,16 +224,16 @@ fun SharedTransitionScope.TimerScreen(
scrollBehavior = scrollBehavior
)
},
bottomBar = { Spacer(Modifier.height(contentPadding.calculateBottomPadding())) },
snackbarHost = { SnackbarHost(snackbarHostState) },
modifier = modifier
.nestedScroll(scrollBehavior.nestedScrollConnection)
) { innerPadding ->
val insets = mergePaddingValues(innerPadding, contentPadding)
LazyColumn(
verticalArrangement = Arrangement.Center,
horizontalAlignment = CenterHorizontally,
contentPadding = insets,
modifier = Modifier
.fillMaxSize()
contentPadding = innerPadding,
modifier = Modifier.fillMaxSize()
) {
item {
Column(horizontalAlignment = CenterHorizontally) {
@@ -411,6 +420,19 @@ fun SharedTransitionScope.TimerScreen(
onClick = {
onAction(TimerAction.ResetTimer)
haptic.performHapticFeedback(HapticFeedbackType.VirtualKey)
@SuppressLint("LocalContextGetResourceValueCall")
scope.launch {
val result = snackbarHostState.showSnackbar(
context.getString(R.string.timer_reset_message),
actionLabel = context.getString(R.string.undo),
withDismissAction = true,
duration = SnackbarDuration.Long
)
if (result == SnackbarResult.ActionPerformed) {
onAction(TimerAction.UndoReset)
}
}
},
colors = IconButtonDefaults.filledTonalIconButtonColors(
containerColor = colorContainer

View File

@@ -1,8 +1,18 @@
/*
* 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/>.
* 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.timerScreen.viewModel
@@ -11,6 +21,7 @@ sealed interface TimerAction {
data class SkipTimer(val fromButton: Boolean) : TimerAction
data object ResetTimer : TimerAction
data object UndoReset : TimerAction
data object StopAlarm : TimerAction
data object ToggleTimer : TimerAction
}

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="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M380,623v-286q0,-12 10.5,-18t20.5,1l223,143q9,6 9,17t-9,17L411,640q-10,7 -20.5,1T380,623ZM120,732v68q0,17 -11.5,28.5T80,840q-17,0 -28.5,-11.5T40,800v-160q0,-17 11.5,-28.5T80,600h160q17,0 28.5,11.5T280,640q0,17 -11.5,28.5T240,680h-58q51,75 129.5,117.5T480,840q104,0 190,-54t132,-145q9,-17 23.5,-27t33.5,-6q18,4 23.5,20.5T879,664q-54,116 -161,186T480,920q-108,0 -202.5,-49.5T120,732ZM83,440q-17,0 -27.5,-12.5T48,398q10,-47 26,-86.5t43,-79.5q10,-15 26,-17t29,11q14,14 14,30.5T175,290q-17,26 -27,52t-18,57q-4,18 -16.5,29.5T83,440ZM440,82q0,19 -11.5,31T398,129q-30,7 -55.5,18T291,175q-16,11 -32.5,10T228,170q-12,-12 -10.5,-27.5T233,116q39,-26 77.5,-42.5T396,48q18,-3 31,7t13,27ZM734,170q-14,14 -31,14.5T670,174q-26,-17 -52,-27t-57,-18q-18,-4 -29.5,-16.5T520,82q0,-17 12.5,-27t29.5,-7q48,9 87,25t79,43q14,10 16,26t-10,28ZM878,440q-19,0 -31,-11.5T831,398q-8,-31 -18.5,-56.5T785,289q-11,-16 -10,-32.5t15,-30.5q12,-12 27.5,-10t26.5,16q27,40 43,79t25,87q3,17 -7,29.5T878,440Z" />
</vector>

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="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="M280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v124q18,7 29,22t11,34v80q0,19 -11,34t-29,22v404q0,33 -23.5,56.5T680,920L280,920ZM394,640h172q14,0 24,-10t10,-24v-132q0,-14 -10,-24t-24,-10h-6v-40q0,-33 -23.5,-56.5T480,320q-33,0 -56.5,23.5T400,400v40h-6q-14,0 -24,10t-10,24v132q0,14 10,24t24,10ZM440,440v-40q0,-17 11.5,-28.5T480,360q17,0 28.5,11.5T520,400v40h-80Z" />
</vector>

View File

@@ -74,5 +74,24 @@
<string name="rate_on_google_play">قيم التطبيق علي جوجل بلاي</string>
<string name="selected">مُختار</string>
<string name="timer_settings_reset_info">أعد المؤقت لتغير الاعدادات</string>
<string name="help_with_translation">المساعدة في الترجمة</string>
<string name="help_with_translation">المساعدة في ترجمة Tomato</string>
<string name="hours_and_minutes_format">%1$dس %2$dد</string>
<string name="hours_format">%1$dس</string>
<string name="minutes_format">%1$dد</string>
<string name="about">عن</string>
<string name="help_with_translation_desc">ترجم Tomato إلى لغتك</string>
<string name="rate_on_google_play_desc">هل أعجبك التطبيق؟ اكتب مراجعة!</string>
<string name="bmc_desc">ادعمني بتبرع صغير</string>
<string name="tomato_plus_desc">التخصيص أكثر باستخدام Tomato+</string>
<string name="license">الترخيص</string>
<string name="media_volume_for_alarm">وضع سماعات الرأس</string>
<string name="media_volume_for_alarm_desc">يعمل فقط على سماعات الرأس. إذا كانت سماعات الرأس مفصولة، يصدر المنبه عبر سماعة الهاتف عند مستوى صوت الوسائط.</string>
<string name="session_only_progress">التقدم في الجلسة فقط</string>
<string name="session_only_progress_desc">عرض التقدم للجلسة الحالية بدلا من سلسلة الجلسات كلها</string>
<string name="auto_start_next_timer_desc">ابدأ المؤقت التالي بعد إيقاف المنبه</string>
<string name="auto_start_next_timer">التشغيل التلقائي في المؤقت القادم</string>
<string name="secure_aod">AOD الآمن</string>
<string name="secure_aod_desc">قم بقفل جهازك تلقائيا بعد انتهاء الوقت، مع إبقاء AOD مرئياً</string>
<string name="timer_reset_message">إعادة ضبط المؤقت</string>
<string name="undo">تراجع</string>
</resources>

View File

@@ -73,6 +73,19 @@
<string name="choose_language">Sprache wählen</string>
<string name="rate_on_google_play">Im Play Store bewerten</string>
<string name="selected">Ausgewählt</string>
<string name="help_with_translation">Hilf beim Übersetzen</string>
<string name="help_with_translation">Hilf beim Übersetzen von Tomato</string>
<string name="timer_settings_reset_info">Den Timer zurücksetzen um Einstellungen zu ändern</string>
<string name="hours_and_minutes_format">%1$dh%2$ddm</string>
<string name="hours_format">%1$dh</string>
<string name="minutes_format">%1$dm</string>
<string name="about">Über</string>
<string name="help_with_translation_desc">Übersetze Tomato in deine Sprache</string>
<string name="rate_on_google_play_desc">Dir gefällt die App? Schreibe eine Bewertung!</string>
<string name="bmc_desc">Unterstütze mich mit einer kleinen Spende</string>
<string name="tomato_plus_desc">Weiter anpassen mit Tomato+</string>
<string name="license">Lizenz</string>
<string name="media_volume_for_alarm">Kopfhörer Modus</string>
<string name="media_volume_for_alarm_desc">Spielt nur auf Kopfhörern. Wenn die Kopfhörer getrennt sind, wieder Alarm in Medienlautstärke abgespielt.</string>
<string name="session_only_progress">Sitzungs-Fortschritt</string>
<string name="session_only_progress_desc">Zeige in den Benachrichtigungen nur den Fortschritt der aktuellen Sitzung an, nicht die gesamte Sequenz.</string>
</resources>

View File

@@ -80,5 +80,10 @@
<string name="tomato_plus_desc">Personnalisez plus avec Tomato+</string>
<string name="about">A propos</string>
<string name="help_with_translation_desc">Traduire Tomato dans votre langue</string>
<string name="rate_on_google_play_desc">Vous aimez l\'appli? Écrivez un avis!</string>
<string name="rate_on_google_play_desc">Vous avez aimé l\'appli? Écrivez un avis!</string>
<string name="license">Licence</string>
<string name="media_volume_for_alarm">Mode casque</string>
<string name="session_only_progress">Progression de la session uniquement</string>
<string name="session_only_progress_desc">Montre uniquement la progression de la session actuelle, au lieu de la séquence entière.</string>
<string name="media_volume_for_alarm_desc">Fonctionne uniquement avec un casque. Si le casque est déconnecté, l\'alarme retentit via le haut-parleur au volume des médias.</string>
</resources>

View File

@@ -47,7 +47,7 @@
<string name="start_next">अगला शुरू करें</string>
<string name="stats">आँकड़े</string>
<string name="stop_alarm">अलार्म बंद करें</string>
<string name="stop_alarm_dialog_text">वर्तमान टाइमर सत्र पूरा हो गया है। अलार्म बंद करने के लिए कहीं भी टैप करें।</string>
<string name="stop_alarm_dialog_text">वर्तमान टाइमर पूरा हो गया है। अलार्म बंद करने के लिए कहीं भी टैप करें।</string>
<string name="stop_alarm_question">अलार्म बंद करें?</string>
<string name="system_default">सिस्टम</string>
<string name="theme">थीम</string>
@@ -72,6 +72,25 @@
<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="help_with_translation">Tomato के अनुवाद में मदद करें</string>
<string name="timer_settings_reset_info">सेटिंग्स बदलने के लिए टाइमर रीसेट करें</string>
<string name="hours_and_minutes_format">%1$dघं %2$dमि</string>
<string name="hours_format">%1$dघं</string>
<string name="minutes_format">%1$dमि</string>
<string name="about">ऐप के बारे में</string>
<string name="help_with_translation_desc">Tomato का अपनी भाषा में अनुवाद करें</string>
<string name="rate_on_google_play_desc">ऐप पसंद आया? रिव्यू लिखें!</string>
<string name="bmc_desc">एक छोटे से दान से मेरी मदद करें</string>
<string name="tomato_plus_desc">Tomato+ के साथ और कस्टमाइज़ करें</string>
<string name="license">लाइसेंस</string>
<string name="media_volume_for_alarm">हेडफ़ोन मोड</string>
<string name="media_volume_for_alarm_desc">अलार्म सिर्फ़ हेडफ़ोन पर बजता है. अगर हेडफ़ोन कनेक्ट नहीं हैं, तो अलार्म मीडिया वॉल्यूम पर स्पीकर से बजता है।</string>
<string name="session_only_progress">सरलीकृत प्रगति</string>
<string name="session_only_progress_desc">पूरे सत्र के बजाय, नोटिफ़िकेशन में केवल मौजूदा टाइमर की प्रगति दिखाएँ</string>
<string name="auto_start_next_timer_desc">अलार्म बंद करने के बाद अगला टाइमर शुरू करें</string>
<string name="auto_start_next_timer">टाइमर स्वयं आरंभ</string>
<string name="secure_aod">सुरक्षित AOD</string>
<string name="secure_aod_desc">तय समय के बाद डिवाइस को स्वयं लॉक कर दें, जबकि AOD दिखता रहे</string>
<string name="timer_reset_message">टाइमर रीसेट किया गया</string>
<string name="undo">पूर्ववत् करें</string>
</resources>

View File

@@ -74,13 +74,15 @@
<string name="always_on_display_desc">タイマー表示中に常時表示ディスプレイに切り替えるには任意の場所をタップしてください</string>
<string name="dnd_desc">タイマーの実行時にサイレントモードをオンにします</string>
<string name="tomato_foss_desc">全機能がこのバージョンでは解放されています。私のアプリが生活に変化をもたらしたのであれば、%1$s で寄付することで私を支援することをご検討ください。</string>
<string name="hours_and_minutes_format">%d時%d分</string>
<string name="hours_format">%d時</string>
<string name="minutes_format">%d分</string>
<string name="about">について</string>
<string name="hours_and_minutes_format">%1$d時%2$d分</string>
<string name="hours_format">%1$d時</string>
<string name="minutes_format">%1$d分</string>
<string name="about">このアプリについて</string>
<string name="help_with_translation_desc">Tomato をあなたの言語に翻訳してください</string>
<string name="rate_on_google_play_desc">アプリが気に入りましたか?レビューを書いてください!</string>
<string name="bmc_desc">小さな寄付でご支援してもよろしいでしょうか</string>
<string name="tomato_plus_desc">Tomato+ を使用してさらにカスタマイズする</string>
<string name="license">ライセンス</string>
<string name="media_volume_for_alarm">ヘッドホンモード</string>
<string name="media_volume_for_alarm_desc">ヘッドホンで再生します。ヘッドホンが接続されていない場合はスピーカーからメディアボリュームで再生されます。</string>
</resources>

View File

@@ -0,0 +1,3 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
</resources>

View File

@@ -75,13 +75,17 @@
<string name="selected">Seçilen</string>
<string name="help_with_translation">Tomato\'nun çevirisine yardım edin</string>
<string name="timer_settings_reset_info">Ayarları değiştirmek için zamanlayıcıyı sıfırlayın</string>
<string name="hours_and_minutes_format">%dsa %ddk</string>
<string name="hours_format">%dsa</string>
<string name="minutes_format">%ddk</string>
<string name="hours_and_minutes_format">%1$dsa %2$ddk</string>
<string name="hours_format">%1$dsa</string>
<string name="minutes_format">%1$ddk</string>
<string name="about">Hakkında</string>
<string name="help_with_translation_desc">Tomato\'yu kendi dilinize çevirin</string>
<string name="rate_on_google_play_desc">Uygulamayı beğendinizmi? Bir yorum bırakın!</string>
<string name="bmc_desc">Beni küçük bir bağışla destekleyin</string>
<string name="tomato_plus_desc">Tomato+ ile daha fazla özelleştirin</string>
<string name="license">Lisans</string>
<string name="media_volume_for_alarm">Kulaklık modu</string>
<string name="media_volume_for_alarm_desc">Sadece kulaklıktan oynatır. Eğer kulaklık devre dışı ise alarmı direkt medya sesi düzeyinde hoparlörden oynatır.</string>
<string name="session_only_progress">Yalnızca bu oturumun ilerlemesi</string>
<string name="session_only_progress_desc">Bildirimlerde tüm ilerlemeyi göstermektense yalnızca bu oturuma ait ilerlemeyi gösterir.</string>
</resources>

View File

@@ -76,12 +76,17 @@
<string name="selected">Обрано</string>
<string name="timer_settings_reset_info">Перезапустіть таймер, щоб змінити налаштування</string>
<string name="help_with_translation">Допомогти з перекладом Tomato</string>
<string name="hours_and_minutes_format">%dгод %dхв</string>
<string name="hours_format">%dгод</string>
<string name="minutes_format">%dхв</string>
<string name="hours_and_minutes_format">%1$dгод %2$dхв</string>
<string name="hours_format">%1$dгод</string>
<string name="minutes_format">%1$dхв</string>
<string name="about">Відомості</string>
<string name="help_with_translation_desc">Перекладіть Tomato на Вашу мову</string>
<string name="rate_on_google_play_desc">Сподобався додаток? Напишіть відгук!</string>
<string name="bmc_desc">Підтримайте мене невеликим пожертвуванням</string>
<string name="tomato_plus_desc">Налаштуйте ще більше з Tomato+</string>
<string name="license">Ліцензія</string>
<string name="media_volume_for_alarm">Режим навушників</string>
<string name="media_volume_for_alarm_desc">Сигнал грає лише в навушниках. Якщо навушники від\'єднані, сигнал грає через динамік на гучності звуку медіа.</string>
<string name="session_only_progress">Прогрес лише поточної сесії</string>
<string name="session_only_progress_desc">Показувати прогрес у сповіщенні лише для поточної сесії, а не для усього порядку.</string>
</resources>

View File

@@ -84,4 +84,8 @@
<string name="bmc_desc">小额捐赠支持</string>
<string name="tomato_plus_desc">用 Tomato+ 进一步定制</string>
<string name="license">许可证</string>
<string name="media_volume_for_alarm">耳机模式</string>
<string name="media_volume_for_alarm_desc">仅在插入耳机时播放。如果耳机连接断开,闹铃以媒体音量通过扬声器播放。</string>
<string name="session_only_progress">仅会话进度</string>
<string name="session_only_progress_desc">在通知中只显示当前会话的进度,而非完整序列的进度。</string>
</resources>

View File

@@ -75,6 +75,19 @@
<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="help_with_translation">協助翻譯 Tomato</string>
<string name="timer_settings_reset_info">重置計時器以變更設定</string>
<string name="hours_and_minutes_format">%1$d小時 %2$d分鐘</string>
<string name="hours_format">%1$d小時</string>
<string name="minutes_format">%1$d分鐘</string>
<string name="about">關於</string>
<string name="help_with_translation_desc">將 Tomato 翻譯成你的語言</string>
<string name="rate_on_google_play_desc">喜歡這個 App 嗎?寫個評論吧!</string>
<string name="bmc_desc">以小額捐款支持我</string>
<string name="tomato_plus_desc">使用 Tomato+ 進一步自訂</string>
<string name="license">授權條款</string>
<string name="media_volume_for_alarm">耳機模式</string>
<string name="media_volume_for_alarm_desc">僅透過耳機播放。若耳機斷線,鬧鐘將以媒體音量從喇叭播放。</string>
<string name="session_only_progress">僅顯示工作階段進度</string>
<string name="session_only_progress_desc">在通知中僅顯示目前工作階段的進度,而非完整流程。</string>
</resources>

View File

@@ -15,7 +15,7 @@
~ If not, see <https://www.gnu.org/licenses/>.
-->
<resources>
<resources xmlns:tools="http://schemas.android.com/tools">
<string name="alarm">Alarm</string>
<string name="alarm_desc">Ring alarm when a timer completes</string>
<string name="alarm_sound">Alarm sound</string>
@@ -68,7 +68,7 @@
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
<string name="rate_on_google_play">Rate on Google Play</string>
<string name="restart">Restart</string>
<string name="selected">Selected</string>
<string name="selected" tools:override="true">Selected</string>
<string name="session_length">Session length</string>
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
<string name="settings">Settings</string>
@@ -81,7 +81,7 @@
<string name="stats">Stats</string>
<string name="stop">Stop</string>
<string name="stop_alarm">Stop alarm</string>
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
<string name="stop_alarm_dialog_text">Current timer is complete. Tap anywhere to stop the alarm.</string>
<string name="stop_alarm_question">Stop Alarm?</string>
<string name="system_default">System</string>
<string name="theme">Theme</string>
@@ -105,6 +105,12 @@
<string name="license">License</string>
<string name="media_volume_for_alarm">Headphone mode</string>
<string name="media_volume_for_alarm_desc">Plays on headphones only. If headphones are disconnected, alarm plays through speaker at media volume.</string>
<string name="session_only_progress">Session-only progress</string>
<string name="session_only_progress_desc">Show progress for the current session only in notifications, rather than the full sequence.</string>
<string name="session_only_progress">Simplified progress</string>
<string name="session_only_progress_desc">Show progress for the current timer only in notifications, rather than the entire session</string>
<string name="auto_start_next_timer_desc">Start next timer after stopping an alarm</string>
<string name="auto_start_next_timer">Auto start next timer</string>
<string name="secure_aod">Secure AOD</string>
<string name="secure_aod_desc">Automatically lock your device after a timeout, while keeping the AOD visible</string>
<string name="timer_reset_message">Timer reset</string>
<string name="undo">Undo</string>
</resources>

View File

@@ -0,0 +1,9 @@
New features:
- AOD mode now uses a lighter font
- New option to auto start next session after stopping an alarm
- New option to disable locking screen while in AOD mode
- Accidentally reset the timer? You can now undo and correct your mistake ;)
Fixes:
- Improved stats screen performance and fixed lag while opening stats screen
- Fixed incorrect alignment of text in navigation toolbar

View File

@@ -12,7 +12,7 @@ ksp = "2.3.3"
lifecycleRuntimeKtx = "2.10.0"
materialKolor = "4.0.5"
navigation3 = "1.0.0"
revenuecat = "9.15.1"
revenuecat = "9.15.2"
room = "2.8.4"
vico = "2.3.6"