Merge branch 'dev'
This commit is contained in:
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||||
@@ -31,10 +41,10 @@ android {
|
|||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "org.nsh07.pomodoro"
|
applicationId = "org.nsh07.pomodoro"
|
||||||
minSdk = 26
|
minSdk = 27
|
||||||
targetSdk = 36
|
targetSdk = 36
|
||||||
versionCode = 13
|
versionCode = 15
|
||||||
versionName = "1.5.0"
|
versionName = "1.6.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,20 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?><!--
|
||||||
|
~ Copyright (c) 2025 Nishant Mishra
|
||||||
|
~
|
||||||
|
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
~
|
||||||
|
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
~ If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro
|
package org.nsh07.pomodoro
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -11,8 +28,6 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import org.nsh07.pomodoro.ui.AppScreen
|
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.settingsScreen.viewModel.SettingsViewModel
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||||
@@ -30,6 +45,12 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
enableEdgeToEdge()
|
enableEdgeToEdge()
|
||||||
|
|
||||||
|
appContainer.activityTurnScreenOn = {
|
||||||
|
setShowWhenLocked(it)
|
||||||
|
setTurnScreenOn(it)
|
||||||
|
}
|
||||||
|
|
||||||
setContent {
|
setContent {
|
||||||
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
|
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
@@ -51,11 +72,18 @@ class MainActivity : ComponentActivity() {
|
|||||||
appContainer.appTimerRepository.colorScheme = colorScheme
|
appContainer.appTimerRepository.colorScheme = colorScheme
|
||||||
}
|
}
|
||||||
|
|
||||||
AppScreen(timerViewModel = timerViewModel)
|
AppScreen(
|
||||||
|
timerViewModel = timerViewModel,
|
||||||
|
isAODEnabled = preferencesState.aodEnabled,
|
||||||
|
setTimerFrequency = {
|
||||||
|
appContainer.appTimerRepository.timerFrequency = it
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onStop() {
|
override fun onStop() {
|
||||||
super.onStop()
|
super.onStop()
|
||||||
// Reduce the timer loop frequency when not visible to save battery power
|
// 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
|
// Increase the timer loop frequency again when visible to make the progress smoother
|
||||||
appContainer.appTimerRepository.timerFrequency = 10f
|
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.data
|
package org.nsh07.pomodoro.data
|
||||||
@@ -27,6 +37,7 @@ interface AppContainer {
|
|||||||
val notificationBuilder: NotificationCompat.Builder
|
val notificationBuilder: NotificationCompat.Builder
|
||||||
val timerState: MutableStateFlow<TimerState>
|
val timerState: MutableStateFlow<TimerState>
|
||||||
val time: MutableStateFlow<Long>
|
val time: MutableStateFlow<Long>
|
||||||
|
var activityTurnScreenOn: (Boolean) -> Unit
|
||||||
}
|
}
|
||||||
|
|
||||||
class DefaultAppContainer(context: Context) : AppContainer {
|
class DefaultAppContainer(context: Context) : AppContainer {
|
||||||
@@ -78,4 +89,6 @@ class DefaultAppContainer(context: Context) : AppContainer {
|
|||||||
MutableStateFlow(appTimerRepository.focusTime)
|
MutableStateFlow(appTimerRepository.focusTime)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override var activityTurnScreenOn: (Boolean) -> Unit = {}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.service
|
package org.nsh07.pomodoro.service
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
@@ -16,6 +33,7 @@ import androidx.core.app.NotificationCompat
|
|||||||
import androidx.core.app.NotificationManagerCompat
|
import androidx.core.app.NotificationManagerCompat
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.SupervisorJob
|
import kotlinx.coroutines.SupervisorJob
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -54,11 +72,12 @@ class TimerService : Service() {
|
|||||||
private var pauseDuration = 0L
|
private var pauseDuration = 0L
|
||||||
|
|
||||||
private var job = SupervisorJob()
|
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 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 {
|
private val vibrator by lazy {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||||
@@ -138,7 +157,7 @@ class TimerService : Service() {
|
|||||||
|
|
||||||
var iterations = -1
|
var iterations = -1
|
||||||
|
|
||||||
scope.launch {
|
timerScope.launch {
|
||||||
while (true) {
|
while (true) {
|
||||||
if (!timerState.value.timerRunning) break
|
if (!timerState.value.timerRunning) break
|
||||||
if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
|
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(
|
fun showTimerNotification(
|
||||||
remainingTime: Int, paused: Boolean = false, complete: Boolean = false
|
remainingTime: Int, paused: Boolean = false, complete: Boolean = false
|
||||||
) {
|
) {
|
||||||
@@ -351,6 +373,13 @@ class TimerService : Service() {
|
|||||||
fun startAlarm() {
|
fun startAlarm() {
|
||||||
if (timerRepository.alarmEnabled) alarm?.start()
|
if (timerRepository.alarmEnabled) alarm?.start()
|
||||||
|
|
||||||
|
appContainer.activityTurnScreenOn(true)
|
||||||
|
|
||||||
|
autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
delay(1 * 60 * 1000)
|
||||||
|
stopAlarm()
|
||||||
|
}
|
||||||
|
|
||||||
if (timerRepository.vibrateEnabled) {
|
if (timerRepository.vibrateEnabled) {
|
||||||
if (!vibrator.hasVibrator()) {
|
if (!vibrator.hasVibrator()) {
|
||||||
return
|
return
|
||||||
@@ -363,6 +392,8 @@ class TimerService : Service() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stopAlarm() {
|
fun stopAlarm() {
|
||||||
|
autoAlarmStopScope?.cancel()
|
||||||
|
|
||||||
if (timerRepository.alarmEnabled) {
|
if (timerRepository.alarmEnabled) {
|
||||||
alarm?.pause()
|
alarm?.pause()
|
||||||
alarm?.seekTo(0)
|
alarm?.seekTo(0)
|
||||||
@@ -372,6 +403,8 @@ class TimerService : Service() {
|
|||||||
vibrator.cancel()
|
vibrator.cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
appContainer.activityTurnScreenOn(false)
|
||||||
|
|
||||||
_timerState.update { currentState ->
|
_timerState.update { currentState ->
|
||||||
currentState.copy(alarmRinging = false)
|
currentState.copy(alarmRinging = false)
|
||||||
}
|
}
|
||||||
|
|||||||
286
app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
Normal file
286
app/src/main/java/org/nsh07/pomodoro/ui/AlwaysOnDisplay.kt
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.view.WindowManager
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.CircularWavyProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.motionScheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalView
|
||||||
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.TimerScreen
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerMode
|
||||||
|
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Always On Display composable. Must be called within a [SharedTransitionScope] which allows
|
||||||
|
* animating the clock and progress indicator
|
||||||
|
*
|
||||||
|
* @param timerState [TimerState] instance. This must be the same instance as the one used on the
|
||||||
|
* root [TimerScreen] composable
|
||||||
|
* @param progress lambda that returns the current progress of the clock
|
||||||
|
* randomized offset for the clock to allow smooth motion with sharedBounds
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SharedTransitionScope.AlwaysOnDisplay(
|
||||||
|
timerState: TimerState,
|
||||||
|
progress: () -> Float,
|
||||||
|
setTimerFrequency: (Float) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
var sharedElementTransitionComplete by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val activity = LocalActivity.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val windowInfo = LocalWindowInfo.current
|
||||||
|
val view = LocalView.current
|
||||||
|
|
||||||
|
val window = remember { (view.context as Activity).window }
|
||||||
|
val insetsController = remember { WindowCompat.getInsetsController(window, view) }
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
setTimerFrequency(1f)
|
||||||
|
window.addFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
||||||
|
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
||||||
|
)
|
||||||
|
activity?.setShowWhenLocked(true)
|
||||||
|
insetsController.apply {
|
||||||
|
hide(WindowInsetsCompat.Type.statusBars())
|
||||||
|
hide(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
}
|
||||||
|
|
||||||
|
onDispose {
|
||||||
|
setTimerFrequency(10f)
|
||||||
|
window.clearFlags(
|
||||||
|
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON or
|
||||||
|
WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
|
||||||
|
)
|
||||||
|
activity?.setShowWhenLocked(false)
|
||||||
|
insetsController.apply {
|
||||||
|
show(WindowInsetsCompat.Type.statusBars())
|
||||||
|
show(WindowInsetsCompat.Type.navigationBars())
|
||||||
|
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(300)
|
||||||
|
sharedElementTransitionComplete = true
|
||||||
|
}
|
||||||
|
|
||||||
|
val primary by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color(0xFFA2A2A2)
|
||||||
|
else {
|
||||||
|
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.primary
|
||||||
|
else colorScheme.tertiary
|
||||||
|
},
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
val secondaryContainer by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color(0xFF1D1D1D)
|
||||||
|
else {
|
||||||
|
if (timerState.timerMode == TimerMode.FOCUS) colorScheme.secondaryContainer
|
||||||
|
else colorScheme.tertiaryContainer
|
||||||
|
},
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
val surface by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color.Black
|
||||||
|
else colorScheme.surface,
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
val onSurface by animateColorAsState(
|
||||||
|
if (sharedElementTransitionComplete) Color(0xFFE3E3E3)
|
||||||
|
else colorScheme.onSurface,
|
||||||
|
animationSpec = motionScheme.slowEffectsSpec()
|
||||||
|
)
|
||||||
|
|
||||||
|
var randomX by remember {
|
||||||
|
mutableIntStateOf(
|
||||||
|
Random.nextInt(
|
||||||
|
16.dp.toIntPx(density),
|
||||||
|
windowInfo.containerSize.width - 266.dp.toIntPx(density)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
var randomY by remember {
|
||||||
|
mutableIntStateOf(
|
||||||
|
Random.nextInt(
|
||||||
|
16.dp.toIntPx(density),
|
||||||
|
windowInfo.containerSize.height - 266.dp.toIntPx(density)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(timerState.timeStr[1]) { // Randomize position every minute
|
||||||
|
if (sharedElementTransitionComplete) {
|
||||||
|
randomX = Random.nextInt(
|
||||||
|
16.dp.toIntPx(density),
|
||||||
|
windowInfo.containerSize.width - 266.dp.toIntPx(density)
|
||||||
|
)
|
||||||
|
randomY = Random.nextInt(
|
||||||
|
16.dp.toIntPx(density),
|
||||||
|
windowInfo.containerSize.height - 266.dp.toIntPx(density)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val x by animateIntAsState(randomX, motionScheme.slowSpatialSpec())
|
||||||
|
val y by animateIntAsState(randomY, motionScheme.slowSpatialSpec())
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(surface)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = Modifier.offset {
|
||||||
|
IntOffset(x, y)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
if (timerState.timerMode == TimerMode.FOCUS) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("focus progress"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
.size(250.dp),
|
||||||
|
color = primary,
|
||||||
|
trackColor = secondaryContainer,
|
||||||
|
strokeWidth = 12.dp,
|
||||||
|
gapSize = 8.dp,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
CircularWavyProgressIndicator(
|
||||||
|
progress = progress,
|
||||||
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("break progress"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
.size(250.dp),
|
||||||
|
color = primary,
|
||||||
|
trackColor = secondaryContainer,
|
||||||
|
stroke = Stroke(
|
||||||
|
width = with(LocalDensity.current) {
|
||||||
|
12.dp.toPx()
|
||||||
|
},
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
),
|
||||||
|
trackStroke = Stroke(
|
||||||
|
width = with(LocalDensity.current) {
|
||||||
|
12.dp.toPx()
|
||||||
|
},
|
||||||
|
cap = StrokeCap.Round,
|
||||||
|
),
|
||||||
|
wavelength = 42.dp,
|
||||||
|
gapSize = 8.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = timerState.timeStr,
|
||||||
|
style = TextStyle(
|
||||||
|
fontFamily = openRundeClock,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
fontSize = 56.sp,
|
||||||
|
letterSpacing = (-2).sp
|
||||||
|
),
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
color = onSurface,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@AlwaysOnDisplay.rememberSharedContentState("clock"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun AlwaysOnDisplayPreview() {
|
||||||
|
val timerState = TimerState()
|
||||||
|
val progress = { 0.5f }
|
||||||
|
TomatoTheme {
|
||||||
|
SharedTransitionLayout {
|
||||||
|
AlwaysOnDisplay(
|
||||||
|
timerState = timerState,
|
||||||
|
progress = progress,
|
||||||
|
setTimerFrequency = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Dp.toIntPx(density: Density) = with(density) { toPx().toInt() }
|
||||||
@@ -1,18 +1,30 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import androidx.compose.animation.ContentTransform
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.Crossfade
|
import androidx.compose.animation.Crossfade
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
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.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -42,7 +54,6 @@ import androidx.navigation3.runtime.entryProvider
|
|||||||
import androidx.navigation3.runtime.rememberNavBackStack
|
import androidx.navigation3.runtime.rememberNavBackStack
|
||||||
import androidx.navigation3.ui.NavDisplay
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import androidx.window.core.layout.WindowSizeClass
|
import androidx.window.core.layout.WindowSizeClass
|
||||||
import org.nsh07.pomodoro.MainActivity.Companion.screens
|
|
||||||
import org.nsh07.pomodoro.service.TimerService
|
import org.nsh07.pomodoro.service.TimerService
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||||
@@ -55,7 +66,9 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
|||||||
@Composable
|
@Composable
|
||||||
fun AppScreen(
|
fun AppScreen(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
|
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
||||||
|
isAODEnabled: Boolean,
|
||||||
|
setTimerFrequency: (Float) -> Unit
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
@@ -78,128 +91,153 @@ fun AppScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
val wide = remember {
|
AnimatedVisibility(
|
||||||
windowSizeClass.isWidthAtLeastBreakpoint(
|
backStack.last() !is Screen.AOD,
|
||||||
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
enter = fadeIn(),
|
||||||
)
|
exit = fadeOut()
|
||||||
}
|
|
||||||
ShortNavigationBar(
|
|
||||||
arrangement =
|
|
||||||
if (wide) ShortNavigationBarArrangement.Centered
|
|
||||||
else ShortNavigationBarArrangement.EqualWeight
|
|
||||||
) {
|
) {
|
||||||
screens.forEach {
|
val wide = remember {
|
||||||
val selected = backStack.last() == it.route
|
windowSizeClass.isWidthAtLeastBreakpoint(
|
||||||
ShortNavigationBarItem(
|
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
||||||
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)) }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
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 ->
|
) { contentPadding ->
|
||||||
NavDisplay(
|
SharedTransitionLayout {
|
||||||
backStack = backStack,
|
NavDisplay(
|
||||||
onBack = { backStack.removeLastOrNull() },
|
backStack = backStack,
|
||||||
transitionSpec = {
|
onBack = backStack::removeLastOrNull,
|
||||||
ContentTransform(
|
transitionSpec = {
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
fadeIn(motionScheme.defaultEffectsSpec())
|
||||||
fadeOut(motionScheme.defaultEffectsSpec())
|
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
|
||||||
)
|
},
|
||||||
},
|
popTransitionSpec = {
|
||||||
popTransitionSpec = {
|
fadeIn(motionScheme.defaultEffectsSpec())
|
||||||
ContentTransform(
|
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
},
|
||||||
fadeOut(motionScheme.defaultEffectsSpec())
|
predictivePopTransitionSpec = {
|
||||||
)
|
fadeIn(motionScheme.defaultEffectsSpec())
|
||||||
},
|
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
|
||||||
predictivePopTransitionSpec = {
|
},
|
||||||
ContentTransform(
|
entryProvider = entryProvider {
|
||||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
entry<Screen.Timer> {
|
||||||
fadeOut(motionScheme.defaultEffectsSpec()) +
|
TimerScreen(
|
||||||
scaleOut(targetScale = 0.7f),
|
timerState = uiState,
|
||||||
)
|
progress = { progress },
|
||||||
},
|
onAction = { action ->
|
||||||
entryProvider = entryProvider {
|
when (action) {
|
||||||
entry<Screen.Timer> {
|
TimerAction.ResetTimer ->
|
||||||
TimerScreen(
|
Intent(context, TimerService::class.java).also {
|
||||||
timerState = uiState,
|
it.action = TimerService.Actions.RESET.toString()
|
||||||
progress = { progress },
|
context.startService(it)
|
||||||
onAction = { action ->
|
}
|
||||||
when (action) {
|
|
||||||
TimerAction.ResetTimer ->
|
|
||||||
Intent(context, TimerService::class.java).also {
|
|
||||||
it.action = TimerService.Actions.RESET.toString()
|
|
||||||
context.startService(it)
|
|
||||||
}
|
|
||||||
|
|
||||||
is TimerAction.SkipTimer ->
|
is TimerAction.SkipTimer ->
|
||||||
Intent(context, TimerService::class.java).also {
|
Intent(context, TimerService::class.java).also {
|
||||||
it.action = TimerService.Actions.SKIP.toString()
|
it.action = TimerService.Actions.SKIP.toString()
|
||||||
context.startService(it)
|
context.startService(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
TimerAction.StopAlarm ->
|
TimerAction.StopAlarm ->
|
||||||
Intent(context, TimerService::class.java).also {
|
Intent(context, TimerService::class.java).also {
|
||||||
it.action = TimerService.Actions.STOP_ALARM.toString()
|
it.action =
|
||||||
context.startService(it)
|
TimerService.Actions.STOP_ALARM.toString()
|
||||||
}
|
context.startService(it)
|
||||||
|
}
|
||||||
|
|
||||||
TimerAction.ToggleTimer ->
|
TimerAction.ToggleTimer ->
|
||||||
Intent(context, TimerService::class.java).also {
|
Intent(context, TimerService::class.java).also {
|
||||||
it.action = TimerService.Actions.TOGGLE.toString()
|
it.action = TimerService.Actions.TOGGLE.toString()
|
||||||
context.startService(it)
|
context.startService(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = modifier
|
||||||
|
.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
if (isAODEnabled) Modifier.clickable {
|
||||||
|
if (backStack.size < 2) backStack.add(Screen.AOD)
|
||||||
}
|
}
|
||||||
}
|
else Modifier
|
||||||
},
|
),
|
||||||
modifier = modifier.padding(
|
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Settings> {
|
entry<Screen.AOD> {
|
||||||
SettingsScreenRoot(
|
AlwaysOnDisplay(
|
||||||
modifier = modifier.padding(
|
timerState = uiState,
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
progress = { progress },
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
setTimerFrequency = setTimerFrequency,
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
modifier = Modifier
|
||||||
|
.then(
|
||||||
|
if (isAODEnabled) Modifier.clickable {
|
||||||
|
if (backStack.size > 1) backStack.removeLastOrNull()
|
||||||
|
}
|
||||||
|
else Modifier
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
entry<Screen.Stats> {
|
entry<Screen.Settings.Main> {
|
||||||
StatsScreenRoot(
|
SettingsScreenRoot(
|
||||||
contentPadding = contentPadding,
|
modifier = modifier.padding(
|
||||||
modifier = modifier.padding(
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
bottom = contentPadding.calculateBottomPadding()
|
)
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
|
|
||||||
|
entry<Screen.Stats> {
|
||||||
|
StatsScreenRoot(
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
modifier = modifier.padding(
|
||||||
|
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||||
|
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||||
|
bottom = contentPadding.calculateBottomPadding()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
62
app/src/main/java/org/nsh07/pomodoro/ui/Navigation.kt
Normal file
62
app/src/main/java/org/nsh07/pomodoro/ui/Navigation.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
|
||||||
|
val mainScreens = listOf(
|
||||||
|
NavItem(
|
||||||
|
Screen.Timer,
|
||||||
|
R.drawable.timer_outlined,
|
||||||
|
R.drawable.timer_filled,
|
||||||
|
R.string.timer
|
||||||
|
),
|
||||||
|
NavItem(
|
||||||
|
Screen.Stats,
|
||||||
|
R.drawable.monitoring,
|
||||||
|
R.drawable.monitoring_filled,
|
||||||
|
R.string.stats
|
||||||
|
),
|
||||||
|
NavItem(
|
||||||
|
Screen.Settings.Main,
|
||||||
|
R.drawable.settings,
|
||||||
|
R.drawable.settings_filled,
|
||||||
|
R.string.settings
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
val settingsScreens = listOf(
|
||||||
|
SettingsNavItem(
|
||||||
|
Screen.Settings.Timer,
|
||||||
|
R.drawable.timer_filled,
|
||||||
|
R.string.timer,
|
||||||
|
listOf(R.string.durations, R.string.session_length, R.string.always_on_display)
|
||||||
|
),
|
||||||
|
SettingsNavItem(
|
||||||
|
Screen.Settings.Alarm,
|
||||||
|
R.drawable.alarm,
|
||||||
|
R.string.alarm,
|
||||||
|
listOf(R.string.alarm_sound, R.string.alarm, R.string.vibrate)
|
||||||
|
),
|
||||||
|
SettingsNavItem(
|
||||||
|
Screen.Settings.Appearance,
|
||||||
|
R.drawable.palette,
|
||||||
|
R.string.appearance,
|
||||||
|
listOf(R.string.color_scheme, R.string.theme, R.string.black_theme)
|
||||||
|
)
|
||||||
|
)
|
||||||
@@ -1,3 +1,20 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui
|
package org.nsh07.pomodoro.ui
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
@@ -10,7 +27,22 @@ sealed class Screen : NavKey {
|
|||||||
object Timer : Screen()
|
object Timer : Screen()
|
||||||
|
|
||||||
@Serializable
|
@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
|
@Serializable
|
||||||
object Stats : Screen()
|
object Stats : Screen()
|
||||||
@@ -21,4 +53,11 @@ data class NavItem(
|
|||||||
@param:DrawableRes val unselectedIcon: Int,
|
@param:DrawableRes val unselectedIcon: Int,
|
||||||
@param:DrawableRes val selectedIcon: Int,
|
@param:DrawableRes val selectedIcon: Int,
|
||||||
@param:StringRes val label: Int
|
@param:StringRes val label: Int
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class SettingsNavItem(
|
||||||
|
val route: Screen.Settings,
|
||||||
|
@param:DrawableRes val icon: Int,
|
||||||
|
@param:StringRes val label: Int,
|
||||||
|
val innerSettings: List<Int>
|
||||||
|
)
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import org.nsh07.pomodoro.R
|
|
||||||
import org.nsh07.pomodoro.ui.ClickableListItem
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ColorSchemePickerListItem(
|
|
||||||
color: Color,
|
|
||||||
items: Int,
|
|
||||||
index: Int,
|
|
||||||
onColorChange: (Color) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
ColorSchemePickerDialog(
|
|
||||||
currentColor = color,
|
|
||||||
setShowDialog = { showDialog = it },
|
|
||||||
onColorChange = onColorChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ClickableListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(R.drawable.palette),
|
|
||||||
contentDescription = null,
|
|
||||||
tint = colorScheme.primary
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(stringResource(R.string.color_scheme)) },
|
|
||||||
supportingContent = {
|
|
||||||
Text(
|
|
||||||
if (color == Color.White) stringResource(R.string.dynamic)
|
|
||||||
else stringResource(R.string.color)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
colors = listItemColors,
|
|
||||||
items = items,
|
|
||||||
index = index,
|
|
||||||
modifier = modifier.fillMaxWidth()
|
|
||||||
) { showDialog = true }
|
|
||||||
}
|
|
||||||
@@ -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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,96 +1,80 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.media.RingtoneManager
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.compose.animation.slideInHorizontally
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.background
|
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.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
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.TextFieldState
|
||||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButtonDefaults
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.LocalTextStyle
|
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.SliderState
|
||||||
import androidx.compose.material3.Switch
|
|
||||||
import androidx.compose.material3.SwitchDefaults
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.material3.rememberSliderState
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.input.ImeAction
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.tooling.preview.Devices
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||||
import kotlinx.coroutines.Dispatchers
|
import androidx.navigation3.runtime.entryProvider
|
||||||
import kotlinx.coroutines.withContext
|
import androidx.navigation3.ui.NavDisplay
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.service.TimerService
|
import org.nsh07.pomodoro.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.PreferencesState
|
||||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreens
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
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)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@@ -101,6 +85,8 @@ fun SettingsScreenRoot(
|
|||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val backStack = viewModel.backStack
|
||||||
|
|
||||||
DisposableEffect(Unit) {
|
DisposableEffect(Unit) {
|
||||||
viewModel.runTextFieldFlowCollection()
|
viewModel.runTextFieldFlowCollection()
|
||||||
onDispose { viewModel.cancelTextFieldFlowCollection() }
|
onDispose { viewModel.cancelTextFieldFlowCollection() }
|
||||||
@@ -133,6 +119,7 @@ fun SettingsScreenRoot(
|
|||||||
|
|
||||||
SettingsScreen(
|
SettingsScreen(
|
||||||
preferencesState = preferencesState,
|
preferencesState = preferencesState,
|
||||||
|
backStack = backStack,
|
||||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||||
@@ -143,6 +130,7 @@ fun SettingsScreenRoot(
|
|||||||
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
||||||
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
||||||
onBlackThemeChange = viewModel::saveBlackTheme,
|
onBlackThemeChange = viewModel::saveBlackTheme,
|
||||||
|
onAodEnabledChange = viewModel::saveAodEnabled,
|
||||||
onAlarmSoundChanged = {
|
onAlarmSoundChanged = {
|
||||||
viewModel.saveAlarmSound(it)
|
viewModel.saveAlarmSound(it)
|
||||||
Intent(context, TimerService::class.java).apply {
|
Intent(context, TimerService::class.java).apply {
|
||||||
@@ -160,6 +148,7 @@ fun SettingsScreenRoot(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun SettingsScreen(
|
private fun SettingsScreen(
|
||||||
preferencesState: PreferencesState,
|
preferencesState: PreferencesState,
|
||||||
|
backStack: SnapshotStateList<Screen.Settings>,
|
||||||
focusTimeInputFieldState: TextFieldState,
|
focusTimeInputFieldState: TextFieldState,
|
||||||
shortBreakTimeInputFieldState: TextFieldState,
|
shortBreakTimeInputFieldState: TextFieldState,
|
||||||
longBreakTimeInputFieldState: TextFieldState,
|
longBreakTimeInputFieldState: TextFieldState,
|
||||||
@@ -170,394 +159,125 @@ private fun SettingsScreen(
|
|||||||
onAlarmEnabledChange: (Boolean) -> Unit,
|
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||||
onVibrateEnabledChange: (Boolean) -> Unit,
|
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||||
onBlackThemeChange: (Boolean) -> Unit,
|
onBlackThemeChange: (Boolean) -> Unit,
|
||||||
|
onAodEnabledChange: (Boolean) -> Unit,
|
||||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||||
onThemeChange: (String) -> Unit,
|
onThemeChange: (String) -> Unit,
|
||||||
onColorSchemeChange: (Color) -> Unit,
|
onColorSchemeChange: (Color) -> Unit,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
val switchColors = SwitchDefaults.colors(
|
|
||||||
checkedIconColor = colorScheme.primary,
|
|
||||||
)
|
|
||||||
|
|
||||||
val themeMap: Map<String, Pair<Int, String>> = remember {
|
NavDisplay(
|
||||||
mapOf(
|
backStack = backStack,
|
||||||
"auto" to Pair(
|
onBack = backStack::removeLastOrNull,
|
||||||
R.drawable.brightness_auto,
|
transitionSpec = {
|
||||||
context.getString(R.string.system_default)
|
(slideInHorizontally(initialOffsetX = { it }))
|
||||||
),
|
.togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
|
||||||
"light" to Pair(R.drawable.light_mode, context.getString(R.string.light)),
|
},
|
||||||
"dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark))
|
popTransitionSpec = {
|
||||||
)
|
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||||
}
|
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||||
val reverseThemeMap: Map<String, String> = remember {
|
},
|
||||||
mapOf(
|
predictivePopTransitionSpec = {
|
||||||
context.getString(R.string.system_default) to "auto",
|
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||||
context.getString(R.string.light) to "light",
|
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||||
context.getString(R.string.dark) to "dark"
|
},
|
||||||
)
|
entryProvider = entryProvider {
|
||||||
}
|
entry<Screen.Settings.Main> {
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
var alarmName by remember { mutableStateOf("...") }
|
TopAppBar(
|
||||||
|
title = {
|
||||||
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 {
|
|
||||||
Text(
|
Text(
|
||||||
stringResource(
|
stringResource(R.string.settings),
|
||||||
R.string.session_length_desc,
|
style = LocalTextStyle.current.copy(
|
||||||
sessionsSliderState.value.toInt()
|
fontFamily = robotoFlexTopBar,
|
||||||
|
fontSize = 32.sp,
|
||||||
|
lineHeight = 32.sp
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
Slider(
|
},
|
||||||
state = sessionsSliderState,
|
subtitle = {},
|
||||||
modifier = Modifier.padding(vertical = 4.dp)
|
colors = topBarColors,
|
||||||
)
|
titleHorizontalAlignment = Alignment.CenterHorizontally,
|
||||||
}
|
scrollBehavior = scrollBehavior
|
||||||
},
|
)
|
||||||
colors = listItemColors,
|
|
||||||
modifier = Modifier.clip(cardShape)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
item { Spacer(Modifier.height(12.dp)) }
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
item {
|
modifier = Modifier
|
||||||
ColorSchemePickerListItem(
|
.background(topBarColors.containerColor)
|
||||||
color = preferencesState.colorScheme.toColor(),
|
.fillMaxSize()
|
||||||
items = 3,
|
.padding(horizontal = 16.dp)
|
||||||
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)
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
painterResource(R.drawable.info),
|
|
||||||
null
|
item { AboutCard() }
|
||||||
)
|
|
||||||
}
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
AnimatedVisibility(expanded) {
|
|
||||||
Text(
|
itemsIndexed(settingsScreens) { index, item ->
|
||||||
stringResource(R.string.pomodoro_info),
|
ClickableListItem(
|
||||||
style = typography.bodyMedium,
|
leadingContent = {
|
||||||
color = colorScheme.onSurfaceVariant,
|
Icon(painterResource(item.icon), null)
|
||||||
modifier = Modifier.padding(8.dp)
|
},
|
||||||
)
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
remember {
|
||||||
|
item.innerSettings.joinToString(", ") {
|
||||||
|
context.getString(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Icon(painterResource(R.drawable.arrow_forward_big), null)
|
||||||
|
},
|
||||||
|
items = settingsScreens.size,
|
||||||
|
index = index
|
||||||
|
) { backStack.add(item.route) }
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<Screen.Settings.Alarm> {
|
||||||
|
AlarmSettings(
|
||||||
|
preferencesState = preferencesState,
|
||||||
|
alarmEnabled = alarmEnabled,
|
||||||
|
vibrateEnabled = vibrateEnabled,
|
||||||
|
alarmSound = alarmSound,
|
||||||
|
onAlarmEnabledChange = onAlarmEnabledChange,
|
||||||
|
onVibrateEnabledChange = onVibrateEnabledChange,
|
||||||
|
onAlarmSoundChanged = onAlarmSoundChanged,
|
||||||
|
onBack = backStack::removeLastOrNull
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<Screen.Settings.Appearance> {
|
||||||
|
AppearanceSettings(
|
||||||
|
preferencesState = preferencesState,
|
||||||
|
onBlackThemeChange = onBlackThemeChange,
|
||||||
|
onThemeChange = onThemeChange,
|
||||||
|
onColorSchemeChange = onColorSchemeChange,
|
||||||
|
onBack = backStack::removeLastOrNull
|
||||||
|
)
|
||||||
|
}
|
||||||
|
entry<Screen.Settings.Timer> {
|
||||||
|
TimerSettings(
|
||||||
|
aodEnabled = preferencesState.aodEnabled,
|
||||||
|
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||||
|
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||||
|
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||||
|
sessionsSliderState = sessionsSliderState,
|
||||||
|
onAodEnabledChange = onAodEnabledChange,
|
||||||
|
onBack = backStack::removeLastOrNull
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Preview(
|
|
||||||
showSystemUi = true,
|
|
||||||
device = Devices.PIXEL_9_PRO
|
|
||||||
)
|
|
||||||
@Composable
|
|
||||||
fun SettingsScreenPreview() {
|
|
||||||
TomatoTheme {
|
|
||||||
SettingsScreen(
|
|
||||||
preferencesState = PreferencesState(),
|
|
||||||
focusTimeInputFieldState = rememberTextFieldState((25).toString()),
|
|
||||||
shortBreakTimeInputFieldState = rememberTextFieldState((5).toString()),
|
|
||||||
longBreakTimeInputFieldState = rememberTextFieldState((15).toString()),
|
|
||||||
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
|
|
||||||
alarmEnabled = true,
|
|
||||||
vibrateEnabled = true,
|
|
||||||
alarmSound = "null",
|
|
||||||
onAlarmEnabledChange = {},
|
|
||||||
onVibrateEnabledChange = {},
|
|
||||||
onBlackThemeChange = {},
|
|
||||||
onAlarmSoundChanged = {},
|
|
||||||
onThemeChange = {},
|
|
||||||
onColorSchemeChange = {},
|
|
||||||
modifier = Modifier.fillMaxSize()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
data class SettingsSwitchItem(
|
|
||||||
val checked: Boolean,
|
|
||||||
@param:DrawableRes val icon: Int,
|
|
||||||
val label: String,
|
|
||||||
val description: String,
|
|
||||||
val onClick: (Boolean) -> Unit
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
data class SettingsSwitchItem(
|
||||||
|
val checked: Boolean,
|
||||||
|
@param:DrawableRes val icon: Int,
|
||||||
|
@param:StringRes val label: Int,
|
||||||
|
@param:StringRes val description: Int,
|
||||||
|
val onClick: (Boolean) -> Unit
|
||||||
|
)
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedContent
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.wrapContentHeight
|
|
||||||
import androidx.compose.foundation.layout.wrapContentWidth
|
|
||||||
import androidx.compose.foundation.selection.selectable
|
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
|
||||||
import androidx.compose.material3.AlertDialogDefaults
|
|
||||||
import androidx.compose.material3.BasicAlertDialog
|
|
||||||
import androidx.compose.material3.ButtonDefaults
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.ListItem
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
|
||||||
import androidx.compose.material3.MaterialTheme.shapes
|
|
||||||
import androidx.compose.material3.Surface
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import org.nsh07.pomodoro.R
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.selectedListItemColors
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
|
||||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
|
||||||
@Composable
|
|
||||||
fun ThemeDialog(
|
|
||||||
themeMap: Map<String, Pair<Int, String>>,
|
|
||||||
reverseThemeMap: Map<String, String>,
|
|
||||||
theme: String,
|
|
||||||
setShowThemeDialog: (Boolean) -> Unit,
|
|
||||||
onThemeChange: (String) -> Unit
|
|
||||||
) {
|
|
||||||
val selectedOption =
|
|
||||||
remember { mutableStateOf(themeMap[theme]!!.second) }
|
|
||||||
|
|
||||||
BasicAlertDialog(
|
|
||||||
onDismissRequest = { setShowThemeDialog(false) }
|
|
||||||
) {
|
|
||||||
Surface(
|
|
||||||
modifier = Modifier
|
|
||||||
.wrapContentWidth()
|
|
||||||
.wrapContentHeight(),
|
|
||||||
shape = shapes.extraLarge,
|
|
||||||
color = colorScheme.surfaceContainer,
|
|
||||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
|
||||||
) {
|
|
||||||
Column(modifier = Modifier.padding(24.dp)) {
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.choose_theme),
|
|
||||||
style = MaterialTheme.typography.headlineSmall
|
|
||||||
)
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
|
||||||
modifier = Modifier.selectableGroup()
|
|
||||||
) {
|
|
||||||
themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry<String, Pair<Int, String>> ->
|
|
||||||
val text = pair.value.second
|
|
||||||
val selected = text == selectedOption.value
|
|
||||||
|
|
||||||
ListItem(
|
|
||||||
leadingContent = {
|
|
||||||
AnimatedContent(selected) {
|
|
||||||
if (it)
|
|
||||||
Icon(painterResource(R.drawable.check), null)
|
|
||||||
else
|
|
||||||
Icon(painterResource(pair.value.first), null)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
headlineContent = {
|
|
||||||
Text(text = text, style = MaterialTheme.typography.bodyLarge)
|
|
||||||
},
|
|
||||||
colors = if (!selected) listItemColors else selectedListItemColors,
|
|
||||||
modifier = Modifier
|
|
||||||
.height(64.dp)
|
|
||||||
.clip(
|
|
||||||
when (index) {
|
|
||||||
0 -> topListItemShape
|
|
||||||
themeMap.size - 1 -> bottomListItemShape
|
|
||||||
else -> middleListItemShape
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.selectable(
|
|
||||||
selected = (text == selectedOption.value),
|
|
||||||
onClick = {
|
|
||||||
selectedOption.value = text
|
|
||||||
onThemeChange(reverseThemeMap[selectedOption.value]!!)
|
|
||||||
},
|
|
||||||
role = Role.RadioButton
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
TextButton(
|
|
||||||
shapes = ButtonDefaults.shapes(),
|
|
||||||
onClick = { setShowThemeDialog(false) },
|
|
||||||
modifier = Modifier.align(Alignment.End)
|
|
||||||
) {
|
|
||||||
Text(stringResource(R.string.ok))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
/*
|
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import org.nsh07.pomodoro.R
|
|
||||||
import org.nsh07.pomodoro.ui.ClickableListItem
|
|
||||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun ThemePickerListItem(
|
|
||||||
theme: String,
|
|
||||||
themeMap: Map<String, Pair<Int, String>>,
|
|
||||||
reverseThemeMap: Map<String, String>,
|
|
||||||
items: Int,
|
|
||||||
index: Int,
|
|
||||||
onThemeChange: (String) -> Unit,
|
|
||||||
modifier: Modifier = Modifier
|
|
||||||
) {
|
|
||||||
var showDialog by rememberSaveable { mutableStateOf(false) }
|
|
||||||
|
|
||||||
if (showDialog) {
|
|
||||||
ThemeDialog(
|
|
||||||
themeMap = themeMap,
|
|
||||||
reverseThemeMap = reverseThemeMap,
|
|
||||||
theme = theme,
|
|
||||||
setShowThemeDialog = { showDialog = it },
|
|
||||||
onThemeChange = onThemeChange
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ClickableListItem(
|
|
||||||
leadingContent = {
|
|
||||||
Icon(
|
|
||||||
painter = painterResource(themeMap[theme]!!.first),
|
|
||||||
contentDescription = null
|
|
||||||
)
|
|
||||||
},
|
|
||||||
headlineContent = { Text(stringResource(R.string.theme)) },
|
|
||||||
supportingContent = {
|
|
||||||
Text(themeMap[theme]!!.second)
|
|
||||||
},
|
|
||||||
colors = listItemColors,
|
|
||||||
items = items,
|
|
||||||
index = index,
|
|
||||||
modifier = modifier.fillMaxWidth()
|
|
||||||
) { showDialog = true }
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||||
|
|
||||||
|
import android.widget.Toast
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.shapes
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.BuildConfig
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
|
||||||
|
// Taken from https://github.com/shub39/Grit/blob/master/app/src/main/java/com/shub39/grit/core/presentation/settings/ui/component/AboutApp.kt
|
||||||
|
@Composable
|
||||||
|
fun AboutCard(modifier: Modifier = Modifier) {
|
||||||
|
val uriHandler = LocalUriHandler.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = colorScheme.primaryContainer,
|
||||||
|
contentColor = colorScheme.onPrimaryContainer
|
||||||
|
),
|
||||||
|
shape = shapes.extraLarge
|
||||||
|
) {
|
||||||
|
val buttonColors = ButtonDefaults.buttonColors(
|
||||||
|
containerColor = colorScheme.onPrimaryContainer,
|
||||||
|
contentColor = colorScheme.primaryContainer
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.app_name),
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontFamily = robotoFlexTopBar
|
||||||
|
)
|
||||||
|
Text(text = "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(modifier = Modifier.weight(1f))
|
||||||
|
|
||||||
|
Row {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
Toast.makeText(context, "Coming soon...", Toast.LENGTH_SHORT).show()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.discord),
|
||||||
|
contentDescription = "Discord",
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(
|
||||||
|
onClick = { uriHandler.openUri("https://github.com/nsh07/Tomato") }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.github),
|
||||||
|
contentDescription = "GitHub",
|
||||||
|
modifier = Modifier.size(24.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 16.dp, bottom = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
colors = buttonColors,
|
||||||
|
onClick = { uriHandler.openUri("https://coff.ee/nsh07") }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.coffee),
|
||||||
|
contentDescription = "Buy me a coffee",
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text = "Buy me a coffee")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Button(
|
||||||
|
colors = buttonColors,
|
||||||
|
onClick = { uriHandler.openUri("https://play.google.com/store/apps/details?id=org.nsh07.pomodoro") }
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.play_store),
|
||||||
|
contentDescription = "Rate on Google Play",
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(text = "Rate on Google Play")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,21 @@
|
|||||||
package org.nsh07.pomodoro.ui
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||||
|
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -17,6 +34,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -27,7 +45,7 @@ fun ClickableListItem(
|
|||||||
supportingContent: @Composable (() -> Unit)? = null,
|
supportingContent: @Composable (() -> Unit)? = null,
|
||||||
leadingContent: @Composable (() -> Unit)? = null,
|
leadingContent: @Composable (() -> Unit)? = null,
|
||||||
trailingContent: @Composable (() -> Unit)? = null,
|
trailingContent: @Composable (() -> Unit)? = null,
|
||||||
colors: ListItemColors = ListItemDefaults.colors(),
|
colors: ListItemColors = listItemColors,
|
||||||
tonalElevation: Dp = ListItemDefaults.Elevation,
|
tonalElevation: Dp = ListItemDefaults.Elevation,
|
||||||
shadowElevation: Dp = ListItemDefaults.Elevation,
|
shadowElevation: Dp = ListItemDefaults.Elevation,
|
||||||
items: Int,
|
items: Int,
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ColorSchemePickerListItem(
|
||||||
|
color: Color,
|
||||||
|
items: Int,
|
||||||
|
index: Int,
|
||||||
|
onColorChange: (Color) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val colorSchemes = listOf(
|
||||||
|
Color(0xfffeb4a7), Color(0xffffb3c0), Color(0xfffcaaff), Color(0xffb9c3ff),
|
||||||
|
Color(0xff62d3ff), Color(0xff44d9f1), Color(0xff52dbc9), Color(0xff78dd77),
|
||||||
|
Color(0xff9fd75c), Color(0xffc1d02d), Color(0xfffabd00), Color(0xffffb86e),
|
||||||
|
Color.White
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.clip(
|
||||||
|
when (index) {
|
||||||
|
0 -> topListItemShape
|
||||||
|
items - 1 -> bottomListItemShape
|
||||||
|
else -> middleListItemShape
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.colors),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text("Dynamic color") },
|
||||||
|
supportingContent = { Text("Adapt theme colors from your wallpaper") },
|
||||||
|
trailingContent = {
|
||||||
|
val checked = color == colorSchemes.last()
|
||||||
|
Switch(
|
||||||
|
checked = checked,
|
||||||
|
onCheckedChange = {
|
||||||
|
if (it) onColorChange(colorSchemes.last())
|
||||||
|
else onColorChange(colorSchemes.first())
|
||||||
|
},
|
||||||
|
thumbContent = {
|
||||||
|
if (checked) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.check),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.clear),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = switchColors
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier.clip(middleListItemShape)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(2.dp))
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.palette),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colorScheme.primary
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.color_scheme)) },
|
||||||
|
supportingContent = {
|
||||||
|
Text(
|
||||||
|
if (color == Color.White) stringResource(R.string.dynamic)
|
||||||
|
else stringResource(R.string.color)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier.clip(middleListItemShape)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(listItemColors.containerColor)
|
||||||
|
.padding(bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
LazyRow(contentPadding = PaddingValues(horizontal = 48.dp)) {
|
||||||
|
items(colorSchemes.dropLast(1)) {
|
||||||
|
ColorPickerButton(
|
||||||
|
it,
|
||||||
|
it == color,
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
) {
|
||||||
|
onColorChange(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ColorPickerButton(
|
||||||
|
color: Color,
|
||||||
|
isSelected: Boolean,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
shapes = IconButtonDefaults.shapes(),
|
||||||
|
colors = IconButtonDefaults.iconButtonColors(containerColor = color),
|
||||||
|
modifier = modifier.size(48.dp),
|
||||||
|
onClick = onClick
|
||||||
|
) {
|
||||||
|
AnimatedContent(isSelected) { isSelected ->
|
||||||
|
when (isSelected) {
|
||||||
|
true -> Icon(
|
||||||
|
painterResource(R.drawable.check),
|
||||||
|
tint = Color.Black,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
|
||||||
|
else ->
|
||||||
|
if (color == Color.White) Icon(
|
||||||
|
painterResource(R.drawable.colors),
|
||||||
|
tint = Color.Black,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,21 @@
|
|||||||
package org.nsh07.pomodoro.ui.settingsScreen
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.input.InputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.OutputTransformation
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldBuffer
|
||||||
|
import androidx.compose.foundation.text.input.insert
|
||||||
|
import androidx.core.text.isDigitsOnly
|
||||||
|
|
||||||
|
object MinutesInputTransformation : InputTransformation {
|
||||||
|
override fun TextFieldBuffer.transformInput() {
|
||||||
|
if (!this.asCharSequence().isDigitsOnly() || this.length > 2) {
|
||||||
|
revertAllChanges()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object MinutesOutputTransformation : OutputTransformation {
|
||||||
|
override fun TextFieldBuffer.transformOutput() {
|
||||||
|
if (this.length == 0) {
|
||||||
|
insert(0, "00")
|
||||||
|
} else if (this.toString().toInt() < 10) {
|
||||||
|
insert(0, "0")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ButtonGroupDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ToggleButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.role
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ThemePickerListItem(
|
||||||
|
theme: String,
|
||||||
|
items: Int,
|
||||||
|
index: Int,
|
||||||
|
onThemeChange: (String) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val themeMap: Map<String, Pair<Int, Int>> = remember {
|
||||||
|
mapOf(
|
||||||
|
"auto" to Pair(
|
||||||
|
R.drawable.brightness_auto,
|
||||||
|
R.string.system_default
|
||||||
|
),
|
||||||
|
"light" to Pair(R.drawable.light_mode, R.string.light),
|
||||||
|
"dark" to Pair(R.drawable.dark_mode, R.string.dark)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.clip(
|
||||||
|
when (index) {
|
||||||
|
0 -> topListItemShape
|
||||||
|
items - 1 -> bottomListItemShape
|
||||||
|
else -> middleListItemShape
|
||||||
|
},
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
AnimatedContent(themeMap[theme]!!.first) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(it),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.theme)) },
|
||||||
|
colors = listItemColors,
|
||||||
|
)
|
||||||
|
|
||||||
|
val options = themeMap.toList()
|
||||||
|
val selectedIndex = options.indexOf(Pair(theme, themeMap[theme]))
|
||||||
|
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(ButtonGroupDefaults.ConnectedSpaceBetween),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(listItemColors.containerColor)
|
||||||
|
.padding(start = 52.dp, end = 16.dp, bottom = 8.dp)
|
||||||
|
) {
|
||||||
|
options.forEachIndexed { index, theme ->
|
||||||
|
val isSelected = selectedIndex == index
|
||||||
|
ToggleButton(
|
||||||
|
checked = isSelected,
|
||||||
|
onCheckedChange = { onThemeChange(theme.first) },
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.semantics { role = Role.RadioButton },
|
||||||
|
shapes =
|
||||||
|
when (index) {
|
||||||
|
0 -> ButtonGroupDefaults.connectedLeadingButtonShapes()
|
||||||
|
options.lastIndex -> ButtonGroupDefaults.connectedTrailingButtonShapes()
|
||||||
|
else -> ButtonGroupDefaults.connectedMiddleButtonShapes()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(theme.second.second),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,254 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.screens
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.RingtoneManager
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AlarmSettings(
|
||||||
|
preferencesState: PreferencesState,
|
||||||
|
alarmEnabled: Boolean,
|
||||||
|
vibrateEnabled: Boolean,
|
||||||
|
alarmSound: String,
|
||||||
|
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||||
|
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||||
|
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
var alarmName by remember { mutableStateOf("...") }
|
||||||
|
|
||||||
|
LaunchedEffect(alarmSound) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
alarmName =
|
||||||
|
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ringtonePickerLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == Activity.RESULT_OK) {
|
||||||
|
val uri =
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
result.data?.getParcelableExtra(
|
||||||
|
RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
|
||||||
|
Uri::class.java
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||||
|
}
|
||||||
|
onAlarmSoundChanged(uri)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val ringtonePickerIntent = remember(alarmSound) {
|
||||||
|
Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
|
||||||
|
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
|
||||||
|
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, context.getString(R.string.alarm_sound))
|
||||||
|
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val switchItems = remember(
|
||||||
|
preferencesState.blackTheme,
|
||||||
|
preferencesState.aodEnabled,
|
||||||
|
alarmEnabled,
|
||||||
|
vibrateEnabled
|
||||||
|
) {
|
||||||
|
listOf(
|
||||||
|
SettingsSwitchItem(
|
||||||
|
checked = alarmEnabled,
|
||||||
|
icon = R.drawable.alarm_on,
|
||||||
|
label = R.string.sound,
|
||||||
|
description = R.string.alarm_desc,
|
||||||
|
onClick = onAlarmEnabledChange
|
||||||
|
),
|
||||||
|
SettingsSwitchItem(
|
||||||
|
checked = vibrateEnabled,
|
||||||
|
icon = R.drawable.mobile_vibrate,
|
||||||
|
label = R.string.vibrate,
|
||||||
|
description = R.string.vibrate_desc,
|
||||||
|
onClick = onVibrateEnabledChange
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
|
LargeFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text(stringResource(R.string.settings))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onBack) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = topBarColors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(painterResource(R.drawable.alarm), null)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(R.string.alarm_sound)) },
|
||||||
|
supportingContent = { Text(alarmName) },
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(topListItemShape)
|
||||||
|
.clickable(onClick = { ringtonePickerLauncher.launch(ringtonePickerIntent) })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
itemsIndexed(switchItems) { index, item ->
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(painterResource(item.icon), contentDescription = null)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
|
supportingContent = { Text(stringResource(item.description)) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = item.checked,
|
||||||
|
onCheckedChange = { item.onClick(it) },
|
||||||
|
thumbContent = {
|
||||||
|
if (item.checked) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.check),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.clear),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = switchColors
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(
|
||||||
|
when (index) {
|
||||||
|
switchItems.lastIndex -> bottomListItemShape
|
||||||
|
else -> middleListItemShape
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AlarmSettingsPreview() {
|
||||||
|
val preferencesState = PreferencesState()
|
||||||
|
AlarmSettings(
|
||||||
|
preferencesState = preferencesState,
|
||||||
|
alarmEnabled = true,
|
||||||
|
vibrateEnabled = false,
|
||||||
|
alarmSound = "",
|
||||||
|
onAlarmEnabledChange = {},
|
||||||
|
onVibrateEnabledChange = {},
|
||||||
|
onAlarmSoundChanged = {},
|
||||||
|
onBack = {})
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.screens
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.components.ColorSchemePickerListItem
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.components.ThemePickerListItem
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.utils.toColor
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AppearanceSettings(
|
||||||
|
preferencesState: PreferencesState,
|
||||||
|
onBlackThemeChange: (Boolean) -> Unit,
|
||||||
|
onThemeChange: (String) -> Unit,
|
||||||
|
onColorSchemeChange: (Color) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
|
LargeFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text(stringResource(R.string.settings))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onBack) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = topBarColors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ColorSchemePickerListItem(
|
||||||
|
color = preferencesState.colorScheme.toColor(),
|
||||||
|
items = 3,
|
||||||
|
index = 0,
|
||||||
|
onColorChange = onColorSchemeChange
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ThemePickerListItem(
|
||||||
|
theme = preferencesState.theme,
|
||||||
|
onThemeChange = onThemeChange,
|
||||||
|
items = 3,
|
||||||
|
index = 1,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(middleListItemShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
val item = SettingsSwitchItem(
|
||||||
|
checked = preferencesState.blackTheme,
|
||||||
|
icon = R.drawable.contrast,
|
||||||
|
label = R.string.black_theme,
|
||||||
|
description = R.string.black_theme_desc,
|
||||||
|
onClick = onBlackThemeChange
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(painterResource(item.icon), contentDescription = null)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
|
supportingContent = { Text(stringResource(item.description)) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = item.checked,
|
||||||
|
onCheckedChange = { item.onClick(it) },
|
||||||
|
thumbContent = {
|
||||||
|
if (item.checked) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.check),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.clear),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = switchColors
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier.clip(bottomListItemShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun AppearanceSettingsPreview() {
|
||||||
|
val preferencesState = PreferencesState()
|
||||||
|
AppearanceSettings(
|
||||||
|
preferencesState = preferencesState,
|
||||||
|
onBlackThemeChange = {},
|
||||||
|
onThemeChange = {},
|
||||||
|
onColorSchemeChange = {},
|
||||||
|
onBack = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.nsh07.pomodoro.ui.settingsScreen.screens
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.input.TextFieldState
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
|
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonDefaults
|
||||||
|
import androidx.compose.material3.LargeFlexibleTopAppBar
|
||||||
|
import androidx.compose.material3.ListItem
|
||||||
|
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||||
|
import androidx.compose.material3.MaterialTheme.typography
|
||||||
|
import androidx.compose.material3.Slider
|
||||||
|
import androidx.compose.material3.SliderState
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.nsh07.pomodoro.R
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.SettingsSwitchItem
|
||||||
|
import org.nsh07.pomodoro.ui.settingsScreen.components.MinuteInputField
|
||||||
|
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.switchColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||||
|
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TimerSettings(
|
||||||
|
aodEnabled: Boolean,
|
||||||
|
focusTimeInputFieldState: TextFieldState,
|
||||||
|
shortBreakTimeInputFieldState: TextFieldState,
|
||||||
|
longBreakTimeInputFieldState: TextFieldState,
|
||||||
|
sessionsSliderState: SliderState,
|
||||||
|
onAodEnabledChange: (Boolean) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||||
|
|
||||||
|
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||||
|
LargeFlexibleTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar)
|
||||||
|
},
|
||||||
|
subtitle = {
|
||||||
|
Text(stringResource(R.string.settings))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onBack) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.arrow_back),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = topBarColors,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
|
||||||
|
LazyColumn(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.background(topBarColors.containerColor)
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(14.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.Center,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState())
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.focus),
|
||||||
|
style = typography.titleSmallEmphasized
|
||||||
|
)
|
||||||
|
MinuteInputField(
|
||||||
|
state = focusTimeInputFieldState,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
topStart = topListItemShape.topStart,
|
||||||
|
bottomStart = topListItemShape.topStart,
|
||||||
|
topEnd = topListItemShape.bottomStart,
|
||||||
|
bottomEnd = topListItemShape.bottomStart
|
||||||
|
),
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.short_break),
|
||||||
|
style = typography.titleSmallEmphasized
|
||||||
|
)
|
||||||
|
MinuteInputField(
|
||||||
|
state = shortBreakTimeInputFieldState,
|
||||||
|
shape = RoundedCornerShape(middleListItemShape.topStart),
|
||||||
|
imeAction = ImeAction.Next
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(Modifier.width(2.dp))
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.long_break),
|
||||||
|
style = typography.titleSmallEmphasized
|
||||||
|
)
|
||||||
|
MinuteInputField(
|
||||||
|
state = longBreakTimeInputFieldState,
|
||||||
|
shape = RoundedCornerShape(
|
||||||
|
topStart = bottomListItemShape.topStart,
|
||||||
|
bottomStart = bottomListItemShape.topStart,
|
||||||
|
topEnd = bottomListItemShape.bottomStart,
|
||||||
|
bottomEnd = bottomListItemShape.bottomStart
|
||||||
|
),
|
||||||
|
imeAction = ImeAction.Done
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Spacer(Modifier.height(12.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(painterResource(R.drawable.clocks), null)
|
||||||
|
},
|
||||||
|
headlineContent = {
|
||||||
|
Text(stringResource(R.string.session_length))
|
||||||
|
},
|
||||||
|
supportingContent = {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
R.string.session_length_desc,
|
||||||
|
sessionsSliderState.value.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
Slider(
|
||||||
|
state = sessionsSliderState,
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier.clip(cardShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
item {
|
||||||
|
val item = SettingsSwitchItem(
|
||||||
|
checked = aodEnabled,
|
||||||
|
icon = R.drawable.aod,
|
||||||
|
label = R.string.always_on_display,
|
||||||
|
description = R.string.always_on_display_desc,
|
||||||
|
onClick = onAodEnabledChange
|
||||||
|
)
|
||||||
|
ListItem(
|
||||||
|
leadingContent = {
|
||||||
|
Icon(
|
||||||
|
painterResource(item.icon),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
headlineContent = { Text(stringResource(item.label)) },
|
||||||
|
supportingContent = { Text(stringResource(item.description)) },
|
||||||
|
trailingContent = {
|
||||||
|
Switch(
|
||||||
|
checked = item.checked,
|
||||||
|
onCheckedChange = { item.onClick(it) },
|
||||||
|
thumbContent = {
|
||||||
|
if (item.checked) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.check),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(R.drawable.clear),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = switchColors
|
||||||
|
)
|
||||||
|
},
|
||||||
|
colors = listItemColors,
|
||||||
|
modifier = Modifier.clip(cardShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
item {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.End,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(vertical = 6.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
FilledTonalIconToggleButton(
|
||||||
|
checked = expanded,
|
||||||
|
onCheckedChange = { expanded = it },
|
||||||
|
shapes = IconButtonDefaults.toggleableShapes(),
|
||||||
|
modifier = Modifier.width(52.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
painterResource(R.drawable.info),
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
AnimatedVisibility(expanded) {
|
||||||
|
Text(
|
||||||
|
stringResource(R.string.pomodoro_info),
|
||||||
|
style = typography.bodyMedium,
|
||||||
|
color = colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
item { Spacer(Modifier.height(12.dp)) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun TimerSettingsPreview() {
|
||||||
|
val focusTimeInputFieldState = TextFieldState("25")
|
||||||
|
val shortBreakTimeInputFieldState = TextFieldState("5")
|
||||||
|
val longBreakTimeInputFieldState = TextFieldState("15")
|
||||||
|
val sessionsSliderState = SliderState(
|
||||||
|
value = 4f,
|
||||||
|
valueRange = 1f..8f,
|
||||||
|
steps = 6
|
||||||
|
)
|
||||||
|
TimerSettings(
|
||||||
|
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||||
|
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||||
|
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||||
|
sessionsSliderState = sessionsSliderState,
|
||||||
|
aodEnabled = true,
|
||||||
|
onBack = {},
|
||||||
|
onAodEnabledChange = {}
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -14,5 +14,6 @@ import androidx.compose.ui.graphics.Color
|
|||||||
data class PreferencesState(
|
data class PreferencesState(
|
||||||
val theme: String = "auto",
|
val theme: String = "auto",
|
||||||
val colorScheme: String = Color.White.toString(),
|
val colorScheme: String = Color.White.toString(),
|
||||||
val blackTheme: Boolean = false
|
val blackTheme: Boolean = false,
|
||||||
|
val aodEnabled: Boolean = false
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
|
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.foundation.text.input.TextFieldState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.SliderState
|
import androidx.compose.material3.SliderState
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.snapshotFlow
|
import androidx.compose.runtime.snapshotFlow
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
@@ -31,12 +42,15 @@ import kotlinx.coroutines.launch
|
|||||||
import org.nsh07.pomodoro.TomatoApplication
|
import org.nsh07.pomodoro.TomatoApplication
|
||||||
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
||||||
import org.nsh07.pomodoro.data.TimerRepository
|
import org.nsh07.pomodoro.data.TimerRepository
|
||||||
|
import org.nsh07.pomodoro.ui.Screen
|
||||||
|
|
||||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
||||||
class SettingsViewModel(
|
class SettingsViewModel(
|
||||||
private val preferenceRepository: AppPreferenceRepository,
|
private val preferenceRepository: AppPreferenceRepository,
|
||||||
private val timerRepository: TimerRepository,
|
private val timerRepository: TimerRepository,
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
|
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
|
||||||
|
|
||||||
private val _preferencesState = MutableStateFlow(PreferencesState())
|
private val _preferencesState = MutableStateFlow(PreferencesState())
|
||||||
val preferencesState = _preferencesState.asStateFlow()
|
val preferencesState = _preferencesState.asStateFlow()
|
||||||
|
|
||||||
@@ -80,12 +94,15 @@ class SettingsViewModel(
|
|||||||
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
||||||
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
||||||
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
||||||
|
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
|
||||||
|
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||||
|
|
||||||
_preferencesState.update { currentState ->
|
_preferencesState.update { currentState ->
|
||||||
currentState.copy(
|
currentState.copy(
|
||||||
theme = theme,
|
theme = theme,
|
||||||
colorScheme = colorScheme,
|
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 {
|
companion object {
|
||||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||||
initializer {
|
initializer {
|
||||||
|
|||||||
@@ -1,9 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
|
*
|
||||||
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.theme
|
package org.nsh07.pomodoro.ui.theme
|
||||||
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.ListItemColors
|
import androidx.compose.material3.ListItemColors
|
||||||
import androidx.compose.material3.ListItemDefaults
|
import androidx.compose.material3.ListItemDefaults
|
||||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
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.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -41,4 +60,9 @@ object CustomColors {
|
|||||||
supportingColor = colorScheme.onSecondaryFixedVariant,
|
supportingColor = colorScheme.onSecondaryFixedVariant,
|
||||||
trailingIconColor = colorScheme.onSecondaryFixedVariant
|
trailingIconColor = colorScheme.onSecondaryFixedVariant
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val switchColors: SwitchColors
|
||||||
|
@Composable get() = SwitchDefaults.colors(
|
||||||
|
checkedIconColor = colorScheme.primary,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,18 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (c) 2025 Nishant Mishra
|
* Copyright (c) 2025 Nishant Mishra
|
||||||
*
|
*
|
||||||
* You should have received a copy of the GNU General Public License
|
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
*
|
||||||
|
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
* License, or (at your option) any later version.
|
||||||
|
*
|
||||||
|
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
* Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
* If not, see <https://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.nsh07.pomodoro.ui.timerScreen
|
package org.nsh07.pomodoro.ui.timerScreen
|
||||||
@@ -75,4 +85,4 @@ fun AlarmDialog(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
|||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedContent
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.SharedTransitionLayout
|
||||||
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.animation.animateColorAsState
|
import androidx.compose.animation.animateColorAsState
|
||||||
import androidx.compose.animation.expandVertically
|
import androidx.compose.animation.expandVertically
|
||||||
import androidx.compose.animation.fadeIn
|
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.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||||
import org.nsh07.pomodoro.R
|
import org.nsh07.pomodoro.R
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
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)
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun TimerScreen(
|
fun SharedTransitionScope.TimerScreen(
|
||||||
timerState: TimerState,
|
timerState: TimerState,
|
||||||
progress: () -> Float,
|
progress: () -> Float,
|
||||||
onAction: (TimerAction) -> Unit,
|
onAction: (TimerAction) -> Unit,
|
||||||
@@ -209,6 +212,12 @@ fun TimerScreen(
|
|||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||||
|
"focus progress"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
.widthIn(max = 350.dp)
|
.widthIn(max = 350.dp)
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
@@ -221,6 +230,12 @@ fun TimerScreen(
|
|||||||
CircularWavyProgressIndicator(
|
CircularWavyProgressIndicator(
|
||||||
progress = progress,
|
progress = progress,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||||
|
"break progress"
|
||||||
|
),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
.widthIn(max = 350.dp)
|
.widthIn(max = 350.dp)
|
||||||
.fillMaxWidth(0.9f)
|
.fillMaxWidth(0.9f)
|
||||||
.aspectRatio(1f),
|
.aspectRatio(1f),
|
||||||
@@ -261,7 +276,11 @@ fun TimerScreen(
|
|||||||
letterSpacing = (-2).sp
|
letterSpacing = (-2).sp
|
||||||
),
|
),
|
||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
maxLines = 1
|
maxLines = 1,
|
||||||
|
modifier = Modifier.sharedBounds(
|
||||||
|
sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
|
||||||
|
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||||
|
)
|
||||||
)
|
)
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
expanded,
|
expanded,
|
||||||
@@ -519,11 +538,13 @@ fun TimerScreenPreview() {
|
|||||||
)
|
)
|
||||||
TomatoTheme {
|
TomatoTheme {
|
||||||
Surface {
|
Surface {
|
||||||
TimerScreen(
|
SharedTransitionLayout {
|
||||||
timerState,
|
TimerScreen(
|
||||||
{ 0.3f },
|
timerState,
|
||||||
{}
|
{ 0.3f },
|
||||||
)
|
{}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,9 @@ class TimerViewModel(
|
|||||||
)
|
)
|
||||||
).toUri()
|
).toUri()
|
||||||
|
|
||||||
|
preferenceRepository.getBooleanPreference("aod_enabled")
|
||||||
|
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||||
|
|
||||||
_time.update { timerRepository.focusTime }
|
_time.update { timerRepository.focusTime }
|
||||||
cycles = 0
|
cycles = 0
|
||||||
startTime = 0L
|
startTime = 0L
|
||||||
|
|||||||
16
app/src/main/res/drawable/aod.xml
Normal file
16
app/src/main/res/drawable/aod.xml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2025 Nishant Mishra
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License
|
||||||
|
~ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#e3e3e3"
|
||||||
|
android:pathData="M360,480h240q17,0 28.5,-11.5T640,440q0,-17 -11.5,-28.5T600,400L360,400q-17,0 -28.5,11.5T320,440q0,17 11.5,28.5T360,480ZM400,620h160q17,0 28.5,-11.5T600,580q0,-17 -11.5,-28.5T560,540L400,540q-17,0 -28.5,11.5T360,580q0,17 11.5,28.5T400,620ZM280,920q-33,0 -56.5,-23.5T200,840v-720q0,-33 23.5,-56.5T280,40h400q33,0 56.5,23.5T760,120v124q18,7 29,22t11,34v80q0,19 -11,34t-29,22v404q0,33 -23.5,56.5T680,920L280,920Z" />
|
||||||
|
</vector>
|
||||||
21
app/src/main/res/drawable/arrow_back.xml
Normal file
21
app/src/main/res/drawable/arrow_back.xml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2025 Nishant Mishra
|
||||||
|
~
|
||||||
|
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
~
|
||||||
|
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Foobar. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#e3e3e3"
|
||||||
|
android:pathData="m313,520 l196,196q12,12 11.5,28T508,772q-12,11 -28,11.5T452,772L188,508q-6,-6 -8.5,-13t-2.5,-15q0,-8 2.5,-15t8.5,-13l264,-264q11,-11 27.5,-11t28.5,11q12,12 12,28.5T508,245L313,440h447q17,0 28.5,11.5T800,480q0,17 -11.5,28.5T760,520L313,520Z" />
|
||||||
|
</vector>
|
||||||
26
app/src/main/res/drawable/arrow_forward_big.xml
Normal file
26
app/src/main/res/drawable/arrow_forward_big.xml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2025 Nishant Mishra
|
||||||
|
~
|
||||||
|
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
~
|
||||||
|
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
~ If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#e3e3e3"
|
||||||
|
android:pathData="m321,880 l-71,-71 329,-329 -329,-329 71,-71 400,400L321,880Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/coffee.xml
Normal file
9
app/src/main/res/drawable/coffee.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="960"
|
||||||
|
android:viewportHeight="960">
|
||||||
|
<path
|
||||||
|
android:fillColor="#e3e3e3"
|
||||||
|
android:pathData="M440,720q-117,0 -198.5,-81.5T160,440v-240q0,-33 23.5,-56.5T240,120h500q58,0 99,41t41,99q0,58 -41,99t-99,41h-20v40q0,117 -81.5,198.5T440,720ZM240,320h400v-120L240,200v120ZM720,320h20q25,0 42.5,-17.5T800,260q0,-25 -17.5,-42.5T740,200h-20v120ZM200,840q-17,0 -28.5,-11.5T160,800q0,-17 11.5,-28.5T200,760h560q17,0 28.5,11.5T800,800q0,17 -11.5,28.5T760,840L200,840Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/discord.xml
Normal file
9
app/src/main/res/drawable/discord.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M20.317,4.37a19.791,19.791 0,0 0,-4.885 -1.515,0.074 0.074,0 0,0 -0.079,0.037c-0.211,0.375 -0.445,0.865 -0.608,1.25 -1.845,-0.276 -3.68,-0.276 -5.487,0 -0.164,-0.393 -0.406,-0.874 -0.618,-1.25a0.077,0.077 0,0 0,-0.079 -0.037,19.736 19.736,0 0,0 -4.885,1.515 0.07,0.07 0,0 0,-0.032 0.028C0.533,9.046 -0.319,13.58 0.099,18.058a0.082,0.082 0,0 0,0.031 0.056c2.053,1.508 4.041,2.423 5.993,3.029a0.078,0.078 0,0 0,0.084 -0.028c0.462,-0.63 0.873,-1.295 1.226,-1.994a0.076,0.076 0,0 0,-0.042 -0.106c-0.653,-0.248 -1.274,-0.549 -1.872,-0.892a0.077,0.077 0,0 1,-0.008 -0.128c0.126,-0.094 0.252,-0.192 0.372,-0.291a0.074,0.074 0,0 1,0.078 -0.01c3.928,1.793 8.18,1.793 12.061,0a0.074,0.074 0,0 1,0.079 0.009c0.12,0.099 0.246,0.198 0.373,0.292a0.077,0.077 0,0 1,-0.007 0.128,12.299 12.299,0 0,1 -1.873,0.891 0.077,0.077 0,0 0,-0.041 0.107c0.36,0.698 0.772,1.363 1.225,1.993a0.076,0.076 0,0 0,0.084 0.029c1.961,-0.607 3.95,-1.522 6.002,-3.029a0.077,0.077 0,0 0,0.031 -0.055c0.5,-5.177 -0.838,-9.674 -3.549,-13.66a0.061,0.061 0,0 0,-0.031 -0.029zM8.02,15.331c-1.183,0 -2.157,-1.086 -2.157,-2.419 0,-1.333 0.956,-2.419 2.157,-2.419 1.211,0 2.176,1.095 2.157,2.419 0,1.333 -0.956,2.419 -2.157,2.419zM15.995,15.331c-1.183,0 -2.157,-1.086 -2.157,-2.419 0,-1.333 0.955,-2.419 2.157,-2.419 1.211,0 2.176,1.095 2.157,2.419 0,1.333 -0.946,2.419 -2.157,2.419Z" />
|
||||||
|
</vector>
|
||||||
9
app/src/main/res/drawable/github.xml
Normal file
9
app/src/main/res/drawable/github.xml
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M12,0.297c-6.63,0 -12,5.373 -12,12 0,5.303 3.438,9.8 8.205,11.385 0.6,0.113 0.82,-0.258 0.82,-0.577 0,-0.285 -0.01,-1.04 -0.015,-2.04 -3.338,0.724 -4.042,-1.61 -4.042,-1.61C4.422,18.07 3.633,17.7 3.633,17.7c-1.087,-0.744 0.084,-0.729 0.084,-0.729 1.205,0.084 1.838,1.236 1.838,1.236 1.07,1.835 2.809,1.305 3.495,0.998 0.108,-0.776 0.417,-1.305 0.76,-1.605 -2.665,-0.3 -5.466,-1.332 -5.466,-5.93 0,-1.31 0.465,-2.38 1.235,-3.22 -0.135,-0.303 -0.54,-1.523 0.105,-3.176 0,0 1.005,-0.322 3.3,1.23 0.96,-0.267 1.98,-0.399 3,-0.405 1.02,0.006 2.04,0.138 3,0.405 2.28,-1.552 3.285,-1.23 3.285,-1.23 0.645,1.653 0.24,2.873 0.12,3.176 0.765,0.84 1.23,1.91 1.23,3.22 0,4.61 -2.805,5.625 -5.475,5.92 0.42,0.36 0.81,1.096 0.81,2.22 0,1.606 -0.015,2.896 -0.015,3.286 0,0.315 0.21,0.69 0.825,0.57C20.565,22.092 24,17.592 24,12.297c0,-6.627 -5.373,-12 -12,-12" />
|
||||||
|
</vector>
|
||||||
10
app/src/main/res/drawable/play_store.xml
Normal file
10
app/src/main/res/drawable/play_store.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="6.35"
|
||||||
|
android:viewportHeight="6.35">
|
||||||
|
<path
|
||||||
|
android:fillColor="#000000"
|
||||||
|
android:pathData="M0.6744,6.3347C0.5991,6.3185 0.471,6.275 0.4362,6.2538 0.4204,6.2442 0.7343,5.9434 1.9712,4.7827L3.5262,3.3236 4.159,3.8713C4.5071,4.1725 4.7868,4.4231 4.7806,4.4283 4.767,4.4398 1.3189,6.2241 1.2202,6.2708 1.0678,6.3428 0.8376,6.3698 0.6744,6.3347ZM0.0712,5.8885C-0.0032,5.736 0,5.8606 0,3.1751c0,-2.6939 -0.0037,-2.5574 0.0735,-2.7187 0.0195,-0.0409 0.0396,-0.0743 0.0445,-0.0743 0.0049,0 0.7041,0.6015 1.5538,1.3366L3.2165,3.0555 1.6709,4.5056C0.8208,5.3031 0.1205,5.9557 0.1146,5.9557c-0.0059,0 -0.0254,-0.0302 -0.0435,-0.0672zM4.5028,3.6306 L3.826,3.0448 4.377,2.5266C4.6801,2.2415 4.9336,2.0083 4.9405,2.0083c0.0168,0 0.9615,0.4887 1.0486,0.5425C6.2037,2.6831 6.35,2.9363 6.35,3.1751 6.35,3.4137 6.2088,3.6595 5.9939,3.7948 5.9253,3.838 5.1959,4.2187 5.1846,4.2171 5.1818,4.2168 4.875,3.9528 4.5028,3.6306ZM3.241,2.5383C3.099,2.4141 2.409,1.8158 1.7076,1.2088 0.5332,0.1924 0.4344,0.1038 0.4572,0.0882 0.5059,0.0547 0.6943,0.008 0.8067,0.0015 0.9426,-0.0064 1.0774,0.0175 1.2002,0.0715 1.247,0.092 2.0206,0.4896 2.9193,0.955L4.5534,1.8012 4.0396,2.2834C3.757,2.5485 3.5198,2.7652 3.5125,2.7648 3.5052,2.7644 3.383,2.6625 3.241,2.5383Z"
|
||||||
|
android:strokeWidth="0.0120146" />
|
||||||
|
</vector>
|
||||||
@@ -36,4 +36,7 @@
|
|||||||
<string name="stats">ئامار</string>
|
<string name="stats">ئامار</string>
|
||||||
<string name="more_info">زانیاری زیاتر</string>
|
<string name="more_info">زانیاری زیاتر</string>
|
||||||
<string name="pause">وەستاندن</string>
|
<string name="pause">وەستاندن</string>
|
||||||
|
<string name="paused">پشوو</string>
|
||||||
|
<string name="completed">تەواوکراو</string>
|
||||||
|
<string name="up_next_notification">دوای ئەمە</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
<string name="productivity_analysis_desc">Duración de la concentración en diferentes momentos del día</string>
|
<string name="productivity_analysis_desc">Duración de la concentración en diferentes momentos del día</string>
|
||||||
<string name="alarm_sound">Sonido de la alarma</string>
|
<string name="alarm_sound">Sonido de la alarma</string>
|
||||||
<string name="black_theme">Tema negro</string>
|
<string name="black_theme">Tema negro</string>
|
||||||
<string name="black_theme_desc">Utiliza un tema oscuro negro puro</string>
|
<string name="black_theme_desc">Utilizar un tema oscuro negro puro</string>
|
||||||
<string name="alarm_desc">Sonar alarma cuando el temporizador finalice</string>
|
<string name="alarm_desc">Sonar alarma cuando el temporizador finalice</string>
|
||||||
<string name="vibrate">Vibrar</string>
|
<string name="vibrate">Vibrar</string>
|
||||||
<string name="vibrate_desc">Vibrar cuando el temporizador finalice</string>
|
<string name="vibrate_desc">Vibrar cuando el temporizador finalice</string>
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
<string name="break_">Descanso</string>
|
<string name="break_">Descanso</string>
|
||||||
<string name="last_week">Semana pasada</string>
|
<string name="last_week">Semana pasada</string>
|
||||||
<string name="focus_per_day_avg">concentración por día (avg)</string>
|
<string name="focus_per_day_avg">concentración por día (avg)</string>
|
||||||
<string name="more_info">Más informes</string>
|
<string name="more_info">Más información</string>
|
||||||
<string name="weekly_productivity_analysis">Análisis de productividad semanal</string>
|
<string name="weekly_productivity_analysis">Análisis de productividad semanal</string>
|
||||||
<string name="last_month">Mes pasado</string>
|
<string name="last_month">Mes pasado</string>
|
||||||
<string name="monthly_productivity_analysis">Análisis de productividad mensual</string>
|
<string name="monthly_productivity_analysis">Análisis de productividad mensual</string>
|
||||||
@@ -56,4 +56,5 @@
|
|||||||
<string name="color">Color</string>
|
<string name="color">Color</string>
|
||||||
<string name="light">Luz</string>
|
<string name="light">Luz</string>
|
||||||
<string name="dark">Oscuro</string>
|
<string name="dark">Oscuro</string>
|
||||||
|
<string name="last_year">Año pasado</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
62
app/src/main/res/values-fr/strings.xml
Normal file
62
app/src/main/res/values-fr/strings.xml
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="start">Démarrer</string>
|
||||||
|
<string name="stop">Arrêter</string>
|
||||||
|
<string name="focus">Concentration</string>
|
||||||
|
<string name="short_break">Pause courte</string>
|
||||||
|
<string name="long_break">Pause longue</string>
|
||||||
|
<string name="exit">Quitter</string>
|
||||||
|
<string name="skip">Passer</string>
|
||||||
|
<string name="stop_alarm">Arrêter l\'alarme</string>
|
||||||
|
<string name="min_remaining_notification">%1$s minutes restantes</string>
|
||||||
|
<string name="paused">En pause</string>
|
||||||
|
<string name="completed">Terminé</string>
|
||||||
|
<string name="up_next_notification">À venir : %1$s (%2$s)</string>
|
||||||
|
<string name="start_next">Démarrer l\'intervalle suivant</string>
|
||||||
|
<string name="choose_color_scheme">Choisir le thème de couleurs</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="color_scheme">Thème de couleurs</string>
|
||||||
|
<string name="dynamic">Dynamique</string>
|
||||||
|
<string name="color">Couleur</string>
|
||||||
|
<string name="system_default">Valeur par défaut du système</string>
|
||||||
|
<string name="alarm">Alarme</string>
|
||||||
|
<string name="light">Clair</string>
|
||||||
|
<string name="dark">Sombre</string>
|
||||||
|
<string name="choose_theme">Choisir le thème</string>
|
||||||
|
<string name="productivity_analysis">Analyse de productivité</string>
|
||||||
|
<string name="productivity_analysis_desc">Durée de concentration selon les moments de la journée</string>
|
||||||
|
<string name="alarm_sound">Son de l\'alarme</string>
|
||||||
|
<string name="black_theme">Thème noir</string>
|
||||||
|
<string name="black_theme_desc">Utiliser un thème sombre noir pur</string>
|
||||||
|
<string name="alarm_desc">Faire sonner l’alarme à la fin du minuteur</string>
|
||||||
|
<string name="vibrate">Vibrer</string>
|
||||||
|
<string name="vibrate_desc">Faire vibrer à la fin du minuteur</string>
|
||||||
|
<string name="theme">Thème</string>
|
||||||
|
<string name="settings">Paramètres</string>
|
||||||
|
<string name="session_length">Durée de la session</string>
|
||||||
|
<string name="session_length_desc">Intervalles de concentration par session : %1$d</string>
|
||||||
|
<string name="pomodoro_info">Une \"session\" est une séquence 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.</string>
|
||||||
|
<string name="stats">Statistiques</string>
|
||||||
|
<string name="today">Aujourd\'hui</string>
|
||||||
|
<string name="break_">Pause</string>
|
||||||
|
<string name="last_week">7 derniers jours</string>
|
||||||
|
<string name="focus_per_day_avg">concentration moyenne par jour</string>
|
||||||
|
<string name="more_info">Plus d\'infos</string>
|
||||||
|
<string name="weekly_productivity_analysis">Analyse hebdomadaire de la productivité</string>
|
||||||
|
<string name="last_month">30 derniers jours</string>
|
||||||
|
<string name="monthly_productivity_analysis">Analyse mensuelle de la productivité</string>
|
||||||
|
<string name="stop_alarm_question">Arrêter l\'alarme ?</string>
|
||||||
|
<string name="stop_alarm_dialog_text">La session actuelle est terminée. Touchez n’importe où pour arrêter l’alarme.</string>
|
||||||
|
<string name="timer_session_count">%1$d sur %2$d</string>
|
||||||
|
<string name="more">Plus</string>
|
||||||
|
<string name="pause">Mettre en pause</string>
|
||||||
|
<string name="play">Démarrer la session</string>
|
||||||
|
<string name="restart">Redémarrer</string>
|
||||||
|
<string name="skip_to_next">Passer au suivant</string>
|
||||||
|
<string name="up_next">À venir</string>
|
||||||
|
<string name="timer">Minuteur</string>
|
||||||
|
<string name="timer_progress">Progression du minuteur</string>
|
||||||
|
<string name="last_year">12 derniers mois</string>
|
||||||
|
<string name="always_on_display_desc">Appuyez n\'importe où lors de l\'affichage du minuteur pour passer en mode AOD</string>
|
||||||
|
<string name="always_on_display">Affichage Permanent</string>
|
||||||
|
</resources>
|
||||||
64
app/src/main/res/values-tr/strings.xml
Normal file
64
app/src/main/res/values-tr/strings.xml
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<string name="start">Başlat</string>
|
||||||
|
<string name="stop">Durdur</string>
|
||||||
|
<string name="focus">Odaklan</string>
|
||||||
|
<string name="short_break">Kısa Mola</string>
|
||||||
|
<string name="long_break">Uzun Mola</string>
|
||||||
|
<string name="exit">Çıkış</string>
|
||||||
|
<string name="skip">Atla</string>
|
||||||
|
<string name="stop_alarm">Alarmı Durdur</string>
|
||||||
|
<string name="min_remaining_notification">%1$s dk kaldı</string>
|
||||||
|
<string name="paused">Duraklatıldı</string>
|
||||||
|
<string name="completed">Tamamlandı</string>
|
||||||
|
<string name="up_next_notification">Sırada: %1$s (%2$s)</string>
|
||||||
|
<string name="start_next">Sıradakini Başlat</string>
|
||||||
|
<string name="choose_color_scheme">Renk şeması seçin</string>
|
||||||
|
<string name="ok">Tamam</string>
|
||||||
|
<string name="color_scheme">Renk şeması</string>
|
||||||
|
<string name="dynamic">Dinamik</string>
|
||||||
|
<string name="color">Renk</string>
|
||||||
|
<string name="system_default">Sistem varsayılanı</string>
|
||||||
|
<string name="light">Açık</string>
|
||||||
|
<string name="dark">Koyu</string>
|
||||||
|
<string name="choose_theme">Tema seçin</string>
|
||||||
|
<string name="productivity_analysis">Verimlilik analizi</string>
|
||||||
|
<string name="productivity_analysis_desc">Günün farklı saatlerindeki odaklanma süreleri</string>
|
||||||
|
<string name="alarm_sound">Alarm sesi</string>
|
||||||
|
<string name="black_theme">Siyah tema</string>
|
||||||
|
<string name="black_theme_desc">Tam siyah koyu tema kullan</string>
|
||||||
|
<string name="alarm_desc">Zamanlayıcı bittiğinde alarm çal</string>
|
||||||
|
<string name="vibrate">Titreşim</string>
|
||||||
|
<string name="vibrate_desc">Zamanlayıcı bittiğinde titre</string>
|
||||||
|
<string name="theme">Tema</string>
|
||||||
|
<string name="settings">Ayarlar</string>
|
||||||
|
<string name="session_length">Oturum uzunluğu</string>
|
||||||
|
<string name="session_length_desc">Bir oturumdaki odaklanma aralığı: %1$d</string>
|
||||||
|
<string name="pomodoro_info">\"Oturum\", odaklanma aralıkları, kısa mola aralıkları ve bir uzun mola aralığı içeren bir pomodoro aralıkları dizisidir. Bir oturumun son molası her zaman uzun moladır.</string>
|
||||||
|
<string name="stats">İstatistikler</string>
|
||||||
|
<string name="today">Bugün</string>
|
||||||
|
<string name="break_">Mola</string>
|
||||||
|
<string name="last_week">Geçen hafta</string>
|
||||||
|
<string name="focus_per_day_avg">günlük odaklanma (ortalama)</string>
|
||||||
|
<string name="more_info">Daha fazla bilgi</string>
|
||||||
|
<string name="weekly_productivity_analysis">Haftalık verimlilik analizi</string>
|
||||||
|
<string name="last_month">Geçen ay</string>
|
||||||
|
<string name="monthly_productivity_analysis">Aylık verimlilik analizi</string>
|
||||||
|
<string name="stop_alarm_question">Alarmı Durdur?</string>
|
||||||
|
<string name="stop_alarm_dialog_text">Mevcut zamanlayıcı oturumu tamamlandı. Alarmı durdurmak için herhangi bir yere dokunun.</string>
|
||||||
|
<string name="timer_session_count">%1$d / %2$d</string>
|
||||||
|
<string name="more">Daha fazla</string>
|
||||||
|
<string name="pause">Duraklat</string>
|
||||||
|
<string name="play">Devam Et</string>
|
||||||
|
<string name="restart">Yeniden Başlat</string>
|
||||||
|
<string name="skip_to_next">Sıradakine Atla</string>
|
||||||
|
<string name="up_next">Sıradaki</string>
|
||||||
|
<string name="timer">Zamanlayıcı</string>
|
||||||
|
<string name="timer_progress">Zamanlayıcı İlerlemesi</string>
|
||||||
|
<string name="last_year">Geçen yıl</string>
|
||||||
|
<string name="alarm">Alarm</string>
|
||||||
|
<string name="always_on_display">Her zaman açık ekran</string>
|
||||||
|
<string name="always_on_display_desc">Zamanlayıcıyı görüntülerken AOD moduna geçmek için herhangi bir yere dokunun</string>
|
||||||
|
<string name="appearance">Görünüm</string>
|
||||||
|
<string name="durations">Süreler</string>
|
||||||
|
</resources>
|
||||||
@@ -55,5 +55,8 @@
|
|||||||
<string name="up_next">接下来是</string>
|
<string name="up_next">接下来是</string>
|
||||||
<string name="timer">计时</string>
|
<string name="timer">计时</string>
|
||||||
<string name="timer_progress">计时器进度</string>
|
<string name="timer_progress">计时器进度</string>
|
||||||
<string name="timer_session_count">%1$d 的 %2$d</string>
|
<string name="timer_session_count">%2$d 中的 %1$d</string>
|
||||||
|
<string name="last_year">去年</string>
|
||||||
|
<string name="always_on_display">息屏显示</string>
|
||||||
|
<string name="always_on_display_desc">查看计时器时点击任意位置切换至 AOD 模式</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,60 +1,82 @@
|
|||||||
|
<!--
|
||||||
|
~ Copyright (c) 2025 Nishant Mishra
|
||||||
|
~
|
||||||
|
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||||
|
~
|
||||||
|
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||||
|
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||||
|
~ License, or (at your option) any later version.
|
||||||
|
~
|
||||||
|
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||||
|
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||||
|
~ Public License for more details.
|
||||||
|
~
|
||||||
|
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||||
|
~ If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
-->
|
||||||
|
|
||||||
<resources>
|
<resources>
|
||||||
<string name="app_name">Tomato</string>
|
|
||||||
<string name="start">Start</string>
|
|
||||||
<string name="stop">Stop</string>
|
|
||||||
<string name="focus">Focus</string>
|
|
||||||
<string name="short_break">Short break</string>
|
|
||||||
<string name="long_break">Long break</string>
|
|
||||||
<string name="exit">Exit</string>
|
|
||||||
<string name="skip">Skip</string>
|
|
||||||
<string name="stop_alarm">Stop alarm</string>
|
|
||||||
<string name="min_remaining_notification">%1$s min remaining</string>
|
|
||||||
<string name="paused">Paused</string>
|
|
||||||
<string name="completed">Completed</string>
|
|
||||||
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
|
|
||||||
<string name="start_next">Start next</string>
|
|
||||||
<string name="choose_color_scheme">Choose color scheme</string>
|
|
||||||
<string name="ok">OK</string>
|
|
||||||
<string name="color_scheme">Color scheme</string>
|
|
||||||
<string name="dynamic">Dynamic</string>
|
|
||||||
<string name="color">Color</string>
|
|
||||||
<string name="system_default">System default</string>
|
|
||||||
<string name="alarm">Alarm</string>
|
<string name="alarm">Alarm</string>
|
||||||
<string name="light">Light</string>
|
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
||||||
<string name="dark">Dark</string>
|
|
||||||
<string name="choose_theme">Choose theme</string>
|
|
||||||
<string name="productivity_analysis">Productivity analysis</string>
|
|
||||||
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
|
||||||
<string name="alarm_sound">Alarm sound</string>
|
<string name="alarm_sound">Alarm sound</string>
|
||||||
|
<string name="always_on_display">Always On Display</string>
|
||||||
|
<string name="always_on_display_desc">Tap anywhere when viewing the timer to switch to AOD mode</string>
|
||||||
|
<string name="app_name">Tomato</string>
|
||||||
<string name="black_theme">Black theme</string>
|
<string name="black_theme">Black theme</string>
|
||||||
<string name="black_theme_desc">Use a pure black dark theme</string>
|
<string name="black_theme_desc">Use a pure black dark theme</string>
|
||||||
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
<string name="break_">Break</string>
|
||||||
<string name="vibrate">Vibrate</string>
|
<string name="choose_color_scheme">Choose color scheme</string>
|
||||||
<string name="vibrate_desc">Vibrate when a timer completes</string>
|
<string name="choose_theme">Choose theme</string>
|
||||||
<string name="theme">Theme</string>
|
<string name="color">Color</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="color_scheme">Color scheme</string>
|
||||||
|
<string name="completed">Completed</string>
|
||||||
|
<string name="dark">Dark</string>
|
||||||
|
<string name="dynamic">Dynamic</string>
|
||||||
|
<string name="exit">Exit</string>
|
||||||
|
<string name="focus">Focus</string>
|
||||||
|
<string name="focus_per_day_avg">focus per day (avg)</string>
|
||||||
|
<string name="last_month">Last month</string>
|
||||||
|
<string name="last_week">Last week</string>
|
||||||
|
<string name="last_year">Last year</string>
|
||||||
|
<string name="light">Light</string>
|
||||||
|
<string name="long_break">Long break</string>
|
||||||
|
<string name="min_remaining_notification">%1$s min remaining</string>
|
||||||
|
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
|
||||||
|
<string name="more">More</string>
|
||||||
|
<string name="more_info">More info</string>
|
||||||
|
<string name="ok">OK</string>
|
||||||
|
<string name="pause">Pause</string>
|
||||||
|
<string name="paused">Paused</string>
|
||||||
|
<string name="play">Play</string>
|
||||||
|
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
|
||||||
|
<string name="productivity_analysis">Productivity analysis</string>
|
||||||
|
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
||||||
|
<string name="restart">Restart</string>
|
||||||
<string name="session_length">Session length</string>
|
<string name="session_length">Session length</string>
|
||||||
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
|
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
|
||||||
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="stats">Stats</string>
|
<string name="short_break">Short break</string>
|
||||||
<string name="today">Today</string>
|
<string name="skip">Skip</string>
|
||||||
<string name="break_">Break</string>
|
|
||||||
<string name="last_week">Last week</string>
|
|
||||||
<string name="focus_per_day_avg">focus per day (avg)</string>
|
|
||||||
<string name="more_info">More info</string>
|
|
||||||
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
|
|
||||||
<string name="last_month">Last month</string>
|
|
||||||
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
|
|
||||||
<string name="stop_alarm_question">Stop Alarm?</string>
|
|
||||||
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
|
||||||
<string name="timer_session_count">%1$d of %2$d</string>
|
|
||||||
<string name="more">More</string>
|
|
||||||
<string name="pause">Pause</string>
|
|
||||||
<string name="play">Play</string>
|
|
||||||
<string name="restart">Restart</string>
|
|
||||||
<string name="skip_to_next">Skip to next</string>
|
<string name="skip_to_next">Skip to next</string>
|
||||||
<string name="up_next">Up next</string>
|
<string name="start">Start</string>
|
||||||
|
<string name="start_next">Start next</string>
|
||||||
|
<string name="stats">Stats</string>
|
||||||
|
<string name="stop">Stop</string>
|
||||||
|
<string name="stop_alarm">Stop alarm</string>
|
||||||
|
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
||||||
|
<string name="stop_alarm_question">Stop Alarm?</string>
|
||||||
|
<string name="system_default">System</string>
|
||||||
|
<string name="theme">Theme</string>
|
||||||
<string name="timer">Timer</string>
|
<string name="timer">Timer</string>
|
||||||
<string name="timer_progress">Timer progress</string>
|
<string name="timer_progress">Timer progress</string>
|
||||||
<string name="last_year">Last year</string>
|
<string name="timer_session_count">%1$d of %2$d</string>
|
||||||
|
<string name="today">Today</string>
|
||||||
|
<string name="up_next">Up next</string>
|
||||||
|
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
|
||||||
|
<string name="vibrate">Vibration</string>
|
||||||
|
<string name="vibrate_desc">Vibrate when a timer completes</string>
|
||||||
|
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
|
||||||
|
<string name="appearance">Appearance</string>
|
||||||
|
<string name="durations">Durations</string>
|
||||||
|
<string name="sound">Sound</string>
|
||||||
</resources>
|
</resources>
|
||||||
6
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
6
fastlane/metadata/android/en-US/changelogs/14.txt
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
New features:
|
||||||
|
- New Always On Display option: click anywhere while on the Timer screen to turn on Always On Display mode, tap again to turn it off
|
||||||
|
|
||||||
|
Translators on Weblate helped add support for French and Turkish in this update
|
||||||
|
|
||||||
|
The AOD feature is still in development. Suggest features and report bugs at https://github.com/nsh07/tomato/issues
|
||||||
8
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
8
fastlane/metadata/android/en-US/changelogs/15.txt
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
New features:
|
||||||
|
- New Always On Display option
|
||||||
|
- Redesigned and simplified Settings screen
|
||||||
|
- New theme and color scheme selector
|
||||||
|
- You can now dismiss alarms without unlocking your phone
|
||||||
|
|
||||||
|
Enhancements:
|
||||||
|
- Alarms now automatically stop ringing after 1 minute
|
||||||
12
fastlane/metadata/android/fr-FR/full_description.txt
Normal file
12
fastlane/metadata/android/fr-FR/full_description.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<i>Tomato</i> est un minuteur Pomodoro minimaliste pour Android, conçu selon les principes de Material 3 Expressive.
|
||||||
|
|
||||||
|
Tomato est entièrement gratuit et open-source, pour toujours. Le code source est disponible sur GitHub : https://github.com/nsh07/Tomato, où vous pouvez aussi signaler des bugs ou proposer de nouvelles fonctionnalités.
|
||||||
|
|
||||||
|
<b>Fonctionnalités:</b>
|
||||||
|
- Interface simple et minimaliste, conforme aux dernières recommandations Material 3 Expressive
|
||||||
|
- Statistiques détaillées du temps de travail/étude, présentées de manière claire et intuitive
|
||||||
|
- Statistiques du jour accessibles immédiatement
|
||||||
|
- Graphiques simple et lisibles de votre semaine et votre mois
|
||||||
|
- Analyse de vos heures les plus productives dans la semaine et le mois
|
||||||
|
- Paramètres du minuteur entièrement personnalisables
|
||||||
|
- Compatibilité avec les Live Updates d’Android 16
|
||||||
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
1
fastlane/metadata/android/fr-FR/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Minuteur Pomodoro minimaliste
|
||||||
12
fastlane/metadata/android/tr-TR/full_description.txt
Normal file
12
fastlane/metadata/android/tr-TR/full_description.txt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
<i>Tomato</i>, Material 3 Expressive tabanlı, Android için minimalist bir Pomodoro sayacıdır.
|
||||||
|
|
||||||
|
Tomato tamamen ücretsizdir ve sonsuza kadar açık kaynaklı kalacaktır. Kaynak koduna ulaşmak, hata bildirmek veya özellik önermek için: https://github.com/nsh07/Tomato
|
||||||
|
|
||||||
|
<b>Özellikler:</b>
|
||||||
|
- En son Material 3 Expressive yönergelerine dayalı basit, minimalist kullanıcı arayüzü
|
||||||
|
- Çalışma/ders çalışma sürelerinizin kolay anlaşılır şekilde sunulan ayrıntılı istatistikleri
|
||||||
|
- Güncel güne ait istatistikler bir bakışta görülebilir
|
||||||
|
- Son hafta ve son aya ait istatistikler, okunması kolay ve temiz bir grafikte gösterilir
|
||||||
|
- Son hafta ve son aya ait, günün hangi saatinde en üretken olduğunuzu gösteren ek istatistikler
|
||||||
|
- Özelleştirilebilir zamanlayıcı parametreleri
|
||||||
|
- Android 16 Canlı Güncellemeler (Live Updates) desteği
|
||||||
1
fastlane/metadata/android/tr-TR/short_description.txt
Normal file
1
fastlane/metadata/android/tr-TR/short_description.txt
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Minimalist Pomodoro sayacı
|
||||||
@@ -1 +1,12 @@
|
|||||||
<p><i>Tomato</i> - мінімалістичний Pomodoro таймер для Android на базі Material 3 Expressive.</p><p><br><b>Особливості:</b></p><ul><li>Простий, мінімалістичний інтерфейс на основі останніх рекомендацій Material 3 Expressive</li><li>Детальна статистика робочого/навчального часу в зрозумілій формі<ul><li>Статистика за поточний день, доступна з одного погляду</li><li>Статистика за останній тиждень і останній місяць, представлена у вигляді зручного для сприйняття чіткого графіку</li><li>Додаткова статистика за останній тиждень і місяць, що показує, в який час дня ви були найбільш продуктивні</li></ul></li><li>Настроювані параметри таймера</li></ul>
|
<i>Tomato</i> - мінімалістичний Pomodoro таймер для Android на базі Material 3 Expressive.
|
||||||
|
|
||||||
|
Tomato повністю безкоштовний та з відкритим вихідним кодом. Ви можете знайти вихідний код і повідомляти про помилки й пропонувати функції на GitHub: https://github.com/nsh07/Tomato.
|
||||||
|
|
||||||
|
<b>Особливості:</b>
|
||||||
|
- Простий, мінімалістичний інтерфейс на основі останніх рекомендацій Material 3 Expressive
|
||||||
|
- Детальна статистика робочого/навчального часу в зрозумілій формі
|
||||||
|
- Статистика за поточний день, доступна з одного погляду
|
||||||
|
- Статистика за останній тиждень і останній місяць, представлена у вигляді зручного для сприйняття чіткого графіку
|
||||||
|
- Додаткова статистика за останній тиждень і місяць, що показує, в який час дня ви були найбільш продуктивні
|
||||||
|
- Настроювані параметри таймера
|
||||||
|
- Підтримка Live Updates (для пристроїв на Android 16)
|
||||||
|
|||||||
@@ -1 +1,12 @@
|
|||||||
<p><i>Tomato</i> 是一个基于Material 3 Expressive的安卓极简主义番茄钟.</p><p><br><b>功能:</b></p><ul><li>基于最新Material 3 Expressive指南的简洁用户界面</li><li>以便于理解的方式提供工作/学习的详细统计数据<ul><li>当日统计数据一目了然</li><li>清楚易读的上周和上月统计图表</li><li>上周和上月的额外统计数据帮您找到一天中最高效的时间段</li></ul></li><li>可自定义的计时器参数</li></ul>
|
<i>Tomato</i> 是一个基于Material 3 Expressive的安卓极简主义番茄钟.
|
||||||
|
|
||||||
|
Tomato 将永远保持完全免费和开源。如果你想获取源代码、报告程序错误(bug)或建议新功能,请访问 https://github.com/nsh07/Tomato。
|
||||||
|
|
||||||
|
<b>功能:</b>
|
||||||
|
- 基于最新Material 3 Expressive指南的简洁用户界面
|
||||||
|
- 以便于理解的方式提供工作/学习的详细统计数据
|
||||||
|
- 当日统计数据一目了然
|
||||||
|
- 清楚易读的上周和上月统计图表
|
||||||
|
- 上周和上月的额外统计数据帮您找到一天中最高效的时间段
|
||||||
|
- 可自定义的计时器参数
|
||||||
|
- 支持 Android 16 即時更新 (Android 16 Live Updates)
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
[versions]
|
[versions]
|
||||||
activityCompose = "1.11.0"
|
activityCompose = "1.11.0"
|
||||||
adaptive = "1.1.0"
|
adaptive = "1.2.0"
|
||||||
agp = "8.11.2"
|
agp = "8.11.2"
|
||||||
composeBom = "2025.10.00"
|
composeBom = "2025.10.01"
|
||||||
coreKtx = "1.17.0"
|
coreKtx = "1.17.0"
|
||||||
espressoCore = "3.7.0"
|
espressoCore = "3.7.0"
|
||||||
junit = "4.13.2"
|
junit = "4.13.2"
|
||||||
junitVersion = "1.3.0"
|
junitVersion = "1.3.0"
|
||||||
kotlin = "2.2.20"
|
kotlin = "2.2.21"
|
||||||
ksp = "2.2.20-2.0.4"
|
ksp = "2.2.20-2.0.4"
|
||||||
lifecycleRuntimeKtx = "2.9.4"
|
lifecycleRuntimeKtx = "2.9.4"
|
||||||
materialKolor = "3.0.1"
|
materialKolor = "3.0.1"
|
||||||
navigation3 = "1.0.0-alpha11"
|
navigation3 = "1.0.0-beta01"
|
||||||
room = "2.8.2"
|
room = "2.8.3"
|
||||||
vico = "2.2.1"
|
vico = "2.2.1"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
|
|||||||
Reference in New Issue
Block a user