Merge branch 'dev'

This commit is contained in:
Nishant Mishra
2025-10-23 23:15:11 +05:30
53 changed files with 2591 additions and 1087 deletions

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/>.
*/
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
@@ -31,10 +41,10 @@ android {
defaultConfig {
applicationId = "org.nsh07.pomodoro"
minSdk = 26
minSdk = 27
targetSdk = 36
versionCode = 13
versionName = "1.5.0"
versionCode = 15
versionName = "1.6.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -1,4 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?><!--
~ 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/>.
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">

View File

@@ -1,3 +1,20 @@
/*
* 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
import android.os.Bundle
@@ -11,8 +28,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.nsh07.pomodoro.ui.AppScreen
import org.nsh07.pomodoro.ui.NavItem
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@@ -30,6 +45,12 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
appContainer.activityTurnScreenOn = {
setShowWhenLocked(it)
setTurnScreenOn(it)
}
setContent {
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
@@ -51,11 +72,18 @@ class MainActivity : ComponentActivity() {
appContainer.appTimerRepository.colorScheme = colorScheme
}
AppScreen(timerViewModel = timerViewModel)
AppScreen(
timerViewModel = timerViewModel,
isAODEnabled = preferencesState.aodEnabled,
setTimerFrequency = {
appContainer.appTimerRepository.timerFrequency = it
}
)
}
}
}
override fun onStop() {
super.onStop()
// Reduce the timer loop frequency when not visible to save battery power
@@ -67,27 +95,4 @@ class MainActivity : ComponentActivity() {
// Increase the timer loop frequency again when visible to make the progress smoother
appContainer.appTimerRepository.timerFrequency = 10f
}
companion object {
val screens = listOf(
NavItem(
Screen.Timer,
R.drawable.timer_outlined,
R.drawable.timer_filled,
R.string.timer
),
NavItem(
Screen.Stats,
R.drawable.monitoring,
R.drawable.monitoring_filled,
R.string.stats
),
NavItem(
Screen.Settings,
R.drawable.settings,
R.drawable.settings_filled,
R.string.settings
)
)
}
}

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.data
@@ -27,6 +37,7 @@ interface AppContainer {
val notificationBuilder: NotificationCompat.Builder
val timerState: MutableStateFlow<TimerState>
val time: MutableStateFlow<Long>
var activityTurnScreenOn: (Boolean) -> Unit
}
class DefaultAppContainer(context: Context) : AppContainer {
@@ -78,4 +89,6 @@ class DefaultAppContainer(context: Context) : AppContainer {
MutableStateFlow(appTimerRepository.focusTime)
}
override var activityTurnScreenOn: (Boolean) -> Unit = {}
}

View File

@@ -1,3 +1,20 @@
/*
* 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.annotation.SuppressLint
@@ -16,6 +33,7 @@ import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.asStateFlow
@@ -54,11 +72,12 @@ class TimerService : Service() {
private var pauseDuration = 0L
private var job = SupervisorJob()
private val scope = CoroutineScope(Dispatchers.IO + job)
private val timerScope = CoroutineScope(Dispatchers.IO + job)
private val skipScope = CoroutineScope(Dispatchers.IO + job)
private var alarm: MediaPlayer? = null
private var autoAlarmStopScope: Job? = null
private var alarm: MediaPlayer? = null
private val vibrator by lazy {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
@@ -138,7 +157,7 @@ class TimerService : Service() {
var iterations = -1
scope.launch {
timerScope.launch {
while (true) {
if (!timerState.value.timerRunning) break
if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
@@ -176,7 +195,10 @@ class TimerService : Service() {
}
}
@SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI
@SuppressLint(
"MissingPermission",
"StringFormatInvalid"
) // We check for the permission when pressing the Play button in the UI
fun showTimerNotification(
remainingTime: Int, paused: Boolean = false, complete: Boolean = false
) {
@@ -351,6 +373,13 @@ class TimerService : Service() {
fun startAlarm() {
if (timerRepository.alarmEnabled) alarm?.start()
appContainer.activityTurnScreenOn(true)
autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch {
delay(1 * 60 * 1000)
stopAlarm()
}
if (timerRepository.vibrateEnabled) {
if (!vibrator.hasVibrator()) {
return
@@ -363,6 +392,8 @@ class TimerService : Service() {
}
fun stopAlarm() {
autoAlarmStopScope?.cancel()
if (timerRepository.alarmEnabled) {
alarm?.pause()
alarm?.seekTo(0)
@@ -372,6 +403,8 @@ class TimerService : Service() {
vibrator.cancel()
}
appContainer.activityTurnScreenOn(false)
_timerState.update { currentState ->
currentState.copy(alarmRinging = false)
}

View File

@@ -0,0 +1,286 @@
/*
* 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
import android.app.Activity
import android.view.WindowManager
import androidx.activity.compose.LocalActivity
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.animateIntAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.CircularWavyProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.motionScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.LocalWindowInfo
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.view.WindowCompat
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.openRundeClock
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
import kotlin.random.Random
/**
* Always On Display composable. Must be called within a [SharedTransitionScope] which allows
* animating the clock and progress indicator
*
* @param timerState [TimerState] instance. This must be the same instance as the one used on the
* root [TimerScreen] composable
* @param progress lambda that returns the current progress of the clock
* randomized offset for the clock to allow smooth motion with sharedBounds
*/
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun SharedTransitionScope.AlwaysOnDisplay(
timerState: TimerState,
progress: () -> Float,
setTimerFrequency: (Float) -> Unit,
modifier: Modifier = Modifier
) {
var sharedElementTransitionComplete by remember { mutableStateOf(false) }
val activity = LocalActivity.current
val density = LocalDensity.current
val windowInfo = LocalWindowInfo.current
val view = LocalView.current
val window = remember { (view.context as Activity).window }
val insetsController = remember { WindowCompat.getInsetsController(window, view) }
DisposableEffect(Unit) {
setTimerFrequency(1f)
window.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
activity?.setShowWhenLocked(true)
insetsController.apply {
hide(WindowInsetsCompat.Type.statusBars())
hide(WindowInsetsCompat.Type.navigationBars())
systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
}
onDispose {
setTimerFrequency(10f)
window.clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
activity?.setShowWhenLocked(false)
insetsController.apply {
show(WindowInsetsCompat.Type.statusBars())
show(WindowInsetsCompat.Type.navigationBars())
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
}
}
}
LaunchedEffect(Unit) {
delay(300)
sharedElementTransitionComplete = true
}
val primary by animateColorAsState(
if (sharedElementTransitionComplete) Color(0xFFA2A2A2)
else {
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
else colorScheme.tertiary
},
animationSpec = motionScheme.slowEffectsSpec()
)
val secondaryContainer by animateColorAsState(
if (sharedElementTransitionComplete) Color(0xFF1D1D1D)
else {
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
else colorScheme.tertiaryContainer
},
animationSpec = motionScheme.slowEffectsSpec()
)
val surface by animateColorAsState(
if (sharedElementTransitionComplete) Color.Black
else colorScheme.surface,
animationSpec = motionScheme.slowEffectsSpec()
)
val onSurface by animateColorAsState(
if (sharedElementTransitionComplete) Color(0xFFE3E3E3)
else colorScheme.onSurface,
animationSpec = motionScheme.slowEffectsSpec()
)
var randomX by remember {
mutableIntStateOf(
Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.width - 266.dp.toIntPx(density)
)
)
}
var randomY by remember {
mutableIntStateOf(
Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.height - 266.dp.toIntPx(density)
)
)
}
LaunchedEffect(timerState.timeStr[1]) { // Randomize position every minute
if (sharedElementTransitionComplete) {
randomX = Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.width - 266.dp.toIntPx(density)
)
randomY = Random.nextInt(
16.dp.toIntPx(density),
windowInfo.containerSize.height - 266.dp.toIntPx(density)
)
}
}
val x by animateIntAsState(randomX, motionScheme.slowSpatialSpec())
val y by animateIntAsState(randomY, motionScheme.slowSpatialSpec())
Box(
modifier = modifier
.fillMaxSize()
.background(surface)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier.offset {
IntOffset(x, y)
}
) {
if (timerState.timerMode == TimerMode.FOCUS) {
CircularProgressIndicator(
progress = progress,
modifier = Modifier
.sharedBounds(
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.size(250.dp),
color = primary,
trackColor = secondaryContainer,
strokeWidth = 12.dp,
gapSize = 8.dp,
)
} else {
CircularWavyProgressIndicator(
progress = progress,
modifier = Modifier
.sharedBounds(
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.size(250.dp),
color = primary,
trackColor = secondaryContainer,
stroke = Stroke(
width = with(LocalDensity.current) {
12.dp.toPx()
},
cap = StrokeCap.Round,
),
trackStroke = Stroke(
width = with(LocalDensity.current) {
12.dp.toPx()
},
cap = StrokeCap.Round,
),
wavelength = 42.dp,
gapSize = 8.dp
)
}
Text(
text = timerState.timeStr,
style = TextStyle(
fontFamily = openRundeClock,
fontWeight = FontWeight.Bold,
fontSize = 56.sp,
letterSpacing = (-2).sp
),
textAlign = TextAlign.Center,
color = onSurface,
maxLines = 1,
modifier = Modifier.sharedBounds(
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
)
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
private fun AlwaysOnDisplayPreview() {
val timerState = TimerState()
val progress = { 0.5f }
TomatoTheme {
SharedTransitionLayout {
AlwaysOnDisplay(
timerState = timerState,
progress = progress,
setTimerFrequency = {}
)
}
}
}
fun Dp.toIntPx(density: Density) = with(density) { toPx().toInt() }

View File

@@ -1,18 +1,30 @@
/*
* 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
import android.content.Intent
import androidx.compose.animation.ContentTransform
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.Crossfade
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
import androidx.compose.foundation.layout.padding
@@ -42,7 +54,6 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass
import org.nsh07.pomodoro.MainActivity.Companion.screens
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
@@ -55,7 +66,9 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@Composable
fun AppScreen(
modifier: Modifier = Modifier,
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
isAODEnabled: Boolean,
setTimerFrequency: (Float) -> Unit
) {
val context = LocalContext.current
@@ -78,128 +91,153 @@ fun AppScreen(
}
}
Scaffold(
bottomBar = {
val wide = remember {
windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
)
}
ShortNavigationBar(
arrangement =
if (wide) ShortNavigationBarArrangement.Centered
else ShortNavigationBarArrangement.EqualWeight
AnimatedVisibility(
backStack.last() !is Screen.AOD,
enter = fadeIn(),
exit = fadeOut()
) {
screens.forEach {
val selected = backStack.last() == it.route
ShortNavigationBarItem(
selected = selected,
onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
{
if (backStack.size < 2) backStack.add(it.route)
else backStack[1] = it.route
}
} else {
{ if (backStack.size > 1) backStack.removeAt(1) }
},
icon = {
Crossfade(selected) { selected ->
if (selected) Icon(painterResource(it.selectedIcon), null)
else Icon(painterResource(it.unselectedIcon), null)
}
},
iconPosition =
if (wide) NavigationItemIconPosition.Start
else NavigationItemIconPosition.Top,
label = { Text(stringResource(it.label)) }
val wide = remember {
windowSizeClass.isWidthAtLeastBreakpoint(
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
)
}
ShortNavigationBar(
arrangement =
if (wide) ShortNavigationBarArrangement.Centered
else ShortNavigationBarArrangement.EqualWeight
) {
mainScreens.forEach {
val selected = backStack.last() == it.route
ShortNavigationBarItem(
selected = selected,
onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
{
if (backStack.size < 2) backStack.add(it.route)
else backStack[1] = it.route
}
} else {
{ if (backStack.size > 1) backStack.removeAt(1) }
},
icon = {
Crossfade(selected) { selected ->
if (selected) Icon(painterResource(it.selectedIcon), null)
else Icon(painterResource(it.unselectedIcon), null)
}
},
iconPosition =
if (wide) NavigationItemIconPosition.Start
else NavigationItemIconPosition.Top,
label = { Text(stringResource(it.label)) }
)
}
}
}
}
) { contentPadding ->
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
transitionSpec = {
ContentTransform(
fadeIn(motionScheme.defaultEffectsSpec()),
fadeOut(motionScheme.defaultEffectsSpec())
)
},
popTransitionSpec = {
ContentTransform(
fadeIn(motionScheme.defaultEffectsSpec()),
fadeOut(motionScheme.defaultEffectsSpec())
)
},
predictivePopTransitionSpec = {
ContentTransform(
fadeIn(motionScheme.defaultEffectsSpec()),
fadeOut(motionScheme.defaultEffectsSpec()) +
scaleOut(targetScale = 0.7f),
)
},
entryProvider = entryProvider {
entry<Screen.Timer> {
TimerScreen(
timerState = uiState,
progress = { progress },
onAction = { action ->
when (action) {
TimerAction.ResetTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.RESET.toString()
context.startService(it)
}
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
onBack = backStack::removeLastOrNull,
transitionSpec = {
fadeIn(motionScheme.defaultEffectsSpec())
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
},
popTransitionSpec = {
fadeIn(motionScheme.defaultEffectsSpec())
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
},
predictivePopTransitionSpec = {
fadeIn(motionScheme.defaultEffectsSpec())
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
},
entryProvider = entryProvider {
entry<Screen.Timer> {
TimerScreen(
timerState = uiState,
progress = { progress },
onAction = { action ->
when (action) {
TimerAction.ResetTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.RESET.toString()
context.startService(it)
}
is TimerAction.SkipTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.SKIP.toString()
context.startService(it)
}
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.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)
TimerAction.ToggleTimer ->
Intent(context, TimerService::class.java).also {
it.action = TimerService.Actions.TOGGLE.toString()
context.startService(it)
}
}
},
modifier = modifier
.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
.then(
if (isAODEnabled) Modifier.clickable {
if (backStack.size < 2) backStack.add(Screen.AOD)
}
}
},
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
else Modifier
),
)
)
}
}
entry<Screen.Settings> {
SettingsScreenRoot(
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
entry<Screen.AOD> {
AlwaysOnDisplay(
timerState = uiState,
progress = { progress },
setTimerFrequency = setTimerFrequency,
modifier = Modifier
.then(
if (isAODEnabled) Modifier.clickable {
if (backStack.size > 1) backStack.removeLastOrNull()
}
else Modifier
)
)
)
}
}
entry<Screen.Stats> {
StatsScreenRoot(
contentPadding = contentPadding,
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
entry<Screen.Settings.Main> {
SettingsScreenRoot(
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
)
)
}
entry<Screen.Stats> {
StatsScreenRoot(
contentPadding = contentPadding,
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),
end = contentPadding.calculateEndPadding(layoutDirection),
bottom = contentPadding.calculateBottomPadding()
)
)
}
}
}
)
)
}
}
}

View File

@@ -0,0 +1,62 @@
/*
* 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
import org.nsh07.pomodoro.R
val mainScreens = listOf(
NavItem(
Screen.Timer,
R.drawable.timer_outlined,
R.drawable.timer_filled,
R.string.timer
),
NavItem(
Screen.Stats,
R.drawable.monitoring,
R.drawable.monitoring_filled,
R.string.stats
),
NavItem(
Screen.Settings.Main,
R.drawable.settings,
R.drawable.settings_filled,
R.string.settings
)
)
val settingsScreens = listOf(
SettingsNavItem(
Screen.Settings.Timer,
R.drawable.timer_filled,
R.string.timer,
listOf(R.string.durations, R.string.session_length, R.string.always_on_display)
),
SettingsNavItem(
Screen.Settings.Alarm,
R.drawable.alarm,
R.string.alarm,
listOf(R.string.alarm_sound, R.string.alarm, R.string.vibrate)
),
SettingsNavItem(
Screen.Settings.Appearance,
R.drawable.palette,
R.string.appearance,
listOf(R.string.color_scheme, R.string.theme, R.string.black_theme)
)
)

View File

@@ -1,3 +1,20 @@
/*
* 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
import androidx.annotation.DrawableRes
@@ -10,7 +27,22 @@ sealed class Screen : NavKey {
object Timer : Screen()
@Serializable
object Settings : Screen()
object AOD : Screen()
@Serializable
sealed class Settings : Screen() {
@Serializable
object Main : Settings()
@Serializable
object Alarm : Settings()
@Serializable
object Appearance : Settings()
@Serializable
object Timer : Settings()
}
@Serializable
object Stats : Screen()
@@ -21,4 +53,11 @@ data class NavItem(
@param:DrawableRes val unselectedIcon: Int,
@param:DrawableRes val selectedIcon: Int,
@param:StringRes val label: Int
)
)
data class SettingsNavItem(
val route: Screen.Settings,
@param:DrawableRes val icon: Int,
@param:StringRes val label: Int,
val innerSettings: List<Int>
)

View File

@@ -1,158 +0,0 @@
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ColorPickerButton(
color: Color,
isSelected: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
IconButton(
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.iconButtonColors(containerColor = color),
modifier = modifier.size(48.dp),
onClick = onClick
) {
AnimatedContent(isSelected) { isSelected ->
when (isSelected) {
true -> Icon(
painterResource(R.drawable.check),
tint = Color.Black,
contentDescription = null
)
else ->
if (color == Color.White) Icon(
painterResource(R.drawable.colors),
tint = Color.Black,
contentDescription = null
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ColorSchemePickerDialog(
currentColor: Color,
modifier: Modifier = Modifier,
setShowDialog: (Boolean) -> Unit,
onColorChange: (Color) -> Unit,
) {
val colorSchemes = listOf(
Color(0xfffeb4a7), Color(0xffffb3c0), Color(0xfffcaaff), Color(0xffb9c3ff),
Color(0xff62d3ff), Color(0xff44d9f1), Color(0xff52dbc9), Color(0xff78dd77),
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
Color.White
)
BasicAlertDialog(
onDismissRequest = { setShowDialog(false) },
modifier = modifier
) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
color = colorScheme.surfaceContainer,
shape = shapes.extraLarge,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = stringResource(R.string.choose_color_scheme),
style = MaterialTheme.typography.headlineSmall
)
Spacer(Modifier.height(16.dp))
Column(Modifier.align(Alignment.CenterHorizontally)) {
(0..11 step 4).forEach {
Row {
colorSchemes.slice(it..it + 3).fastForEach { color ->
ColorPickerButton(
color,
color == currentColor,
modifier = Modifier.padding(4.dp)
) {
onColorChange(color)
}
}
}
}
ColorPickerButton(
colorSchemes.last(),
colorSchemes.last() == currentColor,
modifier = Modifier.padding(4.dp)
) {
onColorChange(colorSchemes.last())
}
}
Spacer(Modifier.height(24.dp))
TextButton(
shapes = ButtonDefaults.shapes(),
onClick = { setShowDialog(false) },
modifier = Modifier.align(Alignment.End)
) {
Text(stringResource(R.string.ok))
}
}
}
}
}
@Preview
@Composable
fun ColorPickerDialogPreview() {
var currentColor by remember { mutableStateOf(Color(0xfffeb4a7)) }
TomatoTheme(darkTheme = true) {
ColorSchemePickerDialog(
currentColor,
setShowDialog = {},
onColorChange = { currentColor = it }
)
}
}

View File

@@ -1,65 +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
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.ClickableListItem
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@Composable
fun ColorSchemePickerListItem(
color: Color,
items: Int,
index: Int,
onColorChange: (Color) -> Unit,
modifier: Modifier = Modifier
) {
var showDialog by rememberSaveable { mutableStateOf(false) }
if (showDialog) {
ColorSchemePickerDialog(
currentColor = color,
setShowDialog = { showDialog = it },
onColorChange = onColorChange
)
}
ClickableListItem(
leadingContent = {
Icon(
painter = painterResource(R.drawable.palette),
contentDescription = null,
tint = colorScheme.primary
)
},
headlineContent = { Text(stringResource(R.string.color_scheme)) },
supportingContent = {
Text(
if (color == Color.White) stringResource(R.string.dynamic)
else stringResource(R.string.color)
)
},
colors = listItemColors,
items = items,
index = index,
modifier = modifier.fillMaxWidth()
) { showDialog = true }
}

View File

@@ -1,25 +0,0 @@
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.compose.foundation.text.input.InputTransformation
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.insert
import androidx.core.text.isDigitsOnly
object MinutesInputTransformation : InputTransformation {
override fun TextFieldBuffer.transformInput() {
if (!this.asCharSequence().isDigitsOnly() || this.length > 2) {
revertAllChanges()
}
}
}
object MinutesOutputTransformation : OutputTransformation {
override fun TextFieldBuffer.transformOutput() {
if (this.length == 0) {
insert(0, "00")
} else if (this.toString().toInt() < 10) {
insert(0, "0")
}
}
}

View File

@@ -1,96 +1,80 @@
/*
* 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.settingsScreen
import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
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
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.ListItem
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.utils.toColor
@OptIn(ExperimentalMaterial3Api::class)
@@ -101,6 +85,8 @@ fun SettingsScreenRoot(
) {
val context = LocalContext.current
val backStack = viewModel.backStack
DisposableEffect(Unit) {
viewModel.runTextFieldFlowCollection()
onDispose { viewModel.cancelTextFieldFlowCollection() }
@@ -133,6 +119,7 @@ fun SettingsScreenRoot(
SettingsScreen(
preferencesState = preferencesState,
backStack = backStack,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
@@ -143,6 +130,7 @@ fun SettingsScreenRoot(
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
onBlackThemeChange = viewModel::saveBlackTheme,
onAodEnabledChange = viewModel::saveAodEnabled,
onAlarmSoundChanged = {
viewModel.saveAlarmSound(it)
Intent(context, TimerService::class.java).apply {
@@ -160,6 +148,7 @@ fun SettingsScreenRoot(
@Composable
private fun SettingsScreen(
preferencesState: PreferencesState,
backStack: SnapshotStateList<Screen.Settings>,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
@@ -170,394 +159,125 @@ private fun SettingsScreen(
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onBlackThemeChange: (Boolean) -> Unit,
onAodEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
val switchColors = SwitchDefaults.colors(
checkedIconColor = colorScheme.primary,
)
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val themeMap: Map<String, Pair<Int, String>> = remember {
mapOf(
"auto" to Pair(
R.drawable.brightness_auto,
context.getString(R.string.system_default)
),
"light" to Pair(R.drawable.light_mode, context.getString(R.string.light)),
"dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark))
)
}
val reverseThemeMap: Map<String, String> = remember {
mapOf(
context.getString(R.string.system_default) to "auto",
context.getString(R.string.light) to "light",
context.getString(R.string.dark) to "dark"
)
}
var alarmName by remember { mutableStateOf("...") }
LaunchedEffect(alarmSound) {
withContext(Dispatchers.IO) {
alarmName =
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
}
}
val ringtonePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
result.data?.getParcelableExtra(
RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
Uri::class.java
)
} else {
@Suppress("DEPRECATION")
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
onAlarmSoundChanged(uri)
}
}
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, stringResource(R.string.alarm_sound))
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
}
val switchItems = remember(preferencesState.blackTheme, alarmEnabled, vibrateEnabled) {
listOf(
SettingsSwitchItem(
checked = preferencesState.blackTheme,
icon = R.drawable.contrast,
label = context.getString(R.string.black_theme),
description = context.getString(R.string.black_theme_desc),
onClick = onBlackThemeChange
),
SettingsSwitchItem(
checked = alarmEnabled,
icon = R.drawable.alarm_on,
label = context.getString(R.string.alarm),
description = context.getString(R.string.alarm_desc),
onClick = onAlarmEnabledChange
),
SettingsSwitchItem(
checked = vibrateEnabled,
icon = R.drawable.mobile_vibrate,
label = context.getString(R.string.vibrate),
description = context.getString(R.string.vibrate_desc),
onClick = onVibrateEnabledChange
)
)
}
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
title = {
Text(
stringResource(R.string.settings),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)
)
},
subtitle = {},
colors = topBarColors,
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item {
Spacer(Modifier.height(12.dp))
}
item {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
stringResource(R.string.focus),
style = typography.titleSmallEmphasized
)
MinuteInputField(
state = focusTimeInputFieldState,
shape = RoundedCornerShape(
topStart = topListItemShape.topStart,
bottomStart = topListItemShape.topStart,
topEnd = topListItemShape.bottomStart,
bottomEnd = topListItemShape.bottomStart
),
imeAction = ImeAction.Next
)
}
Spacer(Modifier.width(2.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
stringResource(R.string.short_break),
style = typography.titleSmallEmphasized
)
MinuteInputField(
state = shortBreakTimeInputFieldState,
shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next
)
}
Spacer(Modifier.width(2.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
stringResource(R.string.long_break),
style = typography.titleSmallEmphasized
)
MinuteInputField(
state = longBreakTimeInputFieldState,
shape = RoundedCornerShape(
topStart = bottomListItemShape.topStart,
bottomStart = bottomListItemShape.topStart,
topEnd = bottomListItemShape.bottomStart,
bottomEnd = bottomListItemShape.bottomStart
),
imeAction = ImeAction.Done
)
}
}
}
item {
Spacer(Modifier.height(12.dp))
}
item {
ListItem(
leadingContent = {
Icon(
painterResource(R.drawable.clocks),
null
)
},
headlineContent = {
Text(stringResource(R.string.session_length))
},
supportingContent = {
Column {
NavDisplay(
backStack = backStack,
onBack = backStack::removeLastOrNull,
transitionSpec = {
(slideInHorizontally(initialOffsetX = { it }))
.togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
},
popTransitionSpec = {
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
},
predictivePopTransitionSpec = {
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
},
entryProvider = entryProvider {
entry<Screen.Settings.Main> {
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
title = {
Text(
stringResource(
R.string.session_length_desc,
sessionsSliderState.value.toInt()
stringResource(R.string.settings),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)
)
Slider(
state = sessionsSliderState,
modifier = Modifier.padding(vertical = 4.dp)
)
}
},
colors = listItemColors,
modifier = Modifier.clip(cardShape)
)
}
},
subtitle = {},
colors = topBarColors,
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior
)
item { Spacer(Modifier.height(12.dp)) }
item {
ColorSchemePickerListItem(
color = preferencesState.colorScheme.toColor(),
items = 3,
index = 0,
onColorChange = onColorSchemeChange
)
}
item {
ThemePickerListItem(
theme = preferencesState.theme,
themeMap = themeMap,
reverseThemeMap = reverseThemeMap,
onThemeChange = onThemeChange,
items = 3,
index = 1,
modifier = Modifier
.clip(middleListItemShape)
)
}
item {
val item = switchItems[0]
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
headlineContent = { Text(item.label) },
supportingContent = { Text(item.description) },
trailingContent = {
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
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(bottomListItemShape)
)
}
item { Spacer(Modifier.height(12.dp)) }
item {
ListItem(
leadingContent = {
Icon(painterResource(R.drawable.alarm), null)
},
headlineContent = { Text(stringResource(R.string.alarm_sound)) },
supportingContent = { Text(alarmName) },
colors = listItemColors,
modifier = Modifier
.clip(topListItemShape)
.clickable(onClick = { ringtonePickerLauncher.launch(intent) })
)
}
itemsIndexed(switchItems.drop(1)) { index, item ->
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
headlineContent = { Text(item.label) },
supportingContent = { Text(item.description) },
trailingContent = {
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
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) {
switchItems.lastIndex - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
)
}
item {
var expanded by remember { mutableStateOf(false) }
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxWidth()
) {
FilledTonalIconToggleButton(
checked = expanded,
onCheckedChange = { expanded = it },
shapes = IconButtonDefaults.toggleableShapes(),
modifier = Modifier.width(52.dp)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
Icon(
painterResource(R.drawable.info),
null
)
}
AnimatedVisibility(expanded) {
Text(
stringResource(R.string.pomodoro_info),
style = typography.bodyMedium,
color = colorScheme.onSurfaceVariant,
modifier = Modifier.padding(8.dp)
)
item { Spacer(Modifier.height(12.dp)) }
item { AboutCard() }
item { Spacer(Modifier.height(12.dp)) }
itemsIndexed(settingsScreens) { index, item ->
ClickableListItem(
leadingContent = {
Icon(painterResource(item.icon), null)
},
headlineContent = { Text(stringResource(item.label)) },
supportingContent = {
Text(
remember {
item.innerSettings.joinToString(", ") {
context.getString(it)
}
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
trailingContent = {
Icon(painterResource(R.drawable.arrow_forward_big), null)
},
items = settingsScreens.size,
index = index
) { backStack.add(item.route) }
}
item { Spacer(Modifier.height(12.dp)) }
}
}
}
entry<Screen.Settings.Alarm> {
AlarmSettings(
preferencesState = preferencesState,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
alarmSound = alarmSound,
onAlarmEnabledChange = onAlarmEnabledChange,
onVibrateEnabledChange = onVibrateEnabledChange,
onAlarmSoundChanged = onAlarmSoundChanged,
onBack = backStack::removeLastOrNull
)
}
entry<Screen.Settings.Appearance> {
AppearanceSettings(
preferencesState = preferencesState,
onBlackThemeChange = onBlackThemeChange,
onThemeChange = onThemeChange,
onColorSchemeChange = onColorSchemeChange,
onBack = backStack::removeLastOrNull
)
}
entry<Screen.Settings.Timer> {
TimerSettings(
aodEnabled = preferencesState.aodEnabled,
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
onAodEnabledChange = onAodEnabledChange,
onBack = backStack::removeLastOrNull
)
}
}
}
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview(
showSystemUi = true,
device = Devices.PIXEL_9_PRO
)
@Composable
fun SettingsScreenPreview() {
TomatoTheme {
SettingsScreen(
preferencesState = PreferencesState(),
focusTimeInputFieldState = rememberTextFieldState((25).toString()),
shortBreakTimeInputFieldState = rememberTextFieldState((5).toString()),
longBreakTimeInputFieldState = rememberTextFieldState((15).toString()),
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
alarmEnabled = true,
vibrateEnabled = true,
alarmSound = "null",
onAlarmEnabledChange = {},
onVibrateEnabledChange = {},
onBlackThemeChange = {},
onAlarmSoundChanged = {},
onThemeChange = {},
onColorSchemeChange = {},
modifier = Modifier.fillMaxSize()
)
}
}
data class SettingsSwitchItem(
val checked: Boolean,
@param:DrawableRes val icon: Int,
val label: String,
val description: String,
val onClick: (Boolean) -> Unit
)

View File

@@ -0,0 +1,29 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
data class SettingsSwitchItem(
val checked: Boolean,
@param:DrawableRes val icon: Int,
@param:StringRes val label: Int,
@param:StringRes val description: Int,
val onClick: (Boolean) -> Unit
)

View File

@@ -1,131 +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
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.selectedListItemColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ThemeDialog(
themeMap: Map<String, Pair<Int, String>>,
reverseThemeMap: Map<String, String>,
theme: String,
setShowThemeDialog: (Boolean) -> Unit,
onThemeChange: (String) -> Unit
) {
val selectedOption =
remember { mutableStateOf(themeMap[theme]!!.second) }
BasicAlertDialog(
onDismissRequest = { setShowThemeDialog(false) }
) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = shapes.extraLarge,
color = colorScheme.surfaceContainer,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = stringResource(R.string.choose_theme),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.selectableGroup()
) {
themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry<String, Pair<Int, String>> ->
val text = pair.value.second
val selected = text == selectedOption.value
ListItem(
leadingContent = {
AnimatedContent(selected) {
if (it)
Icon(painterResource(R.drawable.check), null)
else
Icon(painterResource(pair.value.first), null)
}
},
headlineContent = {
Text(text = text, style = MaterialTheme.typography.bodyLarge)
},
colors = if (!selected) listItemColors else selectedListItemColors,
modifier = Modifier
.height(64.dp)
.clip(
when (index) {
0 -> topListItemShape
themeMap.size - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
.selectable(
selected = (text == selectedOption.value),
onClick = {
selectedOption.value = text
onThemeChange(reverseThemeMap[selectedOption.value]!!)
},
role = Role.RadioButton
)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(
shapes = ButtonDefaults.shapes(),
onClick = { setShowThemeDialog(false) },
modifier = Modifier.align(Alignment.End)
) {
Text(stringResource(R.string.ok))
}
}
}
}
}

View File

@@ -1,63 +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
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.ClickableListItem
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@Composable
fun ThemePickerListItem(
theme: String,
themeMap: Map<String, Pair<Int, String>>,
reverseThemeMap: Map<String, String>,
items: Int,
index: Int,
onThemeChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
var showDialog by rememberSaveable { mutableStateOf(false) }
if (showDialog) {
ThemeDialog(
themeMap = themeMap,
reverseThemeMap = reverseThemeMap,
theme = theme,
setShowThemeDialog = { showDialog = it },
onThemeChange = onThemeChange
)
}
ClickableListItem(
leadingContent = {
Icon(
painter = painterResource(themeMap[theme]!!.first),
contentDescription = null
)
},
headlineContent = { Text(stringResource(R.string.theme)) },
supportingContent = {
Text(themeMap[theme]!!.second)
},
colors = listItemColors,
items = items,
index = index,
modifier = modifier.fillMaxWidth()
) { showDialog = true }
}

View File

@@ -0,0 +1,153 @@
/*
* 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 android.widget.Toast
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
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.LocalContext
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.BuildConfig
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
@Composable
fun AboutCard(modifier: Modifier = Modifier) {
val uriHandler = LocalUriHandler.current
val context = LocalContext.current
Card(
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = colorScheme.primaryContainer,
contentColor = colorScheme.onPrimaryContainer
),
shape = shapes.extraLarge
) {
val buttonColors = ButtonDefaults.buttonColors(
containerColor = colorScheme.onPrimaryContainer,
contentColor = colorScheme.primaryContainer
)
Row(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Column {
Text(
text = stringResource(R.string.app_name),
style = MaterialTheme.typography.titleLarge,
fontFamily = robotoFlexTopBar
)
Text(text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
}
Spacer(modifier = Modifier.weight(1f))
Row {
IconButton(
onClick = {
Toast.makeText(context, "Coming soon...", Toast.LENGTH_SHORT).show()
}
) {
Icon(
painterResource(R.drawable.discord),
contentDescription = "Discord",
modifier = Modifier.size(24.dp)
)
}
IconButton(
onClick = { uriHandler.openUri("https://github.com/nsh07/Tomato") }
) {
Icon(
painterResource(R.drawable.github),
contentDescription = "GitHub",
modifier = Modifier.size(24.dp)
)
}
}
}
FlowRow(
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
Button(
colors = buttonColors,
onClick = { uriHandler.openUri("https://coff.ee/nsh07") }
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
painterResource(R.drawable.coffee),
contentDescription = "Buy me a coffee",
)
Text(text = "Buy me a coffee")
}
}
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 = "Rate on Google Play",
modifier = Modifier.size(20.dp)
)
Text(text = "Rate on Google Play")
}
}
}
}
}

View File

@@ -1,4 +1,21 @@
package org.nsh07.pomodoro.ui
/*
* 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.animation.core.animateDpAsState
import androidx.compose.foundation.clickable
@@ -17,6 +34,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
@@ -27,7 +45,7 @@ fun ClickableListItem(
supportingContent: @Composable (() -> Unit)? = null,
leadingContent: @Composable (() -> Unit)? = null,
trailingContent: @Composable (() -> Unit)? = null,
colors: ListItemColors = ListItemDefaults.colors(),
colors: ListItemColors = listItemColors,
tonalElevation: Dp = ListItemDefaults.Elevation,
shadowElevation: Dp = ListItemDefaults.Elevation,
items: Int,

View File

@@ -0,0 +1,189 @@
/*
* 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.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
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
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@Composable
fun ColorSchemePickerListItem(
color: Color,
items: Int,
index: Int,
onColorChange: (Color) -> Unit,
modifier: Modifier = Modifier
) {
val colorSchemes = listOf(
Color(0xfffeb4a7), Color(0xffffb3c0), Color(0xfffcaaff), Color(0xffb9c3ff),
Color(0xff62d3ff), Color(0xff44d9f1), Color(0xff52dbc9), Color(0xff78dd77),
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
Color.White
)
Column(
modifier
.clip(
when (index) {
0 -> topListItemShape
items - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
) {
ListItem(
leadingContent = {
Icon(
painterResource(R.drawable.colors),
null
)
},
headlineContent = { Text("Dynamic color") },
supportingContent = { Text("Adapt theme colors from your wallpaper") },
trailingContent = {
val checked = color == colorSchemes.last()
Switch(
checked = checked,
onCheckedChange = {
if (it) onColorChange(colorSchemes.last())
else onColorChange(colorSchemes.first())
},
thumbContent = {
if (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(middleListItemShape)
)
Spacer(Modifier.height(2.dp))
ListItem(
leadingContent = {
Icon(
painter = painterResource(R.drawable.palette),
contentDescription = null,
tint = colorScheme.primary
)
},
headlineContent = { Text(stringResource(R.string.color_scheme)) },
supportingContent = {
Text(
if (color == Color.White) stringResource(R.string.dynamic)
else stringResource(R.string.color)
)
},
colors = listItemColors,
modifier = Modifier.clip(middleListItemShape)
)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.background(listItemColors.containerColor)
.padding(bottom = 8.dp)
) {
LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) {
items(colorSchemes.dropLast(1)) {
ColorPickerButton(
it,
it == color,
modifier = Modifier.padding(4.dp)
) {
onColorChange(it)
}
}
}
}
}
}
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ColorPickerButton(
color: Color,
isSelected: Boolean,
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
IconButton(
shapes = IconButtonDefaults.shapes(),
colors = IconButtonDefaults.iconButtonColors(containerColor = color),
modifier = modifier.size(48.dp),
onClick = onClick
) {
AnimatedContent(isSelected) { isSelected ->
when (isSelected) {
true -> Icon(
painterResource(R.drawable.check),
tint = Color.Black,
contentDescription = null
)
else ->
if (color == Color.White) Icon(
painterResource(R.drawable.colors),
tint = Color.Black,
contentDescription = null
)
}
}
}
}

View File

@@ -1,4 +1,21 @@
package org.nsh07.pomodoro.ui.settingsScreen
/*
* 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.animation.animateColorAsState
import androidx.compose.foundation.background

View File

@@ -0,0 +1,42 @@
/*
* 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.text.input.InputTransformation
import androidx.compose.foundation.text.input.OutputTransformation
import androidx.compose.foundation.text.input.TextFieldBuffer
import androidx.compose.foundation.text.input.insert
import androidx.core.text.isDigitsOnly
object MinutesInputTransformation : InputTransformation {
override fun TextFieldBuffer.transformInput() {
if (!this.asCharSequence().isDigitsOnly() || this.length > 2) {
revertAllChanges()
}
}
}
object MinutesOutputTransformation : OutputTransformation {
override fun TextFieldBuffer.transformOutput() {
if (this.length == 0) {
insert(0, "00")
} else if (this.toString().toInt() < 10) {
insert(0, "0")
}
}
}

View File

@@ -0,0 +1,125 @@
/*
* 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.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ThemePickerListItem(
theme: String,
items: Int,
index: Int,
onThemeChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val themeMap: Map<String, Pair<Int, Int>> = remember {
mapOf(
"auto" to Pair(
R.drawable.brightness_auto,
R.string.system_default
),
"light" to Pair(R.drawable.light_mode, R.string.light),
"dark" to Pair(R.drawable.dark_mode, R.string.dark)
)
}
Column(
modifier
.clip(
when (index) {
0 -> topListItemShape
items - 1 -> bottomListItemShape
else -> middleListItemShape
},
),
) {
ListItem(
leadingContent = {
AnimatedContent(themeMap[theme]!!.first) {
Icon(
painter = painterResource(it),
contentDescription = null,
)
}
},
headlineContent = { Text(stringResource(R.string.theme)) },
colors = listItemColors,
)
val options = themeMap.toList()
val selectedIndex = options.indexOf(Pair(theme, themeMap[theme]))
Row(
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
modifier = Modifier
.background(listItemColors.containerColor)
.padding(start = 52.dp, end = 16.dp, bottom = 8.dp)
) {
options.forEachIndexed { index, theme ->
val isSelected = selectedIndex == index
ToggleButton(
checked = isSelected,
onCheckedChange = { onThemeChange(theme.first) },
modifier = Modifier
.weight(1f)
.semantics { role = Role.RadioButton },
shapes =
when (index) {
0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
},
) {
Text(
stringResource(theme.second.second),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
}
}
}
}

View File

@@ -0,0 +1,254 @@
/*
* 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.screens
import android.app.Activity
import android.content.Intent
import android.media.RingtoneManager
import android.net.Uri
import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
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.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AlarmSettings(
preferencesState: PreferencesState,
alarmEnabled: Boolean,
vibrateEnabled: Boolean,
alarmSound: String,
onAlarmEnabledChange: (Boolean) -> Unit,
onVibrateEnabledChange: (Boolean) -> Unit,
onAlarmSoundChanged: (Uri?) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val context = LocalContext.current
var alarmName by remember { mutableStateOf("...") }
LaunchedEffect(alarmSound) {
withContext(Dispatchers.IO) {
alarmName =
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
}
}
val ringtonePickerLauncher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
if (result.resultCode == Activity.RESULT_OK) {
val uri =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
result.data?.getParcelableExtra(
RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
Uri::class.java
)
} else {
@Suppress("DEPRECATION")
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
}
onAlarmSoundChanged(uri)
}
}
val ringtonePickerIntent = remember(alarmSound) {
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
}
}
val switchItems = remember(
preferencesState.blackTheme,
preferencesState.aodEnabled,
alarmEnabled,
vibrateEnabled
) {
listOf(
SettingsSwitchItem(
checked = alarmEnabled,
icon = R.drawable.alarm_on,
label = R.string.sound,
description = R.string.alarm_desc,
onClick = onAlarmEnabledChange
),
SettingsSwitchItem(
checked = vibrateEnabled,
icon = R.drawable.mobile_vibrate,
label = R.string.vibrate,
description = R.string.vibrate_desc,
onClick = onVibrateEnabledChange
)
)
}
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text(stringResource(R.string.settings))
},
navigationIcon = {
IconButton(onBack) {
Icon(
painterResource(R.drawable.arrow_back),
null
)
}
},
colors = topBarColors,
scrollBehavior = scrollBehavior
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item {
Spacer(Modifier.height(14.dp))
}
item {
ListItem(
leadingContent = {
Icon(painterResource(R.drawable.alarm), null)
},
headlineContent = { Text(stringResource(R.string.alarm_sound)) },
supportingContent = { Text(alarmName) },
colors = listItemColors,
modifier = Modifier
.clip(topListItemShape)
.clickable(onClick = { ringtonePickerLauncher.launch(ringtonePickerIntent) })
)
}
itemsIndexed(switchItems) { index, item ->
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
headlineContent = { Text(stringResource(item.label)) },
supportingContent = { Text(stringResource(item.description)) },
trailingContent = {
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
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) {
switchItems.lastIndex -> bottomListItemShape
else -> middleListItemShape
}
)
)
}
item { Spacer(Modifier.height(12.dp)) }
}
}
}
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Preview
@Composable
fun AlarmSettingsPreview() {
val preferencesState = PreferencesState()
AlarmSettings(
preferencesState = preferencesState,
alarmEnabled = true,
vibrateEnabled = false,
alarmSound = "",
onAlarmEnabledChange = {},
onVibrateEnabledChange = {},
onAlarmSoundChanged = {},
onBack = {})
}

View File

@@ -0,0 +1,178 @@
/*
* 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.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.utils.toColor
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun AppearanceSettings(
preferencesState: PreferencesState,
onBlackThemeChange: (Boolean) -> Unit,
onThemeChange: (String) -> Unit,
onColorSchemeChange: (Color) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text(stringResource(R.string.settings))
},
navigationIcon = {
IconButton(onBack) {
Icon(
painterResource(R.drawable.arrow_back),
null
)
}
},
colors = topBarColors,
scrollBehavior = scrollBehavior
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item {
Spacer(Modifier.height(14.dp))
}
item {
ColorSchemePickerListItem(
color = preferencesState.colorScheme.toColor(),
items = 3,
index = 0,
onColorChange = onColorSchemeChange
)
}
item {
ThemePickerListItem(
theme = preferencesState.theme,
onThemeChange = onThemeChange,
items = 3,
index = 1,
modifier = Modifier
.clip(middleListItemShape)
)
}
item {
val item = SettingsSwitchItem(
checked = preferencesState.blackTheme,
icon = R.drawable.contrast,
label = R.string.black_theme,
description = R.string.black_theme_desc,
onClick = onBlackThemeChange
)
ListItem(
leadingContent = {
Icon(painterResource(item.icon), contentDescription = null)
},
headlineContent = { Text(stringResource(item.label)) },
supportingContent = { Text(stringResource(item.description)) },
trailingContent = {
Switch(
checked = item.checked,
onCheckedChange = { item.onClick(it) },
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(bottomListItemShape)
)
}
item { Spacer(Modifier.height(12.dp)) }
}
}
}
@Preview
@Composable
fun AppearanceSettingsPreview() {
val preferencesState = PreferencesState()
AppearanceSettings(
preferencesState = preferencesState,
onBlackThemeChange = {},
onThemeChange = {},
onColorSchemeChange = {},
onBack = {}
)
}

View File

@@ -0,0 +1,317 @@
/*
* 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.screens
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.FilledTonalIconToggleButton
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.LargeFlexibleTopAppBar
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.typography
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderState
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TimerSettings(
aodEnabled: Boolean,
focusTimeInputFieldState: TextFieldState,
shortBreakTimeInputFieldState: TextFieldState,
longBreakTimeInputFieldState: TextFieldState,
sessionsSliderState: SliderState,
onAodEnabledChange: (Boolean) -> Unit,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text(stringResource(R.string.settings))
},
navigationIcon = {
IconButton(onBack) {
Icon(
painterResource(R.drawable.arrow_back),
null
)
}
},
colors = topBarColors,
scrollBehavior = scrollBehavior
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item {
Spacer(Modifier.height(14.dp))
}
item {
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
stringResource(R.string.focus),
style = typography.titleSmallEmphasized
)
MinuteInputField(
state = focusTimeInputFieldState,
shape = RoundedCornerShape(
topStart = topListItemShape.topStart,
bottomStart = topListItemShape.topStart,
topEnd = topListItemShape.bottomStart,
bottomEnd = topListItemShape.bottomStart
),
imeAction = ImeAction.Next
)
}
Spacer(Modifier.width(2.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
stringResource(R.string.short_break),
style = typography.titleSmallEmphasized
)
MinuteInputField(
state = shortBreakTimeInputFieldState,
shape = RoundedCornerShape(middleListItemShape.topStart),
imeAction = ImeAction.Next
)
}
Spacer(Modifier.width(2.dp))
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Text(
stringResource(R.string.long_break),
style = typography.titleSmallEmphasized
)
MinuteInputField(
state = longBreakTimeInputFieldState,
shape = RoundedCornerShape(
topStart = bottomListItemShape.topStart,
bottomStart = bottomListItemShape.topStart,
topEnd = bottomListItemShape.bottomStart,
bottomEnd = bottomListItemShape.bottomStart
),
imeAction = ImeAction.Done
)
}
}
}
item {
Spacer(Modifier.height(12.dp))
}
item {
ListItem(
leadingContent = {
Icon(painterResource(R.drawable.clocks), null)
},
headlineContent = {
Text(stringResource(R.string.session_length))
},
supportingContent = {
Column {
Text(
stringResource(
R.string.session_length_desc,
sessionsSliderState.value.toInt()
)
)
Slider(
state = sessionsSliderState,
modifier = Modifier.padding(vertical = 4.dp)
)
}
},
colors = listItemColors,
modifier = Modifier.clip(cardShape)
)
}
item { Spacer(Modifier.height(12.dp)) }
item {
val item = SettingsSwitchItem(
checked = aodEnabled,
icon = R.drawable.aod,
label = R.string.always_on_display,
description = R.string.always_on_display_desc,
onClick = onAodEnabledChange
)
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) },
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(cardShape)
)
}
item {
var expanded by remember { mutableStateOf(false) }
Column(
horizontalAlignment = Alignment.End,
modifier = Modifier
.padding(vertical = 6.dp)
.fillMaxWidth()
) {
FilledTonalIconToggleButton(
checked = expanded,
onCheckedChange = { expanded = it },
shapes = IconButtonDefaults.toggleableShapes(),
modifier = Modifier.width(52.dp)
) {
Icon(
painterResource(R.drawable.info),
null
)
}
AnimatedVisibility(expanded) {
Text(
stringResource(R.string.pomodoro_info),
style = typography.bodyMedium,
color = colorScheme.onSurfaceVariant,
modifier = Modifier.padding(8.dp)
)
}
}
}
item { Spacer(Modifier.height(12.dp)) }
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Preview
@Composable
private fun TimerSettingsPreview() {
val focusTimeInputFieldState = TextFieldState("25")
val shortBreakTimeInputFieldState = TextFieldState("5")
val longBreakTimeInputFieldState = TextFieldState("15")
val sessionsSliderState = SliderState(
value = 4f,
valueRange = 1f..8f,
steps = 6
)
TimerSettings(
focusTimeInputFieldState = focusTimeInputFieldState,
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
sessionsSliderState = sessionsSliderState,
aodEnabled = true,
onBack = {},
onAodEnabledChange = {}
)
}

View File

@@ -14,5 +14,6 @@ import androidx.compose.ui.graphics.Color
data class PreferencesState(
val theme: String = "auto",
val colorScheme: String = Color.White.toString(),
val blackTheme: Boolean = false
val blackTheme: Boolean = false,
val aodEnabled: Boolean = false
)

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.settingsScreen.viewModel
@@ -11,6 +21,7 @@ import android.net.Uri
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.SliderState
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.graphics.Color
import androidx.lifecycle.ViewModel
@@ -31,12 +42,15 @@ import kotlinx.coroutines.launch
import org.nsh07.pomodoro.TomatoApplication
import org.nsh07.pomodoro.data.AppPreferenceRepository
import org.nsh07.pomodoro.data.TimerRepository
import org.nsh07.pomodoro.ui.Screen
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
class SettingsViewModel(
private val preferenceRepository: AppPreferenceRepository,
private val timerRepository: TimerRepository,
) : ViewModel() {
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
private val _preferencesState = MutableStateFlow(PreferencesState())
val preferencesState = _preferencesState.asStateFlow()
@@ -80,12 +94,15 @@ class SettingsViewModel(
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
?: preferenceRepository.saveBooleanPreference("black_theme", false)
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_preferencesState.update { currentState ->
currentState.copy(
theme = theme,
colorScheme = colorScheme,
blackTheme = blackTheme
blackTheme = blackTheme,
aodEnabled = aodEnabled
)
}
}
@@ -196,6 +213,15 @@ class SettingsViewModel(
}
}
fun saveAodEnabled(aodEnabled: Boolean) {
viewModelScope.launch {
_preferencesState.update { currentState ->
currentState.copy(aodEnabled = aodEnabled)
}
preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
}
}
companion object {
val Factory: ViewModelProvider.Factory = viewModelFactory {
initializer {

View File

@@ -1,9 +1,28 @@
/*
* 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.theme
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ListItemColors
import androidx.compose.material3.ListItemDefaults
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.SwitchColors
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
@@ -41,4 +60,9 @@ object CustomColors {
supportingColor = colorScheme.onSecondaryFixedVariant,
trailingIconColor = colorScheme.onSecondaryFixedVariant
)
val switchColors: SwitchColors
@Composable get() = SwitchDefaults.colors(
checkedIconColor = colorScheme.primary,
)
}

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
@@ -75,4 +85,4 @@ fun AlarmDialog(
}
}
}
}
}

View File

@@ -13,6 +13,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.SharedTransitionLayout
import androidx.compose.animation.SharedTransitionScope
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
@@ -81,6 +83,7 @@ import androidx.compose.ui.tooling.preview.Devices
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 org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
@@ -91,7 +94,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun TimerScreen(
fun SharedTransitionScope.TimerScreen(
timerState: TimerState,
progress: () -> Float,
onAction: (TimerAction) -> Unit,
@@ -209,6 +212,12 @@ fun TimerScreen(
CircularProgressIndicator(
progress = progress,
modifier = Modifier
.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"focus progress"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
@@ -221,6 +230,12 @@ fun TimerScreen(
CircularWavyProgressIndicator(
progress = progress,
modifier = Modifier
.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState(
"break progress"
),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
.widthIn(max = 350.dp)
.fillMaxWidth(0.9f)
.aspectRatio(1f),
@@ -261,7 +276,11 @@ fun TimerScreen(
letterSpacing = (-2).sp
),
textAlign = TextAlign.Center,
maxLines = 1
maxLines = 1,
modifier = Modifier.sharedBounds(
sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
animatedVisibilityScope = LocalNavAnimatedContentScope.current
)
)
AnimatedVisibility(
expanded,
@@ -519,11 +538,13 @@ fun TimerScreenPreview() {
)
TomatoTheme {
Surface {
TimerScreen(
timerState,
{ 0.3f },
{}
)
SharedTransitionLayout {
TimerScreen(
timerState,
{ 0.3f },
{}
)
}
}
}
}

View File

@@ -95,6 +95,9 @@ class TimerViewModel(
)
).toUri()
preferenceRepository.getBooleanPreference("aod_enabled")
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
_time.update { timerRepository.focusTime }
cycles = 0
startTime = 0L

View File

@@ -0,0 +1,16 @@
<!--
~ 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/>.
-->
<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="M360,480h240q17,0 28.5,-11.5T640,440q0,-17 -11.5,-28.5T600,400L360,400q-17,0 -28.5,11.5T320,440q0,17 11.5,28.5T360,480ZM400,620h160q17,0 28.5,-11.5T600,580q0,-17 -11.5,-28.5T560,540L400,540q-17,0 -28.5,11.5T360,580q0,17 11.5,28.5T400,620ZM280,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,920Z" />
</vector>

View File

@@ -0,0 +1,21 @@
<!--
~ 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 Foobar. 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="m313,520 l196,196q12,12 11.5,28T508,772q-12,11 -28,11.5T452,772L188,508q-6,-6 -8.5,-13t-2.5,-15q0,-8 2.5,-15t8.5,-13l264,-264q11,-11 27.5,-11t28.5,11q12,12 12,28.5T508,245L313,440h447q17,0 28.5,11.5T800,480q0,17 -11.5,28.5T760,520L313,520Z" />
</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="m321,880 l-71,-71 329,-329 -329,-329 71,-71 400,400L321,880Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<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="M440,720q-117,0 -198.5,-81.5T160,440v-240q0,-33 23.5,-56.5T240,120h500q58,0 99,41t41,99q0,58 -41,99t-99,41h-20v40q0,117 -81.5,198.5T440,720ZM240,320h400v-120L240,200v120ZM720,320h20q25,0 42.5,-17.5T800,260q0,-25 -17.5,-42.5T740,200h-20v120ZM200,840q-17,0 -28.5,-11.5T160,800q0,-17 11.5,-28.5T200,760h560q17,0 28.5,11.5T800,800q0,17 -11.5,28.5T760,840L200,840Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<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="M20.317,4.37a19.791,19.791 0,0 0,-4.885 -1.515,0.074 0.074,0 0,0 -0.079,0.037c-0.211,0.375 -0.445,0.865 -0.608,1.25 -1.845,-0.276 -3.68,-0.276 -5.487,0 -0.164,-0.393 -0.406,-0.874 -0.618,-1.25a0.077,0.077 0,0 0,-0.079 -0.037,19.736 19.736,0 0,0 -4.885,1.515 0.07,0.07 0,0 0,-0.032 0.028C0.533,9.046 -0.319,13.58 0.099,18.058a0.082,0.082 0,0 0,0.031 0.056c2.053,1.508 4.041,2.423 5.993,3.029a0.078,0.078 0,0 0,0.084 -0.028c0.462,-0.63 0.873,-1.295 1.226,-1.994a0.076,0.076 0,0 0,-0.042 -0.106c-0.653,-0.248 -1.274,-0.549 -1.872,-0.892a0.077,0.077 0,0 1,-0.008 -0.128c0.126,-0.094 0.252,-0.192 0.372,-0.291a0.074,0.074 0,0 1,0.078 -0.01c3.928,1.793 8.18,1.793 12.061,0a0.074,0.074 0,0 1,0.079 0.009c0.12,0.099 0.246,0.198 0.373,0.292a0.077,0.077 0,0 1,-0.007 0.128,12.299 12.299,0 0,1 -1.873,0.891 0.077,0.077 0,0 0,-0.041 0.107c0.36,0.698 0.772,1.363 1.225,1.993a0.076,0.076 0,0 0,0.084 0.029c1.961,-0.607 3.95,-1.522 6.002,-3.029a0.077,0.077 0,0 0,0.031 -0.055c0.5,-5.177 -0.838,-9.674 -3.549,-13.66a0.061,0.061 0,0 0,-0.031 -0.029zM8.02,15.331c-1.183,0 -2.157,-1.086 -2.157,-2.419 0,-1.333 0.956,-2.419 2.157,-2.419 1.211,0 2.176,1.095 2.157,2.419 0,1.333 -0.956,2.419 -2.157,2.419zM15.995,15.331c-1.183,0 -2.157,-1.086 -2.157,-2.419 0,-1.333 0.955,-2.419 2.157,-2.419 1.211,0 2.176,1.095 2.157,2.419 0,1.333 -0.946,2.419 -2.157,2.419Z" />
</vector>

View File

@@ -0,0 +1,9 @@
<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="M12,0.297c-6.63,0 -12,5.373 -12,12 0,5.303 3.438,9.8 8.205,11.385 0.6,0.113 0.82,-0.258 0.82,-0.577 0,-0.285 -0.01,-1.04 -0.015,-2.04 -3.338,0.724 -4.042,-1.61 -4.042,-1.61C4.422,18.07 3.633,17.7 3.633,17.7c-1.087,-0.744 0.084,-0.729 0.084,-0.729 1.205,0.084 1.838,1.236 1.838,1.236 1.07,1.835 2.809,1.305 3.495,0.998 0.108,-0.776 0.417,-1.305 0.76,-1.605 -2.665,-0.3 -5.466,-1.332 -5.466,-5.93 0,-1.31 0.465,-2.38 1.235,-3.22 -0.135,-0.303 -0.54,-1.523 0.105,-3.176 0,0 1.005,-0.322 3.3,1.23 0.96,-0.267 1.98,-0.399 3,-0.405 1.02,0.006 2.04,0.138 3,0.405 2.28,-1.552 3.285,-1.23 3.285,-1.23 0.645,1.653 0.24,2.873 0.12,3.176 0.765,0.84 1.23,1.91 1.23,3.22 0,4.61 -2.805,5.625 -5.475,5.92 0.42,0.36 0.81,1.096 0.81,2.22 0,1.606 -0.015,2.896 -0.015,3.286 0,0.315 0.21,0.69 0.825,0.57C20.565,22.092 24,17.592 24,12.297c0,-6.627 -5.373,-12 -12,-12" />
</vector>

View File

@@ -0,0 +1,10 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="6.35"
android:viewportHeight="6.35">
<path
android:fillColor="#000000"
android:pathData="M0.6744,6.3347C0.5991,6.3185 0.471,6.275 0.4362,6.2538 0.4204,6.2442 0.7343,5.9434 1.9712,4.7827L3.5262,3.3236 4.159,3.8713C4.5071,4.1725 4.7868,4.4231 4.7806,4.4283 4.767,4.4398 1.3189,6.2241 1.2202,6.2708 1.0678,6.3428 0.8376,6.3698 0.6744,6.3347ZM0.0712,5.8885C-0.0032,5.736 0,5.8606 0,3.1751c0,-2.6939 -0.0037,-2.5574 0.0735,-2.7187 0.0195,-0.0409 0.0396,-0.0743 0.0445,-0.0743 0.0049,0 0.7041,0.6015 1.5538,1.3366L3.2165,3.0555 1.6709,4.5056C0.8208,5.3031 0.1205,5.9557 0.1146,5.9557c-0.0059,0 -0.0254,-0.0302 -0.0435,-0.0672zM4.5028,3.6306 L3.826,3.0448 4.377,2.5266C4.6801,2.2415 4.9336,2.0083 4.9405,2.0083c0.0168,0 0.9615,0.4887 1.0486,0.5425C6.2037,2.6831 6.35,2.9363 6.35,3.1751 6.35,3.4137 6.2088,3.6595 5.9939,3.7948 5.9253,3.838 5.1959,4.2187 5.1846,4.2171 5.1818,4.2168 4.875,3.9528 4.5028,3.6306ZM3.241,2.5383C3.099,2.4141 2.409,1.8158 1.7076,1.2088 0.5332,0.1924 0.4344,0.1038 0.4572,0.0882 0.5059,0.0547 0.6943,0.008 0.8067,0.0015 0.9426,-0.0064 1.0774,0.0175 1.2002,0.0715 1.247,0.092 2.0206,0.4896 2.9193,0.955L4.5534,1.8012 4.0396,2.2834C3.757,2.5485 3.5198,2.7652 3.5125,2.7648 3.5052,2.7644 3.383,2.6625 3.241,2.5383Z"
android:strokeWidth="0.0120146" />
</vector>

View File

@@ -36,4 +36,7 @@
<string name="stats">ئامار</string>
<string name="more_info">زانیاری زیاتر</string>
<string name="pause">وەستاندن</string>
<string name="paused">پشوو</string>
<string name="completed">تەواوکراو</string>
<string name="up_next_notification">دوای ئەمە</string>
</resources>

View File

@@ -16,7 +16,7 @@
<string name="productivity_analysis_desc">Duración de la concentración en diferentes momentos del día</string>
<string name="alarm_sound">Sonido de la alarma</string>
<string name="black_theme">Tema negro</string>
<string name="black_theme_desc">Utiliza un tema oscuro negro puro</string>
<string name="black_theme_desc">Utilizar un tema oscuro negro puro</string>
<string name="alarm_desc">Sonar alarma cuando el temporizador finalice</string>
<string name="vibrate">Vibrar</string>
<string name="vibrate_desc">Vibrar cuando el temporizador finalice</string>
@@ -30,7 +30,7 @@
<string name="break_">Descanso</string>
<string name="last_week">Semana pasada</string>
<string name="focus_per_day_avg">concentración por día (avg)</string>
<string name="more_info">Más informes</string>
<string name="more_info">Más información</string>
<string name="weekly_productivity_analysis">Análisis de productividad semanal</string>
<string name="last_month">Mes pasado</string>
<string name="monthly_productivity_analysis">Análisis de productividad mensual</string>
@@ -56,4 +56,5 @@
<string name="color">Color</string>
<string name="light">Luz</string>
<string name="dark">Oscuro</string>
<string name="last_year">Año pasado</string>
</resources>

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="start">Démarrer</string>
<string name="stop">Arrêter</string>
<string name="focus">Concentration</string>
<string name="short_break">Pause courte</string>
<string name="long_break">Pause longue</string>
<string name="exit">Quitter</string>
<string name="skip">Passer</string>
<string name="stop_alarm">Arrêter l\'alarme</string>
<string name="min_remaining_notification">%1$s minutes restantes</string>
<string name="paused">En pause</string>
<string name="completed">Terminé</string>
<string name="up_next_notification">À venir : %1$s (%2$s)</string>
<string name="start_next">Démarrer l\'intervalle suivant</string>
<string name="choose_color_scheme">Choisir le thème de couleurs</string>
<string name="ok">OK</string>
<string name="color_scheme">Thème de couleurs</string>
<string name="dynamic">Dynamique</string>
<string name="color">Couleur</string>
<string name="system_default">Valeur par défaut du système</string>
<string name="alarm">Alarme</string>
<string name="light">Clair</string>
<string name="dark">Sombre</string>
<string name="choose_theme">Choisir le thème</string>
<string name="productivity_analysis">Analyse de productivité</string>
<string name="productivity_analysis_desc">Durée de concentration selon les moments de la journée</string>
<string name="alarm_sound">Son de l\'alarme</string>
<string name="black_theme">Thème noir</string>
<string name="black_theme_desc">Utiliser un thème sombre noir pur</string>
<string name="alarm_desc">Faire sonner lalarme à la fin du minuteur</string>
<string name="vibrate">Vibrer</string>
<string name="vibrate_desc">Faire vibrer à la fin du minuteur</string>
<string name="theme">Thème</string>
<string name="settings">Paramètres</string>
<string name="session_length">Durée de la session</string>
<string name="session_length_desc">Intervalles de concentration par session : %1$d</string>
<string name="pomodoro_info">Une \"session\" est une séquence dintervalles Pomodoro comprenant des phases de concentration, des pauses courtes et une pause longue. La dernière pause dune session est toujours une pause longue.</string>
<string name="stats">Statistiques</string>
<string name="today">Aujourd\'hui</string>
<string name="break_">Pause</string>
<string name="last_week">7 derniers jours</string>
<string name="focus_per_day_avg">concentration moyenne par jour</string>
<string name="more_info">Plus d\'infos</string>
<string name="weekly_productivity_analysis">Analyse hebdomadaire de la productivité</string>
<string name="last_month">30 derniers jours</string>
<string name="monthly_productivity_analysis">Analyse mensuelle de la productivité</string>
<string name="stop_alarm_question">Arrêter l\'alarme?</string>
<string name="stop_alarm_dialog_text">La session actuelle est terminée. Touchez nimporte où pour arrêter lalarme.</string>
<string name="timer_session_count">%1$d sur %2$d</string>
<string name="more">Plus</string>
<string name="pause">Mettre en pause</string>
<string name="play">Démarrer la session</string>
<string name="restart">Redémarrer</string>
<string name="skip_to_next">Passer au suivant</string>
<string name="up_next">À venir</string>
<string name="timer">Minuteur</string>
<string name="timer_progress">Progression du minuteur</string>
<string name="last_year">12 derniers mois</string>
<string name="always_on_display_desc">Appuyez n\'importe où lors de l\'affichage du minuteur pour passer en mode AOD</string>
<string name="always_on_display">Affichage Permanent</string>
</resources>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="start">Başlat</string>
<string name="stop">Durdur</string>
<string name="focus">Odaklan</string>
<string name="short_break">Kısa Mola</string>
<string name="long_break">Uzun Mola</string>
<string name="exit">Çıkış</string>
<string name="skip">Atla</string>
<string name="stop_alarm">Alarmı Durdur</string>
<string name="min_remaining_notification">%1$s dk kaldı</string>
<string name="paused">Duraklatıldı</string>
<string name="completed">Tamamlandı</string>
<string name="up_next_notification">Sırada: %1$s (%2$s)</string>
<string name="start_next">Sıradakini Başlat</string>
<string name="choose_color_scheme">Renk şeması seçin</string>
<string name="ok">Tamam</string>
<string name="color_scheme">Renk şeması</string>
<string name="dynamic">Dinamik</string>
<string name="color">Renk</string>
<string name="system_default">Sistem varsayılanı</string>
<string name="light">ık</string>
<string name="dark">Koyu</string>
<string name="choose_theme">Tema seçin</string>
<string name="productivity_analysis">Verimlilik analizi</string>
<string name="productivity_analysis_desc">Günün farklı saatlerindeki odaklanma süreleri</string>
<string name="alarm_sound">Alarm sesi</string>
<string name="black_theme">Siyah tema</string>
<string name="black_theme_desc">Tam siyah koyu tema kullan</string>
<string name="alarm_desc">Zamanlayıcı bittiğinde alarm çal</string>
<string name="vibrate">Titreşim</string>
<string name="vibrate_desc">Zamanlayıcı bittiğinde titre</string>
<string name="theme">Tema</string>
<string name="settings">Ayarlar</string>
<string name="session_length">Oturum uzunluğu</string>
<string name="session_length_desc">Bir oturumdaki odaklanma aralığı: %1$d</string>
<string name="pomodoro_info">\"Oturum\", odaklanma aralıkları, kısa mola aralıkları ve bir uzun mola aralığı içeren bir pomodoro aralıkları dizisidir. Bir oturumun son molası her zaman uzun moladır.</string>
<string name="stats">İstatistikler</string>
<string name="today">Bugün</string>
<string name="break_">Mola</string>
<string name="last_week">Geçen hafta</string>
<string name="focus_per_day_avg">günlük odaklanma (ortalama)</string>
<string name="more_info">Daha fazla bilgi</string>
<string name="weekly_productivity_analysis">Haftalık verimlilik analizi</string>
<string name="last_month">Geçen ay</string>
<string name="monthly_productivity_analysis">Aylık verimlilik analizi</string>
<string name="stop_alarm_question">Alarmı Durdur?</string>
<string name="stop_alarm_dialog_text">Mevcut zamanlayıcı oturumu tamamlandı. Alarmı durdurmak için herhangi bir yere dokunun.</string>
<string name="timer_session_count">%1$d / %2$d</string>
<string name="more">Daha fazla</string>
<string name="pause">Duraklat</string>
<string name="play">Devam Et</string>
<string name="restart">Yeniden Başlat</string>
<string name="skip_to_next">Sıradakine Atla</string>
<string name="up_next">Sıradaki</string>
<string name="timer">Zamanlayıcı</string>
<string name="timer_progress">Zamanlayıcı İlerlemesi</string>
<string name="last_year">Geçen yıl</string>
<string name="alarm">Alarm</string>
<string name="always_on_display">Her zaman açık ekran</string>
<string name="always_on_display_desc">Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun</string>
<string name="appearance">Görünüm</string>
<string name="durations">Süreler</string>
</resources>

View File

@@ -55,5 +55,8 @@
<string name="up_next">接下来是</string>
<string name="timer">计时</string>
<string name="timer_progress">计时器进度</string>
<string name="timer_session_count">%1$d 的 %2$d</string>
<string name="timer_session_count">%2$d 的 %1$d</string>
<string name="last_year">去年</string>
<string name="always_on_display">息屏显示</string>
<string name="always_on_display_desc">查看计时器时点击任意位置切换至 AOD 模式</string>
</resources>

View File

@@ -1,60 +1,82 @@
<!--
~ 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/>.
-->
<resources>
<string name="app_name">Tomato</string>
<string name="start">Start</string>
<string name="stop">Stop</string>
<string name="focus">Focus</string>
<string name="short_break">Short break</string>
<string name="long_break">Long break</string>
<string name="exit">Exit</string>
<string name="skip">Skip</string>
<string name="stop_alarm">Stop alarm</string>
<string name="min_remaining_notification">%1$s min remaining</string>
<string name="paused">Paused</string>
<string name="completed">Completed</string>
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
<string name="start_next">Start next</string>
<string name="choose_color_scheme">Choose color scheme</string>
<string name="ok">OK</string>
<string name="color_scheme">Color scheme</string>
<string name="dynamic">Dynamic</string>
<string name="color">Color</string>
<string name="system_default">System default</string>
<string name="alarm">Alarm</string>
<string name="light">Light</string>
<string name="dark">Dark</string>
<string name="choose_theme">Choose theme</string>
<string name="productivity_analysis">Productivity analysis</string>
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
<string name="alarm_desc">Ring alarm when a timer completes</string>
<string name="alarm_sound">Alarm sound</string>
<string name="always_on_display">Always On Display</string>
<string name="always_on_display_desc">Tap anywhere when viewing the timer to switch to AOD mode</string>
<string name="app_name">Tomato</string>
<string name="black_theme">Black theme</string>
<string name="black_theme_desc">Use a pure black dark theme</string>
<string name="alarm_desc">Ring alarm when a timer completes</string>
<string name="vibrate">Vibrate</string>
<string name="vibrate_desc">Vibrate when a timer completes</string>
<string name="theme">Theme</string>
<string name="settings">Settings</string>
<string name="break_">Break</string>
<string name="choose_color_scheme">Choose color scheme</string>
<string name="choose_theme">Choose theme</string>
<string name="color">Color</string>
<string name="color_scheme">Color scheme</string>
<string name="completed">Completed</string>
<string name="dark">Dark</string>
<string name="dynamic">Dynamic</string>
<string name="exit">Exit</string>
<string name="focus">Focus</string>
<string name="focus_per_day_avg">focus per day (avg)</string>
<string name="last_month">Last month</string>
<string name="last_week">Last week</string>
<string name="last_year">Last year</string>
<string name="light">Light</string>
<string name="long_break">Long break</string>
<string name="min_remaining_notification">%1$s min remaining</string>
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
<string name="more">More</string>
<string name="more_info">More info</string>
<string name="ok">OK</string>
<string name="pause">Pause</string>
<string name="paused">Paused</string>
<string name="play">Play</string>
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
<string name="productivity_analysis">Productivity analysis</string>
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
<string name="restart">Restart</string>
<string name="session_length">Session length</string>
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
<string name="stats">Stats</string>
<string name="today">Today</string>
<string name="break_">Break</string>
<string name="last_week">Last week</string>
<string name="focus_per_day_avg">focus per day (avg)</string>
<string name="more_info">More info</string>
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
<string name="last_month">Last month</string>
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
<string name="stop_alarm_question">Stop Alarm?</string>
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
<string name="timer_session_count">%1$d of %2$d</string>
<string name="more">More</string>
<string name="pause">Pause</string>
<string name="play">Play</string>
<string name="restart">Restart</string>
<string name="settings">Settings</string>
<string name="short_break">Short break</string>
<string name="skip">Skip</string>
<string name="skip_to_next">Skip to next</string>
<string name="up_next">Up next</string>
<string name="start">Start</string>
<string name="start_next">Start next</string>
<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_question">Stop Alarm?</string>
<string name="system_default">System</string>
<string name="theme">Theme</string>
<string name="timer">Timer</string>
<string name="timer_progress">Timer progress</string>
<string name="last_year">Last year</string>
<string name="timer_session_count">%1$d of %2$d</string>
<string name="today">Today</string>
<string name="up_next">Up next</string>
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
<string name="vibrate">Vibration</string>
<string name="vibrate_desc">Vibrate when a timer completes</string>
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
<string name="appearance">Appearance</string>
<string name="durations">Durations</string>
<string name="sound">Sound</string>
</resources>

View File

@@ -0,0 +1,6 @@
New features:
- New Always On Display option: click anywhere while on the Timer screen to turn on Always On Display mode, tap again to turn it off
Translators on Weblate helped add support for French and Turkish in this update
The AOD feature is still in development. Suggest features and report bugs at https://github.com/nsh07/tomato/issues

View File

@@ -0,0 +1,8 @@
New features:
- New Always On Display option
- Redesigned and simplified Settings screen
- New theme and color scheme selector
- You can now dismiss alarms without unlocking your phone
Enhancements:
- Alarms now automatically stop ringing after 1 minute

View File

@@ -0,0 +1,12 @@
<i>Tomato</i> est un minuteur Pomodoro minimaliste pour Android, conçu selon les principes de Material 3 Expressive.
Tomato est entièrement gratuit et open-source, pour toujours. Le code source est disponible sur GitHub : https://github.com/nsh07/Tomato, où vous pouvez aussi signaler des bugs ou proposer de nouvelles fonctionnalités.
<b>Fonctionnalités:</b>
- Interface simple et minimaliste, conforme aux dernières recommandations Material 3 Expressive
- Statistiques détaillées du temps de travail/étude, présentées de manière claire et intuitive
- Statistiques du jour accessibles immédiatement
- Graphiques simple et lisibles de votre semaine et votre mois
- Analyse de vos heures les plus productives dans la semaine et le mois
- Paramètres du minuteur entièrement personnalisables
- Compatibilité avec les Live Updates dAndroid 16

View File

@@ -0,0 +1 @@
Minuteur Pomodoro minimaliste

View File

@@ -0,0 +1,12 @@
<i>Tomato</i>, Material 3 Expressive tabanlı, Android için minimalist bir Pomodoro sayacıdır.
Tomato tamamen ücretsizdir ve sonsuza kadar açık kaynaklı kalacaktır. Kaynak koduna ulaşmak, hata bildirmek veya özellik önermek için: https://github.com/nsh07/Tomato
<b>Özellikler:</b>
- En son Material 3 Expressive yönergelerine dayalı basit, minimalist kullanıcı arayüzü
- Çalışma/ders çalışma sürelerinizin kolay anlaşılır şekilde sunulan ayrıntılı istatistikleri
- Güncel güne ait istatistikler bir bakışta görülebilir
- Son hafta ve son aya ait istatistikler, okunması kolay ve temiz bir grafikte gösterilir
- Son hafta ve son aya ait, günün hangi saatinde en üretken olduğunuzu gösteren ek istatistikler
- Özelleştirilebilir zamanlayıcı parametreleri
- Android 16 Canlı Güncellemeler (Live Updates) desteği

View File

@@ -0,0 +1 @@
Minimalist Pomodoro sayacı

View File

@@ -1 +1,12 @@
<p><i>Tomato</i> - мінімалістичний Pomodoro таймер для Android на базі Material 3 Expressive.</p><p><br><b>Особливості:</b></p><ul><li>Простий, мінімалістичний інтерфейс на основі останніх рекомендацій Material 3 Expressive</li><li>Детальна статистика робочого/навчального часу в зрозумілій формі<ul><li>Статистика за поточний день, доступна з одного погляду</li><li>Статистика за останній тиждень і останній місяць, представлена у вигляді зручного для сприйняття чіткого графіку</li><li>Додаткова статистика за останній тиждень і місяць, що показує, в який час дня ви були найбільш продуктивні</li></ul></li><li>Настроювані параметри таймера</li></ul>
<i>Tomato</i> - мінімалістичний Pomodoro таймер для Android на базі Material 3 Expressive.
Tomato повністю безкоштовний та з відкритим вихідним кодом. Ви можете знайти вихідний код і повідомляти про помилки й пропонувати функції на GitHub: https://github.com/nsh07/Tomato.
<b>Особливості:</b>
- Простий, мінімалістичний інтерфейс на основі останніх рекомендацій Material 3 Expressive
- Детальна статистика робочого/навчального часу в зрозумілій формі
- Статистика за поточний день, доступна з одного погляду
- Статистика за останній тиждень і останній місяць, представлена у вигляді зручного для сприйняття чіткого графіку
- Додаткова статистика за останній тиждень і місяць, що показує, в який час дня ви були найбільш продуктивні
- Настроювані параметри таймера
- Підтримка Live Updates (для пристроїв на Android 16)

View File

@@ -1 +1,12 @@
<p><i>Tomato</i> 是一个基于Material 3 Expressive的安卓极简主义番茄钟.</p><p><br><b>功能:</b></p><ul><li>基于最新Material 3 Expressive指南的简洁用户界面</li><li>以便于理解的方式提供工作/学习的详细统计数据<ul><li>当日统计数据一目了然</li><li>清楚易读的上周和上月统计图表</li><li>上周和上月的额外统计数据帮您找到一天中最高效的时间段</li></ul></li><li>可自定义的计时器参数</li></ul>
<i>Tomato</i> 是一个基于Material 3 Expressive的安卓极简主义番茄钟.
Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误bug或建议新功能请访问 https://github.com/nsh07/Tomato。
<b>功能:</b>
- 基于最新Material 3 Expressive指南的简洁用户界面
- 以便于理解的方式提供工作/学习的详细统计数据
- 当日统计数据一目了然
- 清楚易读的上周和上月统计图表
- 上周和上月的额外统计数据帮您找到一天中最高效的时间段
- 可自定义的计时器参数
- 支持 Android 16 即時更新 Android 16 Live Updates

View File

@@ -1,18 +1,18 @@
[versions]
activityCompose = "1.11.0"
adaptive = "1.1.0"
adaptive = "1.2.0"
agp = "8.11.2"
composeBom = "2025.10.00"
composeBom = "2025.10.01"
coreKtx = "1.17.0"
espressoCore = "3.7.0"
junit = "4.13.2"
junitVersion = "1.3.0"
kotlin = "2.2.20"
kotlin = "2.2.21"
ksp = "2.2.20-2.0.4"
lifecycleRuntimeKtx = "2.9.4"
materialKolor = "3.0.1"
navigation3 = "1.0.0-alpha11"
room = "2.8.2"
navigation3 = "1.0.0-beta01"
room = "2.8.3"
vico = "2.2.1"
[libraries]