feat: Add a custom color theme feature

This commit is contained in:
Nishant Mishra
2025-09-27 13:27:45 +05:30
parent a7b6093737
commit 87f224addc
6 changed files with 343 additions and 34 deletions

View File

@@ -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
)
}

View File

@@ -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 }
)
}
}

View File

@@ -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 <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 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 }
}

View File

@@ -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()
)
}

View File

@@ -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<String, Pair<Int, String>>,
reverseThemeMap: Map<String, String>,
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 }
}

View 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="M346,820 L100,574q-10,-10 -15,-22t-5,-25q0,-13 5,-25t15,-22l230,-229 -75,-75q-13,-13 -13.5,-31t12.5,-32q13,-14 32,-14t33,14l367,367q10,10 14.5,22t4.5,25q0,13 -4.5,25T686,574L440,820q-10,10 -22,15t-25,5q-13,0 -25,-5t-22,-15ZM393,314L179,528h428L393,314ZM792,840q-36,0 -61,-25.5T706,752q0,-27 13.5,-51t30.5,-47l19,-24q9,-11 23.5,-11.5T816,629l20,25q16,23 30,47t14,51q0,37 -26,62.5T792,840Z" />
</vector>