diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt index 6424de89..f82904cf 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/App.kt @@ -20,8 +20,11 @@ package dev.krtirtho.spotube import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue 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.graphics.vector.ImageVector import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -100,15 +103,18 @@ fun App( } SpotubeTheme(settings = userSettings) { - AppShell(navigator, navigationState) { - Column { - NavDisplay( - modifier = Modifier.fillMaxSize(), - onBack = navigator::pop, - entries = navigationState.toEntries(koinEntryProvider()) - ) + val baseUITheme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) { + AppShell(navigator, navigationState) { + Column { + NavDisplay( + modifier = Modifier.fillMaxSize(), + onBack = navigator::pop, + entries = navigationState.toEntries(koinEntryProvider()) + ) + } } + content() } - content() } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/AutocompleteTextField.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/AutocompleteTextField.kt index 7883e009..d3dece46 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/AutocompleteTextField.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/AutocompleteTextField.kt @@ -28,8 +28,8 @@ import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState import androidx.compose.foundation.interaction.collectIsHoveredAsState -import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.focusable +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column 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.lazy.LazyColumn import androidx.compose.foundation.lazy.rememberLazyListState -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions 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.IconsaxSearchBroken -private val AutocompleteTextFieldShape = RoundedCornerShape(14.dp) -private val AutocompleteMenuShape = RoundedCornerShape(12.dp) private val AutocompleteTextFieldMinHeight = 44.dp private val AutocompleteMenuDefaultMaxHeight = 300.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 fun AutocompleteTextField( value: String, @@ -115,16 +157,18 @@ fun AutocompleteTextField( visualTransformation: VisualTransformation = VisualTransformation.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, textStyle: TextStyle = TextStyle.Default, - cursorBrush: Color = MaterialTheme.colorScheme.primary, + cursorBrush: Color? = null, onKeyEvent: ((KeyEvent) -> Boolean)? = null, menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight, menuOffset: Dp = AutocompleteMenuDefaultOffset, - menuShape: Shape = AutocompleteMenuShape, - menuShadowElevation: Dp = 8.dp, - menuContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh, + textFieldTheme: BaseUITheme.TextFieldTheme? = null, + menuTheme: BaseUITheme.AutoCompleteMenuTheme? = null, ) { 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 isHovered by interactionSource.collectIsHoveredAsState() @@ -174,19 +218,21 @@ fun AutocompleteTextField( ) } - val gradient = outlinedGradient(colors, false) - val border = when { - isError -> MaterialTheme.colorScheme.error - isFocused -> MaterialTheme.colorScheme.primary - isHovered -> colors.border.copy(alpha = 0.85f) - else -> colors.border + val fieldState = resolveTextFieldState(resolvedTextFieldTheme, isFocused, isHovered) + + val resolvedBorder = if (isError) { + BaseUITheme.Border(MaterialTheme.colorScheme.error, fieldState.border.width) + } else { + fieldState.border } val contentColor = when { - !enabled -> colors.onContainer.copy(alpha = 0.38f) - else -> colors.onContainer + !enabled -> fieldState.foreground.copy(alpha = 0.38f) + else -> fieldState.foreground } + val cursor = cursorBrush?.let { SolidColor(it) } ?: resolvedTextFieldTheme.cursor + fun navigate(delta: Int) { if (items.isEmpty()) return selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size @@ -207,13 +253,25 @@ fun AutocompleteTextField( .fillMaxWidth() .onSizeChanged { textFieldSize = it } .defaultMinSize(minHeight = AutocompleteTextFieldMinHeight) - .then(textFieldShadow(AutocompleteTextFieldShape, isFocused, colors)) - .clip(AutocompleteTextFieldShape) - .background(gradient, AutocompleteTextFieldShape) - .border(BorderStroke(.5.dp, border), AutocompleteTextFieldShape) + .then( + if (fieldState.shadow.elevation > 0.dp) { + Modifier.shadow( + 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 { val highlightBrush = Brush.verticalGradient( - colors = listOf(colors.highlight, Color.Transparent), + colors = listOf(fieldState.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) @@ -226,7 +284,7 @@ fun AutocompleteTextField( ) } } - .padding(horizontal = 14.dp, vertical = 10.dp), + .padding(resolvedTextFieldTheme.padding), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -294,7 +352,7 @@ fun AutocompleteTextField( enabled = enabled, readOnly = readOnly, textStyle = textStyle.copy(color = contentColor), - cursorBrush = SolidColor(cursorBrush), + cursorBrush = cursor, keyboardOptions = keyboardOptions, keyboardActions = wrappedKeyboardActions, singleLine = singleLine, @@ -340,9 +398,9 @@ fun AutocompleteTextField( modifier = Modifier .width(textFieldWidthDp) .heightIn(max = menuMaxHeight) - .shadow(menuShadowElevation, menuShape) - .background(menuContainerColor, menuShape) - .clip(menuShape) + .shadow(resolvedMenuTheme.shadowElevation, resolvedMenuTheme.shape) + .background(resolvedMenuTheme.background, resolvedMenuTheme.shape) + .clip(resolvedMenuTheme.shape) .focusRequester(popupFocusRequester) .focusable() .onFocusChanged { state -> @@ -412,70 +470,73 @@ fun AutocompleteTextField( @Composable private fun AutocompleteTextFieldPreview() { MaterialTheme { - androidx.compose.material3.Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - val artists = remember { - listOf( - "Twenty One Pilots", - "The Beatles", - "Adele", - "Drake", - "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", + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + androidx.compose.material3.Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), + ) { + val artists = remember { + listOf( + "Twenty One Pilots", + "The Beatles", + "Adele", + "Drake", + "Taylor Swift", + "Arctic Monkeys", + "Billie Eilish", ) - }, - modifier = Modifier.fillMaxWidth(), - 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), - ) { + } + 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 = null, - modifier = Modifier.size(16.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, + contentDescription = "Search", ) - Text( - text = item, - style = MaterialTheme.typography.bodyMedium, - ) - } - }, - ) + }, + modifier = Modifier.fillMaxWidth(), + 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, + ) + } + }, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Buttons.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Buttons.kt index 108b2ee2..3e4e9654 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Buttons.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Buttons.kt @@ -44,7 +44,6 @@ import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.Immutable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Alignment @@ -54,8 +53,8 @@ import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.graphicsLayer -import androidx.compose.ui.graphics.luminance import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -71,163 +70,45 @@ import dev.krtirtho.spotube.resources.iconsax.IconsaxNext import dev.krtirtho.spotube.resources.iconsax.IconsaxShare import dev.krtirtho.spotube.resources.iconsax.User -private val ButtonShape = RoundedCornerShape(14.dp) -private val GroupShape = RoundedCornerShape(14.dp) private val BadgeShape = RoundedCornerShape(11.dp) private val ButtonMinHeight = 40.dp private val SquareButtonSize = 40.dp -@Immutable -data class ButtonColors( - val containerLighter: Color, - val containerDarker: Color, - val containerPressed: Color, - val onContainer: Color, - val border: Color, - val accent: Color, - val onAccent: Color, - val secondaryContainer: Color, - val onSecondaryContainer: Color, - val secondaryContainerPressed: Color, - val secondaryHighlight: Color, - val shadow: Color, - val highlight: Color, +private data class ResolvedButtonState( + val colors: BaseUITheme.ButtonColors, + val shape: Shape, + val shadow: BaseUITheme.Shadow, + val border: BaseUITheme.Border, + val padding: PaddingValues, ) @Composable -fun rememberButtonColors(): ButtonColors { - val scheme = MaterialTheme.colorScheme - val isLight = scheme.surface.luminance() > 0.5f - - return remember(scheme) { - ButtonColors( - containerLighter = if (isLight) { - Color.White - } else { - scheme.surfaceContainerHigh - }, - containerDarker = if (isLight) { - Color(0xFFF2F2F4) - } else { - scheme.surfaceContainer - }, - containerPressed = if (isLight) { - Color(0xFFE0E0E3) - } else { - scheme.surfaceContainerHighest - }, - onContainer = scheme.onSurface, - border = scheme.outlineVariant, - accent = scheme.primary, - onAccent = scheme.onPrimary, - secondaryContainer = scheme.secondaryContainer, - onSecondaryContainer = scheme.onSecondaryContainer, - secondaryContainerPressed = scheme.secondaryContainer.copy( - alpha = if (isLight) 0.85f else 0.92f - ), - secondaryHighlight = if (isLight) { - Color.White.copy(alpha = 0.5f) - } else { - Color.White.copy(alpha = 0.08f) - }, - shadow = scheme.onSurface.copy(alpha = 0.12f), - highlight = if (isLight) { - Color.White.copy(alpha = 0.9f) - } else { - Color.White.copy(alpha = 0.06f) - }, - ) +private fun resolveButtonState( + style: BaseUITheme.ButtonStyle, + isPressed: Boolean, + isHovered: Boolean, +): ResolvedButtonState { + return when { + isPressed -> ResolvedButtonState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed) + isHovered -> ResolvedButtonState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered) + else -> ResolvedButtonState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused) } } -@Composable -fun outlinedGradient(colors: ButtonColors, pressed: Boolean): Brush { - val top = if (pressed) colors.containerPressed else colors.containerLighter - val bottom = if (pressed) colors.containerPressed else colors.containerDarker - return remember(colors, pressed) { Brush.verticalGradient(listOf(top, bottom)) } -} - -@Composable -fun primaryGradient(colors: ButtonColors, pressed: Boolean): Brush { - val alpha = if (pressed) 0.85f else 1f - return remember(colors, pressed) { - Brush.verticalGradient( - listOf(colors.accent.copy(alpha = alpha), colors.accent) - ) - } -} - -@Composable -private fun secondaryGradient(colors: ButtonColors, pressed: Boolean): Brush { - val top = if (pressed) colors.secondaryContainerPressed else colors.secondaryContainer - val bottom = if (pressed) { - colors.secondaryContainerPressed - } else { - colors.secondaryContainer.copy( - alpha = if (top.luminance() > 0.5f) 0.92f else 1f - ) - } - return remember(colors, pressed) { Brush.verticalGradient(listOf(top, bottom)) } -} - -@Composable -private fun badgeGradient(colors: ButtonColors): Brush = remember(colors) { - Brush.verticalGradient(listOf(colors.containerLighter, colors.containerDarker)) -} - -@Composable -internal fun buttonShadow( - shape: androidx.compose.ui.graphics.Shape, - pressed: Boolean, - primary: Boolean, - colors: ButtonColors, - hovered: Boolean = false, +private fun Modifier.applyShadow( + shadow: BaseUITheme.Shadow, + shape: Shape, ): Modifier { - val elevation = when { - pressed -> 1.dp - hovered -> 9.dp - else -> 6.dp - } - val ambient = if (primary) { - colors.accent.copy( - alpha = when { - pressed -> 0.2f - hovered -> 0.45f - else -> 0.35f - } + return if (shadow.elevation > 0.dp) { + this.shadow( + elevation = shadow.elevation, + shape = shape, + ambientColor = shadow.ambientColor, + spotColor = shadow.spotColor, ) } else { - colors.shadow.copy( - alpha = when { - pressed -> 0.08f - hovered -> 0.22f - else -> 0.15f - } - ) + this } - val spot = if (primary) { - colors.accent.copy( - alpha = when { - pressed -> 0.25f - hovered -> 0.5f - else -> 0.4f - } - ) - } else { - colors.shadow.copy( - alpha = when { - pressed -> 0.1f - hovered -> 0.26f - else -> 0.18f - } - ) - } - return Modifier.shadow( - elevation = elevation, - shape = shape, - ambientColor = ambient, - spotColor = spot, - ) } @Composable @@ -235,18 +116,16 @@ fun OutlineButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp), hoverOnly: Boolean = false, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable RowScope.() -> Unit, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.outline val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val isHovered by interactionSource.collectIsHoveredAsState() - val gradient = outlinedGradient(colors, isPressed) - val border = colors.border.copy(alpha = if (isPressed) 0.7f else 1f) + val state = resolveButtonState(style, isPressed, isHovered) val lift = if (isHovered && !isPressed) (-1).dp else 0.dp Box( @@ -255,18 +134,12 @@ fun OutlineButton( .hoverable(interactionSource = interactionSource, enabled = enabled) .graphicsLayer { translationY = lift.toPx() } .then( - if (hoverOnly && !isHovered) Modifier else buttonShadow( - shape, - isPressed, - primary = false, - colors, - hovered = isHovered - ) + if (hoverOnly && !isHovered) Modifier else Modifier.applyShadow(state.shadow, state.shape) ) - .clip(shape) + .clip(state.shape) .then( - if (hoverOnly && !isHovered) Modifier else Modifier.background(gradient, shape) - .border(BorderStroke(0.5.dp, border), shape) + if (hoverOnly && !isHovered) Modifier else Modifier.background(state.colors.background, state.shape) + .border(BorderStroke(state.border.width, state.border.color), state.shape) ) .clickable( enabled = enabled, @@ -276,7 +149,7 @@ fun OutlineButton( ) .then(if (hoverOnly && !isHovered) Modifier else Modifier.drawWithCache { val highlightBrush = Brush.verticalGradient( - colors = listOf(colors.highlight, Color.Transparent), + colors = listOf(state.colors.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) @@ -289,7 +162,7 @@ fun OutlineButton( ) } }) - .padding(contentPadding), + .padding(state.padding), contentAlignment = Alignment.Center, ) { Row( @@ -305,16 +178,15 @@ fun PrimaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp), - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable RowScope.() -> Unit, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.primary val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val isHovered by interactionSource.collectIsHoveredAsState() - val gradient = primaryGradient(colors, isPressed) + val state = resolveButtonState(style, isPressed, isHovered) val lift = if (isHovered && !isPressed) (-1).dp else 0.dp Box( @@ -322,10 +194,10 @@ fun PrimaryButton( .defaultMinSize(minHeight = ButtonMinHeight) .hoverable(interactionSource = interactionSource, enabled = enabled) .graphicsLayer { translationY = lift.toPx() } - .then(buttonShadow(shape, isPressed, primary = true, colors, hovered = isHovered)) - .clip(shape) - .background(gradient, shape) - .border(BorderStroke(0.5.dp, colors.accent), shape) + .applyShadow(state.shadow, state.shape) + .clip(state.shape) + .background(state.colors.background, state.shape) + .border(BorderStroke(state.border.width, state.border.color), state.shape) .clickable( enabled = enabled, interactionSource = interactionSource, @@ -334,7 +206,7 @@ fun PrimaryButton( ) .drawWithCache { val highlightBrush = Brush.verticalGradient( - colors = listOf(Color.White.copy(alpha = 0.25f), Color.Transparent), + colors = listOf(state.colors.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) @@ -347,10 +219,10 @@ fun PrimaryButton( ) } } - .padding(contentPadding), + .padding(state.padding), contentAlignment = Alignment.Center, ) { - CompositionLocalProvider(LocalContentColor provides colors.onAccent) { + CompositionLocalProvider(LocalContentColor provides state.colors.foreground) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -365,16 +237,15 @@ fun SecondaryButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp), - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable RowScope.() -> Unit, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.secondary val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val isHovered by interactionSource.collectIsHoveredAsState() - val gradient = secondaryGradient(colors, isPressed) + val state = resolveButtonState(style, isPressed, isHovered) val lift = if (isHovered && !isPressed) (-1).dp else 0.dp Box( @@ -382,13 +253,10 @@ fun SecondaryButton( .defaultMinSize(minHeight = ButtonMinHeight) .hoverable(interactionSource = interactionSource, enabled = enabled) .graphicsLayer { translationY = lift.toPx() } - .then(buttonShadow(shape, isPressed, primary = false, colors, hovered = isHovered)) - .clip(shape) - .background(gradient, shape) - .border( - BorderStroke(0.5.dp, colors.secondaryContainer.copy(alpha = 0.5f)), - shape, - ) + .applyShadow(state.shadow, state.shape) + .clip(state.shape) + .background(state.colors.background, state.shape) + .border(BorderStroke(state.border.width, state.border.color), state.shape) .clickable( enabled = enabled, interactionSource = interactionSource, @@ -397,7 +265,7 @@ fun SecondaryButton( ) .drawWithCache { val highlightBrush = Brush.verticalGradient( - colors = listOf(colors.secondaryHighlight, Color.Transparent), + colors = listOf(state.colors.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) @@ -410,10 +278,10 @@ fun SecondaryButton( ) } } - .padding(contentPadding), + .padding(state.padding), contentAlignment = Alignment.Center, ) { - CompositionLocalProvider(LocalContentColor provides colors.onSecondaryContainer) { + CompositionLocalProvider(LocalContentColor provides state.colors.foreground) { Row( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), @@ -428,40 +296,54 @@ fun IconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.iconButtons.outline OutlineButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - contentPadding = PaddingValues(8.dp), - colors = colors, + theme = style, ) { content() } } @Composable - fun GhostIconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { - OutlineButton( - onClick = onClick, - modifier = modifier, - enabled = enabled, - shape = shape, - contentPadding = PaddingValues(8.dp), - hoverOnly = true, - colors = colors, + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.ghost + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + val isHovered by interactionSource.collectIsHoveredAsState() + val state = resolveButtonState(style, isPressed, isHovered) + val lift = if (isHovered && !isPressed) (-1).dp else 0.dp + + Box( + modifier = modifier + .defaultMinSize(minHeight = ButtonMinHeight) + .hoverable(interactionSource = interactionSource, enabled = enabled) + .graphicsLayer { translationY = lift.toPx() } + .clip(state.shape) + .then( + if (!isHovered && !isPressed) Modifier else Modifier.background(state.colors.background, state.shape) + ) + .clickable( + enabled = enabled, + interactionSource = interactionSource, + indication = ripple(), + onClick = onClick, + ) + .padding(state.padding), + contentAlignment = Alignment.Center, ) { content() } @@ -472,17 +354,16 @@ fun PrimaryIconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.iconButtons.primary PrimaryButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - contentPadding = PaddingValues(8.dp), - colors = colors, + theme = style, ) { content() } @@ -493,17 +374,16 @@ fun SecondaryIconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.iconButtons.secondary SecondaryButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - contentPadding = PaddingValues(8.dp), - colors = colors, + theme = style, ) { content() } @@ -513,15 +393,17 @@ fun SecondaryIconButton( fun ButtonBadge( count: Int, modifier: Modifier = Modifier, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.outline + val state = resolveButtonState(style, isPressed = false, isHovered = false) Box( modifier = modifier .heightIn(min = 22.dp) .defaultMinSize(minWidth = 22.dp) - .background(badgeGradient(colors), BadgeShape) - .border(1.dp, colors.border, BadgeShape) + .background(state.colors.background, BadgeShape) + .border(1.dp, state.border.color, BadgeShape) .padding(horizontal = 7.dp), contentAlignment = Alignment.Center, ) { @@ -531,7 +413,7 @@ fun ButtonBadge( fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, maxLines = 1, softWrap = false, - color = colors.onContainer.copy(alpha = 0.7f), + color = state.colors.foreground.copy(alpha = 0.7f), ) } } @@ -541,18 +423,19 @@ fun GroupButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 11.dp), - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable RowScope.() -> Unit, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.outline val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val isHovered by interactionSource.collectIsHoveredAsState() + val state = resolveButtonState(style, isPressed, isHovered) val overlay = when { - isPressed -> colors.containerPressed - isHovered -> colors.containerPressed.copy(alpha = 0.5f) - else -> Color.Transparent + isPressed -> state.colors.background + isHovered -> state.colors.background + else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)) } val lift = if (isHovered && !isPressed) (-1).dp else 0.dp @@ -568,7 +451,7 @@ fun GroupButton( indication = ripple(), onClick = onClick, ) - .padding(contentPadding), + .padding(state.padding), contentAlignment = Alignment.Center, ) { Row( @@ -584,17 +467,19 @@ fun GroupIconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.outline val interactionSource = remember { MutableInteractionSource() } val isPressed by interactionSource.collectIsPressedAsState() val isHovered by interactionSource.collectIsHoveredAsState() + val state = resolveButtonState(style, isPressed, isHovered) val overlay = when { - isPressed -> colors.containerPressed - isHovered -> colors.containerPressed.copy(alpha = 0.5f) - else -> Color.Transparent + isPressed -> state.colors.background + isHovered -> state.colors.background + else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)) } val lift = if (isHovered && !isPressed) (-1).dp else 0.dp @@ -619,28 +504,23 @@ fun GroupIconButton( @Composable fun ButtonGroup( modifier: Modifier = Modifier, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.outline + val state = resolveButtonState(style, isPressed = false, isHovered = false) + val groupShape = state.shape Surface( modifier = modifier .defaultMinSize(minHeight = ButtonMinHeight) - .then( - buttonShadow( - GroupShape, - pressed = false, - primary = false, - colors, - hovered = false - ) - ), - shape = GroupShape, + .applyShadow(state.shadow, groupShape), + shape = groupShape, color = Color.Transparent, - border = BorderStroke(0.5.dp, colors.border), + border = BorderStroke(state.border.width, state.border.color), ) { Box( - modifier = Modifier.background(outlinedGradient(colors, false), GroupShape), + modifier = Modifier.background(state.colors.background, groupShape), ) { Row(verticalAlignment = Alignment.CenterVertically) { content() @@ -651,14 +531,16 @@ fun ButtonGroup( @Composable fun ButtonGroupDivider( - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, ) { - val colors = colors ?: rememberButtonColors() + val baseTheme = LocalBaseUITheme.current + val style = theme ?: baseTheme.buttons.outline + val state = resolveButtonState(style, isPressed = false, isHovered = false) Box( modifier = Modifier .width(1.dp) .heightIn(min = 20.dp) - .background(colors.border), + .background(state.border.color), ) } @@ -666,46 +548,49 @@ fun ButtonGroupDivider( @Composable private fun ButtonsRow1Preview() { MaterialTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), ) { - OutlineButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxShare, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text( - "Copy link", - maxLines = 1, - softWrap = false, - ) - } - OutlineButton(onClick = {}) { - Icon( - imageVector = Iconsax.User, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text("Login", maxLines = 1, softWrap = false) - } - PrimaryButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxAddSquare, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text( - "Sign Up", - maxLines = 1, - softWrap = false, - fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlineButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxShare, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text( + "Copy link", + maxLines = 1, + softWrap = false, + ) + } + OutlineButton(onClick = {}) { + Icon( + imageVector = Iconsax.User, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text("Login", maxLines = 1, softWrap = false) + } + PrimaryButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxAddSquare, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text( + "Sign Up", + maxLines = 1, + softWrap = false, + fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, + ) + } } } } @@ -716,56 +601,59 @@ private fun ButtonsRow1Preview() { @Composable private fun ButtonsRow2Preview() { MaterialTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), ) { - ButtonGroup { - GroupButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxDocumentText, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text("Documents", maxLines = 1, softWrap = false) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ButtonGroup { + GroupButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxDocumentText, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text("Documents", maxLines = 1, softWrap = false) + } + ButtonGroupDivider() + GroupButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxShare, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text("Export", maxLines = 1, softWrap = false) + } + ButtonGroupDivider() + GroupIconButton(onClick = {}) { + Icon( + imageVector = Iconsax.Iconsax3DotsMore, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } } - ButtonGroupDivider() - GroupButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxShare, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text("Export", maxLines = 1, softWrap = false) - } - ButtonGroupDivider() - GroupIconButton(onClick = {}) { - Icon( - imageVector = Iconsax.Iconsax3DotsMore, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - } - ButtonGroup { - GroupIconButton(onClick = {}) { - Icon( - imageVector = Iconsax.ArrowLeft3, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - ButtonGroupDivider() - GroupIconButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxNext, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) + ButtonGroup { + GroupIconButton(onClick = {}) { + Icon( + imageVector = Iconsax.ArrowLeft3, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + ButtonGroupDivider() + GroupIconButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxNext, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } } } } @@ -777,40 +665,43 @@ private fun ButtonsRow2Preview() { @Composable private fun ButtonsRow3Preview() { MaterialTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), ) { - OutlineButton(onClick = {}) { - Text("Cancel", maxLines = 1, softWrap = false) - } - PrimaryButton(onClick = {}) { - Text( - "Done", - maxLines = 1, - softWrap = false, - fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, - ) - } - IconButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxMagic, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - OutlineButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxHeart, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - Text("Like", maxLines = 1, softWrap = false) - ButtonBadge(count = 2) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlineButton(onClick = {}) { + Text("Cancel", maxLines = 1, softWrap = false) + } + PrimaryButton(onClick = {}) { + Text( + "Done", + maxLines = 1, + softWrap = false, + fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, + ) + } + IconButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxMagic, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + OutlineButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxHeart, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + Text("Like", maxLines = 1, softWrap = false) + ButtonBadge(count = 2) + } } } } @@ -821,38 +712,41 @@ private fun ButtonsRow3Preview() { @Composable private fun ButtonsRow4Preview() { MaterialTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), ) { - ButtonGroup { - GroupIconButton(onClick = {}) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + ButtonGroup { + GroupIconButton(onClick = {}) { + Icon( + imageVector = Iconsax.ArrowLeft3, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + ButtonGroupDivider() + GroupIconButton(onClick = {}) { + Icon( + imageVector = Iconsax.IconsaxNext, + contentDescription = null, + modifier = Modifier.size(18.dp), + ) + } + } + OutlineButton(onClick = {}) { + Text("Forward", maxLines = 1, softWrap = false) Icon( - imageVector = Iconsax.ArrowLeft3, + imageVector = Iconsax.IconsaxArrowSquareUp, contentDescription = null, modifier = Modifier.size(18.dp), ) } - ButtonGroupDivider() - GroupIconButton(onClick = {}) { - Icon( - imageVector = Iconsax.IconsaxNext, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) - } - } - OutlineButton(onClick = {}) { - Text("Forward", maxLines = 1, softWrap = false) - Icon( - imageVector = Iconsax.IconsaxArrowSquareUp, - contentDescription = null, - modifier = Modifier.size(18.dp), - ) } } } @@ -863,27 +757,30 @@ private fun ButtonsRow4Preview() { @Composable private fun ButtonStylesPreview() { MaterialTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(12.dp), + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), ) { - OutlineButton(onClick = {}) { - Text("Outline", maxLines = 1, softWrap = false) - } - SecondaryButton(onClick = {}) { - Text("Secondary", maxLines = 1, softWrap = false) - } - PrimaryButton(onClick = {}) { - Text( - "Primary", - maxLines = 1, - softWrap = false, - fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, - ) + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlineButton(onClick = {}) { + Text("Outline", maxLines = 1, softWrap = false) + } + SecondaryButton(onClick = {}) { + Text("Secondary", maxLines = 1, softWrap = false) + } + PrimaryButton(onClick = {}) { + Text( + "Primary", + maxLines = 1, + softWrap = false, + fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold, + ) + } } } } @@ -903,8 +800,7 @@ fun VariableIconButton( onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - shape: androidx.compose.ui.graphics.Shape = ButtonShape, - colors: ButtonColors? = null, + theme: BaseUITheme.ButtonStyle? = null, content: @Composable () -> Unit, ) { when (variant) { @@ -912,8 +808,7 @@ fun VariableIconButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - colors = colors, + theme = theme, content = content, ) @@ -921,8 +816,7 @@ fun VariableIconButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - colors = colors, + theme = theme, content = content, ) @@ -930,8 +824,7 @@ fun VariableIconButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - colors = colors, + theme = theme, content = content, ) @@ -939,8 +832,7 @@ fun VariableIconButton( onClick = onClick, modifier = modifier, enabled = enabled, - shape = shape, - colors = colors, + theme = theme, content = content, ) } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Card.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Card.kt new file mode 100644 index 00000000..61e23930 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Card.kt @@ -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 . + */ + +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)) + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/CheckBox.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/CheckBox.kt index d3ebf331..f9c47abb 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/CheckBox.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/CheckBox.kt @@ -39,6 +39,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.ripple import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.remember 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.Color import androidx.compose.ui.graphics.Path +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.StrokeCap import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Stroke @@ -65,9 +67,28 @@ enum class CheckBoxState { CLEAR; } -private val CheckBoxShape = RoundedCornerShape(6.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 fun CheckBox( state: CheckBoxState = CheckBoxState.UNSELECTED, @@ -75,69 +96,25 @@ fun CheckBox( modifier: Modifier = Modifier, enabled: Boolean = true, 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 isPressed by source.collectIsPressedAsState() val isHovered by source.collectIsHoveredAsState() - val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp val isSelected = state == CheckBoxState.SELECTED val isIndeterminate = state == CheckBoxState.INDETERMINATE val isFilled = isSelected || isIndeterminate - val backgroundBrush = if (isFilled) { - primaryGradient(colors, isPressed) - } else { - 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 style = if (isFilled) checkBoxTheme.selected else checkBoxTheme.unselected + val resolved = resolveCheckBoxState(style, isPressed, isHovered) + val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp - val shadowElevation = when { - isPressed -> 1.dp - isFilled && isHovered -> 8.dp - 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 borderColor = when { + !enabled -> resolved.colors.foreground.copy(alpha = 0.38f) + else -> resolved.border.color } val checkProgress by animateFloatAsState( @@ -155,28 +132,29 @@ fun CheckBox( modifier = modifier .size(CheckBoxSize) .graphicsLayer { translationY = lift.toPx() } - .shadow( - elevation = shadowElevation, - shape = CheckBoxShape, - ambientColor = shadowAmbient, - spotColor = shadowSpot, + .then( + if (resolved.shadow.elevation > 0.dp) { + Modifier.shadow( + elevation = resolved.shadow.elevation, + shape = resolved.shape, + ambientColor = resolved.shadow.ambientColor, + spotColor = resolved.shadow.spotColor, + ) + } else Modifier ) - .clip(CheckBoxShape) - .background(backgroundBrush, CheckBoxShape) - .border(BorderStroke(borderWidth, borderColor), CheckBoxShape) + .clip(resolved.shape) + .background(resolved.colors.background, resolved.shape) + .border(BorderStroke(0.5.dp, borderColor), resolved.shape) .drawWithCache { - val highlight = Brush.verticalGradient( - colors = listOf( - if (isFilled) Color.White.copy(alpha = 0.25f) else colors.highlight, - Color.Transparent, - ), + val highlightBrush = Brush.verticalGradient( + colors = listOf(resolved.colors.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) onDrawWithContent { drawContent() drawRect( - brush = highlight, + brush = highlightBrush, topLeft = Offset.Zero, size = size, ) @@ -215,7 +193,7 @@ fun CheckBox( } drawPath( path = path, - color = colors.onAccent, + color = checkBoxTheme.checkmarkColor, style = Stroke( width = stroke, cap = StrokeCap.Round, @@ -233,8 +211,8 @@ fun CheckBox( .background( brush = Brush.horizontalGradient( colors = listOf( - colors.onAccent, - colors.onAccent.copy(alpha = 0.92f), + checkBoxTheme.checkmarkColor, + checkBoxTheme.checkmarkColor.copy(alpha = 0.92f), ) ), ), @@ -247,38 +225,41 @@ fun CheckBox( @Composable fun CheckBoxPreview() { MaterialTheme { - Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CheckBox(state = CheckBoxState.SELECTED, onClick = {}) - Text("Selected", style = MaterialTheme.typography.bodyMedium) - } - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CheckBox(state = CheckBoxState.UNSELECTED, onClick = {}) - Text("Unselected", style = MaterialTheme.typography.bodyMedium) - } - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {}) - Text("Indeterminate", style = MaterialTheme.typography.bodyMedium) - } - Row( - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false) - Text("Disabled", style = MaterialTheme.typography.bodyMedium) + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), + ) { + Column(verticalArrangement = Arrangement.spacedBy(16.dp)) { + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CheckBox(state = CheckBoxState.SELECTED, onClick = {}) + Text("Selected", style = MaterialTheme.typography.bodyMedium) + } + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CheckBox(state = CheckBoxState.UNSELECTED, onClick = {}) + Text("Unselected", style = MaterialTheme.typography.bodyMedium) + } + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {}) + Text("Indeterminate", style = MaterialTheme.typography.bodyMedium) + } + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false) + Text("Disabled", style = MaterialTheme.typography.bodyMedium) + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/ChipTabs.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/ChipTabs.kt index 1738ea26..2b988826 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/ChipTabs.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/ChipTabs.kt @@ -27,12 +27,10 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.LocalContentColor import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.Text @@ -45,57 +43,78 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithCache +import androidx.compose.ui.draw.shadow import androidx.compose.ui.graphics.Brush 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.unit.dp import androidx.compose.ui.unit.sp -private val ChipTabShape = RoundedCornerShape(10.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 fun ChipTab( selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier, enabled: Boolean = true, - contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 4.dp), + theme: BaseUITheme.ChipTabTheme? = null, 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 isPressed by interactionSource.collectIsPressedAsState() val isHovered by interactionSource.collectIsHoveredAsState() - val gradient = if (selected) { - 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 + val state = resolveChipState(style, isPressed, isHovered) Box( modifier = modifier .defaultMinSize(minHeight = ChipTabMinHeight) .hoverable(interactionSource = interactionSource, enabled = enabled) - .then( - buttonShadow( - shape = ChipTabShape, - pressed = isPressed, - primary = selected, - colors = colors, - hovered = isHovered, - ) - ) - .clip(ChipTabShape) - .background(gradient, ChipTabShape) - .border(BorderStroke(0.5.dp, borderColor), ChipTabShape) + .applyChipShadow(state.shadow, state.shape) + .clip(state.shape) + .background(state.colors.background, state.shape) + .border(BorderStroke(state.border.width, state.border.color), state.shape) .clickable( enabled = enabled, interactionSource = interactionSource, @@ -103,13 +122,8 @@ fun ChipTab( onClick = onClick, ) .drawWithCache { - val highlight = if (selected) { - Color.White.copy(alpha = 0.25f) - } else { - colors.highlight - } val highlightBrush = Brush.verticalGradient( - colors = listOf(highlight, Color.Transparent), + colors = listOf(state.colors.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) @@ -122,11 +136,11 @@ fun ChipTab( ) } } - .padding(contentPadding), + .padding(state.padding), contentAlignment = Alignment.Center, ) { CompositionLocalProvider( - LocalContentColor provides contentColor, + LocalContentColor provides state.colors.foreground, LocalTextStyle provides LocalTextStyle.current.copy( fontWeight = FontWeight.SemiBold, fontSize = 14.sp, @@ -149,14 +163,14 @@ fun ChipTab( modifier: Modifier = Modifier, enabled: Boolean = true, leadingIcon: @Composable (() -> Unit)? = null, - contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp), + theme: BaseUITheme.ChipTabTheme? = null, ) { ChipTab( selected = selected, onClick = onClick, modifier = modifier, enabled = enabled, - contentPadding = contentPadding, + theme = theme, ) { if (leadingIcon != null) { leadingIcon() @@ -168,4 +182,4 @@ fun ChipTab( fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal, ) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Slider.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Slider.kt index affee671..e957797b 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Slider.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Slider.kt @@ -43,6 +43,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableIntStateOf @@ -79,8 +80,10 @@ fun Slider( steps: Int = 0, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, onValueChangeFinished: () -> Unit = {}, + theme: BaseUITheme.SliderTheme? = null, ) { - val colors = rememberButtonColors() + val baseTheme = LocalBaseUITheme.current.slider + val sliderTheme = theme ?: baseTheme val isHovered by interactionSource.collectIsHoveredAsState() val isPressed by interactionSource.collectIsPressedAsState() val density = LocalDensity.current @@ -89,13 +92,30 @@ fun Slider( val range = valueRange.endInclusive - valueRange.start 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 trackInactiveColor = if (enabled) { - colors.border.copy(alpha = 0.6f) - } else { - colors.onContainer.copy(alpha = 0.2f) + val stateColor = when { + isPressed -> sliderTheme.trackActiveColor.pressed + isHovered -> sliderTheme.trackActiveColor.hovered + else -> sliderTheme.trackActiveColor.focused } - 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( targetValue = when { @@ -107,12 +127,6 @@ fun Slider( label = "thumbScale", ) - val thumbElevation = when { - isPressed -> 2.dp - isHovered -> 8.dp - else -> 5.dp - } - Box( modifier = modifier .fillMaxWidth() @@ -241,9 +255,9 @@ fun Slider( val x = trackStart + stepSpacing * i drawCircle( color = if (x <= activeEnd) { - colors.onAccent.copy(alpha = 0.5f) + sliderTheme.stepActiveColor } else { - colors.onContainer.copy(alpha = 0.25f) + sliderTheme.stepInactiveColor }, radius = 2.dp.toPx(), center = Offset(x, trackY), @@ -267,17 +281,17 @@ fun Slider( scaleY = thumbScale } .shadow( - elevation = thumbElevation, + elevation = thumbShadowState.elevation, shape = CircleShape, - ambientColor = colors.accent.copy(alpha = 0.35f), - spotColor = colors.accent.copy(alpha = 0.5f), + ambientColor = thumbShadowState.ambientColor, + spotColor = thumbShadowState.spotColor, ) .background( brush = Brush.radialGradient( colors = listOf( Color.White.copy(alpha = 0.6f), - thumbColor.copy(alpha = 0.95f), - thumbColor, + resolvedThumbColor.copy(alpha = 0.95f), + resolvedThumbColor, ), center = Offset(0f, -0.55f), radius = 1.1f, @@ -296,4 +310,4 @@ fun Slider( ), ) } -} \ No newline at end of file +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/TextField.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/TextField.kt index 1db56ec5..964e7330 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/TextField.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/TextField.kt @@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.defaultMinSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions 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.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape import androidx.compose.ui.graphics.SolidColor import androidx.compose.ui.text.TextStyle 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.IconsaxTrash -private val TextFieldShape = RoundedCornerShape(14.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 -internal fun textFieldShadow( - shape: androidx.compose.ui.graphics.Shape, - focused: Boolean, - colors: ButtonColors, -): Modifier { - val elevation = if (focused) 9.dp else 6.dp - val ambient = if (focused) { - colors.accent.copy(alpha = 0.2f) - } else { - colors.shadow.copy(alpha = 0.15f) +private fun resolveTextFieldState( + theme: BaseUITheme.TextFieldTheme, + isFocused: Boolean, + isHovered: Boolean, +): ResolvedTextFieldState { + return when { + isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused) + isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered) + else -> ResolvedTextFieldState(theme.background.pressed, theme.highlight.pressed, theme.shape.pressed, theme.border.pressed, theme.shadow.pressed, theme.foreground.pressed) } - 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 @@ -108,25 +104,29 @@ fun TextField( visualTransformation: VisualTransformation = VisualTransformation.None, interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, 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 isHovered by interactionSource.collectIsHoveredAsState() - val gradient = outlinedGradient(colors, false) - val border = when { - isError -> MaterialTheme.colorScheme.error - isFocused -> MaterialTheme.colorScheme.primary - isHovered -> colors.border.copy(alpha = 0.85f) - else -> colors.border + val state = resolveTextFieldState(textFieldTheme, isFocused, isHovered) + + val resolvedBorder = if (isError) { + BaseUITheme.Border(MaterialTheme.colorScheme.error, state.border.width) + } else { + state.border } val contentColor = when { - !enabled -> colors.onContainer.copy(alpha = 0.38f) - else -> colors.onContainer + !enabled -> state.foreground.copy(alpha = 0.38f) + else -> state.foreground } + val cursor = cursorBrush?.let { SolidColor(it) } ?: textFieldTheme.cursor + Column(modifier = modifier) { AnimatedVisibility( visible = label != null, @@ -139,13 +139,22 @@ fun TextField( Box( modifier = Modifier .defaultMinSize(minHeight = TextFieldMinHeight) - .then(textFieldShadow(TextFieldShape, isFocused, colors)) - .clip(TextFieldShape) - .background(gradient, TextFieldShape) - .border(BorderStroke(0.5.dp, border), TextFieldShape) + .then( + if (state.shadow.elevation > 0.dp) { + Modifier.shadow( + 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 { val highlightBrush = Brush.verticalGradient( - colors = listOf(colors.highlight, Color.Transparent), + colors = listOf(state.highlight, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) @@ -158,7 +167,7 @@ fun TextField( ) } } - .padding(horizontal = 14.dp, vertical = 10.dp), + .padding(textFieldTheme.padding), ) { Row( verticalAlignment = Alignment.CenterVertically, @@ -177,7 +186,7 @@ fun TextField( enabled = enabled, readOnly = readOnly, textStyle = textStyle.copy(color = contentColor), - cursorBrush = SolidColor(cursorBrush), + cursorBrush = cursor, keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, singleLine = singleLine, @@ -210,36 +219,39 @@ fun TextField( @Composable fun TextFieldPreview() { MaterialTheme { - androidx.compose.material3.Surface( - color = MaterialTheme.colorScheme.background, - modifier = Modifier.padding(24.dp), - ) { - TextField( - modifier = Modifier.padding(top = 4.dp), - value = "Twenty One Pilots", - onValueChange = {}, - placeholder = { Text("Placeholder") }, - label = { - Text( - "Search Field", - 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, - contentDescription = "Search Icon", - ) - }, - trailingIcon = { - androidx.compose.material3.Icon( - imageVector = Iconsax.IconsaxTrash, - contentDescription = "Clear Icon", - ) - }, - ) + val theme = rememberBaseUITheme() + CompositionLocalProvider(LocalBaseUITheme provides theme) { + androidx.compose.material3.Surface( + color = MaterialTheme.colorScheme.background, + modifier = Modifier.padding(24.dp), + ) { + TextField( + modifier = Modifier.padding(top = 4.dp), + value = "Twenty One Pilots", + onValueChange = {}, + placeholder = { Text("Placeholder") }, + label = { + Text( + "Search Field", + 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, + contentDescription = "Search Icon", + ) + }, + trailingIcon = { + androidx.compose.material3.Icon( + imageVector = Iconsax.IconsaxTrash, + contentDescription = "Clear Icon", + ) + }, + ) + } } } } diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt new file mode 100644 index 00000000..5a1aef29 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/base/Theme.kt @@ -0,0 +1,1652 @@ +/* + * 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 . + */ + +package dev.krtirtho.spotube.core.ui.base + +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.luminance +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import dev.krtirtho.spotube.core.ui.base.BaseUITheme.AutoCompleteMenuTheme +import dev.krtirtho.spotube.core.ui.base.BaseUITheme.CheckBoxTheme +import dev.krtirtho.spotube.core.ui.base.BaseUITheme.ChipTabTheme +import dev.krtirtho.spotube.core.ui.base.BaseUITheme.SliderTheme +import dev.krtirtho.spotube.core.ui.base.BaseUITheme.TextFieldTheme + +@Stable +data class BaseUITheme( + val buttons: ButtonVariants, + val iconButtons: ButtonVariants, + val textField: TextFieldTheme, + val autoCompleteMenuTheme: AutoCompleteMenuTheme, + val slider: SliderTheme, + val checkBox: CheckBoxTheme, + val chipTab: ChipTabTheme, + val card: CardTheme, + val listRowTile: ListRowTheme, +) { + + @Stable + data class InteractionState( + val hovered: T, + val pressed: T, + val focused: T, + ) { + companion object { + fun fromSingleValue(value: T): InteractionState { + return InteractionState(value, value, value) + } + } + } + + @Stable + data class AdvancedInteractionState( + val hovered: T, + val pressed: T, + val focused: T, + val selected: T, + val disabled: T, + ) { + companion object { + fun fromSingleValue(value: T): AdvancedInteractionState { + return AdvancedInteractionState(value, value, value, value, value) + } + } + } + + @Stable + data class Border( + val color: Color, + val width: Dp, + ) + + @Stable + data class Shadow( + val elevation: Dp, + val clip: Boolean = elevation > 0.dp, + val ambientColor: Color, + val spotColor: Color, + ) + + @Stable + data class ButtonVariants( + val primary: ButtonStyle, + val secondary: ButtonStyle, + val outline: ButtonStyle, + val ghost: ButtonStyle, + ) + + @Stable + data class ButtonStyle( + val colors: InteractionState, + val shape: InteractionState, + val shadow: InteractionState, + val border: InteractionState, + val padding: InteractionState, + ) + + @Stable + data class ButtonColors( + val background: Brush, + val foreground: Color, + val highlight: Color, + ) + + data class CardTheme( + val background: Brush, + val shape: Shape, + val border: Border, + val padding: PaddingValues, + val shadow: Shadow, + ) + + data class ListRowTheme( + val background: AdvancedInteractionState, + val shape: AdvancedInteractionState, + val border: AdvancedInteractionState, + val shadow: AdvancedInteractionState, + val padding: PaddingValues, + val textStyle: AdvancedInteractionState, + val foreground: AdvancedInteractionState, + ) + + data class TextFieldTheme( + val background: InteractionState, + val highlight: InteractionState, + val shape: InteractionState, + val border: InteractionState, + val shadow: InteractionState, + val padding: PaddingValues, + val textStyle: TextStyle, + val cursor: Brush, + val foreground: InteractionState, + ) + + data class AutoCompleteMenuTheme( + val background: Brush, + val shape: Shape, + val border: Border, + val padding: PaddingValues, + val shadowElevation: Dp, + ) + + data class SliderTheme( + val trackActiveColor: InteractionState, + val trackInactiveColor: InteractionState, + val thumbColor: InteractionState, + val thumbShadow: InteractionState, + val stepActiveColor: Color, + val stepInactiveColor: Color, + ) + + data class CheckBoxTheme( + val selected: ButtonStyle, + val unselected: ButtonStyle, + val checkmarkColor: Color, + ) + + data class ChipTabTheme( + val selected: ButtonStyle, + val unselected: ButtonStyle, + ) + + companion object { + private val defaultShape = RoundedCornerShape(14.dp) + private val defaultShapeState = + InteractionState(defaultShape, defaultShape, defaultShape) + private val noBorder = Border(Color.Transparent, 0.dp) + private val noBorderState = InteractionState(noBorder, noBorder, noBorder) + private val noShadow = Shadow(0.dp, false, Color.Transparent, Color.Transparent) + private val noShadowState = InteractionState(noShadow, noShadow, noShadow) + private val defaultButtonPadding = InteractionState( + PaddingValues(horizontal = 20.dp, vertical = 10.dp), + PaddingValues(horizontal = 20.dp, vertical = 10.dp), + PaddingValues(horizontal = 20.dp, vertical = 10.dp), + ) + private val iconButtonPadding = InteractionState( + PaddingValues(8.dp), + PaddingValues(8.dp), + PaddingValues(8.dp), + ) + + val Default: BaseUITheme = BaseUITheme( + buttons = ButtonVariants( + primary = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1ED760).copy(alpha = 0.85f), + Color(0xFF1DB954) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.15f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + ), + shape = defaultShapeState, + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.45f), + Color(0xFF1DB954).copy(alpha = 0.5f) + ), + pressed = Shadow( + 1.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.2f), + Color(0xFF1DB954).copy(alpha = 0.25f) + ), + focused = Shadow( + 6.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.35f), + Color(0xFF1DB954).copy(alpha = 0.4f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFF1DB954), 0.5.dp), + pressed = Border(Color(0xFF1DB954), 0.5.dp), + focused = Border(Color(0xFF1DB954), 0.5.dp), + ), + padding = defaultButtonPadding, + ), + secondary = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E0), + Color(0xFFD0D0D0) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.5f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFD0D0D0), + Color(0xFFC0C0C0) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.3f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E0), + Color(0xFFD0D0D0) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.5f), + ), + ), + shape = defaultShapeState, + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color.Black.copy(alpha = 0.22f), + Color.Black.copy(alpha = 0.26f) + ), + pressed = Shadow( + 1.dp, + true, + Color.Black.copy(alpha = 0.08f), + Color.Black.copy(alpha = 0.1f) + ), + focused = Shadow( + 6.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFFE0E0E0).copy(alpha = 0.5f), 0.5.dp), + pressed = Border(Color(0xFFD0D0D0).copy(alpha = 0.5f), 0.5.dp), + focused = Border(Color(0xFFE0E0E0).copy(alpha = 0.5f), 0.5.dp), + ), + padding = defaultButtonPadding, + ), + outline = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3), + Color(0xFFE0E0E3) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + ), + shape = defaultShapeState, + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color.Black.copy(alpha = 0.22f), + Color.Black.copy(alpha = 0.26f) + ), + pressed = Shadow( + 1.dp, + true, + Color.Black.copy(alpha = 0.08f), + Color.Black.copy(alpha = 0.1f) + ), + focused = Shadow( + 6.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFFCCCCCC), 0.5.dp), + pressed = Border(Color(0xFFCCCCCC).copy(alpha = 0.7f), 0.5.dp), + focused = Border(Color(0xFFCCCCCC), 0.5.dp), + ), + padding = defaultButtonPadding, + ), + ghost = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3).copy(alpha = 0.5f), + Color(0xFFE0E0E3).copy(alpha = 0.5f) + ) + ), + foreground = Color.Black, + highlight = Color.Transparent, + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3), + Color(0xFFE0E0E3) + ) + ), + foreground = Color.Black, + highlight = Color.Transparent, + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.Transparent, + Color.Transparent + ) + ), + foreground = Color.Black, + highlight = Color.Transparent, + ), + ), + shape = defaultShapeState, + shadow = noShadowState, + border = noBorderState, + padding = defaultButtonPadding, + ), + ), + iconButtons = ButtonVariants( + primary = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1ED760).copy(alpha = 0.85f), + Color(0xFF1DB954) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.15f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + ), + shape = defaultShapeState, + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.45f), + Color(0xFF1DB954).copy(alpha = 0.5f) + ), + pressed = Shadow( + 1.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.2f), + Color(0xFF1DB954).copy(alpha = 0.25f) + ), + focused = Shadow( + 6.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.35f), + Color(0xFF1DB954).copy(alpha = 0.4f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFF1DB954), 0.5.dp), + pressed = Border(Color(0xFF1DB954), 0.5.dp), + focused = Border(Color(0xFF1DB954), 0.5.dp), + ), + padding = iconButtonPadding, + ), + secondary = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E0), + Color(0xFFD0D0D0) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.5f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFD0D0D0), + Color(0xFFC0C0C0) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.3f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E0), + Color(0xFFD0D0D0) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.5f), + ), + ), + shape = defaultShapeState, + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color.Black.copy(alpha = 0.22f), + Color.Black.copy(alpha = 0.26f) + ), + pressed = Shadow( + 1.dp, + true, + Color.Black.copy(alpha = 0.08f), + Color.Black.copy(alpha = 0.1f) + ), + focused = Shadow( + 6.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFFE0E0E0).copy(alpha = 0.5f), 0.5.dp), + pressed = Border(Color(0xFFD0D0D0).copy(alpha = 0.5f), 0.5.dp), + focused = Border(Color(0xFFE0E0E0).copy(alpha = 0.5f), 0.5.dp), + ), + padding = iconButtonPadding, + ), + outline = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3), + Color(0xFFE0E0E3) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + ), + shape = defaultShapeState, + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color.Black.copy(alpha = 0.22f), + Color.Black.copy(alpha = 0.26f) + ), + pressed = Shadow( + 1.dp, + true, + Color.Black.copy(alpha = 0.08f), + Color.Black.copy(alpha = 0.1f) + ), + focused = Shadow( + 6.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFFCCCCCC), 0.5.dp), + pressed = Border(Color(0xFFCCCCCC).copy(alpha = 0.7f), 0.5.dp), + focused = Border(Color(0xFFCCCCCC), 0.5.dp), + ), + padding = iconButtonPadding, + ), + ghost = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3).copy(alpha = 0.5f), + Color(0xFFE0E0E3).copy(alpha = 0.5f) + ) + ), + foreground = Color.Black, + highlight = Color.Transparent, + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3), + Color(0xFFE0E0E3) + ) + ), + foreground = Color.Black, + highlight = Color.Transparent, + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.Transparent, + Color.Transparent + ) + ), + foreground = Color.Black, + highlight = Color.Transparent, + ), + ), + shape = defaultShapeState, + shadow = noShadowState, + border = noBorderState, + padding = iconButtonPadding, + ), + ), + textField = TextFieldTheme( + background = InteractionState( + hovered = Brush.verticalGradient(listOf(Color(0xFFF5F5F5), Color(0xFFE8E8E8))), + pressed = Brush.verticalGradient(listOf(Color.White, Color(0xFFF2F2F4))), + focused = Brush.verticalGradient(listOf(Color.White, Color(0xFFF2F2F4))), + ), + highlight = InteractionState( + hovered = Color.White.copy(alpha = 0.9f), + pressed = Color.White.copy(alpha = 0.9f), + focused = Color.White.copy(alpha = 0.9f), + ), + shape = InteractionState( + hovered = defaultShape, + pressed = defaultShape, + focused = defaultShape, + ), + border = InteractionState( + hovered = Border(Color(0xFFCCCCCC).copy(alpha = 0.85f), 0.5.dp), + pressed = Border(Color(0xFFCCCCCC), 0.5.dp), + focused = Border(Color(0xFF1DB954), 0.5.dp), + ), + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color.Black.copy(alpha = 0.22f), + Color.Black.copy(alpha = 0.26f) + ), + pressed = Shadow( + 6.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + focused = Shadow( + 9.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.2f), + Color(0xFF1DB954).copy(alpha = 0.25f) + ), + ), + padding = PaddingValues(horizontal = 14.dp, vertical = 10.dp), + textStyle = TextStyle.Default, + cursor = SolidColor(Color(0xFF1DB954)), + foreground = InteractionState( + hovered = Color.Black, + pressed = Color.Black, + focused = Color.Black, + ), + ), + autoCompleteMenuTheme = AutoCompleteMenuTheme( + background = Brush.verticalGradient(listOf(Color(0xFFF5F5F5), Color(0xFFE8E8E8))), + shape = RoundedCornerShape(12.dp), + border = Border(Color(0xFFCCCCCC), 0.5.dp), + padding = PaddingValues(0.dp), + shadowElevation = 8.dp, + ), + slider = SliderTheme( + trackActiveColor = InteractionState( + hovered = Color(0xFF1DB954), + pressed = Color(0xFF1DB954), + focused = Color(0xFF1DB954), + ), + trackInactiveColor = InteractionState( + hovered = Color(0xFFCCCCCC).copy(alpha = 0.6f), + pressed = Color(0xFFCCCCCC).copy(alpha = 0.6f), + focused = Color(0xFFCCCCCC).copy(alpha = 0.6f), + ), + thumbColor = InteractionState( + hovered = Color(0xFF1DB954), + pressed = Color(0xFF1DB954), + focused = Color(0xFF1DB954), + ), + thumbShadow = InteractionState( + hovered = Shadow( + 8.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.4f), + Color(0xFF1DB954).copy(alpha = 0.5f) + ), + pressed = Shadow( + 2.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.2f), + Color(0xFF1DB954).copy(alpha = 0.25f) + ), + focused = Shadow( + 5.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.35f), + Color(0xFF1DB954).copy(alpha = 0.4f) + ), + ), + stepActiveColor = Color.White.copy(alpha = 0.5f), + stepInactiveColor = Color.Black.copy(alpha = 0.25f), + ), + checkBox = CheckBoxTheme( + selected = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1ED760).copy(alpha = 0.85f), + Color(0xFF1DB954) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.15f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + ), + shape = InteractionState( + RoundedCornerShape(6.dp), + RoundedCornerShape(6.dp), + RoundedCornerShape(6.dp) + ), + shadow = InteractionState( + hovered = Shadow( + 8.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.4f), + Color(0xFF1DB954).copy(alpha = 0.45f) + ), + pressed = Shadow( + 1.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.2f), + Color(0xFF1DB954).copy(alpha = 0.25f) + ), + focused = Shadow( + 6.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.3f), + Color(0xFF1DB954).copy(alpha = 0.35f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFF1DB954), 0.5.dp), + pressed = Border(Color(0xFF1DB954), 0.5.dp), + focused = Border(Color(0xFF1DB954), 0.5.dp), + ), + padding = InteractionState( + PaddingValues(0.dp), + PaddingValues(0.dp), + PaddingValues(0.dp) + ), + ), + unselected = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3), + Color(0xFFE0E0E3) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + ), + shape = InteractionState( + RoundedCornerShape(6.dp), + RoundedCornerShape(6.dp), + RoundedCornerShape(6.dp) + ), + shadow = InteractionState( + hovered = Shadow( + 5.dp, + true, + Color.Black.copy(alpha = 0.2f), + Color.Black.copy(alpha = 0.24f) + ), + pressed = Shadow( + 1.dp, + true, + Color.Black.copy(alpha = 0.08f), + Color.Black.copy(alpha = 0.1f) + ), + focused = Shadow( + 3.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFFCCCCCC), 0.5.dp), + pressed = Border(Color(0xFFCCCCCC), 0.5.dp), + focused = Border(Color(0xFFCCCCCC), 0.5.dp), + ), + padding = InteractionState( + PaddingValues(0.dp), + PaddingValues(0.dp), + PaddingValues(0.dp) + ), + ), + checkmarkColor = Color.White, + ), + chipTab = ChipTabTheme( + selected = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1ED760).copy(alpha = 0.85f), + Color(0xFF1DB954) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.15f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFF1DB954), + Color(0xFF1ED760) + ) + ), + foreground = Color.White, + highlight = Color.White.copy(alpha = 0.25f), + ), + ), + shape = InteractionState( + RoundedCornerShape(10.dp), + RoundedCornerShape(10.dp), + RoundedCornerShape(10.dp) + ), + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.45f), + Color(0xFF1DB954).copy(alpha = 0.5f) + ), + pressed = Shadow( + 1.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.2f), + Color(0xFF1DB954).copy(alpha = 0.25f) + ), + focused = Shadow( + 6.dp, + true, + Color(0xFF1DB954).copy(alpha = 0.35f), + Color(0xFF1DB954).copy(alpha = 0.4f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFF1DB954), 0.5.dp), + pressed = Border(Color(0xFF1DB954), 0.5.dp), + focused = Border(Color(0xFF1DB954), 0.5.dp), + ), + padding = InteractionState( + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + ), + ), + unselected = ButtonStyle( + colors = InteractionState( + hovered = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + pressed = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color(0xFFE0E0E3), + Color(0xFFE0E0E3) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + focused = ButtonColors( + background = Brush.verticalGradient( + listOf( + Color.White, + Color(0xFFF2F2F4) + ) + ), + foreground = Color.Black, + highlight = Color.White.copy(alpha = 0.9f), + ), + ), + shape = InteractionState( + RoundedCornerShape(10.dp), + RoundedCornerShape(10.dp), + RoundedCornerShape(10.dp) + ), + shadow = InteractionState( + hovered = Shadow( + 9.dp, + true, + Color.Black.copy(alpha = 0.22f), + Color.Black.copy(alpha = 0.26f) + ), + pressed = Shadow( + 1.dp, + true, + Color.Black.copy(alpha = 0.08f), + Color.Black.copy(alpha = 0.1f) + ), + focused = Shadow( + 6.dp, + true, + Color.Black.copy(alpha = 0.15f), + Color.Black.copy(alpha = 0.18f) + ), + ), + border = InteractionState( + hovered = Border(Color(0xFFCCCCCC), 0.5.dp), + pressed = Border(Color(0xFFCCCCCC).copy(alpha = 0.7f), 0.5.dp), + focused = Border(Color(0xFFCCCCCC), 0.5.dp), + ), + padding = InteractionState( + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + ), + ), + ), + card = CardTheme( + background = Brush.verticalGradient(listOf(Color.White, Color(0xFFF2F2F4))), + shape = RoundedCornerShape(16.dp), + border = Border(Color(0xFFCCCCCC), 0.5.dp), + padding = PaddingValues(12.dp), + shadow = Shadow(6.dp, true, Color.Black.copy(alpha = 0.15f), Color.Black.copy(alpha = 0.18f)), + ), + listRowTile = ListRowTheme( + background = AdvancedInteractionState( + hovered = Brush.verticalGradient(listOf(Color.White, Color(0xFFF2F2F4))), + pressed = Brush.verticalGradient(listOf(Color(0xFFE0E0E3), Color(0xFFE0E0E3))), + focused = Brush.verticalGradient(listOf(Color.White, Color(0xFFF2F2F4))), + selected = Brush.verticalGradient(listOf(Color(0xFF1DB954).copy(alpha = 0.14f), Color(0xFF1DB954).copy(alpha = 0.14f))), + disabled = Brush.verticalGradient(listOf(Color(0xFFF5F5F5), Color(0xFFF5F5F5))), + ), + shape = AdvancedInteractionState( + hovered = RoundedCornerShape(8.dp), + pressed = RoundedCornerShape(8.dp), + focused = RoundedCornerShape(8.dp), + selected = RoundedCornerShape(8.dp), + disabled = RoundedCornerShape(8.dp), + ), + border = AdvancedInteractionState( + hovered = Border(Color.Transparent, 0.dp), + pressed = Border(Color.Transparent, 0.dp), + focused = Border(Color.Transparent, 0.dp), + selected = Border(Color.Transparent, 0.dp), + disabled = Border(Color.Transparent, 0.dp), + ), + shadow = AdvancedInteractionState( + hovered = Shadow(0.dp, false, Color.Transparent, Color.Transparent), + pressed = Shadow(0.dp, false, Color.Transparent, Color.Transparent), + focused = Shadow(0.dp, false, Color.Transparent, Color.Transparent), + selected = Shadow(0.dp, false, Color.Transparent, Color.Transparent), + disabled = Shadow(0.dp, false, Color.Transparent, Color.Transparent), + ), + padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp), + textStyle = AdvancedInteractionState( + hovered = TextStyle.Default, + pressed = TextStyle.Default, + focused = TextStyle.Default, + selected = TextStyle.Default, + disabled = TextStyle.Default, + ), + foreground = AdvancedInteractionState( + hovered = Color.Black, + pressed = Color.Black, + focused = Color.Black, + selected = Color.Black, + disabled = Color.Black.copy(alpha = 0.38f), + ), + ), + ) + } +} + +val LocalBaseUITheme = staticCompositionLocalOf { BaseUITheme.Default } + +@Composable +fun rememberBaseUITheme(): BaseUITheme { + val scheme = MaterialTheme.colorScheme + val isLight = scheme.surface.luminance() > 0.5f + + val containerLighter = if (isLight) Color.White else scheme.surfaceContainerHigh + val containerDarker = if (isLight) Color(0xFFF2F2F4) else scheme.surfaceContainer + val containerPressed = if (isLight) Color(0xFFE0E0E3) else scheme.surfaceContainerHighest + val highlight = if (isLight) Color.White.copy(alpha = 0.9f) else Color.White.copy(alpha = 0.06f) + val shadowColor = scheme.onSurface.copy(alpha = 0.12f) + val border = scheme.outlineVariant + + val secondaryContainer = scheme.secondaryContainer + val secondaryContainerPressed = secondaryContainer.copy(alpha = if (isLight) 0.85f else 0.92f) + val secondaryHighlight = + if (isLight) Color.White.copy(alpha = 0.5f) else Color.White.copy(alpha = 0.08f) + + val defaultShape = RoundedCornerShape(14.dp) + val defaultShapeState = + BaseUITheme.InteractionState(defaultShape, defaultShape, defaultShape) + val noBorder = BaseUITheme.Border(Color.Transparent, 0.dp) + val noBorderState = BaseUITheme.InteractionState(noBorder, noBorder, noBorder) + val noShadow = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent) + val noShadowState = BaseUITheme.InteractionState(noShadow, noShadow, noShadow) + val defaultButtonPadding = BaseUITheme.InteractionState( + PaddingValues(horizontal = 20.dp, vertical = 10.dp), + PaddingValues(horizontal = 20.dp, vertical = 10.dp), + PaddingValues(horizontal = 20.dp, vertical = 10.dp), + ) + val iconButtonPadding = BaseUITheme.InteractionState( + PaddingValues(8.dp), + PaddingValues(8.dp), + PaddingValues(8.dp), + ) + + val outlineColors = BaseUITheme.InteractionState( + hovered = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + foreground = scheme.onSurface, + highlight = highlight, + ), + pressed = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(containerPressed, containerPressed)), + foreground = scheme.onSurface, + highlight = highlight, + ), + focused = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + foreground = scheme.onSurface, + highlight = highlight, + ), + ) + val outlineShadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 9.dp, + true, + shadowColor.copy(alpha = 0.22f / 0.12f), + shadowColor.copy(alpha = 0.26f / 0.12f) + ), + pressed = BaseUITheme.Shadow( + 1.dp, + true, + shadowColor.copy(alpha = 0.08f / 0.12f), + shadowColor.copy(alpha = 0.1f / 0.12f) + ), + focused = BaseUITheme.Shadow( + 6.dp, + true, + shadowColor.copy(alpha = 0.15f / 0.12f), + shadowColor.copy(alpha = 0.18f / 0.12f) + ), + ) + val outlineBorder = BaseUITheme.InteractionState( + hovered = BaseUITheme.Border(border, 0.5.dp), + pressed = BaseUITheme.Border(border.copy(alpha = 0.7f), 0.5.dp), + focused = BaseUITheme.Border(border, 0.5.dp), + ) + + val primaryColors = BaseUITheme.InteractionState( + hovered = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)), + foreground = scheme.onPrimary, + highlight = Color.White.copy(alpha = 0.25f), + ), + pressed = BaseUITheme.ButtonColors( + background = Brush.verticalGradient( + listOf( + scheme.primary.copy(alpha = 0.85f), + scheme.primary + ) + ), + foreground = scheme.onPrimary, + highlight = Color.White.copy(alpha = 0.25f), + ), + focused = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(scheme.primary, scheme.primary)), + foreground = scheme.onPrimary, + highlight = Color.White.copy(alpha = 0.25f), + ), + ) + val primaryShadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 9.dp, + true, + scheme.primary.copy(alpha = 0.45f), + scheme.primary.copy(alpha = 0.5f) + ), + pressed = BaseUITheme.Shadow( + 1.dp, + true, + scheme.primary.copy(alpha = 0.2f), + scheme.primary.copy(alpha = 0.25f) + ), + focused = BaseUITheme.Shadow( + 6.dp, + true, + scheme.primary.copy(alpha = 0.35f), + scheme.primary.copy(alpha = 0.4f) + ), + ) + val primaryBorder = BaseUITheme.InteractionState( + hovered = BaseUITheme.Border(scheme.primary, 0.5.dp), + pressed = BaseUITheme.Border(scheme.primary, 0.5.dp), + focused = BaseUITheme.Border(scheme.primary, 0.5.dp), + ) + + val secondaryColors = BaseUITheme.InteractionState( + hovered = BaseUITheme.ButtonColors( + background = Brush.verticalGradient( + listOf( + secondaryContainer, + secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f) + ) + ), + foreground = scheme.onSecondaryContainer, + highlight = secondaryHighlight, + ), + pressed = BaseUITheme.ButtonColors( + background = Brush.verticalGradient( + listOf( + secondaryContainerPressed, + secondaryContainerPressed + ) + ), + foreground = scheme.onSecondaryContainer, + highlight = secondaryHighlight, + ), + focused = BaseUITheme.ButtonColors( + background = Brush.verticalGradient( + listOf( + secondaryContainer, + secondaryContainer.copy(alpha = if (secondaryContainer.luminance() > 0.5f) 0.92f else 1f) + ) + ), + foreground = scheme.onSecondaryContainer, + highlight = secondaryHighlight, + ), + ) + val secondaryShadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 9.dp, + true, + shadowColor.copy(alpha = 0.22f / 0.12f), + shadowColor.copy(alpha = 0.26f / 0.12f) + ), + pressed = BaseUITheme.Shadow( + 1.dp, + true, + shadowColor.copy(alpha = 0.08f / 0.12f), + shadowColor.copy(alpha = 0.1f / 0.12f) + ), + focused = BaseUITheme.Shadow( + 6.dp, + true, + shadowColor.copy(alpha = 0.15f / 0.12f), + shadowColor.copy(alpha = 0.18f / 0.12f) + ), + ) + val secondaryBorder = BaseUITheme.InteractionState( + hovered = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), + pressed = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), + focused = BaseUITheme.Border(secondaryContainer.copy(alpha = 0.5f), 0.5.dp), + ) + + val ghostColors = BaseUITheme.InteractionState( + hovered = BaseUITheme.ButtonColors( + background = Brush.verticalGradient( + listOf( + containerPressed.copy(alpha = 0.5f), + containerPressed.copy(alpha = 0.5f) + ) + ), + foreground = scheme.onSurface, + highlight = Color.Transparent, + ), + pressed = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(containerPressed, containerPressed)), + foreground = scheme.onSurface, + highlight = Color.Transparent, + ), + focused = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)), + foreground = scheme.onSurface, + highlight = Color.Transparent, + ), + ) + + val outlineStyle = BaseUITheme.ButtonStyle( + outlineColors, + defaultShapeState, + outlineShadow, + outlineBorder, + defaultButtonPadding + ) + val primaryStyle = BaseUITheme.ButtonStyle( + primaryColors, + defaultShapeState, + primaryShadow, + primaryBorder, + defaultButtonPadding + ) + val secondaryStyle = BaseUITheme.ButtonStyle( + secondaryColors, + defaultShapeState, + secondaryShadow, + secondaryBorder, + defaultButtonPadding + ) + val ghostStyle = BaseUITheme.ButtonStyle( + ghostColors, + defaultShapeState, + noShadowState, + noBorderState, + defaultButtonPadding + ) + + val outlineIconStyle = BaseUITheme.ButtonStyle( + outlineColors, + defaultShapeState, + outlineShadow, + outlineBorder, + iconButtonPadding + ) + val primaryIconStyle = BaseUITheme.ButtonStyle( + primaryColors, + defaultShapeState, + primaryShadow, + primaryBorder, + iconButtonPadding + ) + val secondaryIconStyle = BaseUITheme.ButtonStyle( + secondaryColors, + defaultShapeState, + secondaryShadow, + secondaryBorder, + iconButtonPadding + ) + val ghostIconStyle = BaseUITheme.ButtonStyle( + ghostColors, + defaultShapeState, + noShadowState, + noBorderState, + iconButtonPadding + ) + + val textFieldForeground = BaseUITheme.InteractionState( + hovered = scheme.onSurface, + pressed = scheme.onSurface, + focused = scheme.onSurface, + ) + + val checkBoxShape = RoundedCornerShape(6.dp) + val checkBoxShapeState = + BaseUITheme.InteractionState(checkBoxShape, checkBoxShape, checkBoxShape) + val noPadding = + BaseUITheme.InteractionState(PaddingValues(0.dp), PaddingValues(0.dp), PaddingValues(0.dp)) + + val chipTabShape = RoundedCornerShape(10.dp) + val chipTabShapeState = + BaseUITheme.InteractionState(chipTabShape, chipTabShape, chipTabShape) + val chipTabPadding = BaseUITheme.InteractionState( + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + PaddingValues(horizontal = 14.dp, vertical = 4.dp), + ) + + return BaseUITheme( + buttons = BaseUITheme.ButtonVariants( + primary = primaryStyle, + secondary = secondaryStyle, + outline = outlineStyle, + ghost = ghostStyle, + ), + iconButtons = BaseUITheme.ButtonVariants( + primary = primaryIconStyle, + secondary = secondaryIconStyle, + outline = outlineIconStyle, + ghost = ghostIconStyle, + ), + textField = TextFieldTheme( + background = BaseUITheme.InteractionState( + hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + pressed = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + ), + highlight = BaseUITheme.InteractionState( + hovered = highlight, + pressed = highlight, + focused = highlight, + ), + shape = BaseUITheme.InteractionState( + hovered = defaultShape, + pressed = defaultShape, + focused = defaultShape, + ), + border = BaseUITheme.InteractionState( + hovered = BaseUITheme.Border(border.copy(alpha = 0.85f), 0.5.dp), + pressed = BaseUITheme.Border(border, 0.5.dp), + focused = BaseUITheme.Border(scheme.primary, 0.5.dp), + ), + shadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 9.dp, + true, + shadowColor.copy(alpha = 0.22f / 0.12f), + shadowColor.copy(alpha = 0.26f / 0.12f) + ), + pressed = BaseUITheme.Shadow( + 6.dp, + true, + shadowColor.copy(alpha = 0.15f / 0.12f), + shadowColor.copy(alpha = 0.18f / 0.12f) + ), + focused = BaseUITheme.Shadow( + 9.dp, + true, + scheme.primary.copy(alpha = 0.2f), + scheme.primary.copy(alpha = 0.25f) + ), + ), + padding = PaddingValues(horizontal = 14.dp, vertical = 10.dp), + textStyle = TextStyle.Default, + cursor = SolidColor(scheme.primary), + foreground = textFieldForeground, + ), + autoCompleteMenuTheme = AutoCompleteMenuTheme( + background = Brush.verticalGradient( + listOf( + scheme.surfaceContainerHigh, + scheme.surfaceContainer + ) + ), + shape = RoundedCornerShape(12.dp), + border = BaseUITheme.Border(border, 0.5.dp), + padding = PaddingValues(0.dp), + shadowElevation = 8.dp, + ), + slider = SliderTheme( + trackActiveColor = BaseUITheme.InteractionState( + hovered = scheme.primary, + pressed = scheme.primary, + focused = scheme.primary, + ), + trackInactiveColor = BaseUITheme.InteractionState( + hovered = border.copy(alpha = 0.6f), + pressed = border.copy(alpha = 0.6f), + focused = border.copy(alpha = 0.6f), + ), + thumbColor = BaseUITheme.InteractionState( + hovered = scheme.primary, + pressed = scheme.primary, + focused = scheme.primary, + ), + thumbShadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 8.dp, + true, + scheme.primary.copy(alpha = 0.4f), + scheme.primary.copy(alpha = 0.5f) + ), + pressed = BaseUITheme.Shadow( + 2.dp, + true, + scheme.primary.copy(alpha = 0.2f), + scheme.primary.copy(alpha = 0.25f) + ), + focused = BaseUITheme.Shadow( + 5.dp, + true, + scheme.primary.copy(alpha = 0.35f), + scheme.primary.copy(alpha = 0.4f) + ), + ), + stepActiveColor = scheme.onPrimary.copy(alpha = 0.5f), + stepInactiveColor = scheme.onSurface.copy(alpha = 0.25f), + ), + checkBox = CheckBoxTheme( + selected = BaseUITheme.ButtonStyle( + colors = primaryColors, + shape = checkBoxShapeState, + shadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 8.dp, + true, + scheme.primary.copy(alpha = 0.4f), + scheme.primary.copy(alpha = 0.45f) + ), + pressed = BaseUITheme.Shadow( + 1.dp, + true, + scheme.primary.copy(alpha = 0.2f), + scheme.primary.copy(alpha = 0.25f) + ), + focused = BaseUITheme.Shadow( + 6.dp, + true, + scheme.primary.copy(alpha = 0.3f), + scheme.primary.copy(alpha = 0.35f) + ), + ), + border = primaryBorder, + padding = noPadding, + ), + unselected = BaseUITheme.ButtonStyle( + colors = outlineColors, + shape = checkBoxShapeState, + shadow = BaseUITheme.InteractionState( + hovered = BaseUITheme.Shadow( + 5.dp, + true, + shadowColor.copy(alpha = 0.2f), + shadowColor.copy(alpha = 0.24f) + ), + pressed = BaseUITheme.Shadow( + 1.dp, + true, + shadowColor.copy(alpha = 0.08f), + shadowColor.copy(alpha = 0.1f) + ), + focused = BaseUITheme.Shadow( + 3.dp, + true, + shadowColor.copy(alpha = 0.15f), + shadowColor.copy(alpha = 0.18f) + ), + ), + border = outlineBorder, + padding = noPadding, + ), + checkmarkColor = scheme.onPrimary, + ), + chipTab = ChipTabTheme( + selected = BaseUITheme.ButtonStyle( + colors = primaryColors, + shape = chipTabShapeState, + shadow = primaryShadow, + border = primaryBorder, + padding = chipTabPadding, + ), + unselected = BaseUITheme.ButtonStyle( + colors = outlineColors, + shape = chipTabShapeState, + shadow = outlineShadow, + border = outlineBorder, + padding = chipTabPadding, + ), + ), + card = BaseUITheme.CardTheme( + background = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + shape = RoundedCornerShape(16.dp), + border = BaseUITheme.Border(border, 0.5.dp), + padding = PaddingValues(12.dp), + shadow = BaseUITheme.Shadow(6.dp, true, shadowColor.copy(alpha = 0.15f / 0.12f), shadowColor.copy(alpha = 0.18f / 0.12f)), + ), + listRowTile = BaseUITheme.ListRowTheme( + background = BaseUITheme.AdvancedInteractionState( + hovered = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + pressed = Brush.verticalGradient(listOf(containerPressed, containerPressed)), + focused = Brush.verticalGradient(listOf(containerLighter, containerDarker)), + selected = Brush.verticalGradient(listOf(scheme.primaryContainer.copy(alpha = 0.3f), scheme.primaryContainer.copy(alpha = 0.3f))), + disabled = Brush.verticalGradient(listOf(containerLighter.copy(alpha = 0.5f), containerDarker.copy(alpha = 0.5f))), + ), + shape = BaseUITheme.AdvancedInteractionState( + hovered = RoundedCornerShape(8.dp), + pressed = RoundedCornerShape(8.dp), + focused = RoundedCornerShape(8.dp), + selected = RoundedCornerShape(8.dp), + disabled = RoundedCornerShape(8.dp), + ), + border = BaseUITheme.AdvancedInteractionState( + hovered = BaseUITheme.Border(Color.Transparent, 0.dp), + pressed = BaseUITheme.Border(Color.Transparent, 0.dp), + focused = BaseUITheme.Border(Color.Transparent, 0.dp), + selected = BaseUITheme.Border(Color.Transparent, 0.dp), + disabled = BaseUITheme.Border(Color.Transparent, 0.dp), + ), + shadow = BaseUITheme.AdvancedInteractionState( + hovered = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), + pressed = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), + focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), + selected = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), + disabled = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent), + ), + padding = PaddingValues(horizontal = 8.dp, vertical = 6.dp), + textStyle = BaseUITheme.AdvancedInteractionState( + hovered = TextStyle.Default, + pressed = TextStyle.Default, + focused = TextStyle.Default, + selected = TextStyle.Default, + disabled = TextStyle.Default, + ), + foreground = BaseUITheme.AdvancedInteractionState( + hovered = scheme.onSurface, + pressed = scheme.onSurface, + focused = scheme.onSurface, + selected = scheme.onSurface, + disabled = scheme.onSurface.copy(alpha = 0.38f), + ), + ), + ) +} + +// Helper functions for easier theme overrides + +fun BaseUITheme.ButtonStyle.copyShape(shape: Shape): BaseUITheme.ButtonStyle = + copy(shape = BaseUITheme.InteractionState.fromSingleValue(shape)) + +fun BaseUITheme.ButtonStyle.copyShadow(shadow: BaseUITheme.Shadow): BaseUITheme.ButtonStyle = + copy(shadow = BaseUITheme.InteractionState.fromSingleValue(shadow)) + +fun BaseUITheme.ButtonStyle.copyBorder(border: BaseUITheme.Border): BaseUITheme.ButtonStyle = + copy(border = BaseUITheme.InteractionState.fromSingleValue(border)) + +fun BaseUITheme.ButtonStyle.copyPadding(padding: PaddingValues): BaseUITheme.ButtonStyle = + copy(padding = BaseUITheme.InteractionState.fromSingleValue(padding)) + +fun BaseUITheme.ButtonStyle.copyColors(colors: BaseUITheme.ButtonColors): BaseUITheme.ButtonStyle = + copy(colors = BaseUITheme.InteractionState.fromSingleValue(colors)) + +@Composable +fun invertedButtonStyle(): BaseUITheme.ButtonStyle { + val scheme = MaterialTheme.colorScheme + val isLight = scheme.surface.luminance() > 0.5f + val shadowColor = scheme.onSurface.copy(alpha = 0.12f) + + val invertedColors = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(scheme.onSurface, scheme.onSurface)), + foreground = scheme.surface, + highlight = if (isLight) Color.White.copy(alpha = 0.15f) else Color.White.copy(alpha = 0.08f), + ) + val pressedColors = BaseUITheme.ButtonColors( + background = Brush.verticalGradient(listOf(scheme.onSurfaceVariant, scheme.onSurfaceVariant)), + foreground = scheme.surface, + highlight = if (isLight) Color.White.copy(alpha = 0.1f) else Color.White.copy(alpha = 0.05f), + ) + val border = BaseUITheme.Border(scheme.onSurfaceVariant, 0.5.dp) + val shadow = BaseUITheme.Shadow( + elevation = 6.dp, + clip = true, + ambientColor = shadowColor.copy(alpha = 0.2f), + spotColor = shadowColor.copy(alpha = 0.25f), + ) + + return BaseUITheme.ButtonStyle( + colors = BaseUITheme.InteractionState( + hovered = invertedColors, + pressed = pressedColors, + focused = invertedColors, + ), + shape = BaseUITheme.InteractionState.fromSingleValue(RoundedCornerShape(14.dp)), + shadow = BaseUITheme.InteractionState.fromSingleValue(shadow), + border = BaseUITheme.InteractionState.fromSingleValue(border), + padding = BaseUITheme.InteractionState.fromSingleValue(PaddingValues(8.dp)), + ) +} diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt index 217762af..b9146d6f 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/TrackList.kt @@ -21,10 +21,8 @@ import androidx.compose.animation.AnimatedContent import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.togetherWith -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.hoverable @@ -63,10 +61,10 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.drawWithCache -import androidx.compose.ui.focus.focusModifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Brush import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.luminance import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalDensity 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.spotube.core.ui.base.ButtonGroup 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.CheckBoxState import dev.krtirtho.spotube.core.ui.base.GhostIconButton 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.buttonShadow -import dev.krtirtho.spotube.core.ui.base.rememberButtonColors import dev.krtirtho.spotube.core.ui.misc.SkeletonTree import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer import dev.krtirtho.spotube.core.ui.misc.shimmerApply @@ -348,30 +345,14 @@ fun TrackList( } item(key = "track-card") { - val colors = rememberButtonColors() - val trackListShape = MaterialTheme.shapes.large - - Box( + Card( modifier = Modifier .fillMaxWidth() - .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) + .padding(vertical = 8.dp), ) { Column( modifier = Modifier .fillMaxWidth() - .padding(top = 12.dp, bottom = 12.dp) ) { visibleTracks.forEachIndexed { displayedIndex, track -> if (displayedIndex > 0) { @@ -532,17 +513,20 @@ private fun TrackListRow( onArtistsOverflowClick: () -> Unit, modifier: Modifier = Modifier, ) { - val rowColors = rememberButtonColors() + val rowTheme = LocalBaseUITheme.current.listRowTile val artworkInteractionSource = remember { MutableInteractionSource() } val isArtworkHovered by artworkInteractionSource.collectIsHoveredAsState() val rowInteractionSource = remember { MutableInteractionSource() } val isRowHovered by rowInteractionSource.collectIsHoveredAsState() val rowBackgroundColor = when { - isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f) - isCurrentTrack && isCurrentTrackPlaying -> MaterialTheme.colorScheme.primary.copy(alpha = 0.14f) - isCurrentTrack -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f) - else -> Color.Transparent + isSelected -> rowTheme.background.selected + isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered + isCurrentTrack -> rowTheme.background.focused + 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( modifier = modifier @@ -555,7 +539,7 @@ private fun TrackListRow( .background(rowBackgroundColor) .drawWithCache { val highlightBrush = Brush.verticalGradient( - colors = listOf(rowColors.highlight, Color.Transparent), + colors = listOf(highlightColor, Color.Transparent), startY = 0f, endY = size.height * 0.5f, ) diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt index cf5cfd5b..dd1f0f36 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/core/ui/component/cards/PlayableCard.kt @@ -49,6 +49,8 @@ import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.unit.dp 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.SecondaryIconButton import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer @@ -108,7 +110,11 @@ fun PlayableCard( SecondaryIconButton( onClick = onAddToQueue, modifier = Modifier.size(30.dp), - shape = CircleShape, + theme = LocalBaseUITheme.current.iconButtons.secondary.copy( + shape = BaseUITheme.InteractionState.fromSingleValue( + CircleShape + ) + ) ) { Icon( imageVector = Iconsax.IconsaxAddSquare, @@ -121,7 +127,12 @@ fun PlayableCard( PrimaryIconButton( onClick = onPlay, modifier = Modifier.size(30.dp), - shape = CircleShape, + theme = LocalBaseUITheme.current.iconButtons.secondary.copy( + shape = BaseUITheme.InteractionState.fromSingleValue( + CircleShape + ) + ) + ) { Icon( imageVector = if (isPlaying) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt index dbe796a7..2c2ba3e0 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppExpandedPlayer.kt @@ -83,10 +83,13 @@ import dev.krtirtho.spotube.core.audioplayer.LoopState import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.navigation.NavigationCommands 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.IconButton +import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme 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.DownloadStatus import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel @@ -466,7 +469,10 @@ fun AppExpandedPlayer( textAlign = TextAlign.Center, ) } - IconButton(onClick = onQueue, shape = CircleShape) { + IconButton( + onClick = onQueue, + theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape) + ) { Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue") } } @@ -526,22 +532,14 @@ fun AppExpandedPlayer( } IconButton( onClick = ::onPlayPause, - colors = rememberButtonColors().copy( - containerDarker = MaterialTheme.colorScheme.onSurface, - containerLighter = MaterialTheme.colorScheme.onSurface, - containerPressed = MaterialTheme.colorScheme.onSurfaceVariant, - shadow = MaterialTheme.colorScheme.onSurfaceVariant, - border = MaterialTheme.colorScheme.onSurfaceVariant - ), + theme = invertedButtonStyle().copyShape(CircleShape), modifier = Modifier .size(72.dp), - shape = CircleShape, ) { Icon( if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, contentDescription = if (playerUiState.isPlaying) "Pause" else "Play", modifier = Modifier.size(30.dp), - tint = MaterialTheme.colorScheme.surface, ) } GhostIconButton(onClick = ::onSkipNext) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt index 957c020e..4b6a824a 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppLargePlayer.kt @@ -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.VariableIconButton 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.DownloadsViewModel import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID @@ -279,21 +280,13 @@ fun AppLargePlayer( } IconButton( onClick = ::onPlayPause, - colors = rememberButtonColors().copy( - containerDarker = MaterialTheme.colorScheme.onSurface, - containerLighter = MaterialTheme.colorScheme.onSurface, - containerPressed = MaterialTheme.colorScheme.onSurfaceVariant, - shadow = MaterialTheme.colorScheme.onSurfaceVariant, - border = MaterialTheme.colorScheme.onSurfaceVariant - ), + theme = invertedButtonStyle().copyShape(CircleShape), modifier = Modifier .size(50.dp), - shape = CircleShape, ) { Icon( if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay, contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause", - tint = MaterialTheme.colorScheme.surface, ) } GhostIconButton(onClick = ::onSkipNext) { diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt index cc2b4fce..2158b3ad 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/AppSidebar.kt @@ -21,14 +21,8 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut -import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.background -import androidx.compose.foundation.border 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.Box 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.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.material3.ripple 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.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment 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.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp import dev.krtirtho.spotube.core.navigation.NavigationState import dev.krtirtho.spotube.core.navigation.Navigator 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.buttonShadow -import dev.krtirtho.spotube.core.ui.base.outlinedGradient -import dev.krtirtho.spotube.core.ui.base.rememberButtonColors +import dev.krtirtho.spotube.core.ui.base.copyPadding import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator import dev.krtirtho.spotube.modules.library.LibraryState import dev.krtirtho.spotube.modules.library.LibraryTab @@ -215,66 +201,16 @@ fun SidebarItem( SecondaryButton( onClick = onClick, modifier = buttonModifier, - contentPadding = contentPadding, + theme = LocalBaseUITheme.current.buttons.secondary.copyPadding(contentPadding), content = itemContent, ) } else { - val colors = rememberButtonColors() - val interactionSource = remember { MutableInteractionSource() } - val isHovered by interactionSource.collectIsHoveredAsState() - val isPressed by interactionSource.collectIsPressedAsState() - val gradient = outlinedGradient(colors, isPressed) - val border = colors.border.copy(alpha = if (isPressed) 0.7f else 1f) - 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, - ) - } + OutlineButton( + onClick = onClick, + modifier = buttonModifier, + theme = LocalBaseUITheme.current.buttons.outline.copyPadding(contentPadding), + hoverOnly = true, + content = itemContent, + ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt index c146bb8d..1356c83a 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/PlayerHeartButton.kt @@ -27,6 +27,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue import dev.krtirtho.spotube.core.audioplayer.QueueEntry 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.SavedTracksViewModel import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks @@ -69,7 +71,7 @@ fun PlayerHeartButton( IconButton( onClick = ::onLike, enabled = isSavedTrackState is SavedState.Success, - shape = CircleShape, + theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape), ) { Icon( imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart, diff --git a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt index 20b60180..dcf194c1 100644 --- a/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt +++ b/composeApp/src/commonMain/kotlin/dev/krtirtho/spotube/modules/shell/player_queue/PlayerQueueContent.kt @@ -62,8 +62,10 @@ import compose.icons.feathericons.MoreVertical import dev.krtirtho.spotube.core.audioplayer.QueueEntry import dev.krtirtho.spotube.core.di.rememberLogger 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.TextField +import dev.krtirtho.spotube.core.ui.base.copyShape import dev.krtirtho.spotube.resources.iconsax.Iconsax import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove @@ -188,7 +190,7 @@ fun PlayerQueueContent( ) SecondaryIconButton( onClick = viewModel::clearQueue, - shape = MaterialTheme.shapes.small, + theme = LocalBaseUITheme.current.iconButtons.secondary.copyShape(MaterialTheme.shapes.small), ) { Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue") }