diff --git a/app/build.gradle.kts b/app/build.gradle.kts index e9f14c9..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 @@ -31,10 +41,10 @@ android { defaultConfig { applicationId = "org.nsh07.pomodoro" - minSdk = 26 + minSdk = 27 targetSdk = 36 - versionCode = 13 - versionName = "1.5.0" + versionCode = 15 + versionName = "1.6.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 19ff772..3f3f018 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,4 +1,20 @@ - + + diff --git a/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt b/app/src/main/java/org/nsh07/pomodoro/MainActivity.kt index baf7924..a129af6 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 @@ -30,6 +45,12 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() + + appContainer.activityTurnScreenOn = { + setShowWhenLocked(it) + setTurnScreenOn(it) + } + setContent { val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle() @@ -51,11 +72,18 @@ class MainActivity : ComponentActivity() { appContainer.appTimerRepository.colorScheme = colorScheme } - AppScreen(timerViewModel = timerViewModel) + AppScreen( + timerViewModel = timerViewModel, + isAODEnabled = preferencesState.aodEnabled, + setTimerFrequency = { + appContainer.appTimerRepository.timerFrequency = it + } + ) } } } + override fun onStop() { super.onStop() // Reduce the timer loop frequency when not visible to save battery power @@ -67,27 +95,4 @@ class MainActivity : ComponentActivity() { // Increase the timer loop frequency again when visible to make the progress smoother appContainer.appTimerRepository.timerFrequency = 10f } - - companion object { - val screens = listOf( - NavItem( - Screen.Timer, - R.drawable.timer_outlined, - R.drawable.timer_filled, - R.string.timer - ), - NavItem( - Screen.Stats, - R.drawable.monitoring, - R.drawable.monitoring_filled, - R.string.stats - ), - NavItem( - Screen.Settings, - R.drawable.settings, - R.drawable.settings_filled, - R.string.settings - ) - ) - } } \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt index a681968..f36a179 100644 --- a/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.kt +++ b/app/src/main/java/org/nsh07/pomodoro/data/AppContainer.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.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 49012df..b2423d1 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 @@ -16,6 +33,7 @@ import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.asStateFlow @@ -54,11 +72,12 @@ class TimerService : Service() { private var pauseDuration = 0L private var job = SupervisorJob() - private val scope = CoroutineScope(Dispatchers.IO + job) + private val timerScope = CoroutineScope(Dispatchers.IO + job) private val skipScope = CoroutineScope(Dispatchers.IO + job) - private var alarm: MediaPlayer? = null + private var autoAlarmStopScope: Job? = null + private var alarm: MediaPlayer? = null private val vibrator by lazy { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager @@ -138,7 +157,7 @@ class TimerService : Service() { var iterations = -1 - scope.launch { + timerScope.launch { while (true) { if (!timerState.value.timerRunning) break if (startTime == 0L) startTime = SystemClock.elapsedRealtime() @@ -176,7 +195,10 @@ class TimerService : Service() { } } - @SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI + @SuppressLint( + "MissingPermission", + "StringFormatInvalid" + ) // We check for the permission when pressing the Play button in the UI fun showTimerNotification( remainingTime: Int, paused: Boolean = false, complete: Boolean = false ) { @@ -351,6 +373,13 @@ class TimerService : Service() { fun startAlarm() { if (timerRepository.alarmEnabled) alarm?.start() + appContainer.activityTurnScreenOn(true) + + autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch { + delay(1 * 60 * 1000) + stopAlarm() + } + if (timerRepository.vibrateEnabled) { if (!vibrator.hasVibrator()) { return @@ -363,6 +392,8 @@ class TimerService : Service() { } fun stopAlarm() { + autoAlarmStopScope?.cancel() + if (timerRepository.alarmEnabled) { alarm?.pause() alarm?.seekTo(0) @@ -372,6 +403,8 @@ class TimerService : Service() { vibrator.cancel() } + appContainer.activityTurnScreenOn(false) + _timerState.update { currentState -> currentState.copy(alarmRinging = false) } 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..f5d4af4 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt @@ -0,0 +1,286 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui + +import android.app.Activity +import android.view.WindowManager +import androidx.activity.compose.LocalActivity +import androidx.compose.animation.SharedTransitionLayout +import androidx.compose.animation.SharedTransitionScope +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.animateIntAsState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.size +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.CircularWavyProgressIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.motionScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableIntStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalView +import androidx.compose.ui.platform.LocalWindowInfo +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.Density +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowInsetsControllerCompat +import androidx.navigation3.ui.LocalNavAnimatedContentScope +import kotlinx.coroutines.delay +import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock +import org.nsh07.pomodoro.ui.theme.TomatoTheme +import org.nsh07.pomodoro.ui.timerScreen.TimerScreen +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode +import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState +import kotlin.random.Random + +/** + * Always On Display composable. Must be called within a [SharedTransitionScope] which allows + * animating the clock and progress indicator + * + * @param timerState [TimerState] instance. This must be the same instance as the one used on the + * root [TimerScreen] composable + * @param progress lambda that returns the current progress of the clock + * randomized offset for the clock to allow smooth motion with sharedBounds + */ +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun SharedTransitionScope.AlwaysOnDisplay( + timerState: TimerState, + progress: () -> Float, + setTimerFrequency: (Float) -> Unit, + modifier: Modifier = Modifier +) { + var sharedElementTransitionComplete by remember { mutableStateOf(false) } + + val activity = LocalActivity.current + val density = LocalDensity.current + val windowInfo = LocalWindowInfo.current + val view = LocalView.current + + val window = remember { (view.context as Activity).window } + val insetsController = remember { WindowCompat.getInsetsController(window, view) } + + DisposableEffect(Unit) { + setTimerFrequency(1f) + window.addFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or + WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + ) + activity?.setShowWhenLocked(true) + insetsController.apply { + hide(WindowInsetsCompat.Type.statusBars()) + hide(WindowInsetsCompat.Type.navigationBars()) + systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE + } + + onDispose { + setTimerFrequency(10f) + window.clearFlags( + WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or + WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON + ) + activity?.setShowWhenLocked(false) + insetsController.apply { + show(WindowInsetsCompat.Type.statusBars()) + show(WindowInsetsCompat.Type.navigationBars()) + systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } + } + } + + LaunchedEffect(Unit) { + delay(300) + sharedElementTransitionComplete = true + } + + val primary by animateColorAsState( + if (sharedElementTransitionComplete) Color(0xFFA2A2A2) + else { + if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary + else colorScheme.tertiary + }, + animationSpec = motionScheme.slowEffectsSpec() + ) + val secondaryContainer by animateColorAsState( + if (sharedElementTransitionComplete) Color(0xFF1D1D1D) + else { + if (timerState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer + else colorScheme.tertiaryContainer + }, + animationSpec = motionScheme.slowEffectsSpec() + ) + val surface by animateColorAsState( + if (sharedElementTransitionComplete) Color.Black + else colorScheme.surface, + animationSpec = motionScheme.slowEffectsSpec() + ) + val onSurface by animateColorAsState( + if (sharedElementTransitionComplete) Color(0xFFE3E3E3) + else colorScheme.onSurface, + animationSpec = motionScheme.slowEffectsSpec() + ) + + var randomX by remember { + mutableIntStateOf( + Random.nextInt( + 16.dp.toIntPx(density), + windowInfo.containerSize.width - 266.dp.toIntPx(density) + ) + ) + } + var randomY by remember { + mutableIntStateOf( + Random.nextInt( + 16.dp.toIntPx(density), + windowInfo.containerSize.height - 266.dp.toIntPx(density) + ) + ) + } + + LaunchedEffect(timerState.timeStr[1]) { // Randomize position every minute + if (sharedElementTransitionComplete) { + randomX = Random.nextInt( + 16.dp.toIntPx(density), + windowInfo.containerSize.width - 266.dp.toIntPx(density) + ) + randomY = Random.nextInt( + 16.dp.toIntPx(density), + windowInfo.containerSize.height - 266.dp.toIntPx(density) + ) + } + } + + val x by animateIntAsState(randomX, motionScheme.slowSpatialSpec()) + val y by animateIntAsState(randomY, motionScheme.slowSpatialSpec()) + + Box( + modifier = modifier + .fillMaxSize() + .background(surface) + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.offset { + IntOffset(x, y) + } + ) { + if (timerState.timerMode == TimerMode.FOCUS) { + CircularProgressIndicator( + progress = progress, + modifier = Modifier + .sharedBounds( + sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) + .size(250.dp), + color = primary, + trackColor = secondaryContainer, + strokeWidth = 12.dp, + gapSize = 8.dp, + ) + } else { + CircularWavyProgressIndicator( + progress = progress, + modifier = Modifier + .sharedBounds( + sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) + .size(250.dp), + color = primary, + trackColor = secondaryContainer, + stroke = Stroke( + width = with(LocalDensity.current) { + 12.dp.toPx() + }, + cap = StrokeCap.Round, + ), + trackStroke = Stroke( + width = with(LocalDensity.current) { + 12.dp.toPx() + }, + cap = StrokeCap.Round, + ), + wavelength = 42.dp, + gapSize = 8.dp + ) + } + + Text( + text = timerState.timeStr, + style = TextStyle( + fontFamily = openRundeClock, + fontWeight = FontWeight.Bold, + fontSize = 56.sp, + letterSpacing = (-2).sp + ), + textAlign = TextAlign.Center, + color = onSurface, + maxLines = 1, + modifier = Modifier.sharedBounds( + sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"), + animatedVisibilityScope = LocalNavAnimatedContentScope.current + ) + ) + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Preview +@Composable +private fun AlwaysOnDisplayPreview() { + val timerState = TimerState() + val progress = { 0.5f } + TomatoTheme { + SharedTransitionLayout { + AlwaysOnDisplay( + timerState = timerState, + progress = progress, + setTimerFrequency = {} + ) + } + } +} + +fun Dp.toIntPx(density: Density) = with(density) { toPx().toInt() } 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..905e377 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/AppScreen.kt @@ -1,18 +1,30 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * 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 android.content.Intent -import androidx.compose.animation.ContentTransform +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.Crossfade +import androidx.compose.animation.SharedTransitionLayout import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.animation.scaleOut +import androidx.compose.animation.togetherWith +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.calculateEndPadding import androidx.compose.foundation.layout.calculateStartPadding import androidx.compose.foundation.layout.padding @@ -42,7 +54,6 @@ import androidx.navigation3.runtime.entryProvider import androidx.navigation3.runtime.rememberNavBackStack import androidx.navigation3.ui.NavDisplay import androidx.window.core.layout.WindowSizeClass -import org.nsh07.pomodoro.MainActivity.Companion.screens import org.nsh07.pomodoro.service.TimerService import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot @@ -55,7 +66,9 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel @Composable fun AppScreen( modifier: Modifier = Modifier, - timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory) + timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory), + isAODEnabled: Boolean, + setTimerFrequency: (Float) -> Unit ) { val context = LocalContext.current @@ -78,128 +91,153 @@ fun AppScreen( } } + Scaffold( bottomBar = { - val wide = remember { - windowSizeClass.isWidthAtLeastBreakpoint( - WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND - ) - } - ShortNavigationBar( - arrangement = - if (wide) ShortNavigationBarArrangement.Centered - else ShortNavigationBarArrangement.EqualWeight + AnimatedVisibility( + backStack.last() !is Screen.AOD, + enter = fadeIn(), + exit = fadeOut() ) { - screens.forEach { - val selected = backStack.last() == it.route - ShortNavigationBarItem( - selected = selected, - onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens - { - if (backStack.size < 2) backStack.add(it.route) - else backStack[1] = it.route - } - } else { - { if (backStack.size > 1) backStack.removeAt(1) } - }, - icon = { - Crossfade(selected) { selected -> - if (selected) Icon(painterResource(it.selectedIcon), null) - else Icon(painterResource(it.unselectedIcon), null) - } - }, - iconPosition = - if (wide) NavigationItemIconPosition.Start - else NavigationItemIconPosition.Top, - label = { Text(stringResource(it.label)) } + val wide = remember { + windowSizeClass.isWidthAtLeastBreakpoint( + WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND ) } + ShortNavigationBar( + arrangement = + if (wide) ShortNavigationBarArrangement.Centered + else ShortNavigationBarArrangement.EqualWeight + ) { + mainScreens.forEach { + val selected = backStack.last() == it.route + ShortNavigationBarItem( + selected = selected, + onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens + { + if (backStack.size < 2) backStack.add(it.route) + else backStack[1] = it.route + } + } else { + { if (backStack.size > 1) backStack.removeAt(1) } + }, + icon = { + Crossfade(selected) { selected -> + if (selected) Icon(painterResource(it.selectedIcon), null) + else Icon(painterResource(it.unselectedIcon), null) + } + }, + iconPosition = + if (wide) NavigationItemIconPosition.Start + else NavigationItemIconPosition.Top, + label = { Text(stringResource(it.label)) } + ) + } + } } } ) { contentPadding -> - NavDisplay( - backStack = backStack, - onBack = { backStack.removeLastOrNull() }, - transitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) - ) - }, - popTransitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) - ) - }, - predictivePopTransitionSpec = { - ContentTransform( - fadeIn(motionScheme.defaultEffectsSpec()), - fadeOut(motionScheme.defaultEffectsSpec()) + - scaleOut(targetScale = 0.7f), - ) - }, - entryProvider = entryProvider { - entry { - TimerScreen( - timerState = uiState, - progress = { progress }, - onAction = { action -> - when (action) { - TimerAction.ResetTimer -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.RESET.toString() - context.startService(it) - } + SharedTransitionLayout { + NavDisplay( + backStack = backStack, + onBack = backStack::removeLastOrNull, + transitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) + .togetherWith(fadeOut(motionScheme.defaultEffectsSpec())) + }, + popTransitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) + .togetherWith(fadeOut(motionScheme.defaultEffectsSpec())) + }, + predictivePopTransitionSpec = { + fadeIn(motionScheme.defaultEffectsSpec()) + .togetherWith(fadeOut(motionScheme.defaultEffectsSpec())) + }, + entryProvider = entryProvider { + entry { + TimerScreen( + timerState = uiState, + progress = { progress }, + onAction = { action -> + when (action) { + TimerAction.ResetTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.RESET.toString() + context.startService(it) + } - is TimerAction.SkipTimer -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.SKIP.toString() - context.startService(it) - } + is TimerAction.SkipTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.SKIP.toString() + context.startService(it) + } - TimerAction.StopAlarm -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.STOP_ALARM.toString() - context.startService(it) - } + TimerAction.StopAlarm -> + Intent(context, TimerService::class.java).also { + it.action = + TimerService.Actions.STOP_ALARM.toString() + context.startService(it) + } - TimerAction.ToggleTimer -> - Intent(context, TimerService::class.java).also { - it.action = TimerService.Actions.TOGGLE.toString() - context.startService(it) + TimerAction.ToggleTimer -> + Intent(context, TimerService::class.java).also { + it.action = TimerService.Actions.TOGGLE.toString() + context.startService(it) + } + } + }, + modifier = modifier + .padding( + start = contentPadding.calculateStartPadding(layoutDirection), + end = contentPadding.calculateEndPadding(layoutDirection), + bottom = contentPadding.calculateBottomPadding() + ) + .then( + if (isAODEnabled) Modifier.clickable { + if (backStack.size < 2) backStack.add(Screen.AOD) } - } - }, - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() + else Modifier + ), ) - ) - } + } - entry { - SettingsScreenRoot( - modifier = modifier.padding( - start = contentPadding.calculateStartPadding(layoutDirection), - end = contentPadding.calculateEndPadding(layoutDirection), - bottom = contentPadding.calculateBottomPadding() + entry { + AlwaysOnDisplay( + timerState = uiState, + progress = { progress }, + setTimerFrequency = setTimerFrequency, + modifier = Modifier + .then( + if (isAODEnabled) Modifier.clickable { + if (backStack.size > 1) backStack.removeLastOrNull() + } + else Modifier + ) ) - ) - } + } - entry { - StatsScreenRoot( - contentPadding = contentPadding, - 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 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 85482cf..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 @@ -10,7 +27,22 @@ sealed class Screen : NavKey { object Timer : Screen() @Serializable - object Settings : Screen() + object AOD : Screen() + + @Serializable + sealed class Settings : Screen() { + @Serializable + object Main : Settings() + + @Serializable + object Alarm : Settings() + + @Serializable + object Appearance : Settings() + + @Serializable + object Timer : Settings() + } @Serializable object Stats : Screen() @@ -21,4 +53,11 @@ data class NavItem( @param:DrawableRes val unselectedIcon: Int, @param:DrawableRes val selectedIcon: Int, @param:StringRes val label: Int -) \ 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/ColorSchemePickerDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt deleted file mode 100644 index 519f8c7..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt +++ /dev/null @@ -1,158 +0,0 @@ -package org.nsh07.pomodoro.ui.settingsScreen - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.MaterialTheme.shapes -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import androidx.compose.ui.util.fastForEach -import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.ui.theme.TomatoTheme - -@OptIn(ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ColorPickerButton( - color: Color, - isSelected: Boolean, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - IconButton( - shapes = IconButtonDefaults.shapes(), - colors = IconButtonDefaults.iconButtonColors(containerColor = color), - modifier = modifier.size(48.dp), - onClick = onClick - ) { - AnimatedContent(isSelected) { isSelected -> - when (isSelected) { - true -> Icon( - painterResource(R.drawable.check), - tint = Color.Black, - contentDescription = null - ) - - else -> - if (color == Color.White) Icon( - painterResource(R.drawable.colors), - tint = Color.Black, - contentDescription = null - ) - } - } - } -} - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ColorSchemePickerDialog( - currentColor: Color, - modifier: Modifier = Modifier, - setShowDialog: (Boolean) -> Unit, - onColorChange: (Color) -> Unit, -) { - val colorSchemes = listOf( - Color(0xfffeb4a7), Color(0xffffb3c0), Color(0xfffcaaff), Color(0xffb9c3ff), - Color(0xff62d3ff), Color(0xff44d9f1), Color(0xff52dbc9), Color(0xff78dd77), - Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e), - Color.White - ) - - BasicAlertDialog( - onDismissRequest = { setShowDialog(false) }, - modifier = modifier - ) { - Surface( - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight(), - color = colorScheme.surfaceContainer, - shape = shapes.extraLarge, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column(modifier = Modifier.padding(24.dp)) { - Text( - text = stringResource(R.string.choose_color_scheme), - style = MaterialTheme.typography.headlineSmall - ) - - Spacer(Modifier.height(16.dp)) - - Column(Modifier.align(Alignment.CenterHorizontally)) { - (0..11 step 4).forEach { - Row { - colorSchemes.slice(it..it + 3).fastForEach { color -> - ColorPickerButton( - color, - color == currentColor, - modifier = Modifier.padding(4.dp) - ) { - onColorChange(color) - } - } - } - } - ColorPickerButton( - colorSchemes.last(), - colorSchemes.last() == currentColor, - modifier = Modifier.padding(4.dp) - ) { - onColorChange(colorSchemes.last()) - } - } - - Spacer(Modifier.height(24.dp)) - - TextButton( - shapes = ButtonDefaults.shapes(), - onClick = { setShowDialog(false) }, - modifier = Modifier.align(Alignment.End) - ) { - Text(stringResource(R.string.ok)) - } - } - } - } -} - -@Preview -@Composable -fun ColorPickerDialogPreview() { - var currentColor by remember { mutableStateOf(Color(0xfffeb4a7)) } - TomatoTheme(darkTheme = true) { - ColorSchemePickerDialog( - currentColor, - setShowDialog = {}, - onColorChange = { currentColor = it } - ) - } -} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt deleted file mode 100644 index 088d5be..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (c) 2025 Nishant Mishra - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.nsh07.pomodoro.ui.settingsScreen - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Icon -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.ui.ClickableListItem -import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors - -@Composable -fun ColorSchemePickerListItem( - color: Color, - items: Int, - index: Int, - onColorChange: (Color) -> Unit, - modifier: Modifier = Modifier -) { - var showDialog by rememberSaveable { mutableStateOf(false) } - - if (showDialog) { - ColorSchemePickerDialog( - currentColor = color, - setShowDialog = { showDialog = it }, - onColorChange = onColorChange - ) - } - - ClickableListItem( - leadingContent = { - Icon( - painter = painterResource(R.drawable.palette), - contentDescription = null, - tint = colorScheme.primary - ) - }, - headlineContent = { Text(stringResource(R.string.color_scheme)) }, - supportingContent = { - Text( - if (color == Color.White) stringResource(R.string.dynamic) - else stringResource(R.string.color) - ) - }, - colors = listItemColors, - items = items, - index = index, - modifier = modifier.fillMaxWidth() - ) { showDialog = true } -} \ No newline at end of file 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 67b00a0..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 @@ -1,96 +1,80 @@ /* * Copyright (c) 2025 Nishant Mishra * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . + * 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.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInHorizontally +import androidx.compose.animation.slideOutHorizontally +import androidx.compose.animation.togetherWith import androidx.compose.foundation.background -import androidx.compose.foundation.clickable -import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.input.TextFieldState -import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.FilledTonalIconToggleButton import androidx.compose.material3.Icon -import androidx.compose.material3.IconButtonDefaults -import androidx.compose.material3.ListItem import androidx.compose.material3.LocalTextStyle -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.MaterialTheme.typography -import androidx.compose.material3.Slider import androidx.compose.material3.SliderState -import androidx.compose.material3.Switch -import androidx.compose.material3.SwitchDefaults import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberSliderState import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.input.ImeAction -import androidx.compose.ui.tooling.preview.Devices -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import androidx.navigation3.runtime.entryProvider +import androidx.navigation3.ui.NavDisplay import org.nsh07.pomodoro.R import org.nsh07.pomodoro.service.TimerService +import org.nsh07.pomodoro.ui.Screen +import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard +import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem +import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings +import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings +import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel +import org.nsh07.pomodoro.ui.settingsScreens import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar -import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape -import org.nsh07.pomodoro.ui.theme.TomatoTheme -import org.nsh07.pomodoro.utils.toColor @OptIn(ExperimentalMaterial3Api::class) @@ -101,6 +85,8 @@ fun SettingsScreenRoot( ) { val context = LocalContext.current + val backStack = viewModel.backStack + DisposableEffect(Unit) { viewModel.runTextFieldFlowCollection() onDispose { viewModel.cancelTextFieldFlowCollection() } @@ -133,6 +119,7 @@ fun SettingsScreenRoot( SettingsScreen( preferencesState = preferencesState, + backStack = backStack, focusTimeInputFieldState = focusTimeInputFieldState, shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, longBreakTimeInputFieldState = longBreakTimeInputFieldState, @@ -143,6 +130,7 @@ fun SettingsScreenRoot( onAlarmEnabledChange = viewModel::saveAlarmEnabled, onVibrateEnabledChange = viewModel::saveVibrateEnabled, onBlackThemeChange = viewModel::saveBlackTheme, + onAodEnabledChange = viewModel::saveAodEnabled, onAlarmSoundChanged = { viewModel.saveAlarmSound(it) Intent(context, TimerService::class.java).apply { @@ -160,6 +148,7 @@ fun SettingsScreenRoot( @Composable private fun SettingsScreen( preferencesState: PreferencesState, + backStack: SnapshotStateList, focusTimeInputFieldState: TextFieldState, shortBreakTimeInputFieldState: TextFieldState, longBreakTimeInputFieldState: TextFieldState, @@ -170,394 +159,125 @@ private fun SettingsScreen( onAlarmEnabledChange: (Boolean) -> Unit, onVibrateEnabledChange: (Boolean) -> Unit, onBlackThemeChange: (Boolean) -> Unit, + onAodEnabledChange: (Boolean) -> Unit, onAlarmSoundChanged: (Uri?) -> Unit, onThemeChange: (String) -> Unit, onColorSchemeChange: (Color) -> Unit, modifier: Modifier = Modifier ) { val context = LocalContext.current - val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() - val switchColors = SwitchDefaults.colors( - checkedIconColor = colorScheme.primary, - ) + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() - val themeMap: Map> = remember { - mapOf( - "auto" to Pair( - R.drawable.brightness_auto, - context.getString(R.string.system_default) - ), - "light" to Pair(R.drawable.light_mode, context.getString(R.string.light)), - "dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark)) - ) - } - val reverseThemeMap: Map = remember { - mapOf( - context.getString(R.string.system_default) to "auto", - context.getString(R.string.light) to "light", - context.getString(R.string.dark) to "dark" - ) - } - - var alarmName by remember { mutableStateOf("...") } - - LaunchedEffect(alarmSound) { - withContext(Dispatchers.IO) { - alarmName = - RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: "" - } - } - - val ringtonePickerLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - val uri = - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - result.data?.getParcelableExtra( - RingtoneManager.EXTRA_RINGTONE_PICKED_URI, - Uri::class.java - ) - } else { - @Suppress("DEPRECATION") - result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) - } - onAlarmSoundChanged(uri) - } - } - - val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { - putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) - putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, stringResource(R.string.alarm_sound)) - putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri()) - } - - val switchItems = remember(preferencesState.blackTheme, alarmEnabled, vibrateEnabled) { - listOf( - SettingsSwitchItem( - checked = preferencesState.blackTheme, - icon = R.drawable.contrast, - label = context.getString(R.string.black_theme), - description = context.getString(R.string.black_theme_desc), - onClick = onBlackThemeChange - ), - SettingsSwitchItem( - checked = alarmEnabled, - icon = R.drawable.alarm_on, - label = context.getString(R.string.alarm), - description = context.getString(R.string.alarm_desc), - onClick = onAlarmEnabledChange - ), - SettingsSwitchItem( - checked = vibrateEnabled, - icon = R.drawable.mobile_vibrate, - label = context.getString(R.string.vibrate), - description = context.getString(R.string.vibrate_desc), - onClick = onVibrateEnabledChange - ) - ) - } - - Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { - TopAppBar( - title = { - Text( - stringResource(R.string.settings), - style = LocalTextStyle.current.copy( - fontFamily = robotoFlexTopBar, - fontSize = 32.sp, - lineHeight = 32.sp - ) - ) - }, - subtitle = {}, - colors = topBarColors, - titleHorizontalAlignment = Alignment.CenterHorizontally, - scrollBehavior = scrollBehavior - ) - - LazyColumn( - verticalArrangement = Arrangement.spacedBy(2.dp), - modifier = Modifier - .background(topBarColors.containerColor) - .fillMaxSize() - .padding(horizontal = 16.dp) - ) { - item { - Spacer(Modifier.height(12.dp)) - } - item { - Row( - horizontalArrangement = Arrangement.Center, - modifier = Modifier - .fillMaxWidth() - .horizontalScroll(rememberScrollState()) - ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - stringResource(R.string.focus), - style = typography.titleSmallEmphasized - ) - MinuteInputField( - state = focusTimeInputFieldState, - shape = RoundedCornerShape( - topStart = topListItemShape.topStart, - bottomStart = topListItemShape.topStart, - topEnd = topListItemShape.bottomStart, - bottomEnd = topListItemShape.bottomStart - ), - imeAction = ImeAction.Next - ) - } - Spacer(Modifier.width(2.dp)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - stringResource(R.string.short_break), - style = typography.titleSmallEmphasized - ) - MinuteInputField( - state = shortBreakTimeInputFieldState, - shape = RoundedCornerShape(middleListItemShape.topStart), - imeAction = ImeAction.Next - ) - } - Spacer(Modifier.width(2.dp)) - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(2.dp) - ) { - Text( - stringResource(R.string.long_break), - style = typography.titleSmallEmphasized - ) - MinuteInputField( - state = longBreakTimeInputFieldState, - shape = RoundedCornerShape( - topStart = bottomListItemShape.topStart, - bottomStart = bottomListItemShape.topStart, - topEnd = bottomListItemShape.bottomStart, - bottomEnd = bottomListItemShape.bottomStart - ), - imeAction = ImeAction.Done - ) - } - } - } - item { - Spacer(Modifier.height(12.dp)) - } - item { - ListItem( - leadingContent = { - Icon( - painterResource(R.drawable.clocks), - null - ) - }, - headlineContent = { - Text(stringResource(R.string.session_length)) - }, - supportingContent = { - Column { + NavDisplay( + backStack = backStack, + onBack = backStack::removeLastOrNull, + transitionSpec = { + (slideInHorizontally(initialOffsetX = { it })) + .togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut()) + }, + popTransitionSpec = { + (slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()) + .togetherWith(slideOutHorizontally(targetOffsetX = { it })) + }, + predictivePopTransitionSpec = { + (slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn()) + .togetherWith(slideOutHorizontally(targetOffsetX = { it })) + }, + entryProvider = entryProvider { + entry { + Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { + TopAppBar( + title = { Text( - stringResource( - R.string.session_length_desc, - sessionsSliderState.value.toInt() + stringResource(R.string.settings), + style = LocalTextStyle.current.copy( + fontFamily = robotoFlexTopBar, + fontSize = 32.sp, + lineHeight = 32.sp ) ) - Slider( - state = sessionsSliderState, - modifier = Modifier.padding(vertical = 4.dp) - ) - } - }, - colors = listItemColors, - modifier = Modifier.clip(cardShape) - ) - } + }, + subtitle = {}, + colors = topBarColors, + titleHorizontalAlignment = Alignment.CenterHorizontally, + scrollBehavior = scrollBehavior + ) - item { Spacer(Modifier.height(12.dp)) } - - item { - ColorSchemePickerListItem( - color = preferencesState.colorScheme.toColor(), - items = 3, - index = 0, - onColorChange = onColorSchemeChange - ) - } - item { - ThemePickerListItem( - theme = preferencesState.theme, - themeMap = themeMap, - reverseThemeMap = reverseThemeMap, - onThemeChange = onThemeChange, - items = 3, - index = 1, - modifier = Modifier - .clip(middleListItemShape) - ) - } - item { - val item = switchItems[0] - ListItem( - leadingContent = { - Icon(painterResource(item.icon), contentDescription = null) - }, - headlineContent = { Text(item.label) }, - supportingContent = { Text(item.description) }, - trailingContent = { - Switch( - checked = item.checked, - onCheckedChange = { item.onClick(it) }, - thumbContent = { - if (item.checked) { - Icon( - painter = painterResource(R.drawable.check), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } else { - Icon( - painter = painterResource(R.drawable.clear), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } - }, - colors = switchColors - ) - }, - colors = listItemColors, - modifier = Modifier.clip(bottomListItemShape) - ) - } - - item { Spacer(Modifier.height(12.dp)) } - - item { - ListItem( - leadingContent = { - Icon(painterResource(R.drawable.alarm), null) - }, - headlineContent = { Text(stringResource(R.string.alarm_sound)) }, - supportingContent = { Text(alarmName) }, - colors = listItemColors, - modifier = Modifier - .clip(topListItemShape) - .clickable(onClick = { ringtonePickerLauncher.launch(intent) }) - ) - } - itemsIndexed(switchItems.drop(1)) { index, item -> - ListItem( - leadingContent = { - Icon(painterResource(item.icon), contentDescription = null) - }, - headlineContent = { Text(item.label) }, - supportingContent = { Text(item.description) }, - trailingContent = { - Switch( - checked = item.checked, - onCheckedChange = { item.onClick(it) }, - thumbContent = { - if (item.checked) { - Icon( - painter = painterResource(R.drawable.check), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } else { - Icon( - painter = painterResource(R.drawable.clear), - contentDescription = null, - modifier = Modifier.size(SwitchDefaults.IconSize), - ) - } - }, - colors = switchColors - ) - }, - colors = listItemColors, - modifier = Modifier - .clip( - when (index) { - switchItems.lastIndex - 1 -> bottomListItemShape - else -> middleListItemShape - } - ) - ) - } - item { - var expanded by remember { mutableStateOf(false) } - Column( - horizontalAlignment = Alignment.End, - modifier = Modifier - .padding(vertical = 6.dp) - .fillMaxWidth() - ) { - FilledTonalIconToggleButton( - checked = expanded, - onCheckedChange = { expanded = it }, - shapes = IconButtonDefaults.toggleableShapes(), - modifier = Modifier.width(52.dp) + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier + .background(topBarColors.containerColor) + .fillMaxSize() + .padding(horizontal = 16.dp) ) { - Icon( - painterResource(R.drawable.info), - null - ) - } - AnimatedVisibility(expanded) { - Text( - stringResource(R.string.pomodoro_info), - style = typography.bodyMedium, - color = colorScheme.onSurfaceVariant, - modifier = Modifier.padding(8.dp) - ) + item { Spacer(Modifier.height(12.dp)) } + + item { AboutCard() } + + item { Spacer(Modifier.height(12.dp)) } + + itemsIndexed(settingsScreens) { index, item -> + ClickableListItem( + leadingContent = { + Icon(painterResource(item.icon), null) + }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { + Text( + remember { + item.innerSettings.joinToString(", ") { + context.getString(it) + } + }, + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + }, + trailingContent = { + Icon(painterResource(R.drawable.arrow_forward_big), null) + }, + items = settingsScreens.size, + index = index + ) { backStack.add(item.route) } + } + + item { Spacer(Modifier.height(12.dp)) } } } } + + entry { + AlarmSettings( + preferencesState = preferencesState, + alarmEnabled = alarmEnabled, + vibrateEnabled = vibrateEnabled, + alarmSound = alarmSound, + onAlarmEnabledChange = onAlarmEnabledChange, + onVibrateEnabledChange = onVibrateEnabledChange, + onAlarmSoundChanged = onAlarmSoundChanged, + onBack = backStack::removeLastOrNull + ) + } + 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) -@Preview( - showSystemUi = true, - device = Devices.PIXEL_9_PRO -) -@Composable -fun SettingsScreenPreview() { - TomatoTheme { - SettingsScreen( - preferencesState = PreferencesState(), - focusTimeInputFieldState = rememberTextFieldState((25).toString()), - shortBreakTimeInputFieldState = rememberTextFieldState((5).toString()), - longBreakTimeInputFieldState = rememberTextFieldState((15).toString()), - sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f), - alarmEnabled = true, - vibrateEnabled = true, - alarmSound = "null", - onAlarmEnabledChange = {}, - onVibrateEnabledChange = {}, - onBlackThemeChange = {}, - onAlarmSoundChanged = {}, - onThemeChange = {}, - onColorSchemeChange = {}, - modifier = Modifier.fillMaxSize() - ) - } -} - -data class SettingsSwitchItem( - val checked: Boolean, - @param:DrawableRes val icon: Int, - val label: String, - val description: String, - val onClick: (Boolean) -> Unit -) 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/ThemeDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt deleted file mode 100644 index 67f4d7a..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemeDialog.kt +++ /dev/null @@ -1,131 +0,0 @@ -/* - * Copyright (c) 2025 Nishant Mishra - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.nsh07.pomodoro.ui.settingsScreen - -import androidx.compose.animation.AnimatedContent -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentHeight -import androidx.compose.foundation.layout.wrapContentWidth -import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.selection.selectableGroup -import androidx.compose.material3.AlertDialogDefaults -import androidx.compose.material3.BasicAlertDialog -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi -import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MaterialTheme.colorScheme -import androidx.compose.material3.MaterialTheme.shapes -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role -import androidx.compose.ui.unit.dp -import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors -import org.nsh07.pomodoro.ui.theme.CustomColors.selectedListItemColors -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape -import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape - -@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) -@Composable -fun ThemeDialog( - themeMap: Map>, - reverseThemeMap: Map, - theme: String, - setShowThemeDialog: (Boolean) -> Unit, - onThemeChange: (String) -> Unit -) { - val selectedOption = - remember { mutableStateOf(themeMap[theme]!!.second) } - - BasicAlertDialog( - onDismissRequest = { setShowThemeDialog(false) } - ) { - Surface( - modifier = Modifier - .wrapContentWidth() - .wrapContentHeight(), - shape = shapes.extraLarge, - color = colorScheme.surfaceContainer, - tonalElevation = AlertDialogDefaults.TonalElevation - ) { - Column(modifier = Modifier.padding(24.dp)) { - Text( - text = stringResource(R.string.choose_theme), - style = MaterialTheme.typography.headlineSmall - ) - Spacer(modifier = Modifier.height(16.dp)) - Column( - verticalArrangement = Arrangement.spacedBy(2.dp), - modifier = Modifier.selectableGroup() - ) { - themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry> -> - val text = pair.value.second - val selected = text == selectedOption.value - - ListItem( - leadingContent = { - AnimatedContent(selected) { - if (it) - Icon(painterResource(R.drawable.check), null) - else - Icon(painterResource(pair.value.first), null) - } - }, - headlineContent = { - Text(text = text, style = MaterialTheme.typography.bodyLarge) - }, - colors = if (!selected) listItemColors else selectedListItemColors, - modifier = Modifier - .height(64.dp) - .clip( - when (index) { - 0 -> topListItemShape - themeMap.size - 1 -> bottomListItemShape - else -> middleListItemShape - } - ) - .selectable( - selected = (text == selectedOption.value), - onClick = { - selectedOption.value = text - onThemeChange(reverseThemeMap[selectedOption.value]!!) - }, - role = Role.RadioButton - ) - ) - } - } - Spacer(modifier = Modifier.height(16.dp)) - TextButton( - shapes = ButtonDefaults.shapes(), - onClick = { setShowThemeDialog(false) }, - modifier = Modifier.align(Alignment.End) - ) { - Text(stringResource(R.string.ok)) - } - } - } - } -} \ No newline at end of file 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 deleted file mode 100644 index 2bee9c5..0000000 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Copyright (c) 2025 Nishant Mishra - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -package org.nsh07.pomodoro.ui.settingsScreen - -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.material3.Icon -import androidx.compose.material3.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource -import androidx.compose.ui.res.stringResource -import org.nsh07.pomodoro.R -import org.nsh07.pomodoro.ui.ClickableListItem -import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors - -@Composable -fun ThemePickerListItem( - theme: String, - themeMap: Map>, - reverseThemeMap: Map, - items: Int, - index: Int, - onThemeChange: (String) -> Unit, - modifier: Modifier = Modifier -) { - var showDialog by rememberSaveable { mutableStateOf(false) } - - if (showDialog) { - ThemeDialog( - themeMap = themeMap, - reverseThemeMap = reverseThemeMap, - theme = theme, - setShowThemeDialog = { showDialog = it }, - onThemeChange = onThemeChange - ) - } - - ClickableListItem( - leadingContent = { - Icon( - painter = painterResource(themeMap[theme]!!.first), - contentDescription = null - ) - }, - headlineContent = { Text(stringResource(R.string.theme)) }, - supportingContent = { - Text(themeMap[theme]!!.second) - }, - colors = listItemColors, - items = items, - index = index, - modifier = modifier.fillMaxWidth() - ) { showDialog = true } -} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt new file mode 100644 index 0000000..fe0e1ce --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/AboutCard.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.components + +import android.widget.Toast +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.FlowRow +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.shapes +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.BuildConfig +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar + +// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt +@Composable +fun AboutCard(modifier: Modifier = Modifier) { + val uriHandler = LocalUriHandler.current + val context = LocalContext.current + + Card( + modifier = modifier.fillMaxWidth(), + colors = CardDefaults.cardColors( + containerColor = colorScheme.primaryContainer, + contentColor = colorScheme.onPrimaryContainer + ), + shape = shapes.extraLarge + ) { + val buttonColors = ButtonDefaults.buttonColors( + containerColor = colorScheme.onPrimaryContainer, + contentColor = colorScheme.primaryContainer + ) + + Row( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Column { + Text( + text = stringResource(R.string.app_name), + style = MaterialTheme.typography.titleLarge, + fontFamily = robotoFlexTopBar + ) + Text(text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})") + } + + Spacer(modifier = Modifier.weight(1f)) + + Row { + IconButton( + onClick = { + Toast.makeText(context, "Coming soon...", Toast.LENGTH_SHORT).show() + } + ) { + Icon( + painterResource(R.drawable.discord), + contentDescription = "Discord", + modifier = Modifier.size(24.dp) + ) + } + + IconButton( + onClick = { uriHandler.openUri("https://github.com/nsh07/Tomato") } + ) { + Icon( + painterResource(R.drawable.github), + contentDescription = "GitHub", + modifier = Modifier.size(24.dp) + ) + } + } + } + + FlowRow( + modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + Button( + colors = buttonColors, + onClick = { uriHandler.openUri("https://coff.ee/nsh07") } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(R.drawable.coffee), + contentDescription = "Buy me a coffee", + ) + + Text(text = "Buy me a coffee") + } + } + + Button( + colors = buttonColors, + onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=org.nsh07.pomodoro") } + ) { + Row( + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + painterResource(R.drawable.play_store), + contentDescription = "Rate on Google Play", + modifier = Modifier.size(20.dp) + ) + + Text(text = "Rate on Google Play") + } + } + } + } +} \ No newline at end of file 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 74% 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 2d05a68..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 @@ -1,4 +1,21 @@ -package org.nsh07.pomodoro.ui +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.components import androidx.compose.animation.core.animateDpAsState import androidx.compose.foundation.clickable @@ -17,6 +34,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors @OptIn(ExperimentalMaterial3ExpressiveApi::class) @Composable @@ -27,7 +45,7 @@ fun ClickableListItem( supportingContent: @Composable (() -> Unit)? = null, leadingContent: @Composable (() -> Unit)? = null, trailingContent: @Composable (() -> Unit)? = null, - colors: ListItemColors = ListItemDefaults.colors(), + colors: ListItemColors = listItemColors, tonalElevation: Dp = ListItemDefaults.Elevation, shadowElevation: Dp = ListItemDefaults.Elevation, items: Int, 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 new file mode 100644 index 0000000..03549b3 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ColorSchemePickerListItem.kt @@ -0,0 +1,189 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyRow +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape + +@Composable +fun ColorSchemePickerListItem( + color: Color, + items: Int, + index: Int, + onColorChange: (Color) -> Unit, + modifier: Modifier = Modifier +) { + val colorSchemes = listOf( + Color(0xfffeb4a7), Color(0xffffb3c0), Color(0xfffcaaff), Color(0xffb9c3ff), + Color(0xff62d3ff), Color(0xff44d9f1), Color(0xff52dbc9), Color(0xff78dd77), + Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e), + Color.White + ) + + Column( + modifier + .clip( + when (index) { + 0 -> topListItemShape + items - 1 -> bottomListItemShape + else -> middleListItemShape + } + ) + ) { + ListItem( + leadingContent = { + Icon( + painterResource(R.drawable.colors), + null + ) + }, + headlineContent = { Text("Dynamic color") }, + supportingContent = { Text("Adapt theme colors from your wallpaper") }, + trailingContent = { + val checked = color == colorSchemes.last() + Switch( + checked = checked, + onCheckedChange = { + if (it) onColorChange(colorSchemes.last()) + else onColorChange(colorSchemes.first()) + }, + thumbContent = { + if (checked) { + Icon( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } else { + Icon( + painter = painterResource(R.drawable.clear), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + colors = switchColors + ) + }, + colors = listItemColors, + modifier = Modifier.clip(middleListItemShape) + ) + Spacer(Modifier.height(2.dp)) + ListItem( + leadingContent = { + Icon( + painter = painterResource(R.drawable.palette), + contentDescription = null, + tint = colorScheme.primary + ) + }, + headlineContent = { Text(stringResource(R.string.color_scheme)) }, + supportingContent = { + Text( + if (color == Color.White) stringResource(R.string.dynamic) + else stringResource(R.string.color) + ) + }, + colors = listItemColors, + modifier = Modifier.clip(middleListItemShape) + ) + + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .background(listItemColors.containerColor) + .padding(bottom = 8.dp) + ) { + LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) { + items(colorSchemes.dropLast(1)) { + ColorPickerButton( + it, + it == color, + modifier = Modifier.padding(4.dp) + ) { + onColorChange(it) + } + } + } + } + } +} + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ColorPickerButton( + color: Color, + isSelected: Boolean, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { + IconButton( + shapes = IconButtonDefaults.shapes(), + colors = IconButtonDefaults.iconButtonColors(containerColor = color), + modifier = modifier.size(48.dp), + onClick = onClick + ) { + AnimatedContent(isSelected) { isSelected -> + when (isSelected) { + true -> Icon( + painterResource(R.drawable.check), + tint = Color.Black, + contentDescription = null + ) + + else -> + if (color == Color.White) Icon( + painterResource(R.drawable.colors), + tint = Color.Black, + contentDescription = null + ) + } + } + } +} 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/components/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt new file mode 100644 index 0000000..0392765 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/components/ThemePickerListItem.kt @@ -0,0 +1,125 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.components + +import androidx.compose.animation.AnimatedContent +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ButtonGroupDefaults +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.ListItem +import androidx.compose.material3.Text +import androidx.compose.material3.ToggleButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ThemePickerListItem( + theme: String, + items: Int, + index: Int, + onThemeChange: (String) -> Unit, + modifier: Modifier = Modifier, +) { + val themeMap: Map> = remember { + mapOf( + "auto" to Pair( + R.drawable.brightness_auto, + R.string.system_default + ), + "light" to Pair(R.drawable.light_mode, R.string.light), + "dark" to Pair(R.drawable.dark_mode, R.string.dark) + ) + } + + Column( + modifier + .clip( + when (index) { + 0 -> topListItemShape + items - 1 -> bottomListItemShape + else -> middleListItemShape + }, + ), + ) { + ListItem( + leadingContent = { + AnimatedContent(themeMap[theme]!!.first) { + Icon( + painter = painterResource(it), + contentDescription = null, + ) + } + }, + headlineContent = { Text(stringResource(R.string.theme)) }, + colors = listItemColors, + ) + + val options = themeMap.toList() + val selectedIndex = options.indexOf(Pair(theme, themeMap[theme])) + + Row( + horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween), + modifier = Modifier + .background(listItemColors.containerColor) + .padding(start = 52.dp, end = 16.dp, bottom = 8.dp) + ) { + options.forEachIndexed { index, theme -> + val isSelected = selectedIndex == index + ToggleButton( + checked = isSelected, + onCheckedChange = { onThemeChange(theme.first) }, + modifier = Modifier + .weight(1f) + .semantics { role = Role.RadioButton }, + shapes = + when (index) { + 0 -> ButtonGroupDefaults.connectedLeadingButtonShapes() + options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes() + else -> ButtonGroupDefaults.connectedMiddleButtonShapes() + }, + ) { + Text( + stringResource(theme.second.second), + maxLines = 1, + overflow = TextOverflow.Ellipsis + ) + } + } + } + } +} 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 new file mode 100644 index 0000000..bc46e07 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AlarmSettings.kt @@ -0,0 +1,254 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.screens + +import android.app.Activity +import android.content.Intent +import android.media.RingtoneManager +import android.net.Uri +import android.os.Build +import androidx.activity.compose.rememberLauncherForActivityResult +import androidx.activity.result.contract.ActivityResultContracts +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.itemsIndexed +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.ListItem +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.core.net.toUri +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState +import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors +import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun AlarmSettings( + preferencesState: PreferencesState, + alarmEnabled: Boolean, + vibrateEnabled: Boolean, + alarmSound: String, + onAlarmEnabledChange: (Boolean) -> Unit, + onVibrateEnabledChange: (Boolean) -> Unit, + onAlarmSoundChanged: (Uri?) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + val context = LocalContext.current + + var alarmName by remember { mutableStateOf("...") } + + LaunchedEffect(alarmSound) { + withContext(Dispatchers.IO) { + alarmName = + RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: "" + } + } + + val ringtonePickerLauncher = rememberLauncherForActivityResult( + contract = ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val uri = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + result.data?.getParcelableExtra( + RingtoneManager.EXTRA_RINGTONE_PICKED_URI, + Uri::class.java + ) + } else { + @Suppress("DEPRECATION") + result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI) + } + onAlarmSoundChanged(uri) + } + } + + val ringtonePickerIntent = remember(alarmSound) { + Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply { + putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM) + putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound)) + putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri()) + } + } + + val switchItems = remember( + preferencesState.blackTheme, + preferencesState.aodEnabled, + alarmEnabled, + vibrateEnabled + ) { + listOf( + SettingsSwitchItem( + checked = alarmEnabled, + icon = R.drawable.alarm_on, + label = R.string.sound, + description = R.string.alarm_desc, + onClick = onAlarmEnabledChange + ), + SettingsSwitchItem( + checked = vibrateEnabled, + icon = R.drawable.mobile_vibrate, + label = R.string.vibrate, + description = R.string.vibrate_desc, + onClick = onVibrateEnabledChange + ) + ) + } + + Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { + LargeFlexibleTopAppBar( + title = { + Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar) + }, + subtitle = { + Text(stringResource(R.string.settings)) + }, + navigationIcon = { + IconButton(onBack) { + Icon( + painterResource(R.drawable.arrow_back), + null + ) + } + }, + colors = topBarColors, + scrollBehavior = scrollBehavior + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier + .background(topBarColors.containerColor) + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(Modifier.height(14.dp)) + } + + item { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.alarm), null) + }, + headlineContent = { Text(stringResource(R.string.alarm_sound)) }, + supportingContent = { Text(alarmName) }, + colors = listItemColors, + modifier = Modifier + .clip(topListItemShape) + .clickable(onClick = { ringtonePickerLauncher.launch(ringtonePickerIntent) }) + ) + } + itemsIndexed(switchItems) { index, item -> + ListItem( + leadingContent = { + Icon(painterResource(item.icon), contentDescription = null) + }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { Text(stringResource(item.description)) }, + trailingContent = { + Switch( + checked = item.checked, + onCheckedChange = { item.onClick(it) }, + thumbContent = { + if (item.checked) { + Icon( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } else { + Icon( + painter = painterResource(R.drawable.clear), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + colors = switchColors + ) + }, + colors = listItemColors, + modifier = Modifier + .clip( + when (index) { + switchItems.lastIndex -> bottomListItemShape + else -> middleListItemShape + } + ) + ) + } + + item { Spacer(Modifier.height(12.dp)) } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Preview +@Composable +fun AlarmSettingsPreview() { + val preferencesState = PreferencesState() + AlarmSettings( + preferencesState = preferencesState, + alarmEnabled = true, + vibrateEnabled = false, + alarmSound = "", + onAlarmEnabledChange = {}, + onVibrateEnabledChange = {}, + onAlarmSoundChanged = {}, + onBack = {}) +} 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 new file mode 100644 index 0000000..eb55fb8 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/AppearanceSettings.kt @@ -0,0 +1,178 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.screens + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.ListItem +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem +import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem +import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem +import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState +import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors +import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape +import org.nsh07.pomodoro.utils.toColor + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun AppearanceSettings( + preferencesState: PreferencesState, + onBlackThemeChange: (Boolean) -> Unit, + onThemeChange: (String) -> Unit, + onColorSchemeChange: (Color) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + + Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { + LargeFlexibleTopAppBar( + title = { + Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar) + }, + subtitle = { + Text(stringResource(R.string.settings)) + }, + navigationIcon = { + IconButton(onBack) { + Icon( + painterResource(R.drawable.arrow_back), + null + ) + } + }, + colors = topBarColors, + scrollBehavior = scrollBehavior + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier + .background(topBarColors.containerColor) + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(Modifier.height(14.dp)) + } + item { + ColorSchemePickerListItem( + color = preferencesState.colorScheme.toColor(), + items = 3, + index = 0, + onColorChange = onColorSchemeChange + ) + } + item { + ThemePickerListItem( + theme = preferencesState.theme, + onThemeChange = onThemeChange, + items = 3, + index = 1, + modifier = Modifier + .clip(middleListItemShape) + ) + } + item { + val item = SettingsSwitchItem( + checked = preferencesState.blackTheme, + icon = R.drawable.contrast, + label = R.string.black_theme, + description = R.string.black_theme_desc, + onClick = onBlackThemeChange + ) + ListItem( + leadingContent = { + Icon(painterResource(item.icon), contentDescription = null) + }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { Text(stringResource(item.description)) }, + trailingContent = { + Switch( + checked = item.checked, + onCheckedChange = { item.onClick(it) }, + thumbContent = { + if (item.checked) { + Icon( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } else { + Icon( + painter = painterResource(R.drawable.clear), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + colors = switchColors + ) + }, + colors = listItemColors, + modifier = Modifier.clip(bottomListItemShape) + ) + } + + item { Spacer(Modifier.height(12.dp)) } + } + } +} + +@Preview +@Composable +fun AppearanceSettingsPreview() { + val preferencesState = PreferencesState() + AppearanceSettings( + preferencesState = preferencesState, + onBlackThemeChange = {}, + onThemeChange = {}, + onColorSchemeChange = {}, + onBack = {} + ) +} 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 new file mode 100644 index 0000000..01dddba --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/screens/TimerSettings.kt @@ -0,0 +1,317 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * This file is part of Tomato - a minimalist pomodoro timer for Android. + * + * Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU + * General Public License as published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even + * the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General + * Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Tomato. + * If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen.screens + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.background +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.input.TextFieldState +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilledTonalIconToggleButton +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LargeFlexibleTopAppBar +import androidx.compose.material3.ListItem +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.MaterialTheme.typography +import androidx.compose.material3.Slider +import androidx.compose.material3.SliderState +import androidx.compose.material3.Switch +import androidx.compose.material3.SwitchDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.nsh07.pomodoro.R +import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem +import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField +import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar +import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors +import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors +import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape +import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape + +@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun TimerSettings( + aodEnabled: Boolean, + focusTimeInputFieldState: TextFieldState, + shortBreakTimeInputFieldState: TextFieldState, + longBreakTimeInputFieldState: TextFieldState, + sessionsSliderState: SliderState, + onAodEnabledChange: (Boolean) -> Unit, + onBack: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior() + + Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) { + LargeFlexibleTopAppBar( + title = { + Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar) + }, + subtitle = { + Text(stringResource(R.string.settings)) + }, + navigationIcon = { + IconButton(onBack) { + Icon( + painterResource(R.drawable.arrow_back), + null + ) + } + }, + colors = topBarColors, + scrollBehavior = scrollBehavior + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(2.dp), + modifier = Modifier + .background(topBarColors.containerColor) + .fillMaxSize() + .padding(horizontal = 16.dp) + ) { + item { + Spacer(Modifier.height(14.dp)) + } + item { + Row( + horizontalArrangement = Arrangement.Center, + modifier = Modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + stringResource(R.string.focus), + style = typography.titleSmallEmphasized + ) + MinuteInputField( + state = focusTimeInputFieldState, + shape = RoundedCornerShape( + topStart = topListItemShape.topStart, + bottomStart = topListItemShape.topStart, + topEnd = topListItemShape.bottomStart, + bottomEnd = topListItemShape.bottomStart + ), + imeAction = ImeAction.Next + ) + } + Spacer(Modifier.width(2.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + stringResource(R.string.short_break), + style = typography.titleSmallEmphasized + ) + MinuteInputField( + state = shortBreakTimeInputFieldState, + shape = RoundedCornerShape(middleListItemShape.topStart), + imeAction = ImeAction.Next + ) + } + Spacer(Modifier.width(2.dp)) + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(2.dp) + ) { + Text( + stringResource(R.string.long_break), + style = typography.titleSmallEmphasized + ) + MinuteInputField( + state = longBreakTimeInputFieldState, + shape = RoundedCornerShape( + topStart = bottomListItemShape.topStart, + bottomStart = bottomListItemShape.topStart, + topEnd = bottomListItemShape.bottomStart, + bottomEnd = bottomListItemShape.bottomStart + ), + imeAction = ImeAction.Done + ) + } + } + } + item { + Spacer(Modifier.height(12.dp)) + } + item { + ListItem( + leadingContent = { + Icon(painterResource(R.drawable.clocks), null) + }, + headlineContent = { + Text(stringResource(R.string.session_length)) + }, + supportingContent = { + Column { + Text( + stringResource( + R.string.session_length_desc, + sessionsSliderState.value.toInt() + ) + ) + Slider( + state = sessionsSliderState, + modifier = Modifier.padding(vertical = 4.dp) + ) + } + }, + colors = listItemColors, + modifier = Modifier.clip(cardShape) + ) + } + item { Spacer(Modifier.height(12.dp)) } + item { + val item = SettingsSwitchItem( + checked = aodEnabled, + icon = R.drawable.aod, + label = R.string.always_on_display, + description = R.string.always_on_display_desc, + onClick = onAodEnabledChange + ) + ListItem( + leadingContent = { + Icon( + painterResource(item.icon), + contentDescription = null, + modifier = Modifier.padding(top = 4.dp) + ) + }, + headlineContent = { Text(stringResource(item.label)) }, + supportingContent = { Text(stringResource(item.description)) }, + trailingContent = { + Switch( + checked = item.checked, + onCheckedChange = { item.onClick(it) }, + thumbContent = { + if (item.checked) { + Icon( + painter = painterResource(R.drawable.check), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } else { + Icon( + painter = painterResource(R.drawable.clear), + contentDescription = null, + modifier = Modifier.size(SwitchDefaults.IconSize), + ) + } + }, + colors = switchColors + ) + }, + colors = listItemColors, + modifier = Modifier.clip(cardShape) + ) + } + + item { + var expanded by remember { mutableStateOf(false) } + Column( + horizontalAlignment = Alignment.End, + modifier = Modifier + .padding(vertical = 6.dp) + .fillMaxWidth() + ) { + FilledTonalIconToggleButton( + checked = expanded, + onCheckedChange = { expanded = it }, + shapes = IconButtonDefaults.toggleableShapes(), + modifier = Modifier.width(52.dp) + ) { + Icon( + painterResource(R.drawable.info), + null + ) + } + AnimatedVisibility(expanded) { + Text( + stringResource(R.string.pomodoro_info), + style = typography.bodyMedium, + color = colorScheme.onSurfaceVariant, + modifier = Modifier.padding(8.dp) + ) + } + } + } + + item { Spacer(Modifier.height(12.dp)) } + } + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun TimerSettingsPreview() { + val focusTimeInputFieldState = TextFieldState("25") + val shortBreakTimeInputFieldState = TextFieldState("5") + val longBreakTimeInputFieldState = TextFieldState("15") + val sessionsSliderState = SliderState( + value = 4f, + valueRange = 1f..8f, + steps = 6 + ) + TimerSettings( + focusTimeInputFieldState = focusTimeInputFieldState, + shortBreakTimeInputFieldState = shortBreakTimeInputFieldState, + longBreakTimeInputFieldState = longBreakTimeInputFieldState, + sessionsSliderState = sessionsSliderState, + aodEnabled = true, + onBack = {}, + onAodEnabledChange = {} + ) +} \ No newline at end of file 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..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() @@ -80,12 +94,15 @@ class SettingsViewModel( ?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString()) val blackTheme = preferenceRepository.getBooleanPreference("black_theme") ?: preferenceRepository.saveBooleanPreference("black_theme", false) + val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled") + ?: preferenceRepository.saveBooleanPreference("aod_enabled", false) _preferencesState.update { currentState -> currentState.copy( theme = theme, colorScheme = colorScheme, - blackTheme = blackTheme + blackTheme = blackTheme, + aodEnabled = aodEnabled ) } } @@ -196,6 +213,15 @@ class SettingsViewModel( } } + fun saveAodEnabled(aodEnabled: Boolean) { + viewModelScope.launch { + _preferencesState.update { currentState -> + currentState.copy(aodEnabled = aodEnabled) + } + preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled) + } + } + companion object { val Factory: ViewModelProvider.Factory = viewModelFactory { initializer { 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 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..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,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.timerScreen @@ -75,4 +85,4 @@ fun AlarmDialog( } } } -} \ No newline at end of file +} 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 }, + {} + ) + } } } } 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/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 @@ + + + + + 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/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 @@ + + + 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-fr/strings.xml b/app/src/main/res/values-fr/strings.xml new file mode 100644 index 0000000..60b8b1d --- /dev/null +++ b/app/src/main/res/values-fr/strings.xml @@ -0,0 +1,62 @@ + + + Démarrer + Arrêter + Concentration + 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 + 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 new file mode 100644 index 0000000..a06754d --- /dev/null +++ b/app/src/main/res/values-tr/strings.xml @@ -0,0 +1,64 @@ + + + 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 + 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 + diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index fcc8a7d..ad99d0d 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -55,5 +55,8 @@ 接下来是 计时 计时器进度 - %1$d 的 %2$d + %2$d 中的 %1$d + 去年 + 息屏显示 + 查看计时器时点击任意位置切换至 AOD 模式 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 067cff5..cf51e15 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,60 +1,82 @@ + + - 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 + Theme Timer Timer progress - Last year + %1$d of %2$d + Today + Up next + Up next: %1$s (%2$s) + Vibration + Vibrate when a timer completes + Weekly productivity analysis + Appearance + Durations + Sound \ No newline at end of file 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 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/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 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ı 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) 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) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e776d2..783dd5d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,18 @@ [versions] activityCompose = "1.11.0" -adaptive = "1.1.0" +adaptive = "1.2.0" agp = "8.11.2" -composeBom = "2025.10.00" +composeBom = "2025.10.01" coreKtx = "1.17.0" espressoCore = "3.7.0" junit = "4.13.2" junitVersion = "1.3.0" -kotlin = "2.2.20" +kotlin = "2.2.21" ksp = "2.2.20-2.0.4" lifecycleRuntimeKtx = "2.9.4" materialKolor = "3.0.1" -navigation3 = "1.0.0-alpha11" -room = "2.8.2" +navigation3 = "1.0.0-beta01" +room = "2.8.3" vico = "2.2.1" [libraries]