From e2dc4f8dd22c95eaeed8d223ad777da348e3e7ec Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 20 Oct 2025 06:56:08 +0200 Subject: [PATCH 01/35] Translated using Weblate (Kurdish (Central)) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 68.9% (40 of 58 strings) Translated using Weblate (Spanish) Currently translated at 86.2% (50 of 58 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 98.2% (57 of 58 strings) Translated using Weblate (Ukrainian) Currently translated at 100.0% (13 of 13 strings) Co-authored-by: Hosted Weblate Co-authored-by: Javier Mtz Co-authored-by: mastawbeqezwan Co-authored-by: vintagezero Co-authored-by: 大王叫我来巡山 Translate-URL: https://hosted.weblate.org/projects/tomato/store-descriptions/uk/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/ckb/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/es/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/zh_Hans/ Translation: Tomato/Store descriptions Translation: Tomato/Strings --- app/src/main/res/values-ckb/strings.xml | 3 +++ app/src/main/res/values-es/strings.xml | 5 +++-- app/src/main/res/values-zh-rCN/strings.xml | 2 +- fastlane/metadata/android/uk/full_description.txt | 13 ++++++++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/app/src/main/res/values-ckb/strings.xml b/app/src/main/res/values-ckb/strings.xml index b5a25e8..c22541d 100644 --- a/app/src/main/res/values-ckb/strings.xml +++ b/app/src/main/res/values-ckb/strings.xml @@ -36,4 +36,7 @@ ئامار زانیاری زیاتر وەستاندن + پشوو + تەواوکراو + دوای ئەمە diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 5a729c0..9c43c91 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -16,7 +16,7 @@ Duración de la concentración en diferentes momentos del día Sonido de la alarma Tema negro - Utiliza un tema oscuro negro puro + Utilizar un tema oscuro negro puro Sonar alarma cuando el temporizador finalice Vibrar Vibrar cuando el temporizador finalice @@ -30,7 +30,7 @@ Descanso Semana pasada concentración por día (avg) - Más informes + Más información Análisis de productividad semanal Mes pasado Análisis de productividad mensual @@ -56,4 +56,5 @@ Color Luz Oscuro + Año pasado diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fcc8a7d..b273b02 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -55,5 +55,5 @@ 接下来是 计时 计时器进度 - %1$d 的 %2$d + %2$d 中的 %1$d diff --git a/fastlane/metadata/android/uk/full_description.txt b/fastlane/metadata/android/uk/full_description.txt index 5c73fb5..175d3c5 100644 --- a/fastlane/metadata/android/uk/full_description.txt +++ b/fastlane/metadata/android/uk/full_description.txt @@ -1 +1,12 @@ -

Tomato - мінімалістичний Pomodoro таймер для Android на базі Material 3 Expressive.


Особливості:

  • Простий, мінімалістичний інтерфейс на основі останніх рекомендацій Material 3 Expressive
  • Детальна статистика робочого/навчального часу в зрозумілій формі
    • Статистика за поточний день, доступна з одного погляду
    • Статистика за останній тиждень і останній місяць, представлена у вигляді зручного для сприйняття чіткого графіку
    • Додаткова статистика за останній тиждень і місяць, що показує, в який час дня ви були найбільш продуктивні
  • Настроювані параметри таймера
+Tomato - мінімалістичний Pomodoro таймер для Android на базі Material 3 Expressive. + +Tomato повністю безкоштовний та з відкритим вихідним кодом. Ви можете знайти вихідний код і повідомляти про помилки й пропонувати функції на GitHub: https://github.com/nsh07/Tomato. + +Особливості: +- Простий, мінімалістичний інтерфейс на основі останніх рекомендацій Material 3 Expressive +- Детальна статистика робочого/навчального часу в зрозумілій формі + - Статистика за поточний день, доступна з одного погляду + - Статистика за останній тиждень і останній місяць, представлена у вигляді зручного для сприйняття чіткого графіку + - Додаткова статистика за останній тиждень і місяць, що показує, в який час дня ви були найбільш продуктивні +- Настроювані параметри таймера +- Підтримка Live Updates (для пристроїв на Android 16) From 01c75077c7ab575a989adc3142a867ce8235cf03 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 20 Oct 2025 11:53:44 +0530 Subject: [PATCH 02/35] feat(ui): implement a simple click mechanism to switch to AOD --- .../java/org/nsh07/pomodoro/MainActivity.kt | 2 +- .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 271 ++++++++++-------- 2 files changed, 151 insertions(+), 122 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index baf7924..9a40b41 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -51,7 +51,7 @@ class MainActivity : ComponentActivity() { appContainer.appTimerRepository.colorScheme = colorScheme } - AppScreen(timerViewModel = timerViewModel) + AppScreen(timerViewModel = timerViewModel, isAODEnabled = true) } } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 59390fd..4bbdc6e 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -8,13 +8,17 @@ package org.nsh07.pomodoro.ui import android.content.Intent +import androidx.compose.animation.AnimatedContent import androidx.compose.animation.ContentTransform import androidx.compose.animation.Crossfade 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.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -25,13 +29,17 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.ShortNavigationBar import androidx.compose.material3.ShortNavigationBarArrangement import androidx.compose.material3.ShortNavigationBarItem +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource @@ -55,7 +63,8 @@ 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 ) { val context = LocalContext.current @@ -69,6 +78,7 @@ fun AppScreen( val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val backStack = rememberNavBackStack(Screen.Timer) + var showAOD by remember { mutableStateOf(false) } if (uiState.alarmRinging) AlarmDialog { @@ -78,128 +88,147 @@ fun AppScreen( } } - Scaffold( - bottomBar = { - val wide = remember { - windowSizeClass.isWidthAtLeastBreakpoint( - WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND + AnimatedContent( + showAOD, + transitionSpec = { fadeIn().togetherWith(fadeOut()) } + ) { aod -> + if (!aod) { + Scaffold( + bottomBar = { + val wide = remember { + windowSizeClass.isWidthAtLeastBreakpoint( + WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND + ) + } + ShortNavigationBar( + arrangement = + if (wide) ShortNavigationBarArrangement.Centered + else ShortNavigationBarArrangement.EqualWeight + ) { + 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)) } + ) + } + } + }, + modifier = Modifier + .then( + if (isAODEnabled) Modifier.clickable { showAOD = !showAOD } + else Modifier + ) + ) { 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 { + 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) + } + + TimerAction.StopAlarm -> + Intent(context, TimerService::class.java).also { + it.action = + TimerService.Actions.STOP_ALARM.toString() + context.startService(it) + } + + TimerAction.ToggleTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.TOGGLE.toString() + context.startService(it) + } + } + }, + modifier = modifier.padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding() + ) + ) + } + + entry { + SettingsScreenRoot( + modifier = modifier.padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding() + ) + ) + } + + entry { + StatsScreenRoot( + contentPadding = contentPadding, + modifier = modifier.padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding() + ) + ) + } + } ) } - ShortNavigationBar( - arrangement = - if (wide) ShortNavigationBarArrangement.Centered - else ShortNavigationBarArrangement.EqualWeight - ) { - 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)) } - ) - } - } + } else { + Surface( + color = Color.Black, + modifier = Modifier + .fillMaxSize() + .clickable { showAOD = !showAOD }) {} } - ) { 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 { - 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) - } - - TimerAction.StopAlarm -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.STOP_ALARM.toString() - context.startService(it) - } - - TimerAction.ToggleTimer -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.TOGGLE.toString() - context.startService(it) - } - } - }, - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() - ) - ) - } - - entry { - SettingsScreenRoot( - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() - ) - ) - } - - entry { - StatsScreenRoot( - contentPadding = contentPadding, - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() - ) - ) - } - } - ) } } \ No newline at end of file From 33f47dc4c7c0d478eb9cb34621c3f4830d35ce59 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 20 Oct 2025 13:40:39 +0530 Subject: [PATCH 03/35] feat(ui): implement an AOD screen and transition animations --- .../org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 196 ++++++++++++ .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 296 +++++++++--------- .../main/java/org/nsh07/pomodoro/ui/Screen.kt | 3 + .../pomodoro/ui/timerScreen/TimerScreen.kt | 35 ++- 4 files changed, 378 insertions(+), 152 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt new file mode 100644 index 0000000..e96d4dc --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -0,0 +1,196 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.nsh07.pomodoro.ui + +import android.app.Activity +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.animateColorAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +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.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.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.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.viewModel.TimerMode +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SharedTransitionScope.AlwaysOnDisplay( + timerState: TimerState, + progress: () -> Float, + modifier: Modifier = Modifier +) { + var sharedElementTransitionComplete by remember { mutableStateOf(false) } + + val view = LocalView.current + val window = remember { (view.context as Activity).window } + val insetsController = remember { WindowCompat.getInsetsController(window, view) } + + DisposableEffect(Unit) { + insetsController.apply { + hide(WindowInsetsCompat.Type.statusBars()) + hide(WindowInsetsCompat.Type.navigationBars()) + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + onDispose { + 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() + ) + + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .fillMaxSize() + .background(surface) + ) { + 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 = 94.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 + ) + } + } +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 4bbdc6e..b26212e 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -8,17 +8,16 @@ package org.nsh07.pomodoro.ui import android.content.Intent -import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.ContentTransform 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.fillMaxSize import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi @@ -29,17 +28,13 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.ShortNavigationBar import androidx.compose.material3.ShortNavigationBarArrangement import androidx.compose.material3.ShortNavigationBarItem -import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberUpdatedState -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.painterResource @@ -78,7 +73,6 @@ fun AppScreen( val windowSizeClass = currentWindowAdaptiveInfo().windowSizeClass val backStack = rememberNavBackStack(Screen.Timer) - var showAOD by remember { mutableStateOf(false) } if (uiState.alarmRinging) AlarmDialog { @@ -88,147 +82,159 @@ fun AppScreen( } } - AnimatedContent( - showAOD, - transitionSpec = { fadeIn().togetherWith(fadeOut()) } - ) { aod -> - if (!aod) { - Scaffold( - bottomBar = { - val wide = remember { - windowSizeClass.isWidthAtLeastBreakpoint( - WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND - ) - } - ShortNavigationBar( - arrangement = - if (wide) ShortNavigationBarArrangement.Centered - else ShortNavigationBarArrangement.EqualWeight - ) { - 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)) } - ) - } - } - }, - modifier = Modifier - .then( - if (isAODEnabled) Modifier.clickable { showAOD = !showAOD } - else Modifier + + Scaffold( + bottomBar = { + AnimatedVisibility( + backStack.last() !is Screen.AOD, + enter = fadeIn(), + exit = fadeOut() + ) { + val wide = remember { + windowSizeClass.isWidthAtLeastBreakpoint( + WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND ) - ) { contentPadding -> - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - transitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) + } + ShortNavigationBar( + arrangement = + if (wide) ShortNavigationBarArrangement.Centered + else ShortNavigationBarArrangement.EqualWeight + ) { + 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)) } ) - }, - popTransitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) - ) - }, - predictivePopTransitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) + - scaleOut(targetScale = 0.7f), - ) - }, - entryProvider = entryProvider { - entry { - 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) - } - - TimerAction.StopAlarm -> - Intent(context, TimerService::class.java).also { - it.action = - TimerService.Actions.STOP_ALARM.toString() - context.startService(it) - } - - TimerAction.ToggleTimer -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.TOGGLE.toString() - context.startService(it) - } - } - }, - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() - ) - ) - } - - entry { - SettingsScreenRoot( - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() - ) - ) - } - - entry { - StatsScreenRoot( - contentPadding = contentPadding, - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() - ) - ) - } } - ) + } } - } else { - Surface( - color = Color.Black, - modifier = Modifier - .fillMaxSize() - .clickable { showAOD = !showAOD }) {} + } + ) { contentPadding -> + SharedTransitionLayout { + 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 { + 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) + } + + TimerAction.StopAlarm -> + Intent(context, TimerService::class.java).also { + it.action = + TimerService.Actions.STOP_ALARM.toString() + context.startService(it) + } + + TimerAction.ToggleTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.TOGGLE.toString() + context.startService(it) + } + } + }, + modifier = modifier + .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) + } + else Modifier + ), + ) + } + + entry { + AlwaysOnDisplay( + timerState = uiState, + progress = { progress }, + modifier = Modifier + .then( + if (isAODEnabled) Modifier.clickable { + if (backStack.size > 1) backStack.removeLastOrNull() + } + else Modifier + ) + ) + } + + entry { + SettingsScreenRoot( + modifier = modifier.padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding() + ) + ) + } + + entry { + StatsScreenRoot( + contentPadding = contentPadding, + modifier = modifier.padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding() + ) + ) + } + } + ) } } } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt index 85482cf..2a43655 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt @@ -9,6 +9,9 @@ sealed class Screen : NavKey { @Serializable object Timer : Screen() + @Serializable + object AOD : Screen() + @Serializable object Settings : Screen() diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt index bdb563d..68b9094 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/TimerScreen.kt @@ -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 }, + {} + ) + } } } } From aea6a9902e2818bf400e0af29f064623ccfd2b5f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 20 Oct 2025 13:48:53 +0530 Subject: [PATCH 04/35] fix(ui): Reduce AOD wavy progress wavelength to fix inconsistent-looking waves in AOD vs TimerScreen --- app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index e96d4dc..66d9252 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -155,7 +155,7 @@ fun SharedTransitionScope.AlwaysOnDisplay( }, cap = StrokeCap.Round, ), - wavelength = 94.dp, + wavelength = 42.dp, gapSize = 8.dp ) } From 37f5fb433ef7b3d2e52d536e9d436b5cf06ef5d9 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 20 Oct 2025 14:02:48 +0530 Subject: [PATCH 05/35] feat(ui): keep screen on in AOD mode --- app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index 66d9252..8009ecb 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -65,6 +65,7 @@ fun SharedTransitionScope.AlwaysOnDisplay( val insetsController = remember { WindowCompat.getInsetsController(window, view) } DisposableEffect(Unit) { + view.keepScreenOn = true insetsController.apply { hide(WindowInsetsCompat.Type.statusBars()) hide(WindowInsetsCompat.Type.navigationBars()) @@ -72,6 +73,7 @@ fun SharedTransitionScope.AlwaysOnDisplay( WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } onDispose { + view.keepScreenOn = false insetsController.apply { show(WindowInsetsCompat.Type.statusBars()) show(WindowInsetsCompat.Type.navigationBars()) From 98061ad3dd9b73034f57e95352de1646134456f0 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 20 Oct 2025 16:03:26 +0200 Subject: [PATCH 06/35] Added translation using Weblate (French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (58 of 58 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (13 of 13 strings) Co-authored-by: Alans Co-authored-by: Hosted Weblate Co-authored-by: Loïc de Porcaro Co-authored-by: 大王叫我来巡山 Translate-URL: https://hosted.weblate.org/projects/tomato/store-descriptions/zh_Hans/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/zh_Hans/ Translation: Tomato/Store descriptions Translation: Tomato/Strings --- app/src/main/res/values-fr/strings.xml | 3 +++ app/src/main/res/values-zh-rCN/strings.xml | 1 + .../metadata/android/zh-CN/full_description.txt | 13 ++++++++++++- 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/values-fr/strings.xml diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..55344e5 --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index b273b02..43e1708 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -56,4 +56,5 @@ 计时 计时器进度 %2$d 中的 %1$d + 去年 diff --git a/fastlane/metadata/android/zh-CN/full_description.txt b/fastlane/metadata/android/zh-CN/full_description.txt index 08a5f6c..a1505f3 100644 --- a/fastlane/metadata/android/zh-CN/full_description.txt +++ b/fastlane/metadata/android/zh-CN/full_description.txt @@ -1 +1,12 @@ -

Tomato 是一个基于Material 3 Expressive的安卓极简主义番茄钟.


功能:

  • 基于最新Material 3 Expressive指南的简洁用户界面
  • 以便于理解的方式提供工作/学习的详细统计数据
    • 当日统计数据一目了然
    • 清楚易读的上周和上月统计图表
    • 上周和上月的额外统计数据帮您找到一天中最高效的时间段
  • 可自定义的计时器参数
+Tomato 是一个基于Material 3 Expressive的安卓极简主义番茄钟. + +Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误(bug)或建议新功能,请访问 https://github.com/nsh07/Tomato。 + +功能: +- 基于最新Material 3 Expressive指南的简洁用户界面 +- 以便于理解的方式提供工作/学习的详细统计数据 + - 当日统计数据一目了然 + - 清楚易读的上周和上月统计图表 + - 上周和上月的额外统计数据帮您找到一天中最高效的时间段 +- 可自定义的计时器参数 +- 支持 Android 16 即時更新 (Android 16 Live Updates) From e03afb253f49018043cece980dc55539051502c1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 20 Oct 2025 17:38:42 +0200 Subject: [PATCH 07/35] Translated using Weblate (French) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (58 of 58 strings) Translated using Weblate (French) Currently translated at 100.0% (13 of 13 strings) Co-authored-by: Hosted Weblate Co-authored-by: Loïc de Porcaro Translate-URL: https://hosted.weblate.org/projects/tomato/store-descriptions/fr/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/fr/ Translation: Tomato/Store descriptions Translation: Tomato/Strings --- app/src/main/res/values-fr/strings.xml | 59 ++++++++++++++++++- .../android/fr-FR/full_description.txt | 12 ++++ .../android/fr-FR/short_description.txt | 1 + 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/fr-FR/full_description.txt create mode 100644 fastlane/metadata/android/fr-FR/short_description.txt diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 55344e5..63b07eb 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,3 +1,60 @@ - \ No newline at end of file + Démarrer + Arrêter + Travail + Pause courte + Pause longue + Quitter + Passer + Arrêter l\'alarme + %1$s minutes restantes + En pause + Terminé + À venir : %1$s (%2$s) + Démarrer l\'intervalle suivant + Choisir le thème de couleurs + OK + Thème de couleurs + Dynamique + Couleur + Valeur par défaut du système + Alarme + Clair + Sombre + Choisir le thème + Analyse de productivité + Durée de concentration selon les moments de la journée + Son de l\'alarme + Thème noir + Utiliser un thème sombre noir pur + Faire sonner l’alarme à la fin du minuteur + Vibrer + Faire vibrer à la fin du minuteur + Thème + Paramètres + Durée de la session + Intervalles de concentration par session : %1$d + Une \"session\" est une séquence d’intervalles Pomodoro comprenant des phases de concentration, des pauses courtes et une pause longue. La dernière pause d’une session est toujours une pause longue. + Statistiques + Aujourd\'hui + Pause + 7 derniers jours + concentration moyenne par jour + Plus d\'infos + Analyse hebdomadaire de la productivité + 30 derniers jours + Analyse mensuelle de la productivité + Arrêter l\'alarme ? + La session actuelle est terminée. Touchez n’importe où pour arrêter l’alarme. + %1$d sur %2$d + Plus + Mettre en pause + Démarrer la session + Redémarrer + Passer au suivant + À venir + Minuteur + Progression du minuteur + 12 derniers mois + diff --git a/fastlane/metadata/android/fr-FR/full_description.txt b/fastlane/metadata/android/fr-FR/full_description.txt new file mode 100644 index 0000000..28e0d00 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/full_description.txt @@ -0,0 +1,12 @@ +Tomato 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. + +Fonctionnalités: +- 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 d’Android 16 diff --git a/fastlane/metadata/android/fr-FR/short_description.txt b/fastlane/metadata/android/fr-FR/short_description.txt new file mode 100644 index 0000000..1185050 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/short_description.txt @@ -0,0 +1 @@ +Minuteur Pomodoro minimaliste From 460403e6e3e5043b1ed7d43d9189445906dad224 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 20 Oct 2025 21:20:33 +0200 Subject: [PATCH 08/35] Added translation using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translated using Weblate (French) Currently translated at 100.0% (58 of 58 strings) Co-authored-by: Hosted Weblate Co-authored-by: Loïc de Porcaro Co-authored-by: Mertcan Atmaca Translate-URL: https://hosted.weblate.org/projects/tomato/strings/fr/ Translation: Tomato/Strings --- app/src/main/res/values-fr/strings.xml | 2 +- app/src/main/res/values-tr/strings.xml | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 app/src/main/res/values-tr/strings.xml diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 63b07eb..c2374e6 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -2,7 +2,7 @@ Démarrer Arrêter - Travail + Concentration Pause courte Pause longue Quitter diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml new file mode 100644 index 0000000..55344e5 --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file From b16aeb499d8e4f7adb3ab2cb2264f6a766cd0e02 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 21 Oct 2025 09:26:05 +0530 Subject: [PATCH 09/35] feat(ui): allow phone to be locked while still showing AOD, bump minSDK to 27 --- app/build.gradle.kts | 2 +- .../org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9f14c9..107cf83 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,7 @@ android { defaultConfig { applicationId = "org.nsh07.pomodoro" - minSdk = 26 + minSdk = 27 targetSdk = 36 versionCode = 13 versionName = "1.5.0" diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index 8009ecb..3234814 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -8,6 +8,8 @@ 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 @@ -61,19 +63,29 @@ fun SharedTransitionScope.AlwaysOnDisplay( var sharedElementTransitionComplete by remember { mutableStateOf(false) } val view = LocalView.current + val activity = LocalActivity.current val window = remember { (view.context as Activity).window } val insetsController = remember { WindowCompat.getInsetsController(window, view) } DisposableEffect(Unit) { - view.keepScreenOn = true + 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 { - view.keepScreenOn = false + 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()) From 518f172054184c717bc35f3b56a5d8981f3db4cc Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 21 Oct 2025 10:35:21 +0530 Subject: [PATCH 10/35] feat(ui): implement randomizing position in AOD to prevent burn-in --- .../org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 176 ++++++++++++------ 1 file changed, 119 insertions(+), 57 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index 3234814..ce78c12 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -13,9 +13,11 @@ 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 @@ -27,6 +29,7 @@ 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 @@ -37,10 +40,14 @@ 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 @@ -50,9 +57,20 @@ 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( @@ -62,8 +80,11 @@ fun SharedTransitionScope.AlwaysOnDisplay( ) { var sharedElementTransitionComplete by remember { mutableStateOf(false) } - val view = LocalView.current 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) } @@ -126,70 +147,109 @@ fun SharedTransitionScope.AlwaysOnDisplay( 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) + val y by animateIntAsState(randomY) + Box( - contentAlignment = Alignment.Center, modifier = modifier .fillMaxSize() .background(surface) ) { - 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, + 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 ), - trackStroke = Stroke( - width = with(LocalDensity.current) { - 12.dp.toPx() - }, - cap = StrokeCap.Round, - ), - wavelength = 42.dp, - gapSize = 8.dp + textAlign = TextAlign.Center, + color = onSurface, + maxLines = 1, + modifier = Modifier.sharedBounds( + sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) ) } - - 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 - ) - ) } } @@ -208,3 +268,5 @@ private fun AlwaysOnDisplayPreview() { } } } + +fun Dp.toIntPx(density: Density) = with(density) { toPx().toInt() } From 4293f0d5f13e030ff97fe226a56ad5909ef40b5a Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 21 Oct 2025 11:41:38 +0530 Subject: [PATCH 11/35] feat(settings): add a settings option to disable AOD disabled by default --- .../java/org/nsh07/pomodoro/MainActivity.kt | 5 +- .../org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 4 +- .../ui/settingsScreen/SettingsScreen.kt | 61 +++++++---- .../pomodoro/ui/settingsScreen/ThemeDialog.kt | 22 ++-- .../ui/settingsScreen/ThemePickerListItem.kt | 4 +- .../viewModel/PreferencesState.kt | 3 +- .../viewModel/SettingsViewModel.kt | 14 ++- .../timerScreen/viewModel/TimerViewModel.kt | 3 + app/src/main/res/drawable/aod.xml | 16 +++ app/src/main/res/values/strings.xml | 100 +++++++++--------- 10 files changed, 148 insertions(+), 84 deletions(-) create mode 100644 app/src/main/res/drawable/aod.xml diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 9a40b41..a98932a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -51,7 +51,10 @@ class MainActivity : ComponentActivity() { appContainer.appTimerRepository.colorScheme = colorScheme } - AppScreen(timerViewModel = timerViewModel, isAODEnabled = true) + AppScreen( + timerViewModel = timerViewModel, + isAODEnabled = preferencesState.aodEnabled + ) } } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index ce78c12..8225cc2 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -177,8 +177,8 @@ fun SharedTransitionScope.AlwaysOnDisplay( } } - val x by animateIntAsState(randomX) - val y by animateIntAsState(randomY) + val x by animateIntAsState(randomX, motionScheme.slowSpatialSpec()) + val y by animateIntAsState(randomY, motionScheme.slowSpatialSpec()) Box( modifier = modifier diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 67b00a0..b2bca48 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -15,6 +15,7 @@ import android.os.Build import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes +import androidx.annotation.StringRes import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background import androidx.compose.foundation.clickable @@ -143,6 +144,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 { @@ -170,6 +172,7 @@ private fun SettingsScreen( onAlarmEnabledChange: (Boolean) -> Unit, onVibrateEnabledChange: (Boolean) -> Unit, onBlackThemeChange: (Boolean) -> Unit, + onAodEnabledChange: (Boolean) -> Unit, onAlarmSoundChanged: (Uri?) -> Unit, onThemeChange: (String) -> Unit, onColorSchemeChange: (Color) -> Unit, @@ -181,14 +184,14 @@ private fun SettingsScreen( checkedIconColor = colorScheme.primary, ) - val themeMap: Map> = remember { + val themeMap: Map> = remember { mapOf( "auto" to Pair( R.drawable.brightness_auto, - context.getString(R.string.system_default) + 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)) + "light" to Pair(R.drawable.light_mode, R.string.light), + "dark" to Pair(R.drawable.dark_mode, R.string.dark) ) } val reverseThemeMap: Map = remember { @@ -232,27 +235,39 @@ private fun SettingsScreen( putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri()) } - val switchItems = remember(preferencesState.blackTheme, alarmEnabled, vibrateEnabled) { + val switchItems = remember( + preferencesState.blackTheme, + preferencesState.aodEnabled, + 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), + label = R.string.black_theme, + description = R.string.black_theme_desc, onClick = onBlackThemeChange ), + SettingsSwitchItem( + checked = preferencesState.aodEnabled, + icon = R.drawable.aod, + label = R.string.always_on_display, + description = R.string.always_on_display_desc, + onClick = onAodEnabledChange + ), SettingsSwitchItem( checked = alarmEnabled, icon = R.drawable.alarm_on, - label = context.getString(R.string.alarm), - description = context.getString(R.string.alarm_desc), + label = R.string.alarm, + description = 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), + label = R.string.vibrate, + description = R.string.vibrate_desc, onClick = onVibrateEnabledChange ) ) @@ -404,14 +419,13 @@ private fun SettingsScreen( .clip(middleListItemShape) ) } - item { - val item = switchItems[0] + itemsIndexed(switchItems.take(2)) { index, item -> ListItem( leadingContent = { Icon(painterResource(item.icon), contentDescription = null) }, - headlineContent = { Text(item.label) }, - supportingContent = { Text(item.description) }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { Text(stringResource(item.description)) }, trailingContent = { Switch( checked = item.checked, @@ -435,7 +449,9 @@ private fun SettingsScreen( ) }, colors = listItemColors, - modifier = Modifier.clip(bottomListItemShape) + modifier = Modifier + .padding(top = if (index != 0) 16.dp else 0.dp) + .clip(if (index == 0) bottomListItemShape else cardShape) ) } @@ -454,13 +470,13 @@ private fun SettingsScreen( .clickable(onClick = { ringtonePickerLauncher.launch(intent) }) ) } - itemsIndexed(switchItems.drop(1)) { index, item -> + itemsIndexed(switchItems.drop(2)) { index, item -> ListItem( leadingContent = { Icon(painterResource(item.icon), contentDescription = null) }, - headlineContent = { Text(item.label) }, - supportingContent = { Text(item.description) }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { Text(stringResource(item.description)) }, trailingContent = { Switch( checked = item.checked, @@ -487,7 +503,7 @@ private fun SettingsScreen( modifier = Modifier .clip( when (index) { - switchItems.lastIndex - 1 -> bottomListItemShape + switchItems.lastIndex - 2 -> bottomListItemShape else -> middleListItemShape } ) @@ -546,6 +562,7 @@ fun SettingsScreenPreview() { onAlarmEnabledChange = {}, onVibrateEnabledChange = {}, onBlackThemeChange = {}, + onAodEnabledChange = {}, onAlarmSoundChanged = {}, onThemeChange = {}, onColorSchemeChange = {}, @@ -557,7 +574,7 @@ fun SettingsScreenPreview() { data class SettingsSwitchItem( val checked: Boolean, @param:DrawableRes val icon: Int, - val label: String, - val description: String, + @param:StringRes val label: Int, + @param:StringRes val description: Int, val onClick: (Boolean) -> Unit ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt index 67f4d7a..1de30cd 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt @@ -31,11 +31,12 @@ 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.mutableIntStateOf 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.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.Role @@ -50,14 +51,16 @@ import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape @OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) @Composable fun ThemeDialog( - themeMap: Map>, + themeMap: Map>, reverseThemeMap: Map, theme: String, setShowThemeDialog: (Boolean) -> Unit, onThemeChange: (String) -> Unit ) { val selectedOption = - remember { mutableStateOf(themeMap[theme]!!.second) } + remember { mutableIntStateOf(themeMap[theme]!!.second) } + + val context = LocalContext.current BasicAlertDialog( onDismissRequest = { setShowThemeDialog(false) } @@ -80,7 +83,7 @@ fun ThemeDialog( verticalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.selectableGroup() ) { - themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry> -> + themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry> -> val text = pair.value.second val selected = text == selectedOption.value @@ -94,7 +97,10 @@ fun ThemeDialog( } }, headlineContent = { - Text(text = text, style = MaterialTheme.typography.bodyLarge) + Text( + text = stringResource(text), + style = MaterialTheme.typography.bodyLarge + ) }, colors = if (!selected) listItemColors else selectedListItemColors, modifier = Modifier @@ -110,7 +116,11 @@ fun ThemeDialog( selected = (text == selectedOption.value), onClick = { selectedOption.value = text - onThemeChange(reverseThemeMap[selectedOption.value]!!) + onThemeChange( + reverseThemeMap[context.getString( + selectedOption.intValue + )]!! + ) }, role = Role.RadioButton ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt index 2bee9c5..0c66f00 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt @@ -25,7 +25,7 @@ import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors @Composable fun ThemePickerListItem( theme: String, - themeMap: Map>, + themeMap: Map>, reverseThemeMap: Map, items: Int, index: Int, @@ -53,7 +53,7 @@ fun ThemePickerListItem( }, headlineContent = { Text(stringResource(R.string.theme)) }, supportingContent = { - Text(themeMap[theme]!!.second) + Text(stringResource(themeMap[theme]!!.second)) }, colors = listItemColors, items = items, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt index 0ae4849..7231852 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/PreferencesState.kt @@ -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 ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 5f604c1..454bf82 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -80,12 +80,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 +199,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 { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt index 2202d42..53c23b5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/viewModel/TimerViewModel.kt @@ -95,6 +95,9 @@ class TimerViewModel( ) ).toUri() + preferenceRepository.getBooleanPreference("aod_enabled") + ?: preferenceRepository.saveBooleanPreference("aod_enabled", false) + _time.update { timerRepository.focusTime } cycles = 0 startTime = 0L diff --git a/app/src/main/res/drawable/aod.xml b/app/src/main/res/drawable/aod.xml new file mode 100644 index 0000000..ab821c8 --- /dev/null +++ b/app/src/main/res/drawable/aod.xml @@ -0,0 +1,16 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 067cff5..c185a08 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,60 +1,62 @@ - Tomato - Start - Stop - Focus - Short break - Long break - Exit - Skip - Stop alarm - %1$s min remaining - Paused - Completed - Up next: %1$s (%2$s) - Start next - Choose color scheme - OK - Color scheme - Dynamic - Color - System default Alarm - Light - Dark - Choose theme - Productivity analysis - Focus durations at different times of the day + Ring alarm when a timer completes Alarm sound + Always On Display + Tap anywhere when viewing the timer to switch to AOD mode + Tomato Black theme Use a pure black dark theme - Ring alarm when a timer completes - Vibrate - Vibrate when a timer completes - Theme - Settings + Break + Choose color scheme + Choose theme + Color + Color scheme + Completed + Dark + Dynamic + Exit + Focus + focus per day (avg) + Last month + Last week + Last year + Light + Long break + %1$s min remaining + Monthly productivity analysis + More + More info + OK + Pause + Paused + Play + 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. + Productivity analysis + Focus durations at different times of the day + Restart Session length Focus intervals in one session: %1$d - 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. - Stats - Today - Break - Last week - focus per day (avg) - More info - Weekly productivity analysis - Last month - Monthly productivity analysis - Stop Alarm? - Current timer session is complete. Tap anywhere to stop the alarm. - %1$d of %2$d - More - Pause - Play - Restart + Settings + Short break + Skip Skip to next - Up next + Start + Start next + Stats + Stop + Stop alarm + Current timer session is complete. Tap anywhere to stop the alarm. + Stop Alarm? + System default + Theme Timer Timer progress - Last year + %1$d of %2$d + Today + Up next + Up next: %1$s (%2$s) + Vibrate + Vibrate when a timer completes + Weekly productivity analysis \ No newline at end of file From 174a1627c38027d7069c777e93902644b8f49a31 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Mon, 20 Oct 2025 11:24:53 +0530 Subject: [PATCH 12/35] docs(readme): update README with Star History section --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index ce25364..1562443 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,7 @@ This app was made possible by the following libraries: Navigation - [Room](https://developer.android.com/jetpack/androidx/releases/room) - SQLite Database - [Vico](https://github.com/patrykandpatrick/vico) - Graphs and charts +- [MaterialKolor](https://github.com/jordond/materialkolor)- Material 3 color schemes ### Fonts: @@ -145,3 +146,9 @@ This app was made possible by the following libraries: - [Open Runde](https://github.com/lauridskern/open-runde) by Laurids Kern
+ +## Star History + +Please give this repo a star if you liked my work + +[![Star History Chart](https://app.repohistory.com/api/svg?repo=nsh07/Tomato&type=Date&background=F9FAEF&color=4C662B)](https://app.repohistory.com/star-history) From 7e16690333889e0701d4156164b1bb428fedda1b Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 21 Oct 2025 13:32:43 +0530 Subject: [PATCH 13/35] chore(release): bump version, update changelog --- app/build.gradle.kts | 4 ++-- fastlane/metadata/android/en-US/changelogs/14.txt | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/14.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 107cf83..0b48dfc 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -33,8 +33,8 @@ android { applicationId = "org.nsh07.pomodoro" minSdk = 27 targetSdk = 36 - versionCode = 13 - versionName = "1.5.0" + versionCode = 14 + versionName = "1.6.0-alpha01" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/14.txt b/fastlane/metadata/android/en-US/changelogs/14.txt new file mode 100644 index 0000000..a028e30 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/14.txt @@ -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 \ No newline at end of file From d0ad8bc718c5d2cebfa80399a3c153d400417f2f Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 21 Oct 2025 08:15:05 +0200 Subject: [PATCH 14/35] Translated using Weblate (Turkish) Currently translated at 100.0% (58 of 58 strings) Translated using Weblate (Turkish) Currently translated at 100.0% (13 of 13 strings) Co-authored-by: Hosted Weblate Co-authored-by: Mertcan Atmaca Translate-URL: https://hosted.weblate.org/projects/tomato/store-descriptions/tr/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/tr/ Translation: Tomato/Store descriptions Translation: Tomato/Strings --- app/src/main/res/values-tr/strings.xml | 59 ++++++++++++++++++- .../android/tr-TR/full_description.txt | 12 ++++ .../android/tr-TR/short_description.txt | 1 + 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 fastlane/metadata/android/tr-TR/full_description.txt create mode 100644 fastlane/metadata/android/tr-TR/short_description.txt diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 55344e5..0a675a5 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -1,3 +1,60 @@ - \ No newline at end of file + Başlat + Durdur + Odaklan + Kısa Mola + Uzun Mola + Çıkış + Atla + Alarmı Durdur + %1$s dk kaldı + Duraklatıldı + Tamamlandı + Sırada: %1$s (%2$s) + Sıradakini Başlat + Renk şeması seçin + Tamam + Renk şeması + Dinamik + Renk + Sistem varsayılanı + Açık + Koyu + Tema seçin + Verimlilik analizi + Günün farklı saatlerindeki odaklanma süreleri + Alarm sesi + Siyah tema + Tam siyah koyu tema kullan + Zamanlayıcı bittiğinde alarm çal + Titreşim + Zamanlayıcı bittiğinde titre + Tema + Ayarlar + Oturum uzunluğu + Bir oturumdaki odaklanma aralığı: %1$d + \"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. + İstatistikler + Bugün + Mola + Geçen hafta + günlük odaklanma (ortalama) + Daha fazla bilgi + Haftalık verimlilik analizi + Geçen ay + Aylık verimlilik analizi + Alarmı Durdur? + Mevcut zamanlayıcı oturumu tamamlandı. Alarmı durdurmak için herhangi bir yere dokunun. + %1$d / %2$d + Daha fazla + Duraklat + Devam Et + Yeniden Başlat + Sıradakine Atla + Sıradaki + Zamanlayıcı + Zamanlayıcı İlerlemesi + Geçen yıl + Alarm +
diff --git a/fastlane/metadata/android/tr-TR/full_description.txt b/fastlane/metadata/android/tr-TR/full_description.txt new file mode 100644 index 0000000..e61dc3e --- /dev/null +++ b/fastlane/metadata/android/tr-TR/full_description.txt @@ -0,0 +1,12 @@ +Tomato, 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 + +Özellikler: +- 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 diff --git a/fastlane/metadata/android/tr-TR/short_description.txt b/fastlane/metadata/android/tr-TR/short_description.txt new file mode 100644 index 0000000..bac83fe --- /dev/null +++ b/fastlane/metadata/android/tr-TR/short_description.txt @@ -0,0 +1 @@ +Minimalist Pomodoro sayacı From 75d3029250f2c76d918e22962d21a9e8fabe1fcf Mon Sep 17 00:00:00 2001 From: Fandroid745 Date: Tue, 21 Oct 2025 18:47:53 +0530 Subject: [PATCH 15/35] feat:fix screen wake up when timer ends --- app/src/main/AndroidManifest.xml | 1 + .../java/org/nsh07/pomodoro/MainActivity.kt | 3 +- .../nsh07/pomodoro/service/TimerService.kt | 27 +++++++++- .../pomodoro/ui/timerScreen/AlarmDialog.kt | 49 +++++++++++++++++++ 4 files changed, 78 insertions(+), 2 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19ff772..84c8ae7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ + = Build.VERSION_CODES.S) { val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager @@ -89,6 +95,7 @@ class TimerService : Service() { saveTimeToDb() notificationManager.cancel(1) alarm?.release() + wakeLock?.release() } super.onDestroy() } @@ -117,6 +124,7 @@ class TimerService : Service() { return super.onStartCommand(intent, flags, startId) } + @Suppress("DEPRECATION") private fun toggleTimer() { updateProgressSegments() @@ -157,6 +165,20 @@ class TimerService : Service() { if (iterations == 0) showTimerNotification(time.toInt()) if (time < 0) { + val powerManager = this@TimerService.getSystemService(POWER_SERVICE) as PowerManager + wakeLock = powerManager.newWakeLock( + PowerManager.FULL_WAKE_LOCK or + PowerManager.ACQUIRE_CAUSES_WAKEUP or + PowerManager.ON_AFTER_RELEASE, + "PomodoroApp:AlarmWakeLock" + ) + wakeLock?.acquire(2 * 60 * 1000L) + val intent = Intent(this@TimerService, MainActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + startActivity(intent) + + skipTimer() _timerState.update { currentState -> currentState.copy(timerRunning = false) @@ -176,7 +198,7 @@ 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 ) { @@ -372,6 +394,9 @@ class TimerService : Service() { vibrator.cancel() } + wakeLock?.release() + wakeLock = null + _timerState.update { currentState -> currentState.copy(alarmRinging = false) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt index 9b27779..d41c27e 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt @@ -7,6 +7,11 @@ package org.nsh07.pomodoro.ui.timerScreen +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper +import android.os.Build +import android.view.WindowManager import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -24,8 +29,10 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -37,6 +44,38 @@ fun AlarmDialog( modifier: Modifier = Modifier, stopAlarm: () -> Unit ) { + val context = LocalContext.current + val activity = context.findActivity() + + // Set lockscreen flags when dialog appears, remove when it disappears + DisposableEffect(Unit) { + // Show over lockscreen + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + activity?.setShowWhenLocked(true) + activity?.setTurnScreenOn(true) + } else { + @Suppress("DEPRECATION") + activity?.window?.addFlags( + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) + } + + onDispose { + // Remove lockscreen flags when dialog is dismissed + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { + activity?.setShowWhenLocked(false) + activity?.setTurnScreenOn(false) + } else { + @Suppress("DEPRECATION") + activity?.window?.clearFlags( + WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or + WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON + ) + } + } + } + BasicAlertDialog( onDismissRequest = stopAlarm, modifier = modifier @@ -75,4 +114,14 @@ fun AlarmDialog( } } } +} + +// Add this helper function at the file level (outside the composable) +fun Context.findActivity(): Activity? { + var context = this + while (context is ContextWrapper) { + if (context is Activity) return context + context = context.baseContext + } + return null } \ No newline at end of file From 9a41d837e248790b39a0a734291dec36455e5e22 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Tue, 21 Oct 2025 21:32:27 +0530 Subject: [PATCH 16/35] feat(ui): implement an about card --- .../pomodoro/ui/settingsScreen/AboutCard.kt | 147 ++++++++++++++++++ app/src/main/res/drawable/coffee.xml | 9 ++ app/src/main/res/drawable/discord.xml | 9 ++ app/src/main/res/drawable/github.xml | 9 ++ app/src/main/res/drawable/play_store.xml | 10 ++ 5 files changed, 184 insertions(+) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt create mode 100644 app/src/main/res/drawable/coffee.xml create mode 100644 app/src/main/res/drawable/discord.xml create mode 100644 app/src/main/res/drawable/github.xml create mode 100644 app/src/main/res/drawable/play_store.xml diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt new file mode 100644 index 0000000..96da61c --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen + +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.robotoFlexHeadline +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})", + fontFamily = robotoFlexHeadline + ) + } + + 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") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/coffee.xml b/app/src/main/res/drawable/coffee.xml new file mode 100644 index 0000000..05b913c --- /dev/null +++ b/app/src/main/res/drawable/coffee.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/discord.xml b/app/src/main/res/drawable/discord.xml new file mode 100644 index 0000000..e0b6446 --- /dev/null +++ b/app/src/main/res/drawable/discord.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/github.xml b/app/src/main/res/drawable/github.xml new file mode 100644 index 0000000..c60cd31 --- /dev/null +++ b/app/src/main/res/drawable/github.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/play_store.xml b/app/src/main/res/drawable/play_store.xml new file mode 100644 index 0000000..fc7ef76 --- /dev/null +++ b/app/src/main/res/drawable/play_store.xml @@ -0,0 +1,10 @@ + + + From 8e47b11ad2569c66673ba0c110a17f9832d3b890 Mon Sep 17 00:00:00 2001 From: Fandroid745 Date: Tue, 21 Oct 2025 21:48:30 +0530 Subject: [PATCH 17/35] fix:added the requried changes --- .../nsh07/pomodoro/service/TimerService.kt | 7 ++- .../pomodoro/ui/timerScreen/AlarmDialog.kt | 43 +++---------------- 2 files changed, 9 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index 8408b6d..fc104ab 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -2,7 +2,6 @@ package org.nsh07.pomodoro.service import android.annotation.SuppressLint import android.app.Service -import android.content.Context import android.content.Intent import android.media.AudioAttributes import android.media.MediaPlayer @@ -124,7 +123,7 @@ class TimerService : Service() { return super.onStartCommand(intent, flags, startId) } - @Suppress("DEPRECATION") + private fun toggleTimer() { updateProgressSegments() @@ -167,8 +166,8 @@ class TimerService : Service() { if (time < 0) { val powerManager = this@TimerService.getSystemService(POWER_SERVICE) as PowerManager wakeLock = powerManager.newWakeLock( - PowerManager.FULL_WAKE_LOCK or - PowerManager.ACQUIRE_CAUSES_WAKEUP or + PowerManager.SCREEN_BRIGHT_WAKE_LOCK or + PowerManager.ACQUIRE_CAUSES_WAKEUP or PowerManager.ON_AFTER_RELEASE, "PomodoroApp:AlarmWakeLock" ) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt index d41c27e..ed39a73 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt @@ -7,11 +7,7 @@ package org.nsh07.pomodoro.ui.timerScreen -import android.app.Activity -import android.content.Context -import android.content.ContextWrapper -import android.os.Build -import android.view.WindowManager +import androidx.activity.compose.LocalActivity import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -32,7 +28,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp @@ -44,35 +39,18 @@ fun AlarmDialog( modifier: Modifier = Modifier, stopAlarm: () -> Unit ) { - val context = LocalContext.current - val activity = context.findActivity() + val activity = LocalActivity.current // Set lockscreen flags when dialog appears, remove when it disappears DisposableEffect(Unit) { // Show over lockscreen - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - activity?.setShowWhenLocked(true) - activity?.setTurnScreenOn(true) - } else { - @Suppress("DEPRECATION") - activity?.window?.addFlags( - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - ) - } + activity?.setShowWhenLocked(true) + activity?.setTurnScreenOn(true) onDispose { // Remove lockscreen flags when dialog is dismissed - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) { - activity?.setShowWhenLocked(false) - activity?.setTurnScreenOn(false) - } else { - @Suppress("DEPRECATION") - activity?.window?.clearFlags( - WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or - WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON - ) - } + activity?.setShowWhenLocked(false) + activity?.setTurnScreenOn(false) } } @@ -116,12 +94,3 @@ fun AlarmDialog( } } -// Add this helper function at the file level (outside the composable) -fun Context.findActivity(): Activity? { - var context = this - while (context is ContextWrapper) { - if (context is Activity) return context - context = context.baseContext - } - return null -} \ No newline at end of file From 920d9112af9c40753ae6d6a9fa6f1f8f16b8fd34 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 11:48:32 +0530 Subject: [PATCH 18/35] Revert "docs(readme): update README with Star History section" This reverts commit 174a1627c38027d7069c777e93902644b8f49a31. --- README.md | 7 ------- 1 file changed, 7 deletions(-) diff --git a/README.md b/README.md index 1562443..ce25364 100644 --- a/README.md +++ b/README.md @@ -137,7 +137,6 @@ This app was made possible by the following libraries: Navigation - [Room](https://developer.android.com/jetpack/androidx/releases/room) - SQLite Database - [Vico](https://github.com/patrykandpatrick/vico) - Graphs and charts -- [MaterialKolor](https://github.com/jordond/materialkolor)- Material 3 color schemes ### Fonts: @@ -146,9 +145,3 @@ This app was made possible by the following libraries: - [Open Runde](https://github.com/lauridskern/open-runde) by Laurids Kern
- -## Star History - -Please give this repo a star if you liked my work - -[![Star History Chart](https://app.repohistory.com/api/svg?repo=nsh07/Tomato&type=Date&background=F9FAEF&color=4C662B)](https://app.repohistory.com/star-history) From 3818567af08bc2f1666ddcdb3993c42897ca8f62 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 11:53:01 +0530 Subject: [PATCH 19/35] feat(ui): add back icon resource --- app/src/main/res/drawable/arrow_back.xml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 app/src/main/res/drawable/arrow_back.xml diff --git a/app/src/main/res/drawable/arrow_back.xml b/app/src/main/res/drawable/arrow_back.xml new file mode 100644 index 0000000..d867d93 --- /dev/null +++ b/app/src/main/res/drawable/arrow_back.xml @@ -0,0 +1,21 @@ + + + + + From 5849a2cde419811baa1ceb08b1952f85292cc1eb Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 13:10:00 +0530 Subject: [PATCH 20/35] feat(ui): implement separate settings pages --- .../pomodoro/ui/settingsScreen/AboutCard.kt | 20 +- .../ui/settingsScreen/AlarmSettings.kt | 236 +++++++++++++ .../ui/settingsScreen/AppearanceSettings.kt | 199 +++++++++++ .../ui/settingsScreen/TimerSettings.kt | 315 ++++++++++++++++++ .../java/org/nsh07/pomodoro/ui/theme/Color.kt | 24 ++ 5 files changed, 787 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt index 96da61c..499b503 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt @@ -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 . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui.settingsScreen @@ -36,7 +46,6 @@ 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.robotoFlexHeadline 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 @@ -71,10 +80,7 @@ fun AboutCard(modifier: Modifier = Modifier) { style = MaterialTheme.typography.titleLarge, fontFamily = robotoFlexTopBar ) - Text( - text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", - fontFamily = robotoFlexHeadline - ) + Text(text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") } Spacer(modifier = Modifier.weight(1f)) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt new file mode 100644 index 0000000..659d75f --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt @@ -0,0 +1,236 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen + +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.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.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.alarm, + 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("Alarm", fontFamily = robotoFlexTopBar) + }, + subtitle = { + Text("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)) } + } + } +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt new file mode 100644 index 0000000..ca27045 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen + +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.runtime.remember +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.R +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 context = LocalContext.current + + val themeMap: Map> = 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) + ) + } + val reverseThemeMap: Map = remember { + mapOf( + context.getString(R.string.system_default) to "auto", + context.getString(R.string.light) to "light", + context.getString(R.string.dark) to "dark" + ) + } + + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + + Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { + LargeFlexibleTopAppBar( + title = { + Text("Appearance", fontFamily = robotoFlexTopBar) + }, + subtitle = { + Text("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, + themeMap = themeMap, + reverseThemeMap = reverseThemeMap, + 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 = {} + ) +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt new file mode 100644 index 0000000..46354cd --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt @@ -0,0 +1,315 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen + +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.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("Timer", fontFamily = robotoFlexTopBar) + }, + subtitle = { + Text("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 = {} + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt index 738f989..064d7d8 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/theme/Color.kt @@ -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 . + */ + 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, + ) } \ No newline at end of file From d6466156164af2281e5f2f1e07fd6e0b1cdb5354 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 22 Oct 2025 13:02:55 +0200 Subject: [PATCH 21/35] Translated using Weblate (Turkish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (French) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (French) Currently translated at 100.0% (60 of 60 strings) Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (60 of 60 strings) Co-authored-by: Clyde Johanson Co-authored-by: Loïc de Porcaro Co-authored-by: Mertcan Atmaca Co-authored-by: savoie7304 Translate-URL: https://hosted.weblate.org/projects/tomato/strings/fr/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/tr/ Translate-URL: https://hosted.weblate.org/projects/tomato/strings/zh_Hans/ Translation: Tomato/Strings --- app/src/main/res/values-fr/strings.xml | 2 ++ app/src/main/res/values-tr/strings.xml | 2 ++ app/src/main/res/values-zh-rCN/strings.xml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index c2374e6..60b8b1d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -57,4 +57,6 @@ Minuteur Progression du minuteur 12 derniers mois + Appuyez n\'importe où lors de l\'affichage du minuteur pour passer en mode AOD + Affichage Permanent diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 0a675a5..7dc4c8d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -57,4 +57,6 @@ Zamanlayıcı İlerlemesi Geçen yıl Alarm + Her zaman açık ekran + Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 43e1708..ad99d0d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -57,4 +57,6 @@ 计时器进度 %2$d 中的 %1$d 去年 + 息屏显示 + 查看计时器时点击任意位置切换至 AOD 模式 From 612bc27859ddcbb415850b7106bc24ea7c86d185 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 18:24:26 +0530 Subject: [PATCH 22/35] refactor: reorganize folder structure of settingsScreen --- .../settingsScreen/MinutesTransformation.kt | 25 -- .../ui/settingsScreen/SettingsScreen.kt | 399 +----------------- .../ui/settingsScreen/SettingsSwitchItem.kt | 29 ++ .../{ => components}/AboutCard.kt | 2 +- .../ColorSchemePickerDialog.kt | 19 +- .../ColorSchemePickerListItem.kt | 16 +- .../{ => components}/MinuteInputField.kt | 19 +- .../components/MinutesTransformation.kt | 42 ++ .../{ => components}/ThemeDialog.kt | 22 +- .../{ => components}/ThemePickerListItem.kt | 16 +- .../{ => screens}/AlarmSettings.kt | 25 +- .../{ => screens}/AppearanceSettings.kt | 5 +- .../{ => screens}/TimerSettings.kt | 4 +- 13 files changed, 199 insertions(+), 424 deletions(-) delete mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinutesTransformation.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => components}/AboutCard.kt (99%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => components}/ColorSchemePickerDialog.kt (87%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => components}/ColorSchemePickerListItem.kt (73%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => components}/MinuteInputField.kt (77%) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinutesTransformation.kt rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => components}/ThemeDialog.kt (87%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => components}/ThemePickerListItem.kt (72%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => screens}/AlarmSettings.kt (92%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => screens}/AppearanceSettings.kt (96%) rename app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/{ => screens}/TimerSettings.kt (98%) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinutesTransformation.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinutesTransformation.kt deleted file mode 100644 index e68c680..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinutesTransformation.kt +++ /dev/null @@ -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") - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index b2bca48..9bc992a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -1,97 +1,66 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui.settingsScreen -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.annotation.StringRes -import androidx.compose.animation.AnimatedVisibility 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.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.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 org.nsh07.pomodoro.R import org.nsh07.pomodoro.service.TimerService +import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel 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) @@ -178,100 +147,7 @@ private fun SettingsScreen( onColorSchemeChange: (Color) -> Unit, modifier: Modifier = Modifier ) { - val context = LocalContext.current - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - val switchColors = SwitchDefaults.colors( - checkedIconColor = colorScheme.primary, - ) - - val themeMap: Map> = 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) - ) - } - val reverseThemeMap: Map = 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, - preferencesState.aodEnabled, - alarmEnabled, - vibrateEnabled - ) { - listOf( - SettingsSwitchItem( - checked = preferencesState.blackTheme, - icon = R.drawable.contrast, - label = R.string.black_theme, - description = R.string.black_theme_desc, - onClick = onBlackThemeChange - ), - SettingsSwitchItem( - checked = preferencesState.aodEnabled, - icon = R.drawable.aod, - label = R.string.always_on_display, - description = R.string.always_on_display_desc, - onClick = onAodEnabledChange - ), - SettingsSwitchItem( - checked = alarmEnabled, - icon = R.drawable.alarm_on, - label = R.string.alarm, - 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 - ) - ) - } + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { TopAppBar( @@ -298,246 +174,15 @@ private fun SettingsScreen( .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 { - 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 { AboutCard() } 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) - ) - } - itemsIndexed(switchItems.take(2)) { 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 - .padding(top = if (index != 0) 16.dp else 0.dp) - .clip(if (index == 0) bottomListItemShape else cardShape) - ) - } + item {} 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(2)) { 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 - 2 -> 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) - ) { - 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) - ) - } - } - } } } } @@ -570,11 +215,3 @@ fun SettingsScreenPreview() { ) } } - -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 -) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt new file mode 100644 index 0000000..75638e3 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsSwitchItem.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen + +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 +) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt similarity index 99% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt index 499b503..fe0e1ce 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AboutCard.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt @@ -15,7 +15,7 @@ * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.components import android.widget.Toast import androidx.compose.foundation.layout.Arrangement diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt similarity index 87% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt index 519f8c7..052f051 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt @@ -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 . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Column diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt similarity index 73% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt index 088d5be..549ac64 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt @@ -1,11 +1,21 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Icon diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinuteInputField.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt similarity index 77% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinuteInputField.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt index 6a55afc..1b1f449 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/MinuteInputField.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinuteInputField.kt @@ -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 . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.animation.animateColorAsState import androidx.compose.foundation.background diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinutesTransformation.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinutesTransformation.kt new file mode 100644 index 0000000..7598168 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/MinutesTransformation.kt @@ -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 . + */ + +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") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt similarity index 87% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt index 1de30cd..244612b 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt @@ -1,11 +1,21 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.animation.AnimatedContent import androidx.compose.foundation.layout.Arrangement @@ -85,7 +95,7 @@ fun ThemeDialog( ) { themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry> -> val text = pair.value.second - val selected = text == selectedOption.value + val selected = text == selectedOption.intValue ListItem( leadingContent = { @@ -113,9 +123,9 @@ fun ThemeDialog( } ) .selectable( - selected = (text == selectedOption.value), + selected = (text == selectedOption.intValue), onClick = { - selectedOption.value = text + selectedOption.intValue = text onThemeChange( reverseThemeMap[context.getString( selectedOption.intValue diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt similarity index 72% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt index 0c66f00..4701d19 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt @@ -1,11 +1,21 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Icon diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt similarity index 92% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index 659d75f..f971356 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -15,7 +15,7 @@ * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.screens import android.app.Activity import android.content.Intent @@ -57,11 +57,13 @@ 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 @@ -234,3 +236,24 @@ fun AlarmSettings( } } } + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Preview +@Composable +fun AlarmSettingsPreview() { + val preferencesState = PreferencesState( + theme = "auto", + colorScheme = "White", + blackTheme = false, + aodEnabled = false + ) + AlarmSettings( + preferencesState = preferencesState, + alarmEnabled = true, + vibrateEnabled = false, + alarmSound = "", + onAlarmEnabledChange = {}, + onVibrateEnabledChange = {}, + onAlarmSoundChanged = {}, + onBack = {}) +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt similarity index 96% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index ca27045..c9379f3 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/AppearanceSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -15,7 +15,7 @@ * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.screens import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement @@ -48,6 +48,9 @@ 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 diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt similarity index 98% rename from app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index 46354cd..c10d0ab 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -15,7 +15,7 @@ * If not, see . */ -package org.nsh07.pomodoro.ui.settingsScreen +package org.nsh07.pomodoro.ui.settingsScreen.screens import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.background @@ -65,6 +65,8 @@ 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 From cfb1a75d216847844f69912eec1c96fd284c9a24 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 20:25:40 +0530 Subject: [PATCH 23/35] feat(ui): implement navigation between settings pages --- .../java/org/nsh07/pomodoro/MainActivity.kt | 42 ++--- .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 21 ++- .../java/org/nsh07/pomodoro/ui/Navigation.kt | 62 +++++++ .../main/java/org/nsh07/pomodoro/ui/Screen.kt | 40 ++++- .../ui/settingsScreen/SettingsScreen.kt | 163 ++++++++++++++---- .../settingsScreen/screens/AlarmSettings.kt | 11 +- .../screens/AppearanceSettings.kt | 19 +- .../settingsScreen/screens/TimerSettings.kt | 4 +- .../main/res/drawable/arrow_forward_big.xml | 26 +++ app/src/main/res/values/strings.xml | 19 ++ 10 files changed, 319 insertions(+), 88 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/Navigation.kt create mode 100644 app/src/main/res/drawable/arrow_forward_big.xml diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index a98932a..9404106 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -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 . + */ + 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 @@ -70,27 +85,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 - ) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index b26212e..6d1123e 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -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 . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui @@ -45,7 +55,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 @@ -100,7 +109,7 @@ fun AppScreen( if (wide) ShortNavigationBarArrangement.Centered else ShortNavigationBarArrangement.EqualWeight ) { - screens.forEach { + mainScreens.forEach { val selected = backStack.last() == it.route ShortNavigationBarItem( selected = selected, @@ -131,7 +140,7 @@ fun AppScreen( SharedTransitionLayout { NavDisplay( backStack = backStack, - onBack = { backStack.removeLastOrNull() }, + onBack = backStack::removeLastOrNull, transitionSpec = { ContentTransform( fadeIn(motionScheme.defaultEffectsSpec()), @@ -213,7 +222,7 @@ fun AppScreen( ) } - entry { + entry { SettingsScreenRoot( modifier = modifier.padding( start = contentPadding.calculateStartPadding(layoutDirection), diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/Navigation.kt b/app/src/main/java/org/nsh07/pomodoro/ui/Navigation.kt new file mode 100644 index 0000000..2641e7a --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/Navigation.kt @@ -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 . + */ + +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) + ) +) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt index 2a43655..a37fe21 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/Screen.kt @@ -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 . + */ + package org.nsh07.pomodoro.ui import androidx.annotation.DrawableRes @@ -13,7 +30,19 @@ sealed class Screen : NavKey { object AOD : Screen() @Serializable - object Settings : Screen() + sealed class Settings : Screen() { + @Serializable + object Main : Settings() + + @Serializable + object Alarm : Settings() + + @Serializable + object Appearance : Settings() + + @Serializable + object Timer : Settings() + } @Serializable object Stats : Screen() @@ -24,4 +53,11 @@ data class NavItem( @param:DrawableRes val unselectedIcon: Int, @param:DrawableRes val selectedIcon: Int, @param:StringRes val label: Int -) \ No newline at end of file +) + +data class SettingsNavItem( + val route: Screen.Settings, + @param:DrawableRes val icon: Int, + @param:StringRes val label: Int, + val innerSettings: List +) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 9bc992a..ebc7c3f 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -19,6 +19,11 @@ package org.nsh07.pomodoro.ui.settingsScreen import android.content.Intent import android.net.Uri +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.layout.Arrangement import androidx.compose.foundation.layout.Column @@ -27,10 +32,12 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed 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.Icon import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.SliderState import androidx.compose.material3.Text @@ -40,24 +47,36 @@ import androidx.compose.material3.rememberSliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow 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.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.runtime.rememberNavBackStack +import androidx.navigation3.ui.NavDisplay import org.nsh07.pomodoro.R import org.nsh07.pomodoro.service.TimerService +import org.nsh07.pomodoro.ui.ClickableListItem +import org.nsh07.pomodoro.ui.Screen import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard +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.topBarColors import org.nsh07.pomodoro.ui.theme.TomatoTheme @@ -147,44 +166,122 @@ private fun SettingsScreen( onColorSchemeChange: (Color) -> Unit, modifier: Modifier = Modifier ) { + val context = LocalContext.current val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + val backStack = rememberNavBackStack(Screen.Settings.Main) - Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { - TopAppBar( - title = { - Text( - stringResource(R.string.settings), - style = LocalTextStyle.current.copy( - fontFamily = robotoFlexTopBar, - fontSize = 32.sp, - lineHeight = 32.sp + 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 { + 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 { 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 { + AlarmSettings( + preferencesState = preferencesState, + alarmEnabled = alarmEnabled, + vibrateEnabled = vibrateEnabled, + alarmSound = alarmSound, + onAlarmEnabledChange = onAlarmEnabledChange, + onVibrateEnabledChange = onVibrateEnabledChange, + onAlarmSoundChanged = onAlarmSoundChanged, + onBack = backStack::removeLastOrNull ) - }, - 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 { AboutCard() } - - item { Spacer(Modifier.height(12.dp)) } - - item {} - - item { Spacer(Modifier.height(12.dp)) } + } + entry { + AppearanceSettings( + preferencesState = preferencesState, + onBlackThemeChange = onBlackThemeChange, + onThemeChange = onThemeChange, + onColorSchemeChange = onColorSchemeChange, + onBack = backStack::removeLastOrNull + ) + } + entry { + TimerSettings( + aodEnabled = preferencesState.aodEnabled, + focusTimeInputFieldState = focusTimeInputFieldState, + shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, + longBreakTimeInputFieldState = longBreakTimeInputFieldState, + sessionsSliderState = sessionsSliderState, + onAodEnabledChange = onAodEnabledChange, + onBack = backStack::removeLastOrNull + ) + } } - } + ) } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index f971356..3b6b48f 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -151,10 +151,10 @@ fun AlarmSettings( Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { LargeFlexibleTopAppBar( title = { - Text("Alarm", fontFamily = robotoFlexTopBar) + Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar) }, subtitle = { - Text("Settings") + Text(stringResource(R.string.settings)) }, navigationIcon = { IconButton(onBack) { @@ -241,12 +241,7 @@ fun AlarmSettings( @Preview @Composable fun AlarmSettingsPreview() { - val preferencesState = PreferencesState( - theme = "auto", - colorScheme = "White", - blackTheme = false, - aodEnabled = false - ) + val preferencesState = PreferencesState() AlarmSettings( preferencesState = preferencesState, alarmEnabled = true, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index c9379f3..81175af 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -42,7 +42,6 @@ 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.tooling.preview.Preview @@ -70,8 +69,6 @@ fun AppearanceSettings( onBack: () -> Unit, modifier: Modifier = Modifier ) { - val context = LocalContext.current - val themeMap: Map> = remember { mapOf( "auto" to Pair( @@ -82,23 +79,21 @@ fun AppearanceSettings( "dark" to Pair(R.drawable.dark_mode, R.string.dark) ) } - val reverseThemeMap: Map = remember { - mapOf( - context.getString(R.string.system_default) to "auto", - context.getString(R.string.light) to "light", - context.getString(R.string.dark) to "dark" - ) - } + val reverseThemeMap: Map = mapOf( + stringResource(R.string.system_default) to "auto", + stringResource(R.string.light) to "light", + stringResource(R.string.dark) to "dark" + ) val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { LargeFlexibleTopAppBar( title = { - Text("Appearance", fontFamily = robotoFlexTopBar) + Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar) }, subtitle = { - Text("Settings") + Text(stringResource(R.string.settings)) }, navigationIcon = { IconButton(onBack) { diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt index c10d0ab..01dddba 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -93,10 +93,10 @@ fun TimerSettings( Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { LargeFlexibleTopAppBar( title = { - Text("Timer", fontFamily = robotoFlexTopBar) + Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar) }, subtitle = { - Text("Settings") + Text(stringResource(R.string.settings)) }, navigationIcon = { IconButton(onBack) { diff --git a/app/src/main/res/drawable/arrow_forward_big.xml b/app/src/main/res/drawable/arrow_forward_big.xml new file mode 100644 index 0000000..eac1368 --- /dev/null +++ b/app/src/main/res/drawable/arrow_forward_big.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c185a08..2697a07 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,3 +1,20 @@ + + Alarm Ring alarm when a timer completes @@ -59,4 +76,6 @@ Vibrate Vibrate when a timer completes Weekly productivity analysis + Appearance + Durations \ No newline at end of file From 2644974ad5117a9112889bf4a72a3c8a429266a4 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 22:13:19 +0530 Subject: [PATCH 24/35] feat(ui): make settings screen remember its state when navigating --- .../ui/settingsScreen/SettingsScreen.kt | 41 +++---------------- .../viewModel/SettingsViewModel.kt | 18 +++++++- 2 files changed, 21 insertions(+), 38 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index ebc7c3f..47195a3 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed 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.Icon @@ -43,12 +42,12 @@ import androidx.compose.material3.SliderState 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.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -57,14 +56,11 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow -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.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation3.runtime.entryProvider -import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import org.nsh07.pomodoro.R import org.nsh07.pomodoro.service.TimerService @@ -79,7 +75,6 @@ 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.topBarColors -import org.nsh07.pomodoro.ui.theme.TomatoTheme @OptIn(ExperimentalMaterial3Api::class) @@ -90,6 +85,8 @@ fun SettingsScreenRoot( ) { val context = LocalContext.current + val backStack = viewModel.backStack + DisposableEffect(Unit) { viewModel.runTextFieldFlowCollection() onDispose { viewModel.cancelTextFieldFlowCollection() } @@ -122,6 +119,7 @@ fun SettingsScreenRoot( SettingsScreen( preferencesState = preferencesState, + backStack = backStack, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, @@ -150,6 +148,7 @@ fun SettingsScreenRoot( @Composable private fun SettingsScreen( preferencesState: PreferencesState, + backStack: SnapshotStateList, focusTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState, @@ -168,7 +167,6 @@ private fun SettingsScreen( ) { val context = LocalContext.current val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() - val backStack = rememberNavBackStack(Screen.Settings.Main) NavDisplay( backStack = backStack, @@ -283,32 +281,3 @@ private fun SettingsScreen( } ) } - -@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 = {}, - onAodEnabledChange = {}, - onAlarmSoundChanged = {}, - onThemeChange = {}, - onColorSchemeChange = {}, - modifier = Modifier.fillMaxSize() - ) - } -} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt index 454bf82..76ebb8c 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/viewModel/SettingsViewModel.kt @@ -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 . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui.settingsScreen.viewModel @@ -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.Main) + private val _preferencesState = MutableStateFlow(PreferencesState()) val preferencesState = _preferencesState.asStateFlow() From d3299d0818b7dab0a1f9e6daa6a000196844b79b Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 22:54:19 +0530 Subject: [PATCH 25/35] feat(ui): implement waking screen on alarm without wakelock Closes: #46 --- app/src/main/AndroidManifest.xml | 19 ++++++- .../java/org/nsh07/pomodoro/MainActivity.kt | 6 ++- .../org/nsh07/pomodoro/data/AppContainer.kt | 17 ++++++- .../nsh07/pomodoro/service/TimerService.kt | 49 +++++++++---------- .../pomodoro/ui/timerScreen/AlarmDialog.kt | 32 +++++------- 5 files changed, 73 insertions(+), 50 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 84c8ae7..3f3f018 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,20 @@ - + + @@ -7,7 +23,6 @@ - . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.data @@ -27,6 +37,7 @@ interface AppContainer { val notificationBuilder: NotificationCompat.Builder val timerState: MutableStateFlow val time: MutableStateFlow + 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 = {} + } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index fc104ab..e9158c5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -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 . + */ + package org.nsh07.pomodoro.service import android.annotation.SuppressLint @@ -7,7 +24,6 @@ import android.media.AudioAttributes import android.media.MediaPlayer import android.os.Build import android.os.IBinder -import android.os.PowerManager import android.os.SystemClock import android.os.VibrationEffect import android.os.Vibrator @@ -23,7 +39,6 @@ import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking -import org.nsh07.pomodoro.MainActivity import org.nsh07.pomodoro.R import org.nsh07.pomodoro.TomatoApplication import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode @@ -60,10 +75,6 @@ class TimerService : Service() { private val skipScope = CoroutineScope(Dispatchers.IO + job) private var alarm: MediaPlayer? = null - - - private var wakeLock: PowerManager.WakeLock? = null - private val vibrator by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager @@ -94,7 +105,6 @@ class TimerService : Service() { saveTimeToDb() notificationManager.cancel(1) alarm?.release() - wakeLock?.release() } super.onDestroy() } @@ -123,7 +133,6 @@ class TimerService : Service() { return super.onStartCommand(intent, flags, startId) } - private fun toggleTimer() { updateProgressSegments() @@ -164,20 +173,6 @@ class TimerService : Service() { if (iterations == 0) showTimerNotification(time.toInt()) if (time < 0) { - val powerManager = this@TimerService.getSystemService(POWER_SERVICE) as PowerManager - wakeLock = powerManager.newWakeLock( - PowerManager.SCREEN_BRIGHT_WAKE_LOCK or - PowerManager.ACQUIRE_CAUSES_WAKEUP or - PowerManager.ON_AFTER_RELEASE, - "PomodoroApp:AlarmWakeLock" - ) - wakeLock?.acquire(2 * 60 * 1000L) - val intent = Intent(this@TimerService, MainActivity::class.java).apply { - addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP) - } - startActivity(intent) - - skipTimer() _timerState.update { currentState -> currentState.copy(timerRunning = false) @@ -197,7 +192,10 @@ class TimerService : Service() { } } - @SuppressLint("MissingPermission", "StringFormatInvalid") // 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 ) { @@ -372,6 +370,8 @@ class TimerService : Service() { fun startAlarm() { if (timerRepository.alarmEnabled) alarm?.start() + appContainer.activityTurnScreenOn(true) + if (timerRepository.vibrateEnabled) { if (!vibrator.hasVibrator()) { return @@ -393,8 +393,7 @@ class TimerService : Service() { vibrator.cancel() } - wakeLock?.release() - wakeLock = null + appContainer.activityTurnScreenOn(false) _timerState.update { currentState -> currentState.copy(alarmRinging = false) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt index ed39a73..7d13fa7 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/timerScreen/AlarmDialog.kt @@ -1,13 +1,22 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui.timerScreen -import androidx.activity.compose.LocalActivity import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer @@ -25,7 +34,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource @@ -39,21 +47,6 @@ fun AlarmDialog( modifier: Modifier = Modifier, stopAlarm: () -> Unit ) { - val activity = LocalActivity.current - - // Set lockscreen flags when dialog appears, remove when it disappears - DisposableEffect(Unit) { - // Show over lockscreen - activity?.setShowWhenLocked(true) - activity?.setTurnScreenOn(true) - - onDispose { - // Remove lockscreen flags when dialog is dismissed - activity?.setShowWhenLocked(false) - activity?.setTurnScreenOn(false) - } - } - BasicAlertDialog( onDismissRequest = stopAlarm, modifier = modifier @@ -93,4 +86,3 @@ fun AlarmDialog( } } } - From aa4889a1fbba418017eb5c77ff0b0e3c1c8b69af Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Wed, 22 Oct 2025 23:04:22 +0530 Subject: [PATCH 26/35] chore(deps): bump library versions --- gradle/libs.versions.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e776d2..5b7adf6 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,8 +1,8 @@ [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" @@ -11,8 +11,8 @@ kotlin = "2.2.20" 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] From a93e748e04fa2c6a9dc4f4cf6d93c3fb31b8e31a Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 09:33:00 +0530 Subject: [PATCH 27/35] feat(alarm): automatically stop alarm after 1 min Closes: #88 --- .../main/java/org/nsh07/pomodoro/MainActivity.kt | 2 ++ .../org/nsh07/pomodoro/service/TimerService.kt | 14 ++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 6310ff5..3b1fc72 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -45,10 +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() diff --git a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt index e9158c5..b2423d1 100644 --- a/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt +++ b/app/src/main/java/org/nsh07/pomodoro/service/TimerService.kt @@ -33,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 @@ -71,9 +72,11 @@ 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 autoAlarmStopScope: Job? = null + private var alarm: MediaPlayer? = null private val vibrator by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { @@ -154,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() @@ -372,6 +375,11 @@ class TimerService : Service() { appContainer.activityTurnScreenOn(true) + autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch { + delay(1 * 60 * 1000) + stopAlarm() + } + if (timerRepository.vibrateEnabled) { if (!vibrator.hasVibrator()) { return @@ -384,6 +392,8 @@ class TimerService : Service() { } fun stopAlarm() { + autoAlarmStopScope?.cancel() + if (timerRepository.alarmEnabled) { alarm?.pause() alarm?.seekTo(0) From be25eb1a5906d425b9903393d9c05b4625688bb6 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 14:45:16 +0530 Subject: [PATCH 28/35] fix: fix incorrect colors and modify strings --- .../nsh07/pomodoro/ui/ClickableListItem.kt | 20 ++++++++++++++++++- .../components/ColorSchemePickerListItem.kt | 2 -- .../components/ThemePickerListItem.kt | 2 -- .../settingsScreen/screens/AlarmSettings.kt | 2 +- app/src/main/res/values/strings.xml | 3 ++- 5 files changed, 22 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt index 2d05a68..6eae35d 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt @@ -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 . + */ + package org.nsh07.pomodoro.ui import androidx.compose.animation.core.animateDpAsState @@ -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, diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt index 549ac64..e259d25 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt @@ -32,7 +32,6 @@ 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( @@ -67,7 +66,6 @@ fun ColorSchemePickerListItem( else stringResource(R.string.color) ) }, - colors = listItemColors, items = items, index = index, modifier = modifier.fillMaxWidth() diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt index 4701d19..e879cd5 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt @@ -30,7 +30,6 @@ 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( @@ -65,7 +64,6 @@ fun ThemePickerListItem( supportingContent = { Text(stringResource(themeMap[theme]!!.second)) }, - colors = listItemColors, items = items, index = index, modifier = modifier.fillMaxWidth() diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt index 3b6b48f..bc46e07 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -134,7 +134,7 @@ fun AlarmSettings( SettingsSwitchItem( checked = alarmEnabled, icon = R.drawable.alarm_on, - label = R.string.alarm, + label = R.string.sound, description = R.string.alarm_desc, onClick = onAlarmEnabledChange ), diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2697a07..975c571 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -73,9 +73,10 @@ Today Up next Up next: %1$s (%2$s) - Vibrate + Vibration Vibrate when a timer completes Weekly productivity analysis Appearance Durations + Sound \ No newline at end of file From 17293d040cabec600939b9e67684db1566f5c8fc Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 18:49:59 +0530 Subject: [PATCH 29/35] refactor: move ClickableListItem --- .../java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt | 2 +- .../ui/{ => settingsScreen/components}/ClickableListItem.kt | 2 +- .../ui/settingsScreen/components/ColorSchemePickerListItem.kt | 1 - .../ui/settingsScreen/components/ThemePickerListItem.kt | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) rename app/src/main/java/org/nsh07/pomodoro/ui/{ => settingsScreen/components}/ClickableListItem.kt (98%) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 47195a3..512df9a 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -64,9 +64,9 @@ 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.ClickableListItem 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 diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ClickableListItem.kt similarity index 98% rename from app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt rename to app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ClickableListItem.kt index 6eae35d..3f99a28 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ClickableListItem.kt @@ -15,7 +15,7 @@ * If not, see . */ -package org.nsh07.pomodoro.ui +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt index e259d25..6f081ef 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt @@ -31,7 +31,6 @@ 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 @Composable fun ColorSchemePickerListItem( diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt index e879cd5..7cf5594 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt @@ -29,7 +29,6 @@ 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 @Composable fun ThemePickerListItem( From 574c5ec1df092deacf34ceeee15858fd072576c6 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 19:56:26 +0530 Subject: [PATCH 30/35] feat(ui): improve color scheme selection UI --- .../components/ColorSchemePickerDialog.kt | 175 ----------------- .../components/ColorSchemePickerListItem.kt | 181 ++++++++++++++---- 2 files changed, 149 insertions(+), 207 deletions(-) delete mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt deleted file mode 100644 index 052f051..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerDialog.kt +++ /dev/null @@ -1,175 +0,0 @@ -/* - * Copyright (c) 2025 Nishant Mishra - * - * This file is part of Tomato - a minimalist pomodoro timer for Android. - * - * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU - * General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tomato. - * If not, see . - */ - -package org.nsh07.pomodoro.ui.settingsScreen.components - -import androidx.compose.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 } - ) - } -} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt index 6f081ef..03549b3 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt @@ -17,20 +17,39 @@ package org.nsh07.pomodoro.ui.settingsScreen.components -import androidx.compose.foundation.layout.fillMaxWidth +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.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.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( @@ -40,33 +59,131 @@ fun ColorSchemePickerListItem( onColorChange: (Color) -> Unit, modifier: Modifier = Modifier ) { - var showDialog by rememberSaveable { mutableStateOf(false) } + 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 + ) - if (showDialog) { - ColorSchemePickerDialog( - currentColor = color, - setShowDialog = { showDialog = it }, - onColorChange = onColorChange + 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) ) - } - 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) - ) - }, - items = items, - index = index, - modifier = modifier.fillMaxWidth() - ) { showDialog = true } -} \ No newline at end of file + 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 + ) + } + } + } +} From 89f4d4e77c804e36ee850c39a716235a19888281 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 23 Oct 2025 15:21:10 +0200 Subject: [PATCH 31/35] Translated using Weblate (Turkish) Currently translated at 100.0% (62 of 62 strings) Co-authored-by: Hosted Weblate Co-authored-by: Mertcan Atmaca Translate-URL: https://hosted.weblate.org/projects/tomato/strings/tr/ Translation: Tomato/Strings --- app/src/main/res/values-tr/strings.xml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/main/res/values-tr/strings.xml b/app/src/main/res/values-tr/strings.xml index 7dc4c8d..a06754d 100644 --- a/app/src/main/res/values-tr/strings.xml +++ b/app/src/main/res/values-tr/strings.xml @@ -59,4 +59,6 @@ Alarm Her zaman açık ekran Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun + Görünüm + Süreler From 6e6f5477e1693653998d743b1427cfcdb102d925 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 20:54:08 +0530 Subject: [PATCH 32/35] feat(ui): redesign theme selection UI --- .../settingsScreen/components/ThemeDialog.kt | 151 ------------------ .../components/ThemePickerListItem.kt | 121 ++++++++++---- .../screens/AppearanceSettings.kt | 19 --- app/src/main/res/values/strings.xml | 2 +- 4 files changed, 89 insertions(+), 204 deletions(-) delete mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt deleted file mode 100644 index 244612b..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemeDialog.kt +++ /dev/null @@ -1,151 +0,0 @@ -/* - * Copyright (c) 2025 Nishant Mishra - * - * This file is part of Tomato - a minimalist pomodoro timer for Android. - * - * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU - * General Public License as published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even - * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General - * Public License for more details. - * - * You should have received a copy of the GNU General Public License along with Tomato. - * If not, see . - */ - -package org.nsh07.pomodoro.ui.settingsScreen.components - -import androidx.compose.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.mutableIntStateOf -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.platform.LocalContext -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>, - reverseThemeMap: Map, - theme: String, - setShowThemeDialog: (Boolean) -> Unit, - onThemeChange: (String) -> Unit -) { - val selectedOption = - remember { mutableIntStateOf(themeMap[theme]!!.second) } - - val context = LocalContext.current - - 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> -> - val text = pair.value.second - val selected = text == selectedOption.intValue - - ListItem( - leadingContent = { - AnimatedContent(selected) { - if (it) - Icon(painterResource(R.drawable.check), null) - else - Icon(painterResource(pair.value.first), null) - } - }, - headlineContent = { - Text( - text = stringResource(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.intValue), - onClick = { - selectedOption.intValue = text - onThemeChange( - reverseThemeMap[context.getString( - selectedOption.intValue - )]!! - ) - }, - 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)) - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt index 7cf5594..0392765 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt @@ -17,54 +17,109 @@ package org.nsh07.pomodoro.ui.settingsScreen.components -import androidx.compose.foundation.layout.fillMaxWidth +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.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +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, - themeMap: Map>, - reverseThemeMap: Map, items: Int, index: Int, onThemeChange: (String) -> Unit, - modifier: Modifier = Modifier + modifier: Modifier = Modifier, ) { - var showDialog by rememberSaveable { mutableStateOf(false) } - - if (showDialog) { - ThemeDialog( - themeMap = themeMap, - reverseThemeMap = reverseThemeMap, - theme = theme, - setShowThemeDialog = { showDialog = it }, - onThemeChange = onThemeChange + val themeMap: Map> = 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) ) } - ClickableListItem( - leadingContent = { - Icon( - painter = painterResource(themeMap[theme]!!.first), - contentDescription = null - ) - }, - headlineContent = { Text(stringResource(R.string.theme)) }, - supportingContent = { - Text(stringResource(themeMap[theme]!!.second)) - }, - items = items, - index = index, - modifier = modifier.fillMaxWidth() - ) { showDialog = true } -} \ No newline at end of file + 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 + ) + } + } + } + } +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt index 81175af..eb55fb8 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -37,7 +37,6 @@ import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBarDefaults 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.graphics.Color @@ -69,22 +68,6 @@ fun AppearanceSettings( onBack: () -> Unit, modifier: Modifier = Modifier ) { - val themeMap: Map> = 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) - ) - } - val reverseThemeMap: Map = mapOf( - stringResource(R.string.system_default) to "auto", - stringResource(R.string.light) to "light", - stringResource(R.string.dark) to "dark" - ) - val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { @@ -128,8 +111,6 @@ fun AppearanceSettings( item { ThemePickerListItem( theme = preferencesState.theme, - themeMap = themeMap, - reverseThemeMap = reverseThemeMap, onThemeChange = onThemeChange, items = 3, index = 1, diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 975c571..cf51e15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -65,7 +65,7 @@ Stop alarm Current timer session is complete. Tap anywhere to stop the alarm. Stop Alarm? - System default + System Theme Timer Timer progress From b363f5f10f0975a6c2f79cfadbc29f9b7b3f66ea Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 21:09:02 +0530 Subject: [PATCH 33/35] feat(ui): make predictive back animation more subtle --- .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 22 ++++++------------- 1 file changed, 7 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 6d1123e..4d95b23 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -19,12 +19,11 @@ package org.nsh07.pomodoro.ui import android.content.Intent import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.ContentTransform 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 @@ -142,23 +141,16 @@ fun AppScreen( backStack = backStack, onBack = backStack::removeLastOrNull, transitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) - ) + fadeIn(motionScheme.defaultEffectsSpec()) + .togetherWith(fadeOut(motionScheme.defaultEffectsSpec())) }, popTransitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) - ) + fadeIn(motionScheme.defaultEffectsSpec()) + .togetherWith(fadeOut(motionScheme.defaultEffectsSpec())) }, predictivePopTransitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) + - scaleOut(targetScale = 0.7f), - ) + fadeIn(motionScheme.defaultEffectsSpec()) + .togetherWith(fadeOut(motionScheme.defaultEffectsSpec())) }, entryProvider = entryProvider { entry { From 678a86ee548482069ccafd1922c2a9885adce35f Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 21:35:12 +0530 Subject: [PATCH 34/35] feat(timer): reduce timer update frequency in AOD mode --- .../java/org/nsh07/pomodoro/MainActivity.kt | 5 ++++- .../org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt | 20 ++++++++++++++++--- .../java/org/nsh07/pomodoro/ui/AppScreen.kt | 4 +++- 3 files changed, 24 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index 3b1fc72..a129af6 100644 --- a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt +++ b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt @@ -74,7 +74,10 @@ class MainActivity : ComponentActivity() { AppScreen( timerViewModel = timerViewModel, - isAODEnabled = preferencesState.aodEnabled + isAODEnabled = preferencesState.aodEnabled, + setTimerFrequency = { + appContainer.appTimerRepository.timerFrequency = it + } ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt index 8225cc2..f5d4af4 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -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 . + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . */ package org.nsh07.pomodoro.ui @@ -76,6 +86,7 @@ import kotlin.random.Random fun SharedTransitionScope.AlwaysOnDisplay( timerState: TimerState, progress: () -> Float, + setTimerFrequency: (Float) -> Unit, modifier: Modifier = Modifier ) { var sharedElementTransitionComplete by remember { mutableStateOf(false) } @@ -89,6 +100,7 @@ fun SharedTransitionScope.AlwaysOnDisplay( 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 @@ -102,6 +114,7 @@ fun SharedTransitionScope.AlwaysOnDisplay( } onDispose { + setTimerFrequency(10f) window.clearFlags( WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON @@ -263,7 +276,8 @@ private fun AlwaysOnDisplayPreview() { SharedTransitionLayout { AlwaysOnDisplay( timerState = timerState, - progress = progress + progress = progress, + setTimerFrequency = {} ) } } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt index 4d95b23..905e377 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -67,7 +67,8 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel fun AppScreen( modifier: Modifier = Modifier, timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory), - isAODEnabled: Boolean + isAODEnabled: Boolean, + setTimerFrequency: (Float) -> Unit ) { val context = LocalContext.current @@ -204,6 +205,7 @@ fun AppScreen( AlwaysOnDisplay( timerState = uiState, progress = { progress }, + setTimerFrequency = setTimerFrequency, modifier = Modifier .then( if (isAODEnabled) Modifier.clickable { From b48ea9af223bac2c38afb90dc8cd2a58fbacaa09 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Thu, 23 Oct 2025 23:14:51 +0530 Subject: [PATCH 35/35] chore(release): bump version, update changelog --- app/build.gradle.kts | 18 ++++++++++++++---- .../metadata/android/en-US/changelogs/15.txt | 8 ++++++++ gradle/libs.versions.toml | 2 +- 3 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/15.txt diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0b48dfc..139e2d9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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 . + * 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 . */ import org.gradle.api.tasks.testing.logging.TestExceptionFormat @@ -33,8 +43,8 @@ android { applicationId = "org.nsh07.pomodoro" minSdk = 27 targetSdk = 36 - versionCode = 14 - versionName = "1.6.0-alpha01" + versionCode = 15 + versionName = "1.6.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/fastlane/metadata/android/en-US/changelogs/15.txt b/fastlane/metadata/android/en-US/changelogs/15.txt new file mode 100644 index 0000000..c2ac46f --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/15.txt @@ -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 \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5b7adf6..783dd5d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ 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"