mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
Compare commits
3 Commits
4302a27afa
...
dadc302ef0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadc302ef0 | ||
|
|
a52ba68f1f | ||
|
|
a57b359186 |
@ -20,8 +20,11 @@ package dev.krtirtho.spotube
|
|||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
@ -100,6 +103,8 @@ fun App(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpotubeTheme(settings = userSettings) {
|
SpotubeTheme(settings = userSettings) {
|
||||||
|
val baseUITheme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) {
|
||||||
AppShell(navigator, navigationState) {
|
AppShell(navigator, navigationState) {
|
||||||
Column {
|
Column {
|
||||||
NavDisplay(
|
NavDisplay(
|
||||||
@ -112,3 +117,4 @@ fun App(
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -28,8 +28,8 @@ import androidx.compose.foundation.hoverable
|
|||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.focusable
|
import androidx.compose.foundation.focusable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@ -41,7 +41,6 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
@ -87,12 +86,55 @@ import androidx.compose.ui.window.PopupProperties
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||||
|
|
||||||
private val AutocompleteTextFieldShape = RoundedCornerShape(14.dp)
|
|
||||||
private val AutocompleteMenuShape = RoundedCornerShape(12.dp)
|
|
||||||
private val AutocompleteTextFieldMinHeight = 44.dp
|
private val AutocompleteTextFieldMinHeight = 44.dp
|
||||||
private val AutocompleteMenuDefaultMaxHeight = 300.dp
|
private val AutocompleteMenuDefaultMaxHeight = 300.dp
|
||||||
private val AutocompleteMenuDefaultOffset = 4.dp
|
private val AutocompleteMenuDefaultOffset = 4.dp
|
||||||
|
|
||||||
|
private data class ResolvedAutoCompleteTextFieldState(
|
||||||
|
val background: Brush,
|
||||||
|
val highlight: Color,
|
||||||
|
val shape: Shape,
|
||||||
|
val border: BaseUITheme.Border,
|
||||||
|
val shadow: BaseUITheme.Shadow,
|
||||||
|
val foreground: Color,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun resolveTextFieldState(
|
||||||
|
theme: BaseUITheme.TextFieldTheme,
|
||||||
|
isFocused: Boolean,
|
||||||
|
isHovered: Boolean,
|
||||||
|
): ResolvedAutoCompleteTextFieldState {
|
||||||
|
return when {
|
||||||
|
isFocused -> ResolvedAutoCompleteTextFieldState(
|
||||||
|
theme.background.focused,
|
||||||
|
theme.highlight.focused,
|
||||||
|
theme.shape.focused,
|
||||||
|
theme.border.focused,
|
||||||
|
theme.shadow.focused,
|
||||||
|
theme.foreground.focused
|
||||||
|
)
|
||||||
|
|
||||||
|
isHovered -> ResolvedAutoCompleteTextFieldState(
|
||||||
|
theme.background.hovered,
|
||||||
|
theme.highlight.hovered,
|
||||||
|
theme.shape.hovered,
|
||||||
|
theme.border.hovered,
|
||||||
|
theme.shadow.hovered,
|
||||||
|
theme.foreground.hovered
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> ResolvedAutoCompleteTextFieldState(
|
||||||
|
theme.background.pressed,
|
||||||
|
theme.highlight.pressed,
|
||||||
|
theme.shape.pressed,
|
||||||
|
theme.border.pressed,
|
||||||
|
theme.shadow.pressed,
|
||||||
|
theme.foreground.pressed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> AutocompleteTextField(
|
fun <T> AutocompleteTextField(
|
||||||
value: String,
|
value: String,
|
||||||
@ -115,16 +157,18 @@ fun <T> AutocompleteTextField(
|
|||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
textStyle: TextStyle = TextStyle.Default,
|
textStyle: TextStyle = TextStyle.Default,
|
||||||
cursorBrush: Color = MaterialTheme.colorScheme.primary,
|
cursorBrush: Color? = null,
|
||||||
onKeyEvent: ((KeyEvent) -> Boolean)? = null,
|
onKeyEvent: ((KeyEvent) -> Boolean)? = null,
|
||||||
menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight,
|
menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight,
|
||||||
menuOffset: Dp = AutocompleteMenuDefaultOffset,
|
menuOffset: Dp = AutocompleteMenuDefaultOffset,
|
||||||
menuShape: Shape = AutocompleteMenuShape,
|
textFieldTheme: BaseUITheme.TextFieldTheme? = null,
|
||||||
menuShadowElevation: Dp = 8.dp,
|
menuTheme: BaseUITheme.AutoCompleteMenuTheme? = null,
|
||||||
menuContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
|
||||||
) {
|
) {
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
val colors = rememberButtonColors()
|
val baseTextFieldTheme = LocalBaseUITheme.current.textField
|
||||||
|
val baseMenuTheme = LocalBaseUITheme.current.autoCompleteMenuTheme
|
||||||
|
val resolvedTextFieldTheme = textFieldTheme ?: baseTextFieldTheme
|
||||||
|
val resolvedMenuTheme = menuTheme ?: baseMenuTheme
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
|
||||||
@ -174,19 +218,21 @@ fun <T> AutocompleteTextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val gradient = outlinedGradient(colors, false)
|
val fieldState = resolveTextFieldState(resolvedTextFieldTheme, isFocused, isHovered)
|
||||||
val border = when {
|
|
||||||
isError -> MaterialTheme.colorScheme.error
|
val resolvedBorder = if (isError) {
|
||||||
isFocused -> MaterialTheme.colorScheme.primary
|
BaseUITheme.Border(MaterialTheme.colorScheme.error, fieldState.border.width)
|
||||||
isHovered -> colors.border.copy(alpha = 0.85f)
|
} else {
|
||||||
else -> colors.border
|
fieldState.border
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentColor = when {
|
val contentColor = when {
|
||||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
!enabled -> fieldState.foreground.copy(alpha = 0.38f)
|
||||||
else -> colors.onContainer
|
else -> fieldState.foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cursor = cursorBrush?.let { SolidColor(it) } ?: resolvedTextFieldTheme.cursor
|
||||||
|
|
||||||
fun navigate(delta: Int) {
|
fun navigate(delta: Int) {
|
||||||
if (items.isEmpty()) return
|
if (items.isEmpty()) return
|
||||||
selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size
|
selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size
|
||||||
@ -207,13 +253,25 @@ fun <T> AutocompleteTextField(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.onSizeChanged { textFieldSize = it }
|
.onSizeChanged { textFieldSize = it }
|
||||||
.defaultMinSize(minHeight = AutocompleteTextFieldMinHeight)
|
.defaultMinSize(minHeight = AutocompleteTextFieldMinHeight)
|
||||||
.then(textFieldShadow(AutocompleteTextFieldShape, isFocused, colors))
|
.then(
|
||||||
.clip(AutocompleteTextFieldShape)
|
if (fieldState.shadow.elevation > 0.dp) {
|
||||||
.background(gradient, AutocompleteTextFieldShape)
|
Modifier.shadow(
|
||||||
.border(BorderStroke(.5.dp, border), AutocompleteTextFieldShape)
|
elevation = fieldState.shadow.elevation,
|
||||||
|
shape = fieldState.shape,
|
||||||
|
ambientColor = fieldState.shadow.ambientColor,
|
||||||
|
spotColor = fieldState.shadow.spotColor,
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.clip(fieldState.shape)
|
||||||
|
.background(fieldState.background, fieldState.shape)
|
||||||
|
.border(
|
||||||
|
BorderStroke(resolvedBorder.width, resolvedBorder.color),
|
||||||
|
fieldState.shape
|
||||||
|
)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(colors.highlight, Color.Transparent),
|
colors = listOf(fieldState.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
@ -226,7 +284,7 @@ fun <T> AutocompleteTextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
.padding(resolvedTextFieldTheme.padding),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -294,7 +352,7 @@ fun <T> AutocompleteTextField(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
textStyle = textStyle.copy(color = contentColor),
|
textStyle = textStyle.copy(color = contentColor),
|
||||||
cursorBrush = SolidColor(cursorBrush),
|
cursorBrush = cursor,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = wrappedKeyboardActions,
|
keyboardActions = wrappedKeyboardActions,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
@ -340,9 +398,9 @@ fun <T> AutocompleteTextField(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(textFieldWidthDp)
|
.width(textFieldWidthDp)
|
||||||
.heightIn(max = menuMaxHeight)
|
.heightIn(max = menuMaxHeight)
|
||||||
.shadow(menuShadowElevation, menuShape)
|
.shadow(resolvedMenuTheme.shadowElevation, resolvedMenuTheme.shape)
|
||||||
.background(menuContainerColor, menuShape)
|
.background(resolvedMenuTheme.background, resolvedMenuTheme.shape)
|
||||||
.clip(menuShape)
|
.clip(resolvedMenuTheme.shape)
|
||||||
.focusRequester(popupFocusRequester)
|
.focusRequester(popupFocusRequester)
|
||||||
.focusable()
|
.focusable()
|
||||||
.onFocusChanged { state ->
|
.onFocusChanged { state ->
|
||||||
@ -412,6 +470,8 @@ fun <T> AutocompleteTextField(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun AutocompleteTextFieldPreview() {
|
private fun AutocompleteTextFieldPreview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
androidx.compose.material3.Surface(
|
androidx.compose.material3.Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -479,3 +539,4 @@ private fun AutocompleteTextFieldPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -44,7 +44,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.material3.ripple
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.Immutable
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -54,8 +53,8 @@ import androidx.compose.ui.draw.drawWithCache
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.graphics.luminance
|
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
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.IconsaxShare
|
||||||
import dev.krtirtho.spotube.resources.iconsax.User
|
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 BadgeShape = RoundedCornerShape(11.dp)
|
||||||
private val ButtonMinHeight = 40.dp
|
private val ButtonMinHeight = 40.dp
|
||||||
private val SquareButtonSize = 40.dp
|
private val SquareButtonSize = 40.dp
|
||||||
|
|
||||||
@Immutable
|
private data class ResolvedButtonState(
|
||||||
data class ButtonColors(
|
val colors: BaseUITheme.ButtonColors,
|
||||||
val containerLighter: Color,
|
val shape: Shape,
|
||||||
val containerDarker: Color,
|
val shadow: BaseUITheme.Shadow,
|
||||||
val containerPressed: Color,
|
val border: BaseUITheme.Border,
|
||||||
val onContainer: Color,
|
val padding: PaddingValues,
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun rememberButtonColors(): ButtonColors {
|
private fun resolveButtonState(
|
||||||
val scheme = MaterialTheme.colorScheme
|
style: BaseUITheme.ButtonStyle,
|
||||||
val isLight = scheme.surface.luminance() > 0.5f
|
isPressed: Boolean,
|
||||||
|
isHovered: Boolean,
|
||||||
return remember(scheme) {
|
): ResolvedButtonState {
|
||||||
ButtonColors(
|
return when {
|
||||||
containerLighter = if (isLight) {
|
isPressed -> ResolvedButtonState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
||||||
Color.White
|
isHovered -> ResolvedButtonState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
||||||
} else {
|
else -> ResolvedButtonState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused)
|
||||||
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)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
private fun Modifier.applyShadow(
|
||||||
fun outlinedGradient(colors: ButtonColors, pressed: Boolean): Brush {
|
shadow: BaseUITheme.Shadow,
|
||||||
val top = if (pressed) colors.containerPressed else colors.containerLighter
|
shape: Shape,
|
||||||
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,
|
|
||||||
): Modifier {
|
): Modifier {
|
||||||
val elevation = when {
|
return if (shadow.elevation > 0.dp) {
|
||||||
pressed -> 1.dp
|
this.shadow(
|
||||||
hovered -> 9.dp
|
elevation = shadow.elevation,
|
||||||
else -> 6.dp
|
|
||||||
}
|
|
||||||
val ambient = if (primary) {
|
|
||||||
colors.accent.copy(
|
|
||||||
alpha = when {
|
|
||||||
pressed -> 0.2f
|
|
||||||
hovered -> 0.45f
|
|
||||||
else -> 0.35f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
colors.shadow.copy(
|
|
||||||
alpha = when {
|
|
||||||
pressed -> 0.08f
|
|
||||||
hovered -> 0.22f
|
|
||||||
else -> 0.15f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
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,
|
shape = shape,
|
||||||
ambientColor = ambient,
|
ambientColor = shadow.ambientColor,
|
||||||
spotColor = spot,
|
spotColor = shadow.spotColor,
|
||||||
)
|
)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -235,18 +116,16 @@ fun OutlineButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
|
||||||
hoverOnly: Boolean = false,
|
hoverOnly: Boolean = false,
|
||||||
colors: ButtonColors? = null,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = colors ?: rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.buttons.outline
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
val gradient = outlinedGradient(colors, isPressed)
|
val state = resolveButtonState(style, isPressed, isHovered)
|
||||||
val border = colors.border.copy(alpha = if (isPressed) 0.7f else 1f)
|
|
||||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@ -255,18 +134,12 @@ fun OutlineButton(
|
|||||||
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
||||||
.graphicsLayer { translationY = lift.toPx() }
|
.graphicsLayer { translationY = lift.toPx() }
|
||||||
.then(
|
.then(
|
||||||
if (hoverOnly && !isHovered) Modifier else buttonShadow(
|
if (hoverOnly && !isHovered) Modifier else Modifier.applyShadow(state.shadow, state.shape)
|
||||||
shape,
|
|
||||||
isPressed,
|
|
||||||
primary = false,
|
|
||||||
colors,
|
|
||||||
hovered = isHovered
|
|
||||||
)
|
)
|
||||||
)
|
.clip(state.shape)
|
||||||
.clip(shape)
|
|
||||||
.then(
|
.then(
|
||||||
if (hoverOnly && !isHovered) Modifier else Modifier.background(gradient, shape)
|
if (hoverOnly && !isHovered) Modifier else Modifier.background(state.colors.background, state.shape)
|
||||||
.border(BorderStroke(0.5.dp, border), shape)
|
.border(BorderStroke(state.border.width, state.border.color), state.shape)
|
||||||
)
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
@ -276,7 +149,7 @@ fun OutlineButton(
|
|||||||
)
|
)
|
||||||
.then(if (hoverOnly && !isHovered) Modifier else Modifier.drawWithCache {
|
.then(if (hoverOnly && !isHovered) Modifier else Modifier.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(colors.highlight, Color.Transparent),
|
colors = listOf(state.colors.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
@ -289,13 +162,17 @@ fun OutlineButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.padding(contentPadding),
|
.padding(state.padding),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
content = content,
|
content = {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides state.colors.foreground) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -305,16 +182,15 @@ fun PrimaryButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = colors ?: rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.buttons.primary
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
val gradient = primaryGradient(colors, isPressed)
|
val state = resolveButtonState(style, isPressed, isHovered)
|
||||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@ -322,10 +198,10 @@ fun PrimaryButton(
|
|||||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||||
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
||||||
.graphicsLayer { translationY = lift.toPx() }
|
.graphicsLayer { translationY = lift.toPx() }
|
||||||
.then(buttonShadow(shape, isPressed, primary = true, colors, hovered = isHovered))
|
.applyShadow(state.shadow, state.shape)
|
||||||
.clip(shape)
|
.clip(state.shape)
|
||||||
.background(gradient, shape)
|
.background(state.colors.background, state.shape)
|
||||||
.border(BorderStroke(0.5.dp, colors.accent), shape)
|
.border(BorderStroke(state.border.width, state.border.color), state.shape)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
@ -334,7 +210,7 @@ fun PrimaryButton(
|
|||||||
)
|
)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(Color.White.copy(alpha = 0.25f), Color.Transparent),
|
colors = listOf(state.colors.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
@ -347,10 +223,10 @@ fun PrimaryButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(contentPadding),
|
.padding(state.padding),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalContentColor provides colors.onAccent) {
|
CompositionLocalProvider(LocalContentColor provides state.colors.foreground) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@ -365,16 +241,15 @@ fun SecondaryButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = colors ?: rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.buttons.secondary
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
val gradient = secondaryGradient(colors, isPressed)
|
val state = resolveButtonState(style, isPressed, isHovered)
|
||||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
@ -382,13 +257,10 @@ fun SecondaryButton(
|
|||||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||||
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
||||||
.graphicsLayer { translationY = lift.toPx() }
|
.graphicsLayer { translationY = lift.toPx() }
|
||||||
.then(buttonShadow(shape, isPressed, primary = false, colors, hovered = isHovered))
|
.applyShadow(state.shadow, state.shape)
|
||||||
.clip(shape)
|
.clip(state.shape)
|
||||||
.background(gradient, shape)
|
.background(state.colors.background, state.shape)
|
||||||
.border(
|
.border(BorderStroke(state.border.width, state.border.color), state.shape)
|
||||||
BorderStroke(0.5.dp, colors.secondaryContainer.copy(alpha = 0.5f)),
|
|
||||||
shape,
|
|
||||||
)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
@ -397,7 +269,7 @@ fun SecondaryButton(
|
|||||||
)
|
)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(colors.secondaryHighlight, Color.Transparent),
|
colors = listOf(state.colors.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
@ -410,10 +282,10 @@ fun SecondaryButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(contentPadding),
|
.padding(state.padding),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalContentColor provides colors.onSecondaryContainer) {
|
CompositionLocalProvider(LocalContentColor provides state.colors.foreground) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
@ -428,40 +300,60 @@ fun IconButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.iconButtons.outline
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = style,
|
||||||
contentPadding = PaddingValues(8.dp),
|
|
||||||
colors = colors,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
||||||
fun GhostIconButton(
|
fun GhostIconButton(
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val outlineStyle = baseTheme.iconButtons.outline
|
||||||
|
val ghostStyle = theme ?: BaseUITheme.ButtonStyle(
|
||||||
|
colors = BaseUITheme.InteractionState(
|
||||||
|
hovered = outlineStyle.colors.hovered,
|
||||||
|
pressed = outlineStyle.colors.pressed,
|
||||||
|
focused = BaseUITheme.ButtonColors(
|
||||||
|
background = Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent)),
|
||||||
|
foreground = outlineStyle.colors.focused.foreground,
|
||||||
|
highlight = outlineStyle.colors.focused.highlight,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
shape = outlineStyle.shape,
|
||||||
|
shadow = BaseUITheme.InteractionState(
|
||||||
|
hovered = outlineStyle.shadow.hovered,
|
||||||
|
pressed = outlineStyle.shadow.pressed,
|
||||||
|
focused = BaseUITheme.Shadow(0.dp, false, Color.Transparent, Color.Transparent),
|
||||||
|
),
|
||||||
|
border = BaseUITheme.InteractionState(
|
||||||
|
hovered = outlineStyle.border.hovered,
|
||||||
|
pressed = outlineStyle.border.pressed,
|
||||||
|
focused = BaseUITheme.Border(Color.Transparent, 0.dp),
|
||||||
|
),
|
||||||
|
padding = outlineStyle.padding,
|
||||||
|
)
|
||||||
OutlineButton(
|
OutlineButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
|
||||||
contentPadding = PaddingValues(8.dp),
|
|
||||||
hoverOnly = true,
|
hoverOnly = true,
|
||||||
colors = colors,
|
theme = ghostStyle,
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -472,17 +364,16 @@ fun PrimaryIconButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.iconButtons.primary
|
||||||
PrimaryButton(
|
PrimaryButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = style,
|
||||||
contentPadding = PaddingValues(8.dp),
|
|
||||||
colors = colors,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -493,17 +384,16 @@ fun SecondaryIconButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.iconButtons.secondary
|
||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = style,
|
||||||
contentPadding = PaddingValues(8.dp),
|
|
||||||
colors = colors,
|
|
||||||
) {
|
) {
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
@ -513,15 +403,17 @@ fun SecondaryIconButton(
|
|||||||
fun ButtonBadge(
|
fun ButtonBadge(
|
||||||
count: Int,
|
count: Int,
|
||||||
modifier: Modifier = Modifier,
|
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(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.heightIn(min = 22.dp)
|
.heightIn(min = 22.dp)
|
||||||
.defaultMinSize(minWidth = 22.dp)
|
.defaultMinSize(minWidth = 22.dp)
|
||||||
.background(badgeGradient(colors), BadgeShape)
|
.background(state.colors.background, BadgeShape)
|
||||||
.border(1.dp, colors.border, BadgeShape)
|
.border(1.dp, state.border.color, BadgeShape)
|
||||||
.padding(horizontal = 7.dp),
|
.padding(horizontal = 7.dp),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
@ -531,7 +423,7 @@ fun ButtonBadge(
|
|||||||
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
|
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
color = colors.onContainer.copy(alpha = 0.7f),
|
color = state.colors.foreground.copy(alpha = 0.7f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -541,18 +433,19 @@ fun GroupButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 11.dp),
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = colors ?: rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.buttons.outline
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
val state = resolveButtonState(style, isPressed, isHovered)
|
||||||
val overlay = when {
|
val overlay = when {
|
||||||
isPressed -> colors.containerPressed
|
isPressed -> state.colors.background
|
||||||
isHovered -> colors.containerPressed.copy(alpha = 0.5f)
|
isHovered -> state.colors.background
|
||||||
else -> Color.Transparent
|
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||||
}
|
}
|
||||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
||||||
|
|
||||||
@ -568,7 +461,7 @@ fun GroupButton(
|
|||||||
indication = ripple(),
|
indication = ripple(),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
.padding(contentPadding),
|
.padding(state.padding),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
@ -584,17 +477,19 @@ fun GroupIconButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
colors: ButtonColors? = null,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = colors ?: rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current
|
||||||
|
val style = theme ?: baseTheme.buttons.outline
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
val state = resolveButtonState(style, isPressed, isHovered)
|
||||||
val overlay = when {
|
val overlay = when {
|
||||||
isPressed -> colors.containerPressed
|
isPressed -> state.colors.background
|
||||||
isHovered -> colors.containerPressed.copy(alpha = 0.5f)
|
isHovered -> state.colors.background
|
||||||
else -> Color.Transparent
|
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||||
}
|
}
|
||||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
||||||
|
|
||||||
@ -619,28 +514,23 @@ fun GroupIconButton(
|
|||||||
@Composable
|
@Composable
|
||||||
fun ButtonGroup(
|
fun ButtonGroup(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
colors: ButtonColors? = null,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
content: @Composable () -> Unit,
|
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(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||||
.then(
|
.applyShadow(state.shadow, groupShape),
|
||||||
buttonShadow(
|
shape = groupShape,
|
||||||
GroupShape,
|
|
||||||
pressed = false,
|
|
||||||
primary = false,
|
|
||||||
colors,
|
|
||||||
hovered = false
|
|
||||||
)
|
|
||||||
),
|
|
||||||
shape = GroupShape,
|
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
border = BorderStroke(0.5.dp, colors.border),
|
border = BorderStroke(state.border.width, state.border.color),
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.background(outlinedGradient(colors, false), GroupShape),
|
modifier = Modifier.background(state.colors.background, groupShape),
|
||||||
) {
|
) {
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
content()
|
content()
|
||||||
@ -651,14 +541,16 @@ fun ButtonGroup(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ButtonGroupDivider(
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(1.dp)
|
.width(1.dp)
|
||||||
.heightIn(min = 20.dp)
|
.heightIn(min = 20.dp)
|
||||||
.background(colors.border),
|
.background(state.border.color),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -666,6 +558,8 @@ fun ButtonGroupDivider(
|
|||||||
@Composable
|
@Composable
|
||||||
private fun ButtonsRow1Preview() {
|
private fun ButtonsRow1Preview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -711,11 +605,14 @@ private fun ButtonsRow1Preview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ButtonsRow2Preview() {
|
private fun ButtonsRow2Preview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -772,11 +669,14 @@ private fun ButtonsRow2Preview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ButtonsRow3Preview() {
|
private fun ButtonsRow3Preview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -816,11 +716,14 @@ private fun ButtonsRow3Preview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ButtonsRow4Preview() {
|
private fun ButtonsRow4Preview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -858,11 +761,14 @@ private fun ButtonsRow4Preview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Preview(showBackground = true)
|
@Preview(showBackground = true)
|
||||||
@Composable
|
@Composable
|
||||||
private fun ButtonStylesPreview() {
|
private fun ButtonStylesPreview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -889,6 +795,7 @@ private fun ButtonStylesPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class VariableIconButtonVariant {
|
enum class VariableIconButtonVariant {
|
||||||
Outline,
|
Outline,
|
||||||
@ -903,8 +810,7 @@ fun VariableIconButton(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
theme: BaseUITheme.ButtonStyle? = null,
|
||||||
colors: ButtonColors? = null,
|
|
||||||
content: @Composable () -> Unit,
|
content: @Composable () -> Unit,
|
||||||
) {
|
) {
|
||||||
when (variant) {
|
when (variant) {
|
||||||
@ -912,8 +818,7 @@ fun VariableIconButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = theme,
|
||||||
colors = colors,
|
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -921,8 +826,7 @@ fun VariableIconButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = theme,
|
||||||
colors = colors,
|
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -930,8 +834,7 @@ fun VariableIconButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = theme,
|
||||||
colors = colors,
|
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -939,8 +842,7 @@ fun VariableIconButton(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
shape = shape,
|
theme = theme,
|
||||||
colors = colors,
|
|
||||||
content = content,
|
content = content,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,81 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.krtirtho.spotube.core.ui.base
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Card(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
theme: BaseUITheme.CardTheme? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val cardTheme = theme ?: LocalBaseUITheme.current.card
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.then(
|
||||||
|
if (cardTheme.shadow.elevation > 0.dp) {
|
||||||
|
Modifier.shadow(
|
||||||
|
elevation = cardTheme.shadow.elevation,
|
||||||
|
shape = cardTheme.shape,
|
||||||
|
ambientColor = cardTheme.shadow.ambientColor,
|
||||||
|
spotColor = cardTheme.shadow.spotColor,
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.clip(cardTheme.shape)
|
||||||
|
.background(cardTheme.background, cardTheme.shape)
|
||||||
|
.border(BorderStroke(cardTheme.border.width, cardTheme.border.color), cardTheme.shape)
|
||||||
|
.padding(cardTheme.padding),
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@androidx.compose.ui.tooling.preview.Preview
|
||||||
|
private fun CardPreview() {
|
||||||
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
|
Surface(
|
||||||
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
modifier = Modifier.padding(24.dp),
|
||||||
|
) {
|
||||||
|
Card {
|
||||||
|
Box(modifier = Modifier.padding(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -39,6 +39,7 @@ import androidx.compose.material3.Surface
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
@ -50,6 +51,7 @@ import androidx.compose.ui.geometry.Offset
|
|||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Path
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.StrokeCap
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.StrokeJoin
|
import androidx.compose.ui.graphics.StrokeJoin
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
@ -65,9 +67,28 @@ enum class CheckBoxState {
|
|||||||
CLEAR;
|
CLEAR;
|
||||||
}
|
}
|
||||||
|
|
||||||
private val CheckBoxShape = RoundedCornerShape(6.dp)
|
|
||||||
private val CheckBoxSize = 22.dp
|
private val CheckBoxSize = 22.dp
|
||||||
|
|
||||||
|
private data class ResolvedCheckBoxState(
|
||||||
|
val colors: BaseUITheme.ButtonColors,
|
||||||
|
val shape: Shape,
|
||||||
|
val shadow: BaseUITheme.Shadow,
|
||||||
|
val border: BaseUITheme.Border,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun resolveCheckBoxState(
|
||||||
|
style: BaseUITheme.ButtonStyle,
|
||||||
|
isPressed: Boolean,
|
||||||
|
isHovered: Boolean,
|
||||||
|
): ResolvedCheckBoxState {
|
||||||
|
return when {
|
||||||
|
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
||||||
|
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
||||||
|
else -> ResolvedCheckBoxState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CheckBox(
|
fun CheckBox(
|
||||||
state: CheckBoxState = CheckBoxState.UNSELECTED,
|
state: CheckBoxState = CheckBoxState.UNSELECTED,
|
||||||
@ -75,69 +96,25 @@ fun CheckBox(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
interactionSource: MutableInteractionSource? = null,
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
theme: BaseUITheme.CheckBoxTheme? = null,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current.checkBox
|
||||||
|
val checkBoxTheme = theme ?: baseTheme
|
||||||
val source = interactionSource ?: remember { MutableInteractionSource() }
|
val source = interactionSource ?: remember { MutableInteractionSource() }
|
||||||
val isPressed by source.collectIsPressedAsState()
|
val isPressed by source.collectIsPressedAsState()
|
||||||
val isHovered by source.collectIsHoveredAsState()
|
val isHovered by source.collectIsHoveredAsState()
|
||||||
val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
|
|
||||||
|
|
||||||
val isSelected = state == CheckBoxState.SELECTED
|
val isSelected = state == CheckBoxState.SELECTED
|
||||||
val isIndeterminate = state == CheckBoxState.INDETERMINATE
|
val isIndeterminate = state == CheckBoxState.INDETERMINATE
|
||||||
val isFilled = isSelected || isIndeterminate
|
val isFilled = isSelected || isIndeterminate
|
||||||
|
|
||||||
val backgroundBrush = if (isFilled) {
|
val style = if (isFilled) checkBoxTheme.selected else checkBoxTheme.unselected
|
||||||
primaryGradient(colors, isPressed)
|
val resolved = resolveCheckBoxState(style, isPressed, isHovered)
|
||||||
} else {
|
val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
|
||||||
outlinedGradient(colors, false)
|
|
||||||
}
|
|
||||||
val borderColor = when {
|
|
||||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
|
||||||
isFilled -> colors.accent
|
|
||||||
else -> colors.border
|
|
||||||
}
|
|
||||||
val borderWidth = 0.5.dp
|
|
||||||
|
|
||||||
val shadowElevation = when {
|
val borderColor = when {
|
||||||
isPressed -> 1.dp
|
!enabled -> resolved.colors.foreground.copy(alpha = 0.38f)
|
||||||
isFilled && isHovered -> 8.dp
|
else -> resolved.border.color
|
||||||
isFilled -> 6.dp
|
|
||||||
isHovered -> 5.dp
|
|
||||||
else -> 3.dp
|
|
||||||
}
|
|
||||||
val shadowAmbient = if (isFilled) {
|
|
||||||
colors.accent.copy(
|
|
||||||
alpha = when {
|
|
||||||
isPressed -> 0.2f
|
|
||||||
isHovered -> 0.4f
|
|
||||||
else -> 0.3f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
colors.shadow.copy(
|
|
||||||
alpha = when {
|
|
||||||
isPressed -> 0.08f
|
|
||||||
isHovered -> 0.2f
|
|
||||||
else -> 0.15f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
val shadowSpot = if (isFilled) {
|
|
||||||
colors.accent.copy(
|
|
||||||
alpha = when {
|
|
||||||
isPressed -> 0.25f
|
|
||||||
isHovered -> 0.45f
|
|
||||||
else -> 0.35f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
colors.shadow.copy(
|
|
||||||
alpha = when {
|
|
||||||
isPressed -> 0.1f
|
|
||||||
isHovered -> 0.24f
|
|
||||||
else -> 0.18f
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val checkProgress by animateFloatAsState(
|
val checkProgress by animateFloatAsState(
|
||||||
@ -155,28 +132,29 @@ fun CheckBox(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(CheckBoxSize)
|
.size(CheckBoxSize)
|
||||||
.graphicsLayer { translationY = lift.toPx() }
|
.graphicsLayer { translationY = lift.toPx() }
|
||||||
.shadow(
|
.then(
|
||||||
elevation = shadowElevation,
|
if (resolved.shadow.elevation > 0.dp) {
|
||||||
shape = CheckBoxShape,
|
Modifier.shadow(
|
||||||
ambientColor = shadowAmbient,
|
elevation = resolved.shadow.elevation,
|
||||||
spotColor = shadowSpot,
|
shape = resolved.shape,
|
||||||
|
ambientColor = resolved.shadow.ambientColor,
|
||||||
|
spotColor = resolved.shadow.spotColor,
|
||||||
)
|
)
|
||||||
.clip(CheckBoxShape)
|
} else Modifier
|
||||||
.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 {
|
.drawWithCache {
|
||||||
val highlight = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(
|
colors = listOf(resolved.colors.highlight, Color.Transparent),
|
||||||
if (isFilled) Color.White.copy(alpha = 0.25f) else colors.highlight,
|
|
||||||
Color.Transparent,
|
|
||||||
),
|
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
onDrawWithContent {
|
onDrawWithContent {
|
||||||
drawContent()
|
drawContent()
|
||||||
drawRect(
|
drawRect(
|
||||||
brush = highlight,
|
brush = highlightBrush,
|
||||||
topLeft = Offset.Zero,
|
topLeft = Offset.Zero,
|
||||||
size = size,
|
size = size,
|
||||||
)
|
)
|
||||||
@ -215,7 +193,7 @@ fun CheckBox(
|
|||||||
}
|
}
|
||||||
drawPath(
|
drawPath(
|
||||||
path = path,
|
path = path,
|
||||||
color = colors.onAccent,
|
color = checkBoxTheme.checkmarkColor,
|
||||||
style = Stroke(
|
style = Stroke(
|
||||||
width = stroke,
|
width = stroke,
|
||||||
cap = StrokeCap.Round,
|
cap = StrokeCap.Round,
|
||||||
@ -233,8 +211,8 @@ fun CheckBox(
|
|||||||
.background(
|
.background(
|
||||||
brush = Brush.horizontalGradient(
|
brush = Brush.horizontalGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
colors.onAccent,
|
checkBoxTheme.checkmarkColor,
|
||||||
colors.onAccent.copy(alpha = 0.92f),
|
checkBoxTheme.checkmarkColor.copy(alpha = 0.92f),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -247,6 +225,8 @@ fun CheckBox(
|
|||||||
@Composable
|
@Composable
|
||||||
fun CheckBoxPreview() {
|
fun CheckBoxPreview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
Surface(
|
Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -284,3 +264,4 @@ fun CheckBoxPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -27,12 +27,10 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
|||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.LocalContentColor
|
import androidx.compose.material3.LocalContentColor
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
@ -45,57 +43,78 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
private val ChipTabShape = RoundedCornerShape(10.dp)
|
|
||||||
private val ChipTabMinHeight = 36.dp
|
private val ChipTabMinHeight = 36.dp
|
||||||
|
|
||||||
|
private data class ResolvedChipState(
|
||||||
|
val colors: BaseUITheme.ButtonColors,
|
||||||
|
val shape: Shape,
|
||||||
|
val shadow: BaseUITheme.Shadow,
|
||||||
|
val border: BaseUITheme.Border,
|
||||||
|
val padding: androidx.compose.foundation.layout.PaddingValues,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun resolveChipState(
|
||||||
|
style: BaseUITheme.ButtonStyle,
|
||||||
|
isPressed: Boolean,
|
||||||
|
isHovered: Boolean,
|
||||||
|
): ResolvedChipState {
|
||||||
|
return when {
|
||||||
|
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
||||||
|
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
||||||
|
else -> ResolvedChipState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Modifier.applyChipShadow(
|
||||||
|
shadow: BaseUITheme.Shadow,
|
||||||
|
shape: Shape,
|
||||||
|
): Modifier {
|
||||||
|
return if (shadow.elevation > 0.dp) {
|
||||||
|
this.shadow(
|
||||||
|
elevation = shadow.elevation,
|
||||||
|
shape = shape,
|
||||||
|
ambientColor = shadow.ambientColor,
|
||||||
|
spotColor = shadow.spotColor,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ChipTab(
|
fun ChipTab(
|
||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
theme: BaseUITheme.ChipTabTheme? = null,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current.chipTab
|
||||||
|
val chipTheme = theme ?: baseTheme
|
||||||
|
val style = if (selected) chipTheme.selected else chipTheme.unselected
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
val gradient = if (selected) {
|
val state = resolveChipState(style, isPressed, isHovered)
|
||||||
primaryGradient(colors, isPressed)
|
|
||||||
} else {
|
|
||||||
outlinedGradient(colors, isPressed)
|
|
||||||
}
|
|
||||||
val borderColor = if (selected) {
|
|
||||||
colors.accent
|
|
||||||
} else {
|
|
||||||
colors.border.copy(alpha = if (isPressed) 0.7f else 1f)
|
|
||||||
}
|
|
||||||
val contentColor = if (selected) colors.onAccent else colors.onContainer
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = ChipTabMinHeight)
|
.defaultMinSize(minHeight = ChipTabMinHeight)
|
||||||
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
||||||
.then(
|
.applyChipShadow(state.shadow, state.shape)
|
||||||
buttonShadow(
|
.clip(state.shape)
|
||||||
shape = ChipTabShape,
|
.background(state.colors.background, state.shape)
|
||||||
pressed = isPressed,
|
.border(BorderStroke(state.border.width, state.border.color), state.shape)
|
||||||
primary = selected,
|
|
||||||
colors = colors,
|
|
||||||
hovered = isHovered,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.clip(ChipTabShape)
|
|
||||||
.background(gradient, ChipTabShape)
|
|
||||||
.border(BorderStroke(0.5.dp, borderColor), ChipTabShape)
|
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
@ -103,13 +122,8 @@ fun ChipTab(
|
|||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlight = if (selected) {
|
|
||||||
Color.White.copy(alpha = 0.25f)
|
|
||||||
} else {
|
|
||||||
colors.highlight
|
|
||||||
}
|
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(highlight, Color.Transparent),
|
colors = listOf(state.colors.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
@ -122,11 +136,11 @@ fun ChipTab(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(contentPadding),
|
.padding(state.padding),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalContentColor provides contentColor,
|
LocalContentColor provides state.colors.foreground,
|
||||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||||
fontWeight = FontWeight.SemiBold,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = 14.sp,
|
fontSize = 14.sp,
|
||||||
@ -149,14 +163,14 @@ fun ChipTab(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
leadingIcon: @Composable (() -> Unit)? = null,
|
leadingIcon: @Composable (() -> Unit)? = null,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
theme: BaseUITheme.ChipTabTheme? = null,
|
||||||
) {
|
) {
|
||||||
ChipTab(
|
ChipTab(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
contentPadding = contentPadding,
|
theme = theme,
|
||||||
) {
|
) {
|
||||||
if (leadingIcon != null) {
|
if (leadingIcon != null) {
|
||||||
leadingIcon()
|
leadingIcon()
|
||||||
|
|||||||
@ -43,6 +43,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableFloatStateOf
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
@ -79,8 +80,10 @@ fun Slider(
|
|||||||
steps: Int = 0,
|
steps: Int = 0,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
onValueChangeFinished: () -> Unit = {},
|
onValueChangeFinished: () -> Unit = {},
|
||||||
|
theme: BaseUITheme.SliderTheme? = null,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current.slider
|
||||||
|
val sliderTheme = theme ?: baseTheme
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
val isPressed by interactionSource.collectIsPressedAsState()
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
val density = LocalDensity.current
|
val density = LocalDensity.current
|
||||||
@ -89,13 +92,30 @@ fun Slider(
|
|||||||
val range = valueRange.endInclusive - valueRange.start
|
val range = valueRange.endInclusive - valueRange.start
|
||||||
val fraction = if (range > 0f) ((value - valueRange.start) / range).coerceIn(0f, 1f) else 0f
|
val fraction = if (range > 0f) ((value - valueRange.start) / range).coerceIn(0f, 1f) else 0f
|
||||||
|
|
||||||
val trackActiveColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f)
|
val stateColor = when {
|
||||||
val trackInactiveColor = if (enabled) {
|
isPressed -> sliderTheme.trackActiveColor.pressed
|
||||||
colors.border.copy(alpha = 0.6f)
|
isHovered -> sliderTheme.trackActiveColor.hovered
|
||||||
} else {
|
else -> sliderTheme.trackActiveColor.focused
|
||||||
colors.onContainer.copy(alpha = 0.2f)
|
|
||||||
}
|
}
|
||||||
val thumbColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f)
|
val inactiveColor = when {
|
||||||
|
isPressed -> sliderTheme.trackInactiveColor.pressed
|
||||||
|
isHovered -> sliderTheme.trackInactiveColor.hovered
|
||||||
|
else -> sliderTheme.trackInactiveColor.focused
|
||||||
|
}
|
||||||
|
val thumbColor = when {
|
||||||
|
isPressed -> sliderTheme.thumbColor.pressed
|
||||||
|
isHovered -> sliderTheme.thumbColor.hovered
|
||||||
|
else -> sliderTheme.thumbColor.focused
|
||||||
|
}
|
||||||
|
val thumbShadowState = when {
|
||||||
|
isPressed -> sliderTheme.thumbShadow.pressed
|
||||||
|
isHovered -> sliderTheme.thumbShadow.hovered
|
||||||
|
else -> sliderTheme.thumbShadow.focused
|
||||||
|
}
|
||||||
|
|
||||||
|
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
||||||
|
val trackInactiveColor = if (enabled) inactiveColor else inactiveColor.copy(alpha = 0.38f)
|
||||||
|
val resolvedThumbColor = if (enabled) thumbColor else thumbColor.copy(alpha = 0.38f)
|
||||||
|
|
||||||
val thumbScale by animateFloatAsState(
|
val thumbScale by animateFloatAsState(
|
||||||
targetValue = when {
|
targetValue = when {
|
||||||
@ -107,12 +127,6 @@ fun Slider(
|
|||||||
label = "thumbScale",
|
label = "thumbScale",
|
||||||
)
|
)
|
||||||
|
|
||||||
val thumbElevation = when {
|
|
||||||
isPressed -> 2.dp
|
|
||||||
isHovered -> 8.dp
|
|
||||||
else -> 5.dp
|
|
||||||
}
|
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
@ -241,9 +255,9 @@ fun Slider(
|
|||||||
val x = trackStart + stepSpacing * i
|
val x = trackStart + stepSpacing * i
|
||||||
drawCircle(
|
drawCircle(
|
||||||
color = if (x <= activeEnd) {
|
color = if (x <= activeEnd) {
|
||||||
colors.onAccent.copy(alpha = 0.5f)
|
sliderTheme.stepActiveColor
|
||||||
} else {
|
} else {
|
||||||
colors.onContainer.copy(alpha = 0.25f)
|
sliderTheme.stepInactiveColor
|
||||||
},
|
},
|
||||||
radius = 2.dp.toPx(),
|
radius = 2.dp.toPx(),
|
||||||
center = Offset(x, trackY),
|
center = Offset(x, trackY),
|
||||||
@ -267,17 +281,17 @@ fun Slider(
|
|||||||
scaleY = thumbScale
|
scaleY = thumbScale
|
||||||
}
|
}
|
||||||
.shadow(
|
.shadow(
|
||||||
elevation = thumbElevation,
|
elevation = thumbShadowState.elevation,
|
||||||
shape = CircleShape,
|
shape = CircleShape,
|
||||||
ambientColor = colors.accent.copy(alpha = 0.35f),
|
ambientColor = thumbShadowState.ambientColor,
|
||||||
spotColor = colors.accent.copy(alpha = 0.5f),
|
spotColor = thumbShadowState.spotColor,
|
||||||
)
|
)
|
||||||
.background(
|
.background(
|
||||||
brush = Brush.radialGradient(
|
brush = Brush.radialGradient(
|
||||||
colors = listOf(
|
colors = listOf(
|
||||||
Color.White.copy(alpha = 0.6f),
|
Color.White.copy(alpha = 0.6f),
|
||||||
thumbColor.copy(alpha = 0.95f),
|
resolvedThumbColor.copy(alpha = 0.95f),
|
||||||
thumbColor,
|
resolvedThumbColor,
|
||||||
),
|
),
|
||||||
center = Offset(0f, -0.55f),
|
center = Offset(0f, -0.55f),
|
||||||
radius = 1.1f,
|
radius = 1.1f,
|
||||||
|
|||||||
@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.Row
|
|||||||
import androidx.compose.foundation.layout.defaultMinSize
|
import androidx.compose.foundation.layout.defaultMinSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.text.KeyboardActions
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
@ -52,6 +51,7 @@ import androidx.compose.ui.draw.drawWithCache
|
|||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.input.VisualTransformation
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
@ -61,32 +61,28 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
|||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||||
|
|
||||||
private val TextFieldShape = RoundedCornerShape(14.dp)
|
|
||||||
private val TextFieldMinHeight = 44.dp
|
private val TextFieldMinHeight = 44.dp
|
||||||
|
|
||||||
@Composable
|
private data class ResolvedTextFieldState(
|
||||||
internal fun textFieldShadow(
|
val background: Brush,
|
||||||
shape: androidx.compose.ui.graphics.Shape,
|
val highlight: Color,
|
||||||
focused: Boolean,
|
val shape: Shape,
|
||||||
colors: ButtonColors,
|
val border: BaseUITheme.Border,
|
||||||
): Modifier {
|
val shadow: BaseUITheme.Shadow,
|
||||||
val elevation = if (focused) 9.dp else 6.dp
|
val foreground: Color,
|
||||||
val ambient = if (focused) {
|
|
||||||
colors.accent.copy(alpha = 0.2f)
|
|
||||||
} else {
|
|
||||||
colors.shadow.copy(alpha = 0.15f)
|
|
||||||
}
|
|
||||||
val spot = if (focused) {
|
|
||||||
colors.accent.copy(alpha = 0.25f)
|
|
||||||
} else {
|
|
||||||
colors.shadow.copy(alpha = 0.18f)
|
|
||||||
}
|
|
||||||
return Modifier.shadow(
|
|
||||||
elevation = elevation,
|
|
||||||
shape = shape,
|
|
||||||
ambientColor = ambient,
|
|
||||||
spotColor = spot,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@ -108,25 +104,29 @@ fun TextField(
|
|||||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
textStyle: TextStyle = TextStyle.Default,
|
textStyle: TextStyle = TextStyle.Default,
|
||||||
cursorBrush: Color = MaterialTheme.colorScheme.primary,
|
cursorBrush: Color? = null,
|
||||||
|
theme: BaseUITheme.TextFieldTheme? = null,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val baseTheme = LocalBaseUITheme.current.textField
|
||||||
|
val textFieldTheme = theme ?: baseTheme
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
|
||||||
val gradient = outlinedGradient(colors, false)
|
val state = resolveTextFieldState(textFieldTheme, isFocused, isHovered)
|
||||||
val border = when {
|
|
||||||
isError -> MaterialTheme.colorScheme.error
|
val resolvedBorder = if (isError) {
|
||||||
isFocused -> MaterialTheme.colorScheme.primary
|
BaseUITheme.Border(MaterialTheme.colorScheme.error, state.border.width)
|
||||||
isHovered -> colors.border.copy(alpha = 0.85f)
|
} else {
|
||||||
else -> colors.border
|
state.border
|
||||||
}
|
}
|
||||||
|
|
||||||
val contentColor = when {
|
val contentColor = when {
|
||||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
!enabled -> state.foreground.copy(alpha = 0.38f)
|
||||||
else -> colors.onContainer
|
else -> state.foreground
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val cursor = cursorBrush?.let { SolidColor(it) } ?: textFieldTheme.cursor
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
AnimatedVisibility(
|
AnimatedVisibility(
|
||||||
visible = label != null,
|
visible = label != null,
|
||||||
@ -139,13 +139,22 @@ fun TextField(
|
|||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.defaultMinSize(minHeight = TextFieldMinHeight)
|
.defaultMinSize(minHeight = TextFieldMinHeight)
|
||||||
.then(textFieldShadow(TextFieldShape, isFocused, colors))
|
.then(
|
||||||
.clip(TextFieldShape)
|
if (state.shadow.elevation > 0.dp) {
|
||||||
.background(gradient, TextFieldShape)
|
Modifier.shadow(
|
||||||
.border(BorderStroke(0.5.dp, border), TextFieldShape)
|
elevation = state.shadow.elevation,
|
||||||
|
shape = state.shape,
|
||||||
|
ambientColor = state.shadow.ambientColor,
|
||||||
|
spotColor = state.shadow.spotColor,
|
||||||
|
)
|
||||||
|
} else Modifier
|
||||||
|
)
|
||||||
|
.clip(state.shape)
|
||||||
|
.background(state.background, state.shape)
|
||||||
|
.border(BorderStroke(resolvedBorder.width, resolvedBorder.color), state.shape)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(colors.highlight, Color.Transparent),
|
colors = listOf(state.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
@ -158,7 +167,7 @@ fun TextField(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
.padding(textFieldTheme.padding),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@ -177,7 +186,7 @@ fun TextField(
|
|||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
readOnly = readOnly,
|
readOnly = readOnly,
|
||||||
textStyle = textStyle.copy(color = contentColor),
|
textStyle = textStyle.copy(color = contentColor),
|
||||||
cursorBrush = SolidColor(cursorBrush),
|
cursorBrush = cursor,
|
||||||
keyboardOptions = keyboardOptions,
|
keyboardOptions = keyboardOptions,
|
||||||
keyboardActions = keyboardActions,
|
keyboardActions = keyboardActions,
|
||||||
singleLine = singleLine,
|
singleLine = singleLine,
|
||||||
@ -210,6 +219,8 @@ fun TextField(
|
|||||||
@Composable
|
@Composable
|
||||||
fun TextFieldPreview() {
|
fun TextFieldPreview() {
|
||||||
MaterialTheme {
|
MaterialTheme {
|
||||||
|
val theme = rememberBaseUITheme()
|
||||||
|
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||||
androidx.compose.material3.Surface(
|
androidx.compose.material3.Surface(
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
modifier = Modifier.padding(24.dp),
|
modifier = Modifier.padding(24.dp),
|
||||||
@ -243,3 +254,4 @@ fun TextFieldPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,756 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (C) 2026 Kingkor Roy Tirtho and Spotube Contributors
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU Affero General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU Affero General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU Affero General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package dev.krtirtho.spotube.core.ui.base
|
||||||
|
|
||||||
|
import androidx.compose.foundation.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<T>(
|
||||||
|
val hovered: T,
|
||||||
|
val pressed: T,
|
||||||
|
val focused: T,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun <T> fromSingleValue(value: T): InteractionState<T> {
|
||||||
|
return InteractionState(value, value, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class AdvancedInteractionState<T>(
|
||||||
|
val hovered: T,
|
||||||
|
val pressed: T,
|
||||||
|
val focused: T,
|
||||||
|
val selected: T,
|
||||||
|
val disabled: T,
|
||||||
|
) {
|
||||||
|
companion object {
|
||||||
|
fun <T> fromSingleValue(value: T): AdvancedInteractionState<T> {
|
||||||
|
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<ButtonColors>,
|
||||||
|
val shape: InteractionState<Shape>,
|
||||||
|
val shadow: InteractionState<Shadow>,
|
||||||
|
val border: InteractionState<Border>,
|
||||||
|
val padding: InteractionState<PaddingValues>,
|
||||||
|
)
|
||||||
|
|
||||||
|
@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<Brush>,
|
||||||
|
val shape: AdvancedInteractionState<Shape>,
|
||||||
|
val border: AdvancedInteractionState<Border>,
|
||||||
|
val shadow: AdvancedInteractionState<Shadow>,
|
||||||
|
val padding: PaddingValues,
|
||||||
|
val textStyle: AdvancedInteractionState<TextStyle>,
|
||||||
|
val foreground: AdvancedInteractionState<Color>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class TextFieldTheme(
|
||||||
|
val background: InteractionState<Brush>,
|
||||||
|
val highlight: InteractionState<Color>,
|
||||||
|
val shape: InteractionState<Shape>,
|
||||||
|
val border: InteractionState<Border>,
|
||||||
|
val shadow: InteractionState<Shadow>,
|
||||||
|
val padding: PaddingValues,
|
||||||
|
val textStyle: TextStyle,
|
||||||
|
val cursor: Brush,
|
||||||
|
val foreground: InteractionState<Color>,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class AutoCompleteMenuTheme(
|
||||||
|
val background: Brush,
|
||||||
|
val shape: Shape,
|
||||||
|
val border: Border,
|
||||||
|
val padding: PaddingValues,
|
||||||
|
val shadowElevation: Dp,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class SliderTheme(
|
||||||
|
val trackActiveColor: InteractionState<Color>,
|
||||||
|
val trackInactiveColor: InteractionState<Color>,
|
||||||
|
val thumbColor: InteractionState<Color>,
|
||||||
|
val thumbShadow: InteractionState<Shadow>,
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val LocalBaseUITheme = staticCompositionLocalOf<BaseUITheme> {
|
||||||
|
throw IllegalStateException("No BaseUITheme provided")
|
||||||
|
}
|
||||||
|
|
||||||
|
@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
|
||||||
|
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<Shape>(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),
|
||||||
|
shadowColor.copy(alpha = 0.26f)
|
||||||
|
),
|
||||||
|
pressed = BaseUITheme.Shadow(
|
||||||
|
1.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.08f),
|
||||||
|
shadowColor.copy(alpha = 0.1f)
|
||||||
|
),
|
||||||
|
focused = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.15f),
|
||||||
|
shadowColor.copy(alpha = 0.18f)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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),
|
||||||
|
shadowColor.copy(alpha = 0.26f)
|
||||||
|
),
|
||||||
|
pressed = BaseUITheme.Shadow(
|
||||||
|
1.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.08f),
|
||||||
|
shadowColor.copy(alpha = 0.1f)
|
||||||
|
),
|
||||||
|
focused = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.15f),
|
||||||
|
shadowColor.copy(alpha = 0.18f)
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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<Shape>(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<Shape>(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),
|
||||||
|
shadowColor.copy(alpha = 0.26f)
|
||||||
|
),
|
||||||
|
pressed = BaseUITheme.Shadow(
|
||||||
|
6.dp,
|
||||||
|
true,
|
||||||
|
shadowColor.copy(alpha = 0.15f),
|
||||||
|
shadowColor.copy(alpha = 0.18f)
|
||||||
|
),
|
||||||
|
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(0.dp),
|
||||||
|
shadow = BaseUITheme.Shadow(6.dp, true, shadowColor.copy(alpha = 0.15f), shadowColor.copy(alpha = 0.18f)),
|
||||||
|
),
|
||||||
|
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
|
||||||
|
|
||||||
|
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)),
|
||||||
|
)
|
||||||
|
}
|
||||||
@ -21,10 +21,8 @@ import androidx.compose.animation.AnimatedContent
|
|||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.animation.togetherWith
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.hoverable
|
import androidx.compose.foundation.hoverable
|
||||||
@ -63,10 +61,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
import androidx.compose.ui.draw.drawWithCache
|
||||||
import androidx.compose.ui.focus.focusModifier
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalWindowInfo
|
import androidx.compose.ui.platform.LocalWindowInfo
|
||||||
@ -86,14 +84,13 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.Thumbnail
|
|||||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||||
import dev.krtirtho.spotube.core.ui.base.ButtonGroup
|
import dev.krtirtho.spotube.core.ui.base.ButtonGroup
|
||||||
import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider
|
import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.Card
|
||||||
import dev.krtirtho.spotube.core.ui.base.CheckBox
|
import dev.krtirtho.spotube.core.ui.base.CheckBox
|
||||||
import dev.krtirtho.spotube.core.ui.base.CheckBoxState
|
import dev.krtirtho.spotube.core.ui.base.CheckBoxState
|
||||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.GroupIconButton
|
import dev.krtirtho.spotube.core.ui.base.GroupIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
import dev.krtirtho.spotube.core.ui.base.buttonShadow
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
|
||||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||||
@ -348,25 +345,10 @@ fun TrackList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
item(key = "track-card") {
|
item(key = "track-card") {
|
||||||
val colors = rememberButtonColors()
|
Card(
|
||||||
val trackListShape = MaterialTheme.shapes.large
|
|
||||||
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 8.dp)
|
.padding(vertical = 8.dp),
|
||||||
.then(
|
|
||||||
buttonShadow(
|
|
||||||
trackListShape,
|
|
||||||
pressed = false,
|
|
||||||
primary = false,
|
|
||||||
colors,
|
|
||||||
hovered = false
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.clip(trackListShape)
|
|
||||||
.background(MaterialTheme.colorScheme.surfaceContainer, trackListShape)
|
|
||||||
.border(BorderStroke(0.5.dp, colors.border), trackListShape)
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@ -532,17 +514,21 @@ private fun TrackListRow(
|
|||||||
onArtistsOverflowClick: () -> Unit,
|
onArtistsOverflowClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val rowColors = rememberButtonColors()
|
val rowTheme = LocalBaseUITheme.current.listRowTile
|
||||||
|
val isLight = MaterialTheme.colorScheme.surface.luminance() > 0.5f
|
||||||
val artworkInteractionSource = remember { MutableInteractionSource() }
|
val artworkInteractionSource = remember { MutableInteractionSource() }
|
||||||
val isArtworkHovered by artworkInteractionSource.collectIsHoveredAsState()
|
val isArtworkHovered by artworkInteractionSource.collectIsHoveredAsState()
|
||||||
val rowInteractionSource = remember { MutableInteractionSource() }
|
val rowInteractionSource = remember { MutableInteractionSource() }
|
||||||
val isRowHovered by rowInteractionSource.collectIsHoveredAsState()
|
val isRowHovered by rowInteractionSource.collectIsHoveredAsState()
|
||||||
val rowBackgroundColor = when {
|
val rowBackgroundColor = when {
|
||||||
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
isSelected -> rowTheme.background.selected
|
||||||
isCurrentTrack && isCurrentTrackPlaying -> MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
|
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
||||||
isCurrentTrack -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f)
|
isCurrentTrack -> rowTheme.background.focused
|
||||||
else -> Color.Transparent
|
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||||
}
|
}
|
||||||
|
val highlightColor = Color.White.copy(
|
||||||
|
alpha = if (isLight) 0.9f else 0.06f
|
||||||
|
)
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@ -555,7 +541,7 @@ private fun TrackListRow(
|
|||||||
.background(rowBackgroundColor)
|
.background(rowBackgroundColor)
|
||||||
.drawWithCache {
|
.drawWithCache {
|
||||||
val highlightBrush = Brush.verticalGradient(
|
val highlightBrush = Brush.verticalGradient(
|
||||||
colors = listOf(rowColors.highlight, Color.Transparent),
|
colors = listOf(highlightColor, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
endY = size.height * 0.5f,
|
endY = size.height * 0.5f,
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,9 +33,9 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@ -49,14 +49,16 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxAddSquare
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxPauseCircle
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPause
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Play
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxPlay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PlayableCard(
|
fun PlayableCard(
|
||||||
@ -108,7 +110,11 @@ fun PlayableCard(
|
|||||||
SecondaryIconButton(
|
SecondaryIconButton(
|
||||||
onClick = onAddToQueue,
|
onClick = onAddToQueue,
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
shape = RoundedCornerShape(6.dp)
|
theme = LocalBaseUITheme.current.iconButtons.secondary.copy(
|
||||||
|
shape = BaseUITheme.InteractionState.fromSingleValue(
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.IconsaxAddSquare,
|
imageVector = Iconsax.IconsaxAddSquare,
|
||||||
@ -121,12 +127,17 @@ fun PlayableCard(
|
|||||||
PrimaryIconButton(
|
PrimaryIconButton(
|
||||||
onClick = onPlay,
|
onClick = onPlay,
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
shape = RoundedCornerShape(6.dp)
|
theme = LocalBaseUITheme.current.iconButtons.secondary.copy(
|
||||||
|
shape = BaseUITheme.InteractionState.fromSingleValue(
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isPlaying) {
|
imageVector = if (isPlaying) {
|
||||||
Iconsax.IconsaxPauseCircle
|
Iconsax.IconsaxPause
|
||||||
} else Iconsax.Play,
|
} else Iconsax.IconsaxPlay,
|
||||||
contentDescription = "Play",
|
contentDescription = "Play",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -83,10 +83,13 @@ import dev.krtirtho.spotube.core.audioplayer.LoopState
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
import dev.krtirtho.spotube.core.ui.base.Slider
|
import dev.krtirtho.spotube.core.ui.base.Slider
|
||||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.invertedButtonStyle
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadStatus
|
import dev.krtirtho.spotube.modules.downloads.DownloadStatus
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
@ -466,7 +469,10 @@ fun AppExpandedPlayer(
|
|||||||
textAlign = TextAlign.Center,
|
textAlign = TextAlign.Center,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
IconButton(onClick = onQueue, shape = CircleShape) {
|
IconButton(
|
||||||
|
onClick = onQueue,
|
||||||
|
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape)
|
||||||
|
) {
|
||||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -526,20 +532,14 @@ fun AppExpandedPlayer(
|
|||||||
}
|
}
|
||||||
IconButton(
|
IconButton(
|
||||||
onClick = ::onPlayPause,
|
onClick = ::onPlayPause,
|
||||||
colors = rememberButtonColors().copy(
|
theme = invertedButtonStyle().copyShape(CircleShape),
|
||||||
containerDarker = MaterialTheme.colorScheme.onSurface,
|
|
||||||
containerLighter = MaterialTheme.colorScheme.onSurface,
|
|
||||||
containerPressed = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
),
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(72.dp),
|
.size(72.dp),
|
||||||
shape = CircleShape,
|
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||||
modifier = Modifier.size(30.dp),
|
modifier = Modifier.size(30.dp),
|
||||||
tint = MaterialTheme.colorScheme.surface,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onSkipNext) {
|
GhostIconButton(onClick = ::onSkipNext) {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@ -54,9 +55,12 @@ import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.LoopState
|
import dev.krtirtho.spotube.core.audioplayer.LoopState
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.Slider
|
import dev.krtirtho.spotube.core.ui.base.Slider
|
||||||
import dev.krtirtho.spotube.core.ui.base.VariableIconButton
|
import dev.krtirtho.spotube.core.ui.base.VariableIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.VariableIconButtonVariant
|
import dev.krtirtho.spotube.core.ui.base.VariableIconButtonVariant
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.invertedButtonStyle
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||||
@ -274,7 +278,12 @@ fun AppLargePlayer(
|
|||||||
GhostIconButton(onClick = ::onSkipPrevious) {
|
GhostIconButton(onClick = ::onSkipPrevious) {
|
||||||
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
Icon(Iconsax.IconsaxPrevious, contentDescription = "Previous")
|
||||||
}
|
}
|
||||||
GhostIconButton(onClick = ::onPlayPause, modifier = Modifier.size(44.dp)) {
|
IconButton(
|
||||||
|
onClick = ::onPlayPause,
|
||||||
|
theme = invertedButtonStyle().copyShape(CircleShape),
|
||||||
|
modifier = Modifier
|
||||||
|
.size(50.dp),
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
|
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
|
||||||
|
|||||||
@ -21,14 +21,8 @@ import androidx.compose.animation.AnimatedVisibility
|
|||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.BorderStroke
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.border
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.hoverable
|
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
|
||||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@ -42,34 +36,26 @@ import androidx.compose.foundation.layout.height
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.ripple
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.draw.drawWithCache
|
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.krtirtho.spotube.core.navigation.NavigationState
|
import dev.krtirtho.spotube.core.navigation.NavigationState
|
||||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||||
import dev.krtirtho.spotube.core.navigation.Routes
|
import dev.krtirtho.spotube.core.navigation.Routes
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.buttonShadow
|
import dev.krtirtho.spotube.core.ui.base.copyPadding
|
||||||
import dev.krtirtho.spotube.core.ui.base.outlinedGradient
|
|
||||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
|
||||||
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
|
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryState
|
import dev.krtirtho.spotube.modules.library.LibraryState
|
||||||
import dev.krtirtho.spotube.modules.library.LibraryTab
|
import dev.krtirtho.spotube.modules.library.LibraryTab
|
||||||
@ -215,66 +201,16 @@ fun SidebarItem(
|
|||||||
SecondaryButton(
|
SecondaryButton(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = buttonModifier,
|
modifier = buttonModifier,
|
||||||
contentPadding = contentPadding,
|
theme = LocalBaseUITheme.current.buttons.secondary.copyPadding(contentPadding),
|
||||||
content = itemContent,
|
content = itemContent,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val colors = rememberButtonColors()
|
OutlineButton(
|
||||||
val interactionSource = remember { MutableInteractionSource() }
|
|
||||||
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,
|
onClick = onClick,
|
||||||
)
|
modifier = buttonModifier,
|
||||||
.padding(contentPadding),
|
theme = LocalBaseUITheme.current.buttons.outline.copyPadding(contentPadding),
|
||||||
contentAlignment = Alignment.Center,
|
hoverOnly = true,
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
|
||||||
content = itemContent,
|
content = itemContent,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
@ -27,6 +27,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedState
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedState
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||||
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
||||||
@ -69,7 +71,7 @@ fun PlayerHeartButton(
|
|||||||
IconButton(
|
IconButton(
|
||||||
onClick = ::onLike,
|
onClick = ::onLike,
|
||||||
enabled = isSavedTrackState is SavedState.Success,
|
enabled = isSavedTrackState is SavedState.Success,
|
||||||
shape = CircleShape,
|
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape),
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,
|
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,
|
||||||
|
|||||||
@ -37,21 +37,19 @@ import androidx.compose.foundation.lazy.items
|
|||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.DropdownMenuItem
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
import androidx.compose.material3.FilledTonalIconButton
|
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
@ -63,7 +61,11 @@ import compose.icons.FeatherIcons
|
|||||||
import compose.icons.feathericons.MoreVertical
|
import compose.icons.feathericons.MoreVertical
|
||||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||||
import dev.krtirtho.spotube.core.di.rememberLogger
|
import dev.krtirtho.spotube.core.di.rememberLogger
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||||
|
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||||
@ -186,9 +188,9 @@ fun PlayerQueueContent(
|
|||||||
singleLine = true,
|
singleLine = true,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
FilledTonalIconButton(
|
SecondaryIconButton(
|
||||||
onClick = viewModel::clearQueue,
|
onClick = viewModel::clearQueue,
|
||||||
shape = MaterialTheme.shapes.small,
|
theme = LocalBaseUITheme.current.iconButtons.secondary.copyShape(MaterialTheme.shapes.small),
|
||||||
) {
|
) {
|
||||||
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
||||||
}
|
}
|
||||||
@ -333,7 +335,7 @@ private fun QueueItemRow(
|
|||||||
Spacer(modifier = Modifier.width(4.dp))
|
Spacer(modifier = Modifier.width(4.dp))
|
||||||
|
|
||||||
Box {
|
Box {
|
||||||
IconButton(
|
GhostIconButton(
|
||||||
onClick = { showMenu = true },
|
onClick = { showMenu = true },
|
||||||
modifier = Modifier.size(36.dp),
|
modifier = Modifier.size(36.dp),
|
||||||
) {
|
) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user