feat(ui): implement navigation between settings pages

This commit is contained in:
Nishant Mishra
2025-10-22 20:25:40 +05:30
parent 612bc27859
commit cfb1a75d21
10 changed files with 319 additions and 88 deletions

View File

@@ -1,3 +1,20 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro
import android.os.Bundle
@@ -11,8 +28,6 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import org.nsh07.pomodoro.ui.AppScreen
import org.nsh07.pomodoro.ui.NavItem
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.theme.TomatoTheme
import org.nsh07.pomodoro.ui.timerScreen.viewModel.TimerViewModel
@@ -70,27 +85,4 @@ class MainActivity : ComponentActivity() {
// Increase the timer loop frequency again when visible to make the progress smoother
appContainer.appTimerRepository.timerFrequency = 10f
}
companion object {
val screens = listOf(
NavItem(
Screen.Timer,
R.drawable.timer_outlined,
R.drawable.timer_filled,
R.string.timer
),
NavItem(
Screen.Stats,
R.drawable.monitoring,
R.drawable.monitoring_filled,
R.string.stats
),
NavItem(
Screen.Settings,
R.drawable.settings,
R.drawable.settings_filled,
R.string.settings
)
)
}
}

View File

@@ -1,8 +1,18 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui
@@ -45,7 +55,6 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import androidx.window.core.layout.WindowSizeClass
import org.nsh07.pomodoro.MainActivity.Companion.screens
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.settingsScreen.SettingsScreenRoot
import org.nsh07.pomodoro.ui.statsScreen.StatsScreenRoot
@@ -100,7 +109,7 @@ fun AppScreen(
if (wide) ShortNavigationBarArrangement.Centered
else ShortNavigationBarArrangement.EqualWeight
) {
screens.forEach {
mainScreens.forEach {
val selected = backStack.last() == it.route
ShortNavigationBarItem(
selected = selected,
@@ -131,7 +140,7 @@ fun AppScreen(
SharedTransitionLayout {
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
onBack = backStack::removeLastOrNull,
transitionSpec = {
ContentTransform(
fadeIn(motionScheme.defaultEffectsSpec()),
@@ -213,7 +222,7 @@ fun AppScreen(
)
}
entry<Screen.Settings> {
entry<Screen.Settings.Main> {
SettingsScreenRoot(
modifier = modifier.padding(
start = contentPadding.calculateStartPadding(layoutDirection),

View File

@@ -0,0 +1,62 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui
import org.nsh07.pomodoro.R
val mainScreens = listOf(
NavItem(
Screen.Timer,
R.drawable.timer_outlined,
R.drawable.timer_filled,
R.string.timer
),
NavItem(
Screen.Stats,
R.drawable.monitoring,
R.drawable.monitoring_filled,
R.string.stats
),
NavItem(
Screen.Settings.Main,
R.drawable.settings,
R.drawable.settings_filled,
R.string.settings
)
)
val settingsScreens = listOf(
SettingsNavItem(
Screen.Settings.Timer,
R.drawable.timer_filled,
R.string.timer,
listOf(R.string.durations, R.string.session_length, R.string.always_on_display)
),
SettingsNavItem(
Screen.Settings.Alarm,
R.drawable.alarm,
R.string.alarm,
listOf(R.string.alarm_sound, R.string.alarm, R.string.vibrate)
),
SettingsNavItem(
Screen.Settings.Appearance,
R.drawable.palette,
R.string.appearance,
listOf(R.string.color_scheme, R.string.theme, R.string.black_theme)
)
)

View File

@@ -1,3 +1,20 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui
import androidx.annotation.DrawableRes
@@ -13,7 +30,19 @@ sealed class Screen : NavKey {
object AOD : Screen()
@Serializable
object Settings : Screen()
sealed class Settings : Screen() {
@Serializable
object Main : Settings()
@Serializable
object Alarm : Settings()
@Serializable
object Appearance : Settings()
@Serializable
object Timer : Settings()
}
@Serializable
object Stats : Screen()
@@ -24,4 +53,11 @@ data class NavItem(
@param:DrawableRes val unselectedIcon: Int,
@param:DrawableRes val selectedIcon: Int,
@param:StringRes val label: Int
)
)
data class SettingsNavItem(
val route: Screen.Settings,
@param:DrawableRes val icon: Int,
@param:StringRes val label: Int,
val innerSettings: List<Int>
)

View File

@@ -19,6 +19,11 @@ package org.nsh07.pomodoro.ui.settingsScreen
import android.content.Intent
import android.net.Uri
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
@@ -27,10 +32,12 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.input.TextFieldState
import androidx.compose.foundation.text.input.rememberTextFieldState
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.SliderState
import androidx.compose.material3.Text
@@ -40,24 +47,36 @@ import androidx.compose.material3.rememberSliderState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Devices
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberNavBackStack
import androidx.navigation3.ui.NavDisplay
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.service.TimerService
import org.nsh07.pomodoro.ui.ClickableListItem
import org.nsh07.pomodoro.ui.Screen
import org.nsh07.pomodoro.ui.settingsScreen.components.AboutCard
import org.nsh07.pomodoro.ui.settingsScreen.screens.AlarmSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.AppearanceSettings
import org.nsh07.pomodoro.ui.settingsScreen.screens.TimerSettings
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.PreferencesState
import org.nsh07.pomodoro.ui.settingsScreen.viewModel.SettingsViewModel
import org.nsh07.pomodoro.ui.settingsScreens
import org.nsh07.pomodoro.ui.theme.AppFonts.robotoFlexTopBar
import org.nsh07.pomodoro.ui.theme.CustomColors.topBarColors
import org.nsh07.pomodoro.ui.theme.TomatoTheme
@@ -147,44 +166,122 @@ private fun SettingsScreen(
onColorSchemeChange: (Color) -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
val backStack = rememberNavBackStack(Screen.Settings.Main)
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
title = {
Text(
stringResource(R.string.settings),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
NavDisplay(
backStack = backStack,
onBack = backStack::removeLastOrNull,
transitionSpec = {
(slideInHorizontally(initialOffsetX = { it }))
.togetherWith(slideOutHorizontally(targetOffsetX = { -it / 4 }) + fadeOut())
},
popTransitionSpec = {
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
},
predictivePopTransitionSpec = {
(slideInHorizontally(initialOffsetX = { -it / 4 }) + fadeIn())
.togetherWith(slideOutHorizontally(targetOffsetX = { it }))
},
entryProvider = entryProvider {
entry<Screen.Settings.Main> {
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
TopAppBar(
title = {
Text(
stringResource(R.string.settings),
style = LocalTextStyle.current.copy(
fontFamily = robotoFlexTopBar,
fontSize = 32.sp,
lineHeight = 32.sp
)
)
},
subtitle = {},
colors = topBarColors,
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item { Spacer(Modifier.height(12.dp)) }
item { AboutCard() }
item { Spacer(Modifier.height(12.dp)) }
itemsIndexed(settingsScreens) { index, item ->
ClickableListItem(
leadingContent = {
Icon(painterResource(item.icon), null)
},
headlineContent = { Text(stringResource(item.label)) },
supportingContent = {
Text(
remember {
item.innerSettings.joinToString(", ") {
context.getString(it)
}
},
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
trailingContent = {
Icon(painterResource(R.drawable.arrow_forward_big), null)
},
items = settingsScreens.size,
index = index
) { backStack.add(item.route) }
}
item { Spacer(Modifier.height(12.dp)) }
}
}
}
entry<Screen.Settings.Alarm> {
AlarmSettings(
preferencesState = preferencesState,
alarmEnabled = alarmEnabled,
vibrateEnabled = vibrateEnabled,
alarmSound = alarmSound,
onAlarmEnabledChange = onAlarmEnabledChange,
onVibrateEnabledChange = onVibrateEnabledChange,
onAlarmSoundChanged = onAlarmSoundChanged,
onBack = backStack::removeLastOrNull
)
},
subtitle = {},
colors = topBarColors,
titleHorizontalAlignment = Alignment.CenterHorizontally,
scrollBehavior = scrollBehavior
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier
.background(topBarColors.containerColor)
.fillMaxSize()
.padding(horizontal = 16.dp)
) {
item { Spacer(Modifier.height(12.dp)) }
item { AboutCard() }
item { Spacer(Modifier.height(12.dp)) }
item {}
item { Spacer(Modifier.height(12.dp)) }
}
entry<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)

View File

@@ -151,10 +151,10 @@ fun AlarmSettings(
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text("Alarm", fontFamily = robotoFlexTopBar)
Text(stringResource(R.string.alarm), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text("Settings")
Text(stringResource(R.string.settings))
},
navigationIcon = {
IconButton(onBack) {
@@ -241,12 +241,7 @@ fun AlarmSettings(
@Preview
@Composable
fun AlarmSettingsPreview() {
val preferencesState = PreferencesState(
theme = "auto",
colorScheme = "White",
blackTheme = false,
aodEnabled = false
)
val preferencesState = PreferencesState()
AlarmSettings(
preferencesState = preferencesState,
alarmEnabled = true,

View File

@@ -42,7 +42,6 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
@@ -70,8 +69,6 @@ fun AppearanceSettings(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val context = LocalContext.current
val themeMap: Map<String, Pair<Int, Int>> = remember {
mapOf(
"auto" to Pair(
@@ -82,23 +79,21 @@ fun AppearanceSettings(
"dark" to Pair(R.drawable.dark_mode, 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"
)
}
val reverseThemeMap: Map<String, String> = mapOf(
stringResource(R.string.system_default) to "auto",
stringResource(R.string.light) to "light",
stringResource(R.string.dark) to "dark"
)
val scrollBehavior = TopAppBarDefaults.enterAlwaysScrollBehavior()
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text("Appearance", fontFamily = robotoFlexTopBar)
Text(stringResource(R.string.appearance), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text("Settings")
Text(stringResource(R.string.settings))
},
navigationIcon = {
IconButton(onBack) {

View File

@@ -93,10 +93,10 @@ fun TimerSettings(
Column(modifier.nestedScroll(scrollBehavior.nestedScrollConnection)) {
LargeFlexibleTopAppBar(
title = {
Text("Timer", fontFamily = robotoFlexTopBar)
Text(stringResource(R.string.timer), fontFamily = robotoFlexTopBar)
},
subtitle = {
Text("Settings")
Text(stringResource(R.string.settings))
},
navigationIcon = {
IconButton(onBack) {

View File

@@ -0,0 +1,26 @@
<!--
~ Copyright (c) 2025 Nishant Mishra
~
~ This file is part of Tomato - a minimalist pomodoro timer for Android.
~
~ Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
~ General Public License as published by the Free Software Foundation, either version 3 of the
~ License, or (at your option) any later version.
~
~ Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
~ the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
~ Public License for more details.
~
~ You should have received a copy of the GNU General Public License along with Tomato.
~ If not, see <https://www.gnu.org/licenses/>.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e3e3e3"
android:pathData="m321,880 l-71,-71 329,-329 -329,-329 71,-71 400,400L321,880Z" />
</vector>

View File

@@ -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/>.
-->
<resources>
<string name="alarm">Alarm</string>
<string name="alarm_desc">Ring alarm when a timer completes</string>
@@ -59,4 +76,6 @@
<string name="vibrate">Vibrate</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>
</resources>