feat(ui): redesign theme selection UI

This commit is contained in:
Nishant Mishra
2025-10-23 20:54:08 +05:30
parent 89f4d4e77c
commit 6e6f5477e1
4 changed files with 89 additions and 204 deletions

View File

@@ -1,151 +0,0 @@
/*
* Copyright (c) 2025 Nishant Mishra
*
* This file is part of Tomato - a minimalist pomodoro timer for Android.
*
* Tomato is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* Tomato is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Tomato.
* If not, see <https://www.gnu.org/licenses/>.
*/
package org.nsh07.pomodoro.ui.settingsScreen.components
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.foundation.layout.wrapContentWidth
import androidx.compose.foundation.selection.selectable
import androidx.compose.foundation.selection.selectableGroup
import androidx.compose.material3.AlertDialogDefaults
import androidx.compose.material3.BasicAlertDialog
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.MaterialTheme.colorScheme
import androidx.compose.material3.MaterialTheme.shapes
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.CustomColors.selectedListItemColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ThemeDialog(
themeMap: Map<String, Pair<Int, Int>>,
reverseThemeMap: Map<String, String>,
theme: String,
setShowThemeDialog: (Boolean) -> Unit,
onThemeChange: (String) -> Unit
) {
val selectedOption =
remember { mutableIntStateOf(themeMap[theme]!!.second) }
val context = LocalContext.current
BasicAlertDialog(
onDismissRequest = { setShowThemeDialog(false) }
) {
Surface(
modifier = Modifier
.wrapContentWidth()
.wrapContentHeight(),
shape = shapes.extraLarge,
color = colorScheme.surfaceContainer,
tonalElevation = AlertDialogDefaults.TonalElevation
) {
Column(modifier = Modifier.padding(24.dp)) {
Text(
text = stringResource(R.string.choose_theme),
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(16.dp))
Column(
verticalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.selectableGroup()
) {
themeMap.entries.forEachIndexed { index: Int, pair: Map.Entry<String, Pair<Int, Int>> ->
val text = pair.value.second
val selected = text == selectedOption.intValue
ListItem(
leadingContent = {
AnimatedContent(selected) {
if (it)
Icon(painterResource(R.drawable.check), null)
else
Icon(painterResource(pair.value.first), null)
}
},
headlineContent = {
Text(
text = stringResource(text),
style = MaterialTheme.typography.bodyLarge
)
},
colors = if (!selected) listItemColors else selectedListItemColors,
modifier = Modifier
.height(64.dp)
.clip(
when (index) {
0 -> topListItemShape
themeMap.size - 1 -> bottomListItemShape
else -> middleListItemShape
}
)
.selectable(
selected = (text == selectedOption.intValue),
onClick = {
selectedOption.intValue = text
onThemeChange(
reverseThemeMap[context.getString(
selectedOption.intValue
)]!!
)
},
role = Role.RadioButton
)
)
}
}
Spacer(modifier = Modifier.height(16.dp))
TextButton(
shapes = ButtonDefaults.shapes(),
onClick = { setShowThemeDialog(false) },
modifier = Modifier.align(Alignment.End)
) {
Text(stringResource(R.string.ok))
}
}
}
}
}

View File

@@ -17,54 +17,109 @@
package org.nsh07.pomodoro.ui.settingsScreen.components
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ButtonGroupDefaults
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
import androidx.compose.material3.Icon
import androidx.compose.material3.ListItem
import androidx.compose.material3.Text
import androidx.compose.material3.ToggleButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.role
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import org.nsh07.pomodoro.R
import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.bottomListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.middleListItemShape
import org.nsh07.pomodoro.ui.theme.TomatoShapeDefaults.topListItemShape
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
@Composable
fun ThemePickerListItem(
theme: String,
themeMap: Map<String, Pair<Int, Int>>,
reverseThemeMap: Map<String, String>,
items: Int,
index: Int,
onThemeChange: (String) -> Unit,
modifier: Modifier = Modifier
modifier: Modifier = Modifier,
) {
var showDialog by rememberSaveable { mutableStateOf(false) }
if (showDialog) {
ThemeDialog(
themeMap = themeMap,
reverseThemeMap = reverseThemeMap,
theme = theme,
setShowThemeDialog = { showDialog = it },
onThemeChange = onThemeChange
val themeMap: Map<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)
)
}
ClickableListItem(
leadingContent = {
Icon(
painter = painterResource(themeMap[theme]!!.first),
contentDescription = null
)
},
headlineContent = { Text(stringResource(R.string.theme)) },
supportingContent = {
Text(stringResource(themeMap[theme]!!.second))
},
items = items,
index = index,
modifier = modifier.fillMaxWidth()
) { showDialog = true }
}
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
)
}
}
}
}
}

View File

@@ -37,7 +37,6 @@ import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
@@ -69,22 +68,6 @@ fun AppearanceSettings(
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val themeMap: Map<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)
)
}
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)) {
@@ -128,8 +111,6 @@ fun AppearanceSettings(
item {
ThemePickerListItem(
theme = preferencesState.theme,
themeMap = themeMap,
reverseThemeMap = reverseThemeMap,
onThemeChange = onThemeChange,
items = 3,
index = 1,

View File

@@ -65,7 +65,7 @@
<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 default</string>
<string name="system_default">System</string>
<string name="theme">Theme</string>
<string name="timer">Timer</string>
<string name="timer_progress">Timer progress</string>