From 87f224addc02b856ca296da3a24349f9258201e4 Mon Sep 17 00:00:00 2001 From: Nishant Mishra Date: Sat, 27 Sep 2025 13:27:45 +0530 Subject: [PATCH] feat: Add a custom color theme feature --- .../nsh07/pomodoro/ui/ClickableListItem.kt | 80 +++++++++ .../settingsScreen/ColorSchemePickerDialog.kt | 157 ++++++++++++++++++ .../ColorSchemePickerListItem.kt | 64 +++++++ .../ui/settingsScreen/SettingsScreen.kt | 45 ++--- .../ui/settingsScreen/ThemePickerListItem.kt | 15 +- app/src/main/res/drawable/colors.xml | 16 ++ 6 files changed, 343 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt create mode 100644 app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt create mode 100644 app/src/main/res/drawable/colors.xml diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt new file mode 100644 index 0000000..2d05a68 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/ClickableListItem.kt @@ -0,0 +1,80 @@ +package org.nsh07.pomodoro.ui + +import androidx.compose.animation.core.animateDpAsState +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.ListItem +import androidx.compose.material3.ListItemColors +import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.MaterialTheme.motionScheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +@Composable +fun ClickableListItem( + headlineContent: @Composable (() -> Unit), + modifier: Modifier = Modifier, + overlineContent: @Composable (() -> Unit)? = null, + supportingContent: @Composable (() -> Unit)? = null, + leadingContent: @Composable (() -> Unit)? = null, + trailingContent: @Composable (() -> Unit)? = null, + colors: ListItemColors = ListItemDefaults.colors(), + tonalElevation: Dp = ListItemDefaults.Elevation, + shadowElevation: Dp = ListItemDefaults.Elevation, + items: Int, + index: Int, + onClick: () -> Unit +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val top by animateDpAsState( + if (isPressed) 40.dp + else { + if (items == 1 || index == 0) 20.dp + else 4.dp + }, + motionScheme.fastSpatialSpec() + ) + val bottom by animateDpAsState( + if (isPressed) 40.dp + else { + if (items == 1 || index == items - 1) 20.dp + else 4.dp + }, + motionScheme.fastSpatialSpec() + ) + + ListItem( + headlineContent = headlineContent, + modifier = modifier + .clip( + RoundedCornerShape( + topStart = top, + topEnd = top, + bottomStart = bottom, + bottomEnd = bottom + ) + ) + .clickable( + onClick = onClick, + interactionSource = interactionSource, + ), + overlineContent = overlineContent, + supportingContent = supportingContent, + leadingContent = leadingContent, + trailingContent = trailingContent, + colors = colors, + tonalElevation = tonalElevation, + shadowElevation = shadowElevation + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt new file mode 100644 index 0000000..d963ee5 --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerDialog.kt @@ -0,0 +1,157 @@ +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.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 = "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("Ok") + } + } + } + } +} + +@Preview +@Composable +fun ColorPickerDialogPreview() { + var currentColor by remember { mutableStateOf(Color(0xfffeb4a7)) } + TomatoTheme(darkTheme = true) { + ColorSchemePickerDialog( + currentColor, + setShowDialog = {}, + onColorChange = { currentColor = it } + ) + } +} diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt new file mode 100644 index 0000000..bc09f9e --- /dev/null +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ColorSchemePickerListItem.kt @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2025 Nishant Mishra + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package org.nsh07.pomodoro.ui.settingsScreen + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme.colorScheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import 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("Color scheme") }, + supportingContent = { + Text( + if (color == Color.White) "Dynamic" + else "Color" + ) + }, + colors = listItemColors, + items = items, + index = index, + modifier = modifier.fillMaxWidth() + ) { showDialog = true } +} \ No newline at end of file diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt index 580fa90..12bcdac 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/SettingsScreen.kt @@ -53,6 +53,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.rememberSliderState 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 @@ -139,6 +140,7 @@ fun SettingsScreenRoot( } }, onThemeChange = viewModel::saveTheme, + onColorSchemeChange = viewModel::saveColorScheme, modifier = modifier ) } @@ -159,6 +161,7 @@ private fun SettingsScreen( onBlackThemeChange: (Boolean) -> Unit, onAlarmSoundChanged: (Uri?) -> Unit, onThemeChange: (String) -> Unit, + onColorSchemeChange: (Color) -> Unit, modifier: Modifier = Modifier ) { val scrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior() @@ -185,6 +188,12 @@ private fun SettingsScreen( } val context = LocalContext.current + var alarmName by remember { mutableStateOf("") } + + LaunchedEffect(Unit) { + alarmName = RingtoneManager.getRingtone(context, alarmSound.toUri()) + ?.getTitle(context) ?: "" + } val ringtonePickerLauncher = rememberLauncherForActivityResult( contract = ActivityResultContracts.StartActivityForResult() @@ -358,25 +367,11 @@ private fun SettingsScreen( item { Spacer(Modifier.height(12.dp)) } item { - ListItem( - leadingContent = { - Icon( - painter = painterResource(R.drawable.palette), - contentDescription = null, - tint = colorScheme.primary - ) - }, - headlineContent = { Text("Color scheme") }, - supportingContent = { - Text( - if (preferencesState.colorScheme.toColor() == Color.White) "Dynamic" - else "Color" - ) - }, - colors = listItemColors, - modifier = Modifier - .clip(topListItemShape) - .clickable(onClick = {}) + ColorSchemePickerListItem( + color = preferencesState.colorScheme.toColor(), + items = 3, + index = 0, + onColorChange = onColorSchemeChange ) } item { @@ -385,6 +380,8 @@ private fun SettingsScreen( themeMap = themeMap, reverseThemeMap = reverseThemeMap, onThemeChange = onThemeChange, + items = 3, + index = 1, modifier = Modifier .clip(middleListItemShape) ) @@ -432,14 +429,7 @@ private fun SettingsScreen( Icon(painterResource(R.drawable.alarm), null) }, headlineContent = { Text("Alarm sound") }, - supportingContent = { - Text( - remember(alarmSound) { - RingtoneManager.getRingtone(context, alarmSound.toUri()) - ?.getTitle(context) ?: "" - } - ) - }, + supportingContent = { Text(alarmName) }, colors = listItemColors, modifier = Modifier .clip(topListItemShape) @@ -542,6 +532,7 @@ fun SettingsScreenPreview() { onBlackThemeChange = {}, onAlarmSoundChanged = {}, onThemeChange = {}, + onColorSchemeChange = {}, modifier = Modifier.fillMaxSize() ) } diff --git a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt index 4c43549..9fda303 100644 --- a/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt +++ b/app/src/main/java/org/nsh07/pomodoro/ui/settingsScreen/ThemePickerListItem.kt @@ -7,10 +7,8 @@ package org.nsh07.pomodoro.ui.settingsScreen -import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.material3.Icon -import androidx.compose.material3.ListItem import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -19,6 +17,7 @@ import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource +import org.nsh07.pomodoro.ui.ClickableListItem import org.nsh07.pomodoro.ui.theme.CustomColors.listItemColors @Composable @@ -26,6 +25,8 @@ fun ThemePickerListItem( theme: String, themeMap: Map>, reverseThemeMap: Map, + items: Int, + index: Int, onThemeChange: (String) -> Unit, modifier: Modifier = Modifier ) { @@ -41,7 +42,7 @@ fun ThemePickerListItem( ) } - ListItem( + ClickableListItem( leadingContent = { Icon( painter = painterResource(themeMap[theme]!!.first), @@ -53,8 +54,8 @@ fun ThemePickerListItem( Text(themeMap[theme]!!.second) }, colors = listItemColors, - modifier = modifier - .fillMaxWidth() - .clickable { showDialog = true } - ) + items = items, + index = index, + modifier = modifier.fillMaxWidth() + ) { showDialog = true } } \ No newline at end of file diff --git a/app/src/main/res/drawable/colors.xml b/app/src/main/res/drawable/colors.xml new file mode 100644 index 0000000..7449d11 --- /dev/null +++ b/app/src/main/res/drawable/colors.xml @@ -0,0 +1,16 @@ + + + + +