Merge branch 'dev'
This commit is contained in:
@@ -1,8 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
|
||||
@@ -31,10 +41,10 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
applicationId = "org.nsh07.pomodoro"
|
||||
minSdk = 26
|
||||
minSdk = 27
|
||||
targetSdk = 36
|
||||
versionCode = 13
|
||||
versionName = "1.5.0"
|
||||
versionCode = 15
|
||||
versionName = "1.6.0"
|
||||
|
||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,20 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<?xml version="1.0" encoding="utf-8"?><!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
~
|
||||
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
~ Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools">
|
||||
|
||||
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro
|
||||
|
||||
import android.os.Bundle
|
||||
@@ -11,8 +28,6 @@ import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.nsh07.pomodoro.ui.AppScreen
|
||||
import org.nsh07.pomodoro.ui.NavItem
|
||||
import org.nsh07.pomodoro.ui.Screen
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||
@@ -30,6 +45,12 @@ class MainActivity : ComponentActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
|
||||
appContainer.activityTurnScreenOn = {
|
||||
setShowWhenLocked(it)
|
||||
setTurnScreenOn(it)
|
||||
}
|
||||
|
||||
setContent {
|
||||
val preferencesState by settingsViewModel.preferencesState.collectAsStateWithLifecycle()
|
||||
|
||||
@@ -51,11 +72,18 @@ class MainActivity : ComponentActivity() {
|
||||
appContainer.appTimerRepository.colorScheme = colorScheme
|
||||
}
|
||||
|
||||
AppScreen(timerViewModel = timerViewModel)
|
||||
AppScreen(
|
||||
timerViewModel = timerViewModel,
|
||||
isAODEnabled = preferencesState.aodEnabled,
|
||||
setTimerFrequency = {
|
||||
appContainer.appTimerRepository.timerFrequency = it
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// Reduce the timer loop frequency when not visible to save battery power
|
||||
@@ -67,27 +95,4 @@ class MainActivity : ComponentActivity() {
|
||||
// Increase the timer loop frequency again when visible to make the progress smoother
|
||||
appContainer.appTimerRepository.timerFrequency = 10f
|
||||
}
|
||||
|
||||
companion object {
|
||||
val screens = listOf(
|
||||
NavItem(
|
||||
Screen.Timer,
|
||||
R.drawable.timer_outlined,
|
||||
R.drawable.timer_filled,
|
||||
R.string.timer
|
||||
),
|
||||
NavItem(
|
||||
Screen.Stats,
|
||||
R.drawable.monitoring,
|
||||
R.drawable.monitoring_filled,
|
||||
R.string.stats
|
||||
),
|
||||
NavItem(
|
||||
Screen.Settings,
|
||||
R.drawable.settings,
|
||||
R.drawable.settings_filled,
|
||||
R.string.settings
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.data
|
||||
@@ -27,6 +37,7 @@ interface AppContainer {
|
||||
val notificationBuilder: NotificationCompat.Builder
|
||||
val timerState: MutableStateFlow<TimerState>
|
||||
val time: MutableStateFlow<Long>
|
||||
var activityTurnScreenOn: (Boolean) -> Unit
|
||||
}
|
||||
|
||||
class DefaultAppContainer(context: Context) : AppContainer {
|
||||
@@ -78,4 +89,6 @@ class DefaultAppContainer(context: Context) : AppContainer {
|
||||
MutableStateFlow(appTimerRepository.focusTime)
|
||||
}
|
||||
|
||||
override var activityTurnScreenOn: (Boolean) -> Unit = {}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,20 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
@@ -16,6 +33,7 @@ import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -54,11 +72,12 @@ class TimerService : Service() {
|
||||
private var pauseDuration = 0L
|
||||
|
||||
private var job = SupervisorJob()
|
||||
private val scope = CoroutineScope(Dispatchers.IO + job)
|
||||
private val timerScope = CoroutineScope(Dispatchers.IO + job)
|
||||
private val skipScope = CoroutineScope(Dispatchers.IO + job)
|
||||
|
||||
private var alarm: MediaPlayer? = null
|
||||
private var autoAlarmStopScope: Job? = null
|
||||
|
||||
private var alarm: MediaPlayer? = null
|
||||
private val vibrator by lazy {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
val vibratorManager = getSystemService(VIBRATOR_MANAGER_SERVICE) as VibratorManager
|
||||
@@ -138,7 +157,7 @@ class TimerService : Service() {
|
||||
|
||||
var iterations = -1
|
||||
|
||||
scope.launch {
|
||||
timerScope.launch {
|
||||
while (true) {
|
||||
if (!timerState.value.timerRunning) break
|
||||
if (startTime == 0L) startTime = SystemClock.elapsedRealtime()
|
||||
@@ -176,7 +195,10 @@ class TimerService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission") // We check for the permission when pressing the Play button in the UI
|
||||
@SuppressLint(
|
||||
"MissingPermission",
|
||||
"StringFormatInvalid"
|
||||
) // We check for the permission when pressing the Play button in the UI
|
||||
fun showTimerNotification(
|
||||
remainingTime: Int, paused: Boolean = false, complete: Boolean = false
|
||||
) {
|
||||
@@ -351,6 +373,13 @@ class TimerService : Service() {
|
||||
fun startAlarm() {
|
||||
if (timerRepository.alarmEnabled) alarm?.start()
|
||||
|
||||
appContainer.activityTurnScreenOn(true)
|
||||
|
||||
autoAlarmStopScope = CoroutineScope(Dispatchers.IO).launch {
|
||||
delay(1 * 60 * 1000)
|
||||
stopAlarm()
|
||||
}
|
||||
|
||||
if (timerRepository.vibrateEnabled) {
|
||||
if (!vibrator.hasVibrator()) {
|
||||
return
|
||||
@@ -363,6 +392,8 @@ class TimerService : Service() {
|
||||
}
|
||||
|
||||
fun stopAlarm() {
|
||||
autoAlarmStopScope?.cancel()
|
||||
|
||||
if (timerRepository.alarmEnabled) {
|
||||
alarm?.pause()
|
||||
alarm?.seekTo(0)
|
||||
@@ -372,6 +403,8 @@ class TimerService : Service() {
|
||||
vibrator.cancel()
|
||||
}
|
||||
|
||||
appContainer.activityTurnScreenOn(false)
|
||||
|
||||
_timerState.update { currentState ->
|
||||
currentState.copy(alarmRinging = false)
|
||||
}
|
||||
|
||||
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
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.ContentTransform
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.Crossfade
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.calculateEndPadding
|
||||
import androidx.compose.foundation.layout.calculateStartPadding
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -42,7 +54,6 @@ import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.runtime.rememberNavBackStack
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import androidx.window.core.layout.WindowSizeClass
|
||||
import org.nsh07.pomodoro.MainActivity.Companion.screens
|
||||
import org.nsh07.pomodoro.service.TimerService
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
|
||||
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
|
||||
@@ -55,7 +66,9 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
|
||||
@Composable
|
||||
fun AppScreen(
|
||||
modifier: Modifier = Modifier,
|
||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory)
|
||||
timerViewModel: TimerViewModel = viewModel(factory = TimerViewModel.Factory),
|
||||
isAODEnabled: Boolean,
|
||||
setTimerFrequency: (Float) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
@@ -78,128 +91,153 @@ fun AppScreen(
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
val wide = remember {
|
||||
windowSizeClass.isWidthAtLeastBreakpoint(
|
||||
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
||||
)
|
||||
}
|
||||
ShortNavigationBar(
|
||||
arrangement =
|
||||
if (wide) ShortNavigationBarArrangement.Centered
|
||||
else ShortNavigationBarArrangement.EqualWeight
|
||||
AnimatedVisibility(
|
||||
backStack.last() !is Screen.AOD,
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut()
|
||||
) {
|
||||
screens.forEach {
|
||||
val selected = backStack.last() == it.route
|
||||
ShortNavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
|
||||
{
|
||||
if (backStack.size < 2) backStack.add(it.route)
|
||||
else backStack[1] = it.route
|
||||
}
|
||||
} else {
|
||||
{ if (backStack.size > 1) backStack.removeAt(1) }
|
||||
},
|
||||
icon = {
|
||||
Crossfade(selected) { selected ->
|
||||
if (selected) Icon(painterResource(it.selectedIcon), null)
|
||||
else Icon(painterResource(it.unselectedIcon), null)
|
||||
}
|
||||
},
|
||||
iconPosition =
|
||||
if (wide) NavigationItemIconPosition.Start
|
||||
else NavigationItemIconPosition.Top,
|
||||
label = { Text(stringResource(it.label)) }
|
||||
val wide = remember {
|
||||
windowSizeClass.isWidthAtLeastBreakpoint(
|
||||
WindowSizeClass.WIDTH_DP_MEDIUM_LOWER_BOUND
|
||||
)
|
||||
}
|
||||
ShortNavigationBar(
|
||||
arrangement =
|
||||
if (wide) ShortNavigationBarArrangement.Centered
|
||||
else ShortNavigationBarArrangement.EqualWeight
|
||||
) {
|
||||
mainScreens.forEach {
|
||||
val selected = backStack.last() == it.route
|
||||
ShortNavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = if (it.route != Screen.Timer) { // Ensure the backstack does not accumulate screens
|
||||
{
|
||||
if (backStack.size < 2) backStack.add(it.route)
|
||||
else backStack[1] = it.route
|
||||
}
|
||||
} else {
|
||||
{ if (backStack.size > 1) backStack.removeAt(1) }
|
||||
},
|
||||
icon = {
|
||||
Crossfade(selected) { selected ->
|
||||
if (selected) Icon(painterResource(it.selectedIcon), null)
|
||||
else Icon(painterResource(it.unselectedIcon), null)
|
||||
}
|
||||
},
|
||||
iconPosition =
|
||||
if (wide) NavigationItemIconPosition.Start
|
||||
else NavigationItemIconPosition.Top,
|
||||
label = { Text(stringResource(it.label)) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
) { contentPadding ->
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = { backStack.removeLastOrNull() },
|
||||
transitionSpec = {
|
||||
ContentTransform(
|
||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||
fadeOut(motionScheme.defaultEffectsSpec())
|
||||
)
|
||||
},
|
||||
popTransitionSpec = {
|
||||
ContentTransform(
|
||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||
fadeOut(motionScheme.defaultEffectsSpec())
|
||||
)
|
||||
},
|
||||
predictivePopTransitionSpec = {
|
||||
ContentTransform(
|
||||
fadeIn(motionScheme.defaultEffectsSpec()),
|
||||
fadeOut(motionScheme.defaultEffectsSpec()) +
|
||||
scaleOut(targetScale = 0.7f),
|
||||
)
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Timer> {
|
||||
TimerScreen(
|
||||
timerState = uiState,
|
||||
progress = { progress },
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
TimerAction.ResetTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.RESET.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
SharedTransitionLayout {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = backStack::removeLastOrNull,
|
||||
transitionSpec = {
|
||||
fadeIn(motionScheme.defaultEffectsSpec())
|
||||
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
|
||||
},
|
||||
popTransitionSpec = {
|
||||
fadeIn(motionScheme.defaultEffectsSpec())
|
||||
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
|
||||
},
|
||||
predictivePopTransitionSpec = {
|
||||
fadeIn(motionScheme.defaultEffectsSpec())
|
||||
.togetherWith(fadeOut(motionScheme.defaultEffectsSpec()))
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Timer> {
|
||||
TimerScreen(
|
||||
timerState = uiState,
|
||||
progress = { progress },
|
||||
onAction = { action ->
|
||||
when (action) {
|
||||
TimerAction.ResetTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.RESET.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
|
||||
is TimerAction.SkipTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.SKIP.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
is TimerAction.SkipTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.SKIP.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
|
||||
TimerAction.StopAlarm ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.STOP_ALARM.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
TimerAction.StopAlarm ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action =
|
||||
TimerService.Actions.STOP_ALARM.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
|
||||
TimerAction.ToggleTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.TOGGLE.toString()
|
||||
context.startService(it)
|
||||
TimerAction.ToggleTimer ->
|
||||
Intent(context, TimerService::class.java).also {
|
||||
it.action = TimerService.Actions.TOGGLE.toString()
|
||||
context.startService(it)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier
|
||||
.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
)
|
||||
.then(
|
||||
if (isAODEnabled) Modifier.clickable {
|
||||
if (backStack.size < 2) backStack.add(Screen.AOD)
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = modifier.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
else Modifier
|
||||
),
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
entry<Screen.Settings> {
|
||||
SettingsScreenRoot(
|
||||
modifier = modifier.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
entry<Screen.AOD> {
|
||||
AlwaysOnDisplay(
|
||||
timerState = uiState,
|
||||
progress = { progress },
|
||||
setTimerFrequency = setTimerFrequency,
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (isAODEnabled) Modifier.clickable {
|
||||
if (backStack.size > 1) backStack.removeLastOrNull()
|
||||
}
|
||||
else Modifier
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
entry<Screen.Stats> {
|
||||
StatsScreenRoot(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
entry<Screen.Settings.Main> {
|
||||
SettingsScreenRoot(
|
||||
modifier = modifier.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
entry<Screen.Stats> {
|
||||
StatsScreenRoot(
|
||||
contentPadding = contentPadding,
|
||||
modifier = modifier.padding(
|
||||
start = contentPadding.calculateStartPadding(layoutDirection),
|
||||
end = contentPadding.calculateEndPadding(layoutDirection),
|
||||
bottom = contentPadding.calculateBottomPadding()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
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
|
||||
|
||||
import androidx.annotation.DrawableRes
|
||||
@@ -10,7 +27,22 @@ sealed class Screen : NavKey {
|
||||
object Timer : Screen()
|
||||
|
||||
@Serializable
|
||||
object Settings : Screen()
|
||||
object AOD : Screen()
|
||||
|
||||
@Serializable
|
||||
sealed class Settings : Screen() {
|
||||
@Serializable
|
||||
object Main : Settings()
|
||||
|
||||
@Serializable
|
||||
object Alarm : Settings()
|
||||
|
||||
@Serializable
|
||||
object Appearance : Settings()
|
||||
|
||||
@Serializable
|
||||
object Timer : Settings()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Stats : Screen()
|
||||
@@ -21,4 +53,11 @@ data class NavItem(
|
||||
@param:DrawableRes val unselectedIcon: Int,
|
||||
@param:DrawableRes val selectedIcon: Int,
|
||||
@param:StringRes val label: Int
|
||||
)
|
||||
)
|
||||
|
||||
data class SettingsNavItem(
|
||||
val route: Screen.Settings,
|
||||
@param:DrawableRes val icon: Int,
|
||||
@param:StringRes val label: Int,
|
||||
val innerSettings: List<Int>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.media.RingtoneManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.slideInHorizontally
|
||||
import androidx.compose.animation.slideOutHorizontally
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.FilledTonalIconToggleButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButtonDefaults
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.MaterialTheme.typography
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderState
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TopAppBar
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.material3.rememberSliderState
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.DisposableEffect
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import androidx.navigation3.runtime.entryProvider
|
||||
import androidx.navigation3.ui.NavDisplay
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.service.TimerService
|
||||
import org.nsh07.pomodoro.ui.Screen
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.components.ClickableListItem
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
|
||||
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
|
||||
import org.nsh07.pomodoro.ui.settingsScreens
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.cardShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
|
||||
import org.nsh07.pomodoro.ui.theme.TomatoTheme
|
||||
import org.nsh07.pomodoro.utils.toColor
|
||||
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@@ -101,6 +85,8 @@ fun SettingsScreenRoot(
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val backStack = viewModel.backStack
|
||||
|
||||
DisposableEffect(Unit) {
|
||||
viewModel.runTextFieldFlowCollection()
|
||||
onDispose { viewModel.cancelTextFieldFlowCollection() }
|
||||
@@ -133,6 +119,7 @@ fun SettingsScreenRoot(
|
||||
|
||||
SettingsScreen(
|
||||
preferencesState = preferencesState,
|
||||
backStack = backStack,
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||
@@ -143,6 +130,7 @@ fun SettingsScreenRoot(
|
||||
onAlarmEnabledChange = viewModel::saveAlarmEnabled,
|
||||
onVibrateEnabledChange = viewModel::saveVibrateEnabled,
|
||||
onBlackThemeChange = viewModel::saveBlackTheme,
|
||||
onAodEnabledChange = viewModel::saveAodEnabled,
|
||||
onAlarmSoundChanged = {
|
||||
viewModel.saveAlarmSound(it)
|
||||
Intent(context, TimerService::class.java).apply {
|
||||
@@ -160,6 +148,7 @@ fun SettingsScreenRoot(
|
||||
@Composable
|
||||
private fun SettingsScreen(
|
||||
preferencesState: PreferencesState,
|
||||
backStack: SnapshotStateList<Screen.Settings>,
|
||||
focusTimeInputFieldState: TextFieldState,
|
||||
shortBreakTimeInputFieldState: TextFieldState,
|
||||
longBreakTimeInputFieldState: TextFieldState,
|
||||
@@ -170,394 +159,125 @@ private fun SettingsScreen(
|
||||
onAlarmEnabledChange: (Boolean) -> Unit,
|
||||
onVibrateEnabledChange: (Boolean) -> Unit,
|
||||
onBlackThemeChange: (Boolean) -> Unit,
|
||||
onAodEnabledChange: (Boolean) -> Unit,
|
||||
onAlarmSoundChanged: (Uri?) -> Unit,
|
||||
onThemeChange: (String) -> Unit,
|
||||
onColorSchemeChange: (Color) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior()
|
||||
val switchColors = SwitchDefaults.colors(
|
||||
checkedIconColor = colorScheme.primary,
|
||||
)
|
||||
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
|
||||
|
||||
val themeMap: Map<String, Pair<Int, String>> = remember {
|
||||
mapOf(
|
||||
"auto" to Pair(
|
||||
R.drawable.brightness_auto,
|
||||
context.getString(R.string.system_default)
|
||||
),
|
||||
"light" to Pair(R.drawable.light_mode, context.getString(R.string.light)),
|
||||
"dark" to Pair(R.drawable.dark_mode, context.getString(R.string.dark))
|
||||
)
|
||||
}
|
||||
val reverseThemeMap: Map<String, String> = remember {
|
||||
mapOf(
|
||||
context.getString(R.string.system_default) to "auto",
|
||||
context.getString(R.string.light) to "light",
|
||||
context.getString(R.string.dark) to "dark"
|
||||
)
|
||||
}
|
||||
|
||||
var alarmName by remember { mutableStateOf("...") }
|
||||
|
||||
LaunchedEffect(alarmSound) {
|
||||
withContext(Dispatchers.IO) {
|
||||
alarmName =
|
||||
RingtoneManager.getRingtone(context, alarmSound.toUri())?.getTitle(context) ?: ""
|
||||
}
|
||||
}
|
||||
|
||||
val ringtonePickerLauncher = rememberLauncherForActivityResult(
|
||||
contract = ActivityResultContracts.StartActivityForResult()
|
||||
) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val uri =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
result.data?.getParcelableExtra(
|
||||
RingtoneManager.EXTRA_RINGTONE_PICKED_URI,
|
||||
Uri::class.java
|
||||
)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
result.data?.getParcelableExtra(RingtoneManager.EXTRA_RINGTONE_PICKED_URI)
|
||||
}
|
||||
onAlarmSoundChanged(uri)
|
||||
}
|
||||
}
|
||||
|
||||
val intent = Intent(RingtoneManager.ACTION_RINGTONE_PICKER).apply {
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_TYPE, RingtoneManager.TYPE_ALARM)
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_TITLE, stringResource(R.string.alarm_sound))
|
||||
putExtra(RingtoneManager.EXTRA_RINGTONE_EXISTING_URI, alarmSound.toUri())
|
||||
}
|
||||
|
||||
val switchItems = remember(preferencesState.blackTheme, alarmEnabled, vibrateEnabled) {
|
||||
listOf(
|
||||
SettingsSwitchItem(
|
||||
checked = preferencesState.blackTheme,
|
||||
icon = R.drawable.contrast,
|
||||
label = context.getString(R.string.black_theme),
|
||||
description = context.getString(R.string.black_theme_desc),
|
||||
onClick = onBlackThemeChange
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
checked = alarmEnabled,
|
||||
icon = R.drawable.alarm_on,
|
||||
label = context.getString(R.string.alarm),
|
||||
description = context.getString(R.string.alarm_desc),
|
||||
onClick = onAlarmEnabledChange
|
||||
),
|
||||
SettingsSwitchItem(
|
||||
checked = vibrateEnabled,
|
||||
icon = R.drawable.mobile_vibrate,
|
||||
label = context.getString(R.string.vibrate),
|
||||
description = context.getString(R.string.vibrate_desc),
|
||||
onClick = onVibrateEnabledChange
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(R.string.settings),
|
||||
style = LocalTextStyle.current.copy(
|
||||
fontFamily = robotoFlexTopBar,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 32.sp
|
||||
)
|
||||
)
|
||||
},
|
||||
subtitle = {},
|
||||
colors = topBarColors,
|
||||
titleHorizontalAlignment = Alignment.CenterHorizontally,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier
|
||||
.background(topBarColors.containerColor)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
item {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
item {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.horizontalScroll(rememberScrollState())
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.focus),
|
||||
style = typography.titleSmallEmphasized
|
||||
)
|
||||
MinuteInputField(
|
||||
state = focusTimeInputFieldState,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = topListItemShape.topStart,
|
||||
bottomStart = topListItemShape.topStart,
|
||||
topEnd = topListItemShape.bottomStart,
|
||||
bottomEnd = topListItemShape.bottomStart
|
||||
),
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(2.dp))
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.short_break),
|
||||
style = typography.titleSmallEmphasized
|
||||
)
|
||||
MinuteInputField(
|
||||
state = shortBreakTimeInputFieldState,
|
||||
shape = RoundedCornerShape(middleListItemShape.topStart),
|
||||
imeAction = ImeAction.Next
|
||||
)
|
||||
}
|
||||
Spacer(Modifier.width(2.dp))
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
Text(
|
||||
stringResource(R.string.long_break),
|
||||
style = typography.titleSmallEmphasized
|
||||
)
|
||||
MinuteInputField(
|
||||
state = longBreakTimeInputFieldState,
|
||||
shape = RoundedCornerShape(
|
||||
topStart = bottomListItemShape.topStart,
|
||||
bottomStart = bottomListItemShape.topStart,
|
||||
topEnd = bottomListItemShape.bottomStart,
|
||||
bottomEnd = bottomListItemShape.bottomStart
|
||||
),
|
||||
imeAction = ImeAction.Done
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
item {
|
||||
Spacer(Modifier.height(12.dp))
|
||||
}
|
||||
item {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(
|
||||
painterResource(R.drawable.clocks),
|
||||
null
|
||||
)
|
||||
},
|
||||
headlineContent = {
|
||||
Text(stringResource(R.string.session_length))
|
||||
},
|
||||
supportingContent = {
|
||||
Column {
|
||||
NavDisplay(
|
||||
backStack = backStack,
|
||||
onBack = backStack::removeLastOrNull,
|
||||
transitionSpec = {
|
||||
(slideInHorizontally(initialOffsetX = { it }))
|
||||
.togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
|
||||
},
|
||||
popTransitionSpec = {
|
||||
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||
},
|
||||
predictivePopTransitionSpec = {
|
||||
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
|
||||
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
|
||||
},
|
||||
entryProvider = entryProvider {
|
||||
entry<Screen.Settings.Main> {
|
||||
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
stringResource(
|
||||
R.string.session_length_desc,
|
||||
sessionsSliderState.value.toInt()
|
||||
stringResource(R.string.settings),
|
||||
style = LocalTextStyle.current.copy(
|
||||
fontFamily = robotoFlexTopBar,
|
||||
fontSize = 32.sp,
|
||||
lineHeight = 32.sp
|
||||
)
|
||||
)
|
||||
Slider(
|
||||
state = sessionsSliderState,
|
||||
modifier = Modifier.padding(vertical = 4.dp)
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(cardShape)
|
||||
)
|
||||
}
|
||||
},
|
||||
subtitle = {},
|
||||
colors = topBarColors,
|
||||
titleHorizontalAlignment = Alignment.CenterHorizontally,
|
||||
scrollBehavior = scrollBehavior
|
||||
)
|
||||
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
item {
|
||||
ColorSchemePickerListItem(
|
||||
color = preferencesState.colorScheme.toColor(),
|
||||
items = 3,
|
||||
index = 0,
|
||||
onColorChange = onColorSchemeChange
|
||||
)
|
||||
}
|
||||
item {
|
||||
ThemePickerListItem(
|
||||
theme = preferencesState.theme,
|
||||
themeMap = themeMap,
|
||||
reverseThemeMap = reverseThemeMap,
|
||||
onThemeChange = onThemeChange,
|
||||
items = 3,
|
||||
index = 1,
|
||||
modifier = Modifier
|
||||
.clip(middleListItemShape)
|
||||
)
|
||||
}
|
||||
item {
|
||||
val item = switchItems[0]
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(item.icon), contentDescription = null)
|
||||
},
|
||||
headlineContent = { Text(item.label) },
|
||||
supportingContent = { Text(item.description) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = item.checked,
|
||||
onCheckedChange = { item.onClick(it) },
|
||||
thumbContent = {
|
||||
if (item.checked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.check),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.clear),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier.clip(bottomListItemShape)
|
||||
)
|
||||
}
|
||||
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
item {
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(R.drawable.alarm), null)
|
||||
},
|
||||
headlineContent = { Text(stringResource(R.string.alarm_sound)) },
|
||||
supportingContent = { Text(alarmName) },
|
||||
colors = listItemColors,
|
||||
modifier = Modifier
|
||||
.clip(topListItemShape)
|
||||
.clickable(onClick = { ringtonePickerLauncher.launch(intent) })
|
||||
)
|
||||
}
|
||||
itemsIndexed(switchItems.drop(1)) { index, item ->
|
||||
ListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(item.icon), contentDescription = null)
|
||||
},
|
||||
headlineContent = { Text(item.label) },
|
||||
supportingContent = { Text(item.description) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = item.checked,
|
||||
onCheckedChange = { item.onClick(it) },
|
||||
thumbContent = {
|
||||
if (item.checked) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.check),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.clear),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(SwitchDefaults.IconSize),
|
||||
)
|
||||
}
|
||||
},
|
||||
colors = switchColors
|
||||
)
|
||||
},
|
||||
colors = listItemColors,
|
||||
modifier = Modifier
|
||||
.clip(
|
||||
when (index) {
|
||||
switchItems.lastIndex - 1 -> bottomListItemShape
|
||||
else -> middleListItemShape
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
item {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
horizontalAlignment = Alignment.End,
|
||||
modifier = Modifier
|
||||
.padding(vertical = 6.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
FilledTonalIconToggleButton(
|
||||
checked = expanded,
|
||||
onCheckedChange = { expanded = it },
|
||||
shapes = IconButtonDefaults.toggleableShapes(),
|
||||
modifier = Modifier.width(52.dp)
|
||||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp),
|
||||
modifier = Modifier
|
||||
.background(topBarColors.containerColor)
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
Icon(
|
||||
painterResource(R.drawable.info),
|
||||
null
|
||||
)
|
||||
}
|
||||
AnimatedVisibility(expanded) {
|
||||
Text(
|
||||
stringResource(R.string.pomodoro_info),
|
||||
style = typography.bodyMedium,
|
||||
color = colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(8.dp)
|
||||
)
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
item { AboutCard() }
|
||||
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
|
||||
itemsIndexed(settingsScreens) { index, item ->
|
||||
ClickableListItem(
|
||||
leadingContent = {
|
||||
Icon(painterResource(item.icon), null)
|
||||
},
|
||||
headlineContent = { Text(stringResource(item.label)) },
|
||||
supportingContent = {
|
||||
Text(
|
||||
remember {
|
||||
item.innerSettings.joinToString(", ") {
|
||||
context.getString(it)
|
||||
}
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
},
|
||||
trailingContent = {
|
||||
Icon(painterResource(R.drawable.arrow_forward_big), null)
|
||||
},
|
||||
items = settingsScreens.size,
|
||||
index = index
|
||||
) { backStack.add(item.route) }
|
||||
}
|
||||
|
||||
item { Spacer(Modifier.height(12.dp)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entry<Screen.Settings.Alarm> {
|
||||
AlarmSettings(
|
||||
preferencesState = preferencesState,
|
||||
alarmEnabled = alarmEnabled,
|
||||
vibrateEnabled = vibrateEnabled,
|
||||
alarmSound = alarmSound,
|
||||
onAlarmEnabledChange = onAlarmEnabledChange,
|
||||
onVibrateEnabledChange = onVibrateEnabledChange,
|
||||
onAlarmSoundChanged = onAlarmSoundChanged,
|
||||
onBack = backStack::removeLastOrNull
|
||||
)
|
||||
}
|
||||
entry<Screen.Settings.Appearance> {
|
||||
AppearanceSettings(
|
||||
preferencesState = preferencesState,
|
||||
onBlackThemeChange = onBlackThemeChange,
|
||||
onThemeChange = onThemeChange,
|
||||
onColorSchemeChange = onColorSchemeChange,
|
||||
onBack = backStack::removeLastOrNull
|
||||
)
|
||||
}
|
||||
entry<Screen.Settings.Timer> {
|
||||
TimerSettings(
|
||||
aodEnabled = preferencesState.aodEnabled,
|
||||
focusTimeInputFieldState = focusTimeInputFieldState,
|
||||
shortBreakTimeInputFieldState = shortBreakTimeInputFieldState,
|
||||
longBreakTimeInputFieldState = longBreakTimeInputFieldState,
|
||||
sessionsSliderState = sessionsSliderState,
|
||||
onAodEnabledChange = onAodEnabledChange,
|
||||
onBack = backStack::removeLastOrNull
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Preview(
|
||||
showSystemUi = true,
|
||||
device = Devices.PIXEL_9_PRO
|
||||
)
|
||||
@Composable
|
||||
fun SettingsScreenPreview() {
|
||||
TomatoTheme {
|
||||
SettingsScreen(
|
||||
preferencesState = PreferencesState(),
|
||||
focusTimeInputFieldState = rememberTextFieldState((25).toString()),
|
||||
shortBreakTimeInputFieldState = rememberTextFieldState((5).toString()),
|
||||
longBreakTimeInputFieldState = rememberTextFieldState((15).toString()),
|
||||
sessionsSliderState = rememberSliderState(value = 3f, steps = 3, valueRange = 1f..5f),
|
||||
alarmEnabled = true,
|
||||
vibrateEnabled = true,
|
||||
alarmSound = "null",
|
||||
onAlarmEnabledChange = {},
|
||||
onVibrateEnabledChange = {},
|
||||
onBlackThemeChange = {},
|
||||
onAlarmSoundChanged = {},
|
||||
onThemeChange = {},
|
||||
onColorSchemeChange = {},
|
||||
modifier = Modifier.fillMaxSize()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SettingsSwitchItem(
|
||||
val checked: Boolean,
|
||||
@param:DrawableRes val icon: Int,
|
||||
val label: String,
|
||||
val description: String,
|
||||
val onClick: (Boolean) -> Unit
|
||||
)
|
||||
|
||||
@@ -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.foundation.clickable
|
||||
@@ -17,6 +34,7 @@ import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
|
||||
|
||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
@@ -27,7 +45,7 @@ fun ClickableListItem(
|
||||
supportingContent: @Composable (() -> Unit)? = null,
|
||||
leadingContent: @Composable (() -> Unit)? = null,
|
||||
trailingContent: @Composable (() -> Unit)? = null,
|
||||
colors: ListItemColors = ListItemDefaults.colors(),
|
||||
colors: ListItemColors = listItemColors,
|
||||
tonalElevation: Dp = ListItemDefaults.Elevation,
|
||||
shadowElevation: Dp = ListItemDefaults.Elevation,
|
||||
items: Int,
|
||||
@@ -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.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(
|
||||
val theme: String = "auto",
|
||||
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
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.settingsScreen.viewModel
|
||||
@@ -11,6 +21,7 @@ import android.net.Uri
|
||||
import androidx.compose.foundation.text.input.TextFieldState
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.SliderState
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.snapshotFlow
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.ViewModel
|
||||
@@ -31,12 +42,15 @@ import kotlinx.coroutines.launch
|
||||
import org.nsh07.pomodoro.TomatoApplication
|
||||
import org.nsh07.pomodoro.data.AppPreferenceRepository
|
||||
import org.nsh07.pomodoro.data.TimerRepository
|
||||
import org.nsh07.pomodoro.ui.Screen
|
||||
|
||||
@OptIn(FlowPreview::class, ExperimentalMaterial3Api::class)
|
||||
class SettingsViewModel(
|
||||
private val preferenceRepository: AppPreferenceRepository,
|
||||
private val timerRepository: TimerRepository,
|
||||
) : ViewModel() {
|
||||
val backStack = mutableStateListOf<Screen.Settings>(Screen.Settings.Main)
|
||||
|
||||
private val _preferencesState = MutableStateFlow(PreferencesState())
|
||||
val preferencesState = _preferencesState.asStateFlow()
|
||||
|
||||
@@ -80,12 +94,15 @@ class SettingsViewModel(
|
||||
?: preferenceRepository.saveStringPreference("color_scheme", Color.White.toString())
|
||||
val blackTheme = preferenceRepository.getBooleanPreference("black_theme")
|
||||
?: preferenceRepository.saveBooleanPreference("black_theme", false)
|
||||
val aodEnabled = preferenceRepository.getBooleanPreference("aod_enabled")
|
||||
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||
|
||||
_preferencesState.update { currentState ->
|
||||
currentState.copy(
|
||||
theme = theme,
|
||||
colorScheme = colorScheme,
|
||||
blackTheme = blackTheme
|
||||
blackTheme = blackTheme,
|
||||
aodEnabled = aodEnabled
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -196,6 +213,15 @@ class SettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
fun saveAodEnabled(aodEnabled: Boolean) {
|
||||
viewModelScope.launch {
|
||||
_preferencesState.update { currentState ->
|
||||
currentState.copy(aodEnabled = aodEnabled)
|
||||
}
|
||||
preferenceRepository.saveBooleanPreference("aod_enabled", aodEnabled)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val Factory: ViewModelProvider.Factory = viewModelFactory {
|
||||
initializer {
|
||||
|
||||
@@ -1,9 +1,28 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.theme
|
||||
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.ListItemColors
|
||||
import androidx.compose.material3.ListItemDefaults
|
||||
import androidx.compose.material3.MaterialTheme.colorScheme
|
||||
import androidx.compose.material3.SwitchColors
|
||||
import androidx.compose.material3.SwitchDefaults
|
||||
import androidx.compose.material3.TopAppBarColors
|
||||
import androidx.compose.material3.TopAppBarDefaults
|
||||
import androidx.compose.runtime.Composable
|
||||
@@ -41,4 +60,9 @@ object CustomColors {
|
||||
supportingColor = colorScheme.onSecondaryFixedVariant,
|
||||
trailingIconColor = colorScheme.onSecondaryFixedVariant
|
||||
)
|
||||
|
||||
val switchColors: SwitchColors
|
||||
@Composable get() = SwitchDefaults.colors(
|
||||
checkedIconColor = colorScheme.primary,
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,18 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Nishant Mishra
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
* This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
*
|
||||
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
* General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
* Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License along with Tomato.
|
||||
* If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.nsh07.pomodoro.ui.timerScreen
|
||||
@@ -75,4 +85,4 @@ fun AlarmDialog(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.SharedTransitionLayout
|
||||
import androidx.compose.animation.SharedTransitionScope
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -81,6 +83,7 @@ import androidx.compose.ui.tooling.preview.Devices
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.navigation3.ui.LocalNavAnimatedContentScope
|
||||
import org.nsh07.pomodoro.R
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.openRundeClock
|
||||
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
|
||||
@@ -91,7 +94,7 @@ import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerState
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
|
||||
@Composable
|
||||
fun TimerScreen(
|
||||
fun SharedTransitionScope.TimerScreen(
|
||||
timerState: TimerState,
|
||||
progress: () -> Float,
|
||||
onAction: (TimerAction) -> Unit,
|
||||
@@ -209,6 +212,12 @@ fun TimerScreen(
|
||||
CircularProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = Modifier
|
||||
.sharedBounds(
|
||||
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||
"focus progress"
|
||||
),
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||
)
|
||||
.widthIn(max = 350.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.aspectRatio(1f),
|
||||
@@ -221,6 +230,12 @@ fun TimerScreen(
|
||||
CircularWavyProgressIndicator(
|
||||
progress = progress,
|
||||
modifier = Modifier
|
||||
.sharedBounds(
|
||||
sharedContentState = this@TimerScreen.rememberSharedContentState(
|
||||
"break progress"
|
||||
),
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||
)
|
||||
.widthIn(max = 350.dp)
|
||||
.fillMaxWidth(0.9f)
|
||||
.aspectRatio(1f),
|
||||
@@ -261,7 +276,11 @@ fun TimerScreen(
|
||||
letterSpacing = (-2).sp
|
||||
),
|
||||
textAlign = TextAlign.Center,
|
||||
maxLines = 1
|
||||
maxLines = 1,
|
||||
modifier = Modifier.sharedBounds(
|
||||
sharedContentState = this@TimerScreen.rememberSharedContentState("clock"),
|
||||
animatedVisibilityScope = LocalNavAnimatedContentScope.current
|
||||
)
|
||||
)
|
||||
AnimatedVisibility(
|
||||
expanded,
|
||||
@@ -519,11 +538,13 @@ fun TimerScreenPreview() {
|
||||
)
|
||||
TomatoTheme {
|
||||
Surface {
|
||||
TimerScreen(
|
||||
timerState,
|
||||
{ 0.3f },
|
||||
{}
|
||||
)
|
||||
SharedTransitionLayout {
|
||||
TimerScreen(
|
||||
timerState,
|
||||
{ 0.3f },
|
||||
{}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,9 @@ class TimerViewModel(
|
||||
)
|
||||
).toUri()
|
||||
|
||||
preferenceRepository.getBooleanPreference("aod_enabled")
|
||||
?: preferenceRepository.saveBooleanPreference("aod_enabled", false)
|
||||
|
||||
_time.update { timerRepository.focusTime }
|
||||
cycles = 0
|
||||
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="more_info">زانیاری زیاتر</string>
|
||||
<string name="pause">وەستاندن</string>
|
||||
<string name="paused">پشوو</string>
|
||||
<string name="completed">تەواوکراو</string>
|
||||
<string name="up_next_notification">دوای ئەمە</string>
|
||||
</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="alarm_sound">Sonido de la alarma</string>
|
||||
<string name="black_theme">Tema negro</string>
|
||||
<string name="black_theme_desc">Utiliza un tema oscuro negro puro</string>
|
||||
<string name="black_theme_desc">Utilizar un tema oscuro negro puro</string>
|
||||
<string name="alarm_desc">Sonar alarma cuando el temporizador finalice</string>
|
||||
<string name="vibrate">Vibrar</string>
|
||||
<string name="vibrate_desc">Vibrar cuando el temporizador finalice</string>
|
||||
@@ -30,7 +30,7 @@
|
||||
<string name="break_">Descanso</string>
|
||||
<string name="last_week">Semana pasada</string>
|
||||
<string name="focus_per_day_avg">concentración por día (avg)</string>
|
||||
<string name="more_info">Más informes</string>
|
||||
<string name="more_info">Más información</string>
|
||||
<string name="weekly_productivity_analysis">Análisis de productividad semanal</string>
|
||||
<string name="last_month">Mes pasado</string>
|
||||
<string name="monthly_productivity_analysis">Análisis de productividad mensual</string>
|
||||
@@ -56,4 +56,5 @@
|
||||
<string name="color">Color</string>
|
||||
<string name="light">Luz</string>
|
||||
<string name="dark">Oscuro</string>
|
||||
<string name="last_year">Año pasado</string>
|
||||
</resources>
|
||||
|
||||
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="timer">计时</string>
|
||||
<string name="timer_progress">计时器进度</string>
|
||||
<string name="timer_session_count">%1$d 的 %2$d</string>
|
||||
<string name="timer_session_count">%2$d 中的 %1$d</string>
|
||||
<string name="last_year">去年</string>
|
||||
<string name="always_on_display">息屏显示</string>
|
||||
<string name="always_on_display_desc">查看计时器时点击任意位置切换至 AOD 模式</string>
|
||||
</resources>
|
||||
|
||||
@@ -1,60 +1,82 @@
|
||||
<!--
|
||||
~ Copyright (c) 2025 Nishant Mishra
|
||||
~
|
||||
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
|
||||
~
|
||||
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
|
||||
~ General Public License as published by the Free Software Foundation, either version 3 of the
|
||||
~ License, or (at your option) any later version.
|
||||
~
|
||||
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
|
||||
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
|
||||
~ Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License along with Tomato.
|
||||
~ If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<resources>
|
||||
<string name="app_name">Tomato</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="focus">Focus</string>
|
||||
<string name="short_break">Short break</string>
|
||||
<string name="long_break">Long break</string>
|
||||
<string name="exit">Exit</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="stop_alarm">Stop alarm</string>
|
||||
<string name="min_remaining_notification">%1$s min remaining</string>
|
||||
<string name="paused">Paused</string>
|
||||
<string name="completed">Completed</string>
|
||||
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
|
||||
<string name="start_next">Start next</string>
|
||||
<string name="choose_color_scheme">Choose color scheme</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="color_scheme">Color scheme</string>
|
||||
<string name="dynamic">Dynamic</string>
|
||||
<string name="color">Color</string>
|
||||
<string name="system_default">System default</string>
|
||||
<string name="alarm">Alarm</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="choose_theme">Choose theme</string>
|
||||
<string name="productivity_analysis">Productivity analysis</string>
|
||||
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
||||
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
||||
<string name="alarm_sound">Alarm sound</string>
|
||||
<string name="always_on_display">Always On Display</string>
|
||||
<string name="always_on_display_desc">Tap anywhere when viewing the timer to switch to AOD mode</string>
|
||||
<string name="app_name">Tomato</string>
|
||||
<string name="black_theme">Black theme</string>
|
||||
<string name="black_theme_desc">Use a pure black dark theme</string>
|
||||
<string name="alarm_desc">Ring alarm when a timer completes</string>
|
||||
<string name="vibrate">Vibrate</string>
|
||||
<string name="vibrate_desc">Vibrate when a timer completes</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="break_">Break</string>
|
||||
<string name="choose_color_scheme">Choose color scheme</string>
|
||||
<string name="choose_theme">Choose theme</string>
|
||||
<string name="color">Color</string>
|
||||
<string name="color_scheme">Color scheme</string>
|
||||
<string name="completed">Completed</string>
|
||||
<string name="dark">Dark</string>
|
||||
<string name="dynamic">Dynamic</string>
|
||||
<string name="exit">Exit</string>
|
||||
<string name="focus">Focus</string>
|
||||
<string name="focus_per_day_avg">focus per day (avg)</string>
|
||||
<string name="last_month">Last month</string>
|
||||
<string name="last_week">Last week</string>
|
||||
<string name="last_year">Last year</string>
|
||||
<string name="light">Light</string>
|
||||
<string name="long_break">Long break</string>
|
||||
<string name="min_remaining_notification">%1$s min remaining</string>
|
||||
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
|
||||
<string name="more">More</string>
|
||||
<string name="more_info">More info</string>
|
||||
<string name="ok">OK</string>
|
||||
<string name="pause">Pause</string>
|
||||
<string name="paused">Paused</string>
|
||||
<string name="play">Play</string>
|
||||
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
|
||||
<string name="productivity_analysis">Productivity analysis</string>
|
||||
<string name="productivity_analysis_desc">Focus durations at different times of the day</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="session_length">Session length</string>
|
||||
<string name="session_length_desc">Focus intervals in one session: %1$d</string>
|
||||
<string name="pomodoro_info">A \"session\" is a sequence of pomodoro intervals that contain focus intervals, short break intervals, and a long break interval. The last break of a session is always a long break.</string>
|
||||
<string name="stats">Stats</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="break_">Break</string>
|
||||
<string name="last_week">Last week</string>
|
||||
<string name="focus_per_day_avg">focus per day (avg)</string>
|
||||
<string name="more_info">More info</string>
|
||||
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
|
||||
<string name="last_month">Last month</string>
|
||||
<string name="monthly_productivity_analysis">Monthly productivity analysis</string>
|
||||
<string name="stop_alarm_question">Stop Alarm?</string>
|
||||
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
||||
<string name="timer_session_count">%1$d of %2$d</string>
|
||||
<string name="more">More</string>
|
||||
<string name="pause">Pause</string>
|
||||
<string name="play">Play</string>
|
||||
<string name="restart">Restart</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="short_break">Short break</string>
|
||||
<string name="skip">Skip</string>
|
||||
<string name="skip_to_next">Skip to next</string>
|
||||
<string name="up_next">Up next</string>
|
||||
<string name="start">Start</string>
|
||||
<string name="start_next">Start next</string>
|
||||
<string name="stats">Stats</string>
|
||||
<string name="stop">Stop</string>
|
||||
<string name="stop_alarm">Stop alarm</string>
|
||||
<string name="stop_alarm_dialog_text">Current timer session is complete. Tap anywhere to stop the alarm.</string>
|
||||
<string name="stop_alarm_question">Stop Alarm?</string>
|
||||
<string name="system_default">System</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="timer">Timer</string>
|
||||
<string name="timer_progress">Timer progress</string>
|
||||
<string name="last_year">Last year</string>
|
||||
<string name="timer_session_count">%1$d of %2$d</string>
|
||||
<string name="today">Today</string>
|
||||
<string name="up_next">Up next</string>
|
||||
<string name="up_next_notification">Up next: %1$s (%2$s)</string>
|
||||
<string name="vibrate">Vibration</string>
|
||||
<string name="vibrate_desc">Vibrate when a timer completes</string>
|
||||
<string name="weekly_productivity_analysis">Weekly productivity analysis</string>
|
||||
<string name="appearance">Appearance</string>
|
||||
<string name="durations">Durations</string>
|
||||
<string name="sound">Sound</string>
|
||||
</resources>
|
||||
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]
|
||||
activityCompose = "1.11.0"
|
||||
adaptive = "1.1.0"
|
||||
adaptive = "1.2.0"
|
||||
agp = "8.11.2"
|
||||
composeBom = "2025.10.00"
|
||||
composeBom = "2025.10.01"
|
||||
coreKtx = "1.17.0"
|
||||
espressoCore = "3.7.0"
|
||||
junit = "4.13.2"
|
||||
junitVersion = "1.3.0"
|
||||
kotlin = "2.2.20"
|
||||
kotlin = "2.2.21"
|
||||
ksp = "2.2.20-2.0.4"
|
||||
lifecycleRuntimeKtx = "2.9.4"
|
||||
materialKolor = "3.0.1"
|
||||
navigation3 = "1.0.0-alpha11"
|
||||
room = "2.8.2"
|
||||
navigation3 = "1.0.0-beta01"
|
||||
room = "2.8.3"
|
||||
vico = "2.2.1"
|
||||
|
||||
[libraries]
|
||||
|
||||
Reference in New Issue
Block a user