feat: enhance UI components with LocalBaseUITheme for consistent styling and improved interactions

This commit is contained in:
Kingkor Roy Tirtho 2026-07-05 18:58:43 +06:00
parent a57b359186
commit a52ba68f1f
16 changed files with 2524 additions and 885 deletions

View File

@ -20,8 +20,11 @@ package dev.krtirtho.spotube
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -100,15 +103,18 @@ fun App(
} }
SpotubeTheme(settings = userSettings) { SpotubeTheme(settings = userSettings) {
AppShell(navigator, navigationState) { val baseUITheme = rememberBaseUITheme()
Column { CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) {
NavDisplay( AppShell(navigator, navigationState) {
modifier = Modifier.fillMaxSize(), Column {
onBack = navigator::pop, NavDisplay(
entries = navigationState.toEntries(koinEntryProvider()) modifier = Modifier.fillMaxSize(),
) onBack = navigator::pop,
entries = navigationState.toEntries(koinEntryProvider())
)
}
} }
content()
} }
content()
} }
} }

View File

@ -28,8 +28,8 @@ import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsFocusedAsState
import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.focusable import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -41,7 +41,6 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -87,12 +86,55 @@ import androidx.compose.ui.window.PopupProperties
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
private val AutocompleteTextFieldShape = RoundedCornerShape(14.dp)
private val AutocompleteMenuShape = RoundedCornerShape(12.dp)
private val AutocompleteTextFieldMinHeight = 44.dp private val AutocompleteTextFieldMinHeight = 44.dp
private val AutocompleteMenuDefaultMaxHeight = 300.dp private val AutocompleteMenuDefaultMaxHeight = 300.dp
private val AutocompleteMenuDefaultOffset = 4.dp private val AutocompleteMenuDefaultOffset = 4.dp
private data class ResolvedAutoCompleteTextFieldState(
val background: Brush,
val highlight: Color,
val shape: Shape,
val border: BaseUITheme.Border,
val shadow: BaseUITheme.Shadow,
val foreground: Color,
)
@Composable
private fun resolveTextFieldState(
theme: BaseUITheme.TextFieldTheme,
isFocused: Boolean,
isHovered: Boolean,
): ResolvedAutoCompleteTextFieldState {
return when {
isFocused -> ResolvedAutoCompleteTextFieldState(
theme.background.focused,
theme.highlight.focused,
theme.shape.focused,
theme.border.focused,
theme.shadow.focused,
theme.foreground.focused
)
isHovered -> ResolvedAutoCompleteTextFieldState(
theme.background.hovered,
theme.highlight.hovered,
theme.shape.hovered,
theme.border.hovered,
theme.shadow.hovered,
theme.foreground.hovered
)
else -> ResolvedAutoCompleteTextFieldState(
theme.background.pressed,
theme.highlight.pressed,
theme.shape.pressed,
theme.border.pressed,
theme.shadow.pressed,
theme.foreground.pressed
)
}
}
@Composable @Composable
fun <T> AutocompleteTextField( fun <T> AutocompleteTextField(
value: String, value: String,
@ -115,16 +157,18 @@ fun <T> AutocompleteTextField(
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
textStyle: TextStyle = TextStyle.Default, textStyle: TextStyle = TextStyle.Default,
cursorBrush: Color = MaterialTheme.colorScheme.primary, cursorBrush: Color? = null,
onKeyEvent: ((KeyEvent) -> Boolean)? = null, onKeyEvent: ((KeyEvent) -> Boolean)? = null,
menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight, menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight,
menuOffset: Dp = AutocompleteMenuDefaultOffset, menuOffset: Dp = AutocompleteMenuDefaultOffset,
menuShape: Shape = AutocompleteMenuShape, textFieldTheme: BaseUITheme.TextFieldTheme? = null,
menuShadowElevation: Dp = 8.dp, menuTheme: BaseUITheme.AutoCompleteMenuTheme? = null,
menuContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
) { ) {
val density = LocalDensity.current val density = LocalDensity.current
val colors = rememberButtonColors() val baseTextFieldTheme = LocalBaseUITheme.current.textField
val baseMenuTheme = LocalBaseUITheme.current.autoCompleteMenuTheme
val resolvedTextFieldTheme = textFieldTheme ?: baseTextFieldTheme
val resolvedMenuTheme = menuTheme ?: baseMenuTheme
val isFocused by interactionSource.collectIsFocusedAsState() val isFocused by interactionSource.collectIsFocusedAsState()
val isHovered by interactionSource.collectIsHoveredAsState() val isHovered by interactionSource.collectIsHoveredAsState()
@ -174,19 +218,21 @@ fun <T> AutocompleteTextField(
) )
} }
val gradient = outlinedGradient(colors, false) val fieldState = resolveTextFieldState(resolvedTextFieldTheme, isFocused, isHovered)
val border = when {
isError -> MaterialTheme.colorScheme.error val resolvedBorder = if (isError) {
isFocused -> MaterialTheme.colorScheme.primary BaseUITheme.Border(MaterialTheme.colorScheme.error, fieldState.border.width)
isHovered -> colors.border.copy(alpha = 0.85f) } else {
else -> colors.border fieldState.border
} }
val contentColor = when { val contentColor = when {
!enabled -> colors.onContainer.copy(alpha = 0.38f) !enabled -> fieldState.foreground.copy(alpha = 0.38f)
else -> colors.onContainer else -> fieldState.foreground
} }
val cursor = cursorBrush?.let { SolidColor(it) } ?: resolvedTextFieldTheme.cursor
fun navigate(delta: Int) { fun navigate(delta: Int) {
if (items.isEmpty()) return if (items.isEmpty()) return
selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size
@ -207,13 +253,25 @@ fun <T> AutocompleteTextField(
.fillMaxWidth() .fillMaxWidth()
.onSizeChanged { textFieldSize = it } .onSizeChanged { textFieldSize = it }
.defaultMinSize(minHeight = AutocompleteTextFieldMinHeight) .defaultMinSize(minHeight = AutocompleteTextFieldMinHeight)
.then(textFieldShadow(AutocompleteTextFieldShape, isFocused, colors)) .then(
.clip(AutocompleteTextFieldShape) if (fieldState.shadow.elevation > 0.dp) {
.background(gradient, AutocompleteTextFieldShape) Modifier.shadow(
.border(BorderStroke(.5.dp, border), AutocompleteTextFieldShape) elevation = fieldState.shadow.elevation,
shape = fieldState.shape,
ambientColor = fieldState.shadow.ambientColor,
spotColor = fieldState.shadow.spotColor,
)
} else Modifier
)
.clip(fieldState.shape)
.background(fieldState.background, fieldState.shape)
.border(
BorderStroke(resolvedBorder.width, resolvedBorder.color),
fieldState.shape
)
.drawWithCache { .drawWithCache {
val highlightBrush = Brush.verticalGradient( val highlightBrush = Brush.verticalGradient(
colors = listOf(colors.highlight, Color.Transparent), colors = listOf(fieldState.highlight, Color.Transparent),
startY = 0f, startY = 0f,
endY = size.height * 0.5f, endY = size.height * 0.5f,
) )
@ -226,7 +284,7 @@ fun <T> AutocompleteTextField(
) )
} }
} }
.padding(horizontal = 14.dp, vertical = 10.dp), .padding(resolvedTextFieldTheme.padding),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -294,7 +352,7 @@ fun <T> AutocompleteTextField(
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
textStyle = textStyle.copy(color = contentColor), textStyle = textStyle.copy(color = contentColor),
cursorBrush = SolidColor(cursorBrush), cursorBrush = cursor,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = wrappedKeyboardActions, keyboardActions = wrappedKeyboardActions,
singleLine = singleLine, singleLine = singleLine,
@ -340,9 +398,9 @@ fun <T> AutocompleteTextField(
modifier = Modifier modifier = Modifier
.width(textFieldWidthDp) .width(textFieldWidthDp)
.heightIn(max = menuMaxHeight) .heightIn(max = menuMaxHeight)
.shadow(menuShadowElevation, menuShape) .shadow(resolvedMenuTheme.shadowElevation, resolvedMenuTheme.shape)
.background(menuContainerColor, menuShape) .background(resolvedMenuTheme.background, resolvedMenuTheme.shape)
.clip(menuShape) .clip(resolvedMenuTheme.shape)
.focusRequester(popupFocusRequester) .focusRequester(popupFocusRequester)
.focusable() .focusable()
.onFocusChanged { state -> .onFocusChanged { state ->
@ -412,70 +470,73 @@ fun <T> AutocompleteTextField(
@Composable @Composable
private fun AutocompleteTextFieldPreview() { private fun AutocompleteTextFieldPreview() {
MaterialTheme { MaterialTheme {
androidx.compose.material3.Surface( val theme = rememberBaseUITheme()
color = MaterialTheme.colorScheme.background, CompositionLocalProvider(LocalBaseUITheme provides theme) {
modifier = Modifier.padding(24.dp), androidx.compose.material3.Surface(
) { color = MaterialTheme.colorScheme.background,
val artists = remember { modifier = Modifier.padding(24.dp),
listOf( ) {
"Twenty One Pilots", val artists = remember {
"The Beatles", listOf(
"Adele", "Twenty One Pilots",
"Drake", "The Beatles",
"Taylor Swift", "Adele",
"Arctic Monkeys", "Drake",
"Billie Eilish", "Taylor Swift",
) "Arctic Monkeys",
} "Billie Eilish",
var value by remember { mutableStateOf("") }
AutocompleteTextField(
value = value,
onValueChange = {
value = it
},
items = artists.filter {
it.contains(value, ignoreCase = true) && value.isNotEmpty()
},
onItemSelected = { selected ->
value = selected
},
placeholder = { Text("Search artists...") },
leadingIcon = {
androidx.compose.material3.Icon(
imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = "Search",
) )
}, }
modifier = Modifier.fillMaxWidth(), var value by remember { mutableStateOf("") }
itemContent = { item, isSelected ->
Row( AutocompleteTextField(
modifier = Modifier value = value,
.fillMaxWidth() onValueChange = {
.background( value = it
color = if (isSelected) { },
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f) items = artists.filter {
} else { it.contains(value, ignoreCase = true) && value.isNotEmpty()
Color.Transparent },
}, onItemSelected = { selected ->
) value = selected
.padding(horizontal = 14.dp, vertical = 10.dp), },
verticalAlignment = Alignment.CenterVertically, placeholder = { Text("Search artists...") },
horizontalArrangement = Arrangement.spacedBy(10.dp), leadingIcon = {
) {
androidx.compose.material3.Icon( androidx.compose.material3.Icon(
imageVector = Iconsax.IconsaxSearchBroken, imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = null, contentDescription = "Search",
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
) )
Text( },
text = item, modifier = Modifier.fillMaxWidth(),
style = MaterialTheme.typography.bodyMedium, itemContent = { item, isSelected ->
) Row(
} modifier = Modifier
}, .fillMaxWidth()
) .background(
color = if (isSelected) {
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
} else {
Color.Transparent
},
)
.padding(horizontal = 14.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(10.dp),
) {
androidx.compose.material3.Icon(
imageVector = Iconsax.IconsaxSearchBroken,
contentDescription = null,
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = item,
style = MaterialTheme.typography.bodyMedium,
)
}
},
)
}
} }
} }
} }

View File

@ -0,0 +1,81 @@
/*
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package dev.krtirtho.spotube.core.ui.base
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
@Composable
fun Card(
modifier: Modifier = Modifier,
theme: BaseUITheme.CardTheme? = null,
content: @Composable () -> Unit,
) {
val cardTheme = theme ?: LocalBaseUITheme.current.card
Box(
modifier = modifier
.then(
if (cardTheme.shadow.elevation > 0.dp) {
Modifier.shadow(
elevation = cardTheme.shadow.elevation,
shape = cardTheme.shape,
ambientColor = cardTheme.shadow.ambientColor,
spotColor = cardTheme.shadow.spotColor,
)
} else Modifier
)
.clip(cardTheme.shape)
.background(cardTheme.background, cardTheme.shape)
.border(BorderStroke(cardTheme.border.width, cardTheme.border.color), cardTheme.shape)
.padding(cardTheme.padding),
) {
content()
}
}
@Composable
@androidx.compose.ui.tooling.preview.Preview
private fun CardPreview() {
MaterialTheme {
val theme = rememberBaseUITheme()
CompositionLocalProvider(LocalBaseUITheme provides theme) {
Surface(
color = MaterialTheme.colorScheme.background,
modifier = Modifier.padding(24.dp),
) {
Card {
Box(modifier = Modifier.padding(16.dp))
}
}
}
}
}

View File

@ -39,6 +39,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -50,6 +51,7 @@ import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.StrokeJoin
import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.drawscope.Stroke
@ -65,9 +67,28 @@ enum class CheckBoxState {
CLEAR; CLEAR;
} }
private val CheckBoxShape = RoundedCornerShape(6.dp)
private val CheckBoxSize = 22.dp private val CheckBoxSize = 22.dp
private data class ResolvedCheckBoxState(
val colors: BaseUITheme.ButtonColors,
val shape: Shape,
val shadow: BaseUITheme.Shadow,
val border: BaseUITheme.Border,
)
@Composable
private fun resolveCheckBoxState(
style: BaseUITheme.ButtonStyle,
isPressed: Boolean,
isHovered: Boolean,
): ResolvedCheckBoxState {
return when {
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
else -> ResolvedCheckBoxState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused)
}
}
@Composable @Composable
fun CheckBox( fun CheckBox(
state: CheckBoxState = CheckBoxState.UNSELECTED, state: CheckBoxState = CheckBoxState.UNSELECTED,
@ -75,69 +96,25 @@ fun CheckBox(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
interactionSource: MutableInteractionSource? = null, interactionSource: MutableInteractionSource? = null,
theme: BaseUITheme.CheckBoxTheme? = null,
) { ) {
val colors = rememberButtonColors() val baseTheme = LocalBaseUITheme.current.checkBox
val checkBoxTheme = theme ?: baseTheme
val source = interactionSource ?: remember { MutableInteractionSource() } val source = interactionSource ?: remember { MutableInteractionSource() }
val isPressed by source.collectIsPressedAsState() val isPressed by source.collectIsPressedAsState()
val isHovered by source.collectIsHoveredAsState() val isHovered by source.collectIsHoveredAsState()
val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
val isSelected = state == CheckBoxState.SELECTED val isSelected = state == CheckBoxState.SELECTED
val isIndeterminate = state == CheckBoxState.INDETERMINATE val isIndeterminate = state == CheckBoxState.INDETERMINATE
val isFilled = isSelected || isIndeterminate val isFilled = isSelected || isIndeterminate
val backgroundBrush = if (isFilled) { val style = if (isFilled) checkBoxTheme.selected else checkBoxTheme.unselected
primaryGradient(colors, isPressed) val resolved = resolveCheckBoxState(style, isPressed, isHovered)
} else { val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
outlinedGradient(colors, false)
}
val borderColor = when {
!enabled -> colors.onContainer.copy(alpha = 0.38f)
isFilled -> colors.accent
else -> colors.border
}
val borderWidth = 0.5.dp
val shadowElevation = when { val borderColor = when {
isPressed -> 1.dp !enabled -> resolved.colors.foreground.copy(alpha = 0.38f)
isFilled && isHovered -> 8.dp else -> resolved.border.color
isFilled -> 6.dp
isHovered -> 5.dp
else -> 3.dp
}
val shadowAmbient = if (isFilled) {
colors.accent.copy(
alpha = when {
isPressed -> 0.2f
isHovered -> 0.4f
else -> 0.3f
}
)
} else {
colors.shadow.copy(
alpha = when {
isPressed -> 0.08f
isHovered -> 0.2f
else -> 0.15f
}
)
}
val shadowSpot = if (isFilled) {
colors.accent.copy(
alpha = when {
isPressed -> 0.25f
isHovered -> 0.45f
else -> 0.35f
}
)
} else {
colors.shadow.copy(
alpha = when {
isPressed -> 0.1f
isHovered -> 0.24f
else -> 0.18f
}
)
} }
val checkProgress by animateFloatAsState( val checkProgress by animateFloatAsState(
@ -155,28 +132,29 @@ fun CheckBox(
modifier = modifier modifier = modifier
.size(CheckBoxSize) .size(CheckBoxSize)
.graphicsLayer { translationY = lift.toPx() } .graphicsLayer { translationY = lift.toPx() }
.shadow( .then(
elevation = shadowElevation, if (resolved.shadow.elevation > 0.dp) {
shape = CheckBoxShape, Modifier.shadow(
ambientColor = shadowAmbient, elevation = resolved.shadow.elevation,
spotColor = shadowSpot, shape = resolved.shape,
ambientColor = resolved.shadow.ambientColor,
spotColor = resolved.shadow.spotColor,
)
} else Modifier
) )
.clip(CheckBoxShape) .clip(resolved.shape)
.background(backgroundBrush, CheckBoxShape) .background(resolved.colors.background, resolved.shape)
.border(BorderStroke(borderWidth, borderColor), CheckBoxShape) .border(BorderStroke(0.5.dp, borderColor), resolved.shape)
.drawWithCache { .drawWithCache {
val highlight = Brush.verticalGradient( val highlightBrush = Brush.verticalGradient(
colors = listOf( colors = listOf(resolved.colors.highlight, Color.Transparent),
if (isFilled) Color.White.copy(alpha = 0.25f) else colors.highlight,
Color.Transparent,
),
startY = 0f, startY = 0f,
endY = size.height * 0.5f, endY = size.height * 0.5f,
) )
onDrawWithContent { onDrawWithContent {
drawContent() drawContent()
drawRect( drawRect(
brush = highlight, brush = highlightBrush,
topLeft = Offset.Zero, topLeft = Offset.Zero,
size = size, size = size,
) )
@ -215,7 +193,7 @@ fun CheckBox(
} }
drawPath( drawPath(
path = path, path = path,
color = colors.onAccent, color = checkBoxTheme.checkmarkColor,
style = Stroke( style = Stroke(
width = stroke, width = stroke,
cap = StrokeCap.Round, cap = StrokeCap.Round,
@ -233,8 +211,8 @@ fun CheckBox(
.background( .background(
brush = Brush.horizontalGradient( brush = Brush.horizontalGradient(
colors = listOf( colors = listOf(
colors.onAccent, checkBoxTheme.checkmarkColor,
colors.onAccent.copy(alpha = 0.92f), checkBoxTheme.checkmarkColor.copy(alpha = 0.92f),
) )
), ),
), ),
@ -247,38 +225,41 @@ fun CheckBox(
@Composable @Composable
fun CheckBoxPreview() { fun CheckBoxPreview() {
MaterialTheme { MaterialTheme {
Surface( val theme = rememberBaseUITheme()
color = MaterialTheme.colorScheme.background, CompositionLocalProvider(LocalBaseUITheme provides theme) {
modifier = Modifier.padding(24.dp), Surface(
) { color = MaterialTheme.colorScheme.background,
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { modifier = Modifier.padding(24.dp),
Row( ) {
horizontalArrangement = Arrangement.spacedBy(12.dp), Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
verticalAlignment = Alignment.CenterVertically, Row(
) { horizontalArrangement = Arrangement.spacedBy(12.dp),
CheckBox(state = CheckBoxState.SELECTED, onClick = {}) verticalAlignment = Alignment.CenterVertically,
Text("Selected", style = MaterialTheme.typography.bodyMedium) ) {
} CheckBox(state = CheckBoxState.SELECTED, onClick = {})
Row( Text("Selected", style = MaterialTheme.typography.bodyMedium)
horizontalArrangement = Arrangement.spacedBy(12.dp), }
verticalAlignment = Alignment.CenterVertically, Row(
) { horizontalArrangement = Arrangement.spacedBy(12.dp),
CheckBox(state = CheckBoxState.UNSELECTED, onClick = {}) verticalAlignment = Alignment.CenterVertically,
Text("Unselected", style = MaterialTheme.typography.bodyMedium) ) {
} CheckBox(state = CheckBoxState.UNSELECTED, onClick = {})
Row( Text("Unselected", style = MaterialTheme.typography.bodyMedium)
horizontalArrangement = Arrangement.spacedBy(12.dp), }
verticalAlignment = Alignment.CenterVertically, Row(
) { horizontalArrangement = Arrangement.spacedBy(12.dp),
CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {}) verticalAlignment = Alignment.CenterVertically,
Text("Indeterminate", style = MaterialTheme.typography.bodyMedium) ) {
} CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {})
Row( Text("Indeterminate", style = MaterialTheme.typography.bodyMedium)
horizontalArrangement = Arrangement.spacedBy(12.dp), }
verticalAlignment = Alignment.CenterVertically, Row(
) { horizontalArrangement = Arrangement.spacedBy(12.dp),
CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false) verticalAlignment = Alignment.CenterVertically,
Text("Disabled", style = MaterialTheme.typography.bodyMedium) ) {
CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false)
Text("Disabled", style = MaterialTheme.typography.bodyMedium)
}
} }
} }
} }

View File

@ -27,12 +27,10 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -45,57 +43,78 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
private val ChipTabShape = RoundedCornerShape(10.dp)
private val ChipTabMinHeight = 36.dp private val ChipTabMinHeight = 36.dp
private data class ResolvedChipState(
val colors: BaseUITheme.ButtonColors,
val shape: Shape,
val shadow: BaseUITheme.Shadow,
val border: BaseUITheme.Border,
val padding: androidx.compose.foundation.layout.PaddingValues,
)
@Composable
private fun resolveChipState(
style: BaseUITheme.ButtonStyle,
isPressed: Boolean,
isHovered: Boolean,
): ResolvedChipState {
return when {
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
else -> ResolvedChipState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused)
}
}
private fun Modifier.applyChipShadow(
shadow: BaseUITheme.Shadow,
shape: Shape,
): Modifier {
return if (shadow.elevation > 0.dp) {
this.shadow(
elevation = shadow.elevation,
shape = shape,
ambientColor = shadow.ambientColor,
spotColor = shadow.spotColor,
)
} else {
this
}
}
@Composable @Composable
fun ChipTab( fun ChipTab(
selected: Boolean, selected: Boolean,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 4.dp), theme: BaseUITheme.ChipTabTheme? = null,
content: @Composable RowScope.() -> Unit, content: @Composable RowScope.() -> Unit,
) { ) {
val colors = rememberButtonColors() val baseTheme = LocalBaseUITheme.current.chipTab
val chipTheme = theme ?: baseTheme
val style = if (selected) chipTheme.selected else chipTheme.unselected
val interactionSource = remember { MutableInteractionSource() } val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState() val isPressed by interactionSource.collectIsPressedAsState()
val isHovered by interactionSource.collectIsHoveredAsState() val isHovered by interactionSource.collectIsHoveredAsState()
val gradient = if (selected) { val state = resolveChipState(style, isPressed, isHovered)
primaryGradient(colors, isPressed)
} else {
outlinedGradient(colors, isPressed)
}
val borderColor = if (selected) {
colors.accent
} else {
colors.border.copy(alpha = if (isPressed) 0.7f else 1f)
}
val contentColor = if (selected) colors.onAccent else colors.onContainer
Box( Box(
modifier = modifier modifier = modifier
.defaultMinSize(minHeight = ChipTabMinHeight) .defaultMinSize(minHeight = ChipTabMinHeight)
.hoverable(interactionSource = interactionSource, enabled = enabled) .hoverable(interactionSource = interactionSource, enabled = enabled)
.then( .applyChipShadow(state.shadow, state.shape)
buttonShadow( .clip(state.shape)
shape = ChipTabShape, .background(state.colors.background, state.shape)
pressed = isPressed, .border(BorderStroke(state.border.width, state.border.color), state.shape)
primary = selected,
colors = colors,
hovered = isHovered,
)
)
.clip(ChipTabShape)
.background(gradient, ChipTabShape)
.border(BorderStroke(0.5.dp, borderColor), ChipTabShape)
.clickable( .clickable(
enabled = enabled, enabled = enabled,
interactionSource = interactionSource, interactionSource = interactionSource,
@ -103,13 +122,8 @@ fun ChipTab(
onClick = onClick, onClick = onClick,
) )
.drawWithCache { .drawWithCache {
val highlight = if (selected) {
Color.White.copy(alpha = 0.25f)
} else {
colors.highlight
}
val highlightBrush = Brush.verticalGradient( val highlightBrush = Brush.verticalGradient(
colors = listOf(highlight, Color.Transparent), colors = listOf(state.colors.highlight, Color.Transparent),
startY = 0f, startY = 0f,
endY = size.height * 0.5f, endY = size.height * 0.5f,
) )
@ -122,11 +136,11 @@ fun ChipTab(
) )
} }
} }
.padding(contentPadding), .padding(state.padding),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
LocalContentColor provides contentColor, LocalContentColor provides state.colors.foreground,
LocalTextStyle provides LocalTextStyle.current.copy( LocalTextStyle provides LocalTextStyle.current.copy(
fontWeight = FontWeight.SemiBold, fontWeight = FontWeight.SemiBold,
fontSize = 14.sp, fontSize = 14.sp,
@ -149,14 +163,14 @@ fun ChipTab(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
enabled: Boolean = true, enabled: Boolean = true,
leadingIcon: @Composable (() -> Unit)? = null, leadingIcon: @Composable (() -> Unit)? = null,
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), theme: BaseUITheme.ChipTabTheme? = null,
) { ) {
ChipTab( ChipTab(
selected = selected, selected = selected,
onClick = onClick, onClick = onClick,
modifier = modifier, modifier = modifier,
enabled = enabled, enabled = enabled,
contentPadding = contentPadding, theme = theme,
) { ) {
if (leadingIcon != null) { if (leadingIcon != null) {
leadingIcon() leadingIcon()

View File

@ -43,6 +43,7 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
@ -79,8 +80,10 @@ fun Slider(
steps: Int = 0, steps: Int = 0,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
onValueChangeFinished: () -> Unit = {}, onValueChangeFinished: () -> Unit = {},
theme: BaseUITheme.SliderTheme? = null,
) { ) {
val colors = rememberButtonColors() val baseTheme = LocalBaseUITheme.current.slider
val sliderTheme = theme ?: baseTheme
val isHovered by interactionSource.collectIsHoveredAsState() val isHovered by interactionSource.collectIsHoveredAsState()
val isPressed by interactionSource.collectIsPressedAsState() val isPressed by interactionSource.collectIsPressedAsState()
val density = LocalDensity.current val density = LocalDensity.current
@ -89,13 +92,30 @@ fun Slider(
val range = valueRange.endInclusive - valueRange.start val range = valueRange.endInclusive - valueRange.start
val fraction = if (range > 0f) ((value - valueRange.start) / range).coerceIn(0f, 1f) else 0f val fraction = if (range > 0f) ((value - valueRange.start) / range).coerceIn(0f, 1f) else 0f
val trackActiveColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f) val stateColor = when {
val trackInactiveColor = if (enabled) { isPressed -> sliderTheme.trackActiveColor.pressed
colors.border.copy(alpha = 0.6f) isHovered -> sliderTheme.trackActiveColor.hovered
} else { else -> sliderTheme.trackActiveColor.focused
colors.onContainer.copy(alpha = 0.2f)
} }
val thumbColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f) val inactiveColor = when {
isPressed -> sliderTheme.trackInactiveColor.pressed
isHovered -> sliderTheme.trackInactiveColor.hovered
else -> sliderTheme.trackInactiveColor.focused
}
val thumbColor = when {
isPressed -> sliderTheme.thumbColor.pressed
isHovered -> sliderTheme.thumbColor.hovered
else -> sliderTheme.thumbColor.focused
}
val thumbShadowState = when {
isPressed -> sliderTheme.thumbShadow.pressed
isHovered -> sliderTheme.thumbShadow.hovered
else -> sliderTheme.thumbShadow.focused
}
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
val trackInactiveColor = if (enabled) inactiveColor else inactiveColor.copy(alpha = 0.38f)
val resolvedThumbColor = if (enabled) thumbColor else thumbColor.copy(alpha = 0.38f)
val thumbScale by animateFloatAsState( val thumbScale by animateFloatAsState(
targetValue = when { targetValue = when {
@ -107,12 +127,6 @@ fun Slider(
label = "thumbScale", label = "thumbScale",
) )
val thumbElevation = when {
isPressed -> 2.dp
isHovered -> 8.dp
else -> 5.dp
}
Box( Box(
modifier = modifier modifier = modifier
.fillMaxWidth() .fillMaxWidth()
@ -241,9 +255,9 @@ fun Slider(
val x = trackStart + stepSpacing * i val x = trackStart + stepSpacing * i
drawCircle( drawCircle(
color = if (x <= activeEnd) { color = if (x <= activeEnd) {
colors.onAccent.copy(alpha = 0.5f) sliderTheme.stepActiveColor
} else { } else {
colors.onContainer.copy(alpha = 0.25f) sliderTheme.stepInactiveColor
}, },
radius = 2.dp.toPx(), radius = 2.dp.toPx(),
center = Offset(x, trackY), center = Offset(x, trackY),
@ -267,17 +281,17 @@ fun Slider(
scaleY = thumbScale scaleY = thumbScale
} }
.shadow( .shadow(
elevation = thumbElevation, elevation = thumbShadowState.elevation,
shape = CircleShape, shape = CircleShape,
ambientColor = colors.accent.copy(alpha = 0.35f), ambientColor = thumbShadowState.ambientColor,
spotColor = colors.accent.copy(alpha = 0.5f), spotColor = thumbShadowState.spotColor,
) )
.background( .background(
brush = Brush.radialGradient( brush = Brush.radialGradient(
colors = listOf( colors = listOf(
Color.White.copy(alpha = 0.6f), Color.White.copy(alpha = 0.6f),
thumbColor.copy(alpha = 0.95f), resolvedThumbColor.copy(alpha = 0.95f),
thumbColor, resolvedThumbColor,
), ),
center = Offset(0f, -0.55f), center = Offset(0f, -0.55f),
radius = 1.1f, radius = 1.1f,

View File

@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.defaultMinSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
@ -52,6 +51,7 @@ import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.draw.shadow import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.input.VisualTransformation
@ -61,32 +61,28 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
private val TextFieldShape = RoundedCornerShape(14.dp)
private val TextFieldMinHeight = 44.dp private val TextFieldMinHeight = 44.dp
private data class ResolvedTextFieldState(
val background: Brush,
val highlight: Color,
val shape: Shape,
val border: BaseUITheme.Border,
val shadow: BaseUITheme.Shadow,
val foreground: Color,
)
@Composable @Composable
internal fun textFieldShadow( private fun resolveTextFieldState(
shape: androidx.compose.ui.graphics.Shape, theme: BaseUITheme.TextFieldTheme,
focused: Boolean, isFocused: Boolean,
colors: ButtonColors, isHovered: Boolean,
): Modifier { ): ResolvedTextFieldState {
val elevation = if (focused) 9.dp else 6.dp return when {
val ambient = if (focused) { isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused)
colors.accent.copy(alpha = 0.2f) isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered)
} else { else -> ResolvedTextFieldState(theme.background.pressed, theme.highlight.pressed, theme.shape.pressed, theme.border.pressed, theme.shadow.pressed, theme.foreground.pressed)
colors.shadow.copy(alpha = 0.15f)
} }
val spot = if (focused) {
colors.accent.copy(alpha = 0.25f)
} else {
colors.shadow.copy(alpha = 0.18f)
}
return Modifier.shadow(
elevation = elevation,
shape = shape,
ambientColor = ambient,
spotColor = spot,
)
} }
@Composable @Composable
@ -108,25 +104,29 @@ fun TextField(
visualTransformation: VisualTransformation = VisualTransformation.None, visualTransformation: VisualTransformation = VisualTransformation.None,
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
textStyle: TextStyle = TextStyle.Default, textStyle: TextStyle = TextStyle.Default,
cursorBrush: Color = MaterialTheme.colorScheme.primary, cursorBrush: Color? = null,
theme: BaseUITheme.TextFieldTheme? = null,
) { ) {
val colors = rememberButtonColors() val baseTheme = LocalBaseUITheme.current.textField
val textFieldTheme = theme ?: baseTheme
val isFocused by interactionSource.collectIsFocusedAsState() val isFocused by interactionSource.collectIsFocusedAsState()
val isHovered by interactionSource.collectIsHoveredAsState() val isHovered by interactionSource.collectIsHoveredAsState()
val gradient = outlinedGradient(colors, false) val state = resolveTextFieldState(textFieldTheme, isFocused, isHovered)
val border = when {
isError -> MaterialTheme.colorScheme.error val resolvedBorder = if (isError) {
isFocused -> MaterialTheme.colorScheme.primary BaseUITheme.Border(MaterialTheme.colorScheme.error, state.border.width)
isHovered -> colors.border.copy(alpha = 0.85f) } else {
else -> colors.border state.border
} }
val contentColor = when { val contentColor = when {
!enabled -> colors.onContainer.copy(alpha = 0.38f) !enabled -> state.foreground.copy(alpha = 0.38f)
else -> colors.onContainer else -> state.foreground
} }
val cursor = cursorBrush?.let { SolidColor(it) } ?: textFieldTheme.cursor
Column(modifier = modifier) { Column(modifier = modifier) {
AnimatedVisibility( AnimatedVisibility(
visible = label != null, visible = label != null,
@ -139,13 +139,22 @@ fun TextField(
Box( Box(
modifier = Modifier modifier = Modifier
.defaultMinSize(minHeight = TextFieldMinHeight) .defaultMinSize(minHeight = TextFieldMinHeight)
.then(textFieldShadow(TextFieldShape, isFocused, colors)) .then(
.clip(TextFieldShape) if (state.shadow.elevation > 0.dp) {
.background(gradient, TextFieldShape) Modifier.shadow(
.border(BorderStroke(0.5.dp, border), TextFieldShape) elevation = state.shadow.elevation,
shape = state.shape,
ambientColor = state.shadow.ambientColor,
spotColor = state.shadow.spotColor,
)
} else Modifier
)
.clip(state.shape)
.background(state.background, state.shape)
.border(BorderStroke(resolvedBorder.width, resolvedBorder.color), state.shape)
.drawWithCache { .drawWithCache {
val highlightBrush = Brush.verticalGradient( val highlightBrush = Brush.verticalGradient(
colors = listOf(colors.highlight, Color.Transparent), colors = listOf(state.highlight, Color.Transparent),
startY = 0f, startY = 0f,
endY = size.height * 0.5f, endY = size.height * 0.5f,
) )
@ -158,7 +167,7 @@ fun TextField(
) )
} }
} }
.padding(horizontal = 14.dp, vertical = 10.dp), .padding(textFieldTheme.padding),
) { ) {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@ -177,7 +186,7 @@ fun TextField(
enabled = enabled, enabled = enabled,
readOnly = readOnly, readOnly = readOnly,
textStyle = textStyle.copy(color = contentColor), textStyle = textStyle.copy(color = contentColor),
cursorBrush = SolidColor(cursorBrush), cursorBrush = cursor,
keyboardOptions = keyboardOptions, keyboardOptions = keyboardOptions,
keyboardActions = keyboardActions, keyboardActions = keyboardActions,
singleLine = singleLine, singleLine = singleLine,
@ -210,36 +219,39 @@ fun TextField(
@Composable @Composable
fun TextFieldPreview() { fun TextFieldPreview() {
MaterialTheme { MaterialTheme {
androidx.compose.material3.Surface( val theme = rememberBaseUITheme()
color = MaterialTheme.colorScheme.background, CompositionLocalProvider(LocalBaseUITheme provides theme) {
modifier = Modifier.padding(24.dp), androidx.compose.material3.Surface(
) { color = MaterialTheme.colorScheme.background,
TextField( modifier = Modifier.padding(24.dp),
modifier = Modifier.padding(top = 4.dp), ) {
value = "Twenty One Pilots", TextField(
onValueChange = {}, modifier = Modifier.padding(top = 4.dp),
placeholder = { Text("Placeholder") }, value = "Twenty One Pilots",
label = { onValueChange = {},
Text( placeholder = { Text("Placeholder") },
"Search Field", label = {
modifier = Modifier.padding(bottom = 6.dp), Text(
style = MaterialTheme.typography.labelMedium, "Search Field",
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f), modifier = Modifier.padding(bottom = 6.dp),
) style = MaterialTheme.typography.labelMedium,
}, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
leadingIcon = { )
androidx.compose.material3.Icon( },
imageVector = Iconsax.IconsaxSearchBroken, leadingIcon = {
contentDescription = "Search Icon", androidx.compose.material3.Icon(
) imageVector = Iconsax.IconsaxSearchBroken,
}, contentDescription = "Search Icon",
trailingIcon = { )
androidx.compose.material3.Icon( },
imageVector = Iconsax.IconsaxTrash, trailingIcon = {
contentDescription = "Clear Icon", androidx.compose.material3.Icon(
) imageVector = Iconsax.IconsaxTrash,
}, contentDescription = "Clear Icon",
) )
},
)
}
} }
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -21,10 +21,8 @@ import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith import androidx.compose.animation.togetherWith
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.hoverable import androidx.compose.foundation.hoverable
@ -63,10 +61,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.focus.focusModifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalWindowInfo import androidx.compose.ui.platform.LocalWindowInfo
@ -86,14 +84,13 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.Thumbnail
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
import dev.krtirtho.spotube.core.ui.base.ButtonGroup import dev.krtirtho.spotube.core.ui.base.ButtonGroup
import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider
import dev.krtirtho.spotube.core.ui.base.Card
import dev.krtirtho.spotube.core.ui.base.CheckBox import dev.krtirtho.spotube.core.ui.base.CheckBox
import dev.krtirtho.spotube.core.ui.base.CheckBoxState import dev.krtirtho.spotube.core.ui.base.CheckBoxState
import dev.krtirtho.spotube.core.ui.base.GhostIconButton import dev.krtirtho.spotube.core.ui.base.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.GroupIconButton import dev.krtirtho.spotube.core.ui.base.GroupIconButton
import dev.krtirtho.spotube.core.ui.base.IconButton import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.TextField import dev.krtirtho.spotube.core.ui.base.TextField
import dev.krtirtho.spotube.core.ui.base.buttonShadow
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
import dev.krtirtho.spotube.core.ui.misc.shimmerApply import dev.krtirtho.spotube.core.ui.misc.shimmerApply
@ -348,30 +345,14 @@ fun TrackList(
} }
item(key = "track-card") { item(key = "track-card") {
val colors = rememberButtonColors() Card(
val trackListShape = MaterialTheme.shapes.large
Box(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(vertical = 8.dp) .padding(vertical = 8.dp),
.then(
buttonShadow(
trackListShape,
pressed = false,
primary = false,
colors,
hovered = false
)
)
.clip(trackListShape)
.background(MaterialTheme.colorScheme.surfaceContainer, trackListShape)
.border(BorderStroke(0.5.dp, colors.border), trackListShape)
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(top = 12.dp, bottom = 12.dp)
) { ) {
visibleTracks.forEachIndexed { displayedIndex, track -> visibleTracks.forEachIndexed { displayedIndex, track ->
if (displayedIndex > 0) { if (displayedIndex > 0) {
@ -532,17 +513,20 @@ private fun TrackListRow(
onArtistsOverflowClick: () -> Unit, onArtistsOverflowClick: () -> Unit,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val rowColors = rememberButtonColors() val rowTheme = LocalBaseUITheme.current.listRowTile
val artworkInteractionSource = remember { MutableInteractionSource() } val artworkInteractionSource = remember { MutableInteractionSource() }
val isArtworkHovered by artworkInteractionSource.collectIsHoveredAsState() val isArtworkHovered by artworkInteractionSource.collectIsHoveredAsState()
val rowInteractionSource = remember { MutableInteractionSource() } val rowInteractionSource = remember { MutableInteractionSource() }
val isRowHovered by rowInteractionSource.collectIsHoveredAsState() val isRowHovered by rowInteractionSource.collectIsHoveredAsState()
val rowBackgroundColor = when { val rowBackgroundColor = when {
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) isSelected -> rowTheme.background.selected
isCurrentTrack && isCurrentTrackPlaying -> MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
isCurrentTrack -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f) isCurrentTrack -> rowTheme.background.focused
else -> Color.Transparent else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
} }
val highlightColor = Color.White.copy(
alpha = if (rowTheme.foreground.hovered.luminance() > 0.5f) 0.9f else 0.06f
)
Row( Row(
modifier = modifier modifier = modifier
@ -555,7 +539,7 @@ private fun TrackListRow(
.background(rowBackgroundColor) .background(rowBackgroundColor)
.drawWithCache { .drawWithCache {
val highlightBrush = Brush.verticalGradient( val highlightBrush = Brush.verticalGradient(
colors = listOf(rowColors.highlight, Color.Transparent), colors = listOf(highlightColor, Color.Transparent),
startY = 0f, startY = 0f,
endY = size.height * 0.5f, endY = size.height * 0.5f,
) )

View File

@ -49,6 +49,8 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage import coil3.compose.AsyncImage
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
@ -108,7 +110,11 @@ fun PlayableCard(
SecondaryIconButton( SecondaryIconButton(
onClick = onAddToQueue, onClick = onAddToQueue,
modifier = Modifier.size(30.dp), modifier = Modifier.size(30.dp),
shape = CircleShape, theme = LocalBaseUITheme.current.iconButtons.secondary.copy(
shape = BaseUITheme.InteractionState.fromSingleValue(
CircleShape
)
)
) { ) {
Icon( Icon(
imageVector = Iconsax.IconsaxAddSquare, imageVector = Iconsax.IconsaxAddSquare,
@ -121,7 +127,12 @@ fun PlayableCard(
PrimaryIconButton( PrimaryIconButton(
onClick = onPlay, onClick = onPlay,
modifier = Modifier.size(30.dp), modifier = Modifier.size(30.dp),
shape = CircleShape, theme = LocalBaseUITheme.current.iconButtons.secondary.copy(
shape = BaseUITheme.InteractionState.fromSingleValue(
CircleShape
)
)
) { ) {
Icon( Icon(
imageVector = if (isPlaying) { imageVector = if (isPlaying) {

View File

@ -83,10 +83,13 @@ import dev.krtirtho.spotube.core.audioplayer.LoopState
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.audioplayer.QueueEntry
import dev.krtirtho.spotube.core.navigation.NavigationCommands import dev.krtirtho.spotube.core.navigation.NavigationCommands
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
import dev.krtirtho.spotube.core.ui.base.GhostIconButton import dev.krtirtho.spotube.core.ui.base.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.IconButton import dev.krtirtho.spotube.core.ui.base.IconButton
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.Slider import dev.krtirtho.spotube.core.ui.base.Slider
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors import dev.krtirtho.spotube.core.ui.base.copyShape
import dev.krtirtho.spotube.core.ui.base.invertedButtonStyle
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
import dev.krtirtho.spotube.modules.downloads.DownloadStatus import dev.krtirtho.spotube.modules.downloads.DownloadStatus
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
@ -466,7 +469,10 @@ fun AppExpandedPlayer(
textAlign = TextAlign.Center, textAlign = TextAlign.Center,
) )
} }
IconButton(onClick = onQueue, shape = CircleShape) { IconButton(
onClick = onQueue,
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape)
) {
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue") Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
} }
} }
@ -526,22 +532,14 @@ fun AppExpandedPlayer(
} }
IconButton( IconButton(
onClick = ::onPlayPause, onClick = ::onPlayPause,
colors = rememberButtonColors().copy( theme = invertedButtonStyle().copyShape(CircleShape),
containerDarker = MaterialTheme.colorScheme.onSurface,
containerLighter = MaterialTheme.colorScheme.onSurface,
containerPressed = MaterialTheme.colorScheme.onSurfaceVariant,
shadow = MaterialTheme.colorScheme.onSurfaceVariant,
border = MaterialTheme.colorScheme.onSurfaceVariant
),
modifier = Modifier modifier = Modifier
.size(72.dp), .size(72.dp),
shape = CircleShape,
) { ) {
Icon( Icon(
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play", contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
modifier = Modifier.size(30.dp), modifier = Modifier.size(30.dp),
tint = MaterialTheme.colorScheme.surface,
) )
} }
GhostIconButton(onClick = ::onSkipNext) { GhostIconButton(onClick = ::onSkipNext) {

View File

@ -59,7 +59,8 @@ import dev.krtirtho.spotube.core.ui.base.IconButton
import dev.krtirtho.spotube.core.ui.base.Slider import dev.krtirtho.spotube.core.ui.base.Slider
import dev.krtirtho.spotube.core.ui.base.VariableIconButton import dev.krtirtho.spotube.core.ui.base.VariableIconButton
import dev.krtirtho.spotube.core.ui.base.VariableIconButtonVariant import dev.krtirtho.spotube.core.ui.base.VariableIconButtonVariant
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors import dev.krtirtho.spotube.core.ui.base.copyShape
import dev.krtirtho.spotube.core.ui.base.invertedButtonStyle
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
@ -279,21 +280,13 @@ fun AppLargePlayer(
} }
IconButton( IconButton(
onClick = ::onPlayPause, onClick = ::onPlayPause,
colors = rememberButtonColors().copy( theme = invertedButtonStyle().copyShape(CircleShape),
containerDarker = MaterialTheme.colorScheme.onSurface,
containerLighter = MaterialTheme.colorScheme.onSurface,
containerPressed = MaterialTheme.colorScheme.onSurfaceVariant,
shadow = MaterialTheme.colorScheme.onSurfaceVariant,
border = MaterialTheme.colorScheme.onSurfaceVariant
),
modifier = Modifier modifier = Modifier
.size(50.dp), .size(50.dp),
shape = CircleShape,
) { ) {
Icon( Icon(
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause", contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
tint = MaterialTheme.colorScheme.surface,
) )
} }
GhostIconButton(onClick = ::onSkipNext) { GhostIconButton(onClick = ::onSkipNext) {

View File

@ -21,14 +21,8 @@ import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut import androidx.compose.animation.fadeOut
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.hoverable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsHoveredAsState
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
@ -42,34 +36,26 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.collectAsState
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.NavigationState
import dev.krtirtho.spotube.core.navigation.Navigator import dev.krtirtho.spotube.core.navigation.Navigator
import dev.krtirtho.spotube.core.navigation.Routes import dev.krtirtho.spotube.core.navigation.Routes
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.OutlineButton
import dev.krtirtho.spotube.core.ui.base.SecondaryButton import dev.krtirtho.spotube.core.ui.base.SecondaryButton
import dev.krtirtho.spotube.core.ui.base.buttonShadow import dev.krtirtho.spotube.core.ui.base.copyPadding
import dev.krtirtho.spotube.core.ui.base.outlinedGradient
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
import dev.krtirtho.spotube.modules.library.LibraryState import dev.krtirtho.spotube.modules.library.LibraryState
import dev.krtirtho.spotube.modules.library.LibraryTab import dev.krtirtho.spotube.modules.library.LibraryTab
@ -215,66 +201,16 @@ fun SidebarItem(
SecondaryButton( SecondaryButton(
onClick = onClick, onClick = onClick,
modifier = buttonModifier, modifier = buttonModifier,
contentPadding = contentPadding, theme = LocalBaseUITheme.current.buttons.secondary.copyPadding(contentPadding),
content = itemContent, content = itemContent,
) )
} else { } else {
val colors = rememberButtonColors() OutlineButton(
val interactionSource = remember { MutableInteractionSource() } onClick = onClick,
val isHovered by interactionSource.collectIsHoveredAsState() modifier = buttonModifier,
val isPressed by interactionSource.collectIsPressedAsState() theme = LocalBaseUITheme.current.buttons.outline.copyPadding(contentPadding),
val gradient = outlinedGradient(colors, isPressed) hoverOnly = true,
val border = colors.border.copy(alpha = if (isPressed) 0.7f else 1f) content = itemContent,
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp )
val shape = RoundedCornerShape(14.dp)
Box(
modifier = buttonModifier
.hoverable(interactionSource = interactionSource)
.graphicsLayer { translationY = lift.toPx() }
.clip(shape)
.then(
if (isHovered || isPressed) {
buttonShadow(
shape,
isPressed,
primary = false,
colors,
hovered = isHovered
).background(gradient, shape)
.border(BorderStroke(0.5.dp, border), shape)
.drawWithCache {
val highlightBrush = Brush.verticalGradient(
colors = listOf(colors.highlight, Color.Transparent),
startY = 0f,
endY = size.height * 0.5f,
)
onDrawWithContent {
drawContent()
drawRect(
brush = highlightBrush,
topLeft = androidx.compose.ui.geometry.Offset.Zero,
size = size,
)
}
}
} else {
Modifier
}
)
.clickable(
interactionSource = interactionSource,
indication = ripple(),
onClick = onClick,
)
.padding(contentPadding),
contentAlignment = Alignment.Center,
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
content = itemContent,
)
}
} }
} }

View File

@ -27,6 +27,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.audioplayer.QueueEntry
import dev.krtirtho.spotube.core.ui.base.IconButton import dev.krtirtho.spotube.core.ui.base.IconButton
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.copyShape
import dev.krtirtho.spotube.modules.saved_tracks.SavedState import dev.krtirtho.spotube.modules.saved_tracks.SavedState
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
@ -69,7 +71,7 @@ fun PlayerHeartButton(
IconButton( IconButton(
onClick = ::onLike, onClick = ::onLike,
enabled = isSavedTrackState is SavedState.Success, enabled = isSavedTrackState is SavedState.Success,
shape = CircleShape, theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape),
) { ) {
Icon( Icon(
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart, imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,

View File

@ -62,8 +62,10 @@ import compose.icons.feathericons.MoreVertical
import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.audioplayer.QueueEntry
import dev.krtirtho.spotube.core.di.rememberLogger import dev.krtirtho.spotube.core.di.rememberLogger
import dev.krtirtho.spotube.core.ui.base.GhostIconButton import dev.krtirtho.spotube.core.ui.base.GhostIconButton
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
import dev.krtirtho.spotube.core.ui.base.TextField import dev.krtirtho.spotube.core.ui.base.TextField
import dev.krtirtho.spotube.core.ui.base.copyShape
import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.Iconsax
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
@ -188,7 +190,7 @@ fun PlayerQueueContent(
) )
SecondaryIconButton( SecondaryIconButton(
onClick = viewModel::clearQueue, onClick = viewModel::clearQueue,
shape = MaterialTheme.shapes.small, theme = LocalBaseUITheme.current.iconButtons.secondary.copyShape(MaterialTheme.shapes.small),
) { ) {
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue") Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
} }