mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
feat: enhance UI components with LocalBaseUITheme for consistent styling and improved interactions
This commit is contained in:
parent
a57b359186
commit
a52ba68f1f
@ -20,8 +20,11 @@ package dev.krtirtho.spotube
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.rememberBaseUITheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
@ -100,15 +103,18 @@ fun App(
|
||||
}
|
||||
|
||||
SpotubeTheme(settings = userSettings) {
|
||||
AppShell(navigator, navigationState) {
|
||||
Column {
|
||||
NavDisplay(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onBack = navigator::pop,
|
||||
entries = navigationState.toEntries(koinEntryProvider())
|
||||
)
|
||||
val baseUITheme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides baseUITheme) {
|
||||
AppShell(navigator, navigationState) {
|
||||
Column {
|
||||
NavDisplay(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
onBack = navigator::pop,
|
||||
entries = navigationState.toEntries(koinEntryProvider())
|
||||
)
|
||||
}
|
||||
}
|
||||
content()
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,8 +28,8 @@ import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.focusable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@ -41,7 +41,6 @@ import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@ -87,12 +86,55 @@ import androidx.compose.ui.window.PopupProperties
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||
|
||||
private val AutocompleteTextFieldShape = RoundedCornerShape(14.dp)
|
||||
private val AutocompleteMenuShape = RoundedCornerShape(12.dp)
|
||||
private val AutocompleteTextFieldMinHeight = 44.dp
|
||||
private val AutocompleteMenuDefaultMaxHeight = 300.dp
|
||||
private val AutocompleteMenuDefaultOffset = 4.dp
|
||||
|
||||
private data class ResolvedAutoCompleteTextFieldState(
|
||||
val background: Brush,
|
||||
val highlight: Color,
|
||||
val shape: Shape,
|
||||
val border: BaseUITheme.Border,
|
||||
val shadow: BaseUITheme.Shadow,
|
||||
val foreground: Color,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun resolveTextFieldState(
|
||||
theme: BaseUITheme.TextFieldTheme,
|
||||
isFocused: Boolean,
|
||||
isHovered: Boolean,
|
||||
): ResolvedAutoCompleteTextFieldState {
|
||||
return when {
|
||||
isFocused -> ResolvedAutoCompleteTextFieldState(
|
||||
theme.background.focused,
|
||||
theme.highlight.focused,
|
||||
theme.shape.focused,
|
||||
theme.border.focused,
|
||||
theme.shadow.focused,
|
||||
theme.foreground.focused
|
||||
)
|
||||
|
||||
isHovered -> ResolvedAutoCompleteTextFieldState(
|
||||
theme.background.hovered,
|
||||
theme.highlight.hovered,
|
||||
theme.shape.hovered,
|
||||
theme.border.hovered,
|
||||
theme.shadow.hovered,
|
||||
theme.foreground.hovered
|
||||
)
|
||||
|
||||
else -> ResolvedAutoCompleteTextFieldState(
|
||||
theme.background.pressed,
|
||||
theme.highlight.pressed,
|
||||
theme.shape.pressed,
|
||||
theme.border.pressed,
|
||||
theme.shadow.pressed,
|
||||
theme.foreground.pressed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> AutocompleteTextField(
|
||||
value: String,
|
||||
@ -115,16 +157,18 @@ fun <T> AutocompleteTextField(
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
cursorBrush: Color = MaterialTheme.colorScheme.primary,
|
||||
cursorBrush: Color? = null,
|
||||
onKeyEvent: ((KeyEvent) -> Boolean)? = null,
|
||||
menuMaxHeight: Dp = AutocompleteMenuDefaultMaxHeight,
|
||||
menuOffset: Dp = AutocompleteMenuDefaultOffset,
|
||||
menuShape: Shape = AutocompleteMenuShape,
|
||||
menuShadowElevation: Dp = 8.dp,
|
||||
menuContainerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
textFieldTheme: BaseUITheme.TextFieldTheme? = null,
|
||||
menuTheme: BaseUITheme.AutoCompleteMenuTheme? = null,
|
||||
) {
|
||||
val density = LocalDensity.current
|
||||
val colors = rememberButtonColors()
|
||||
val baseTextFieldTheme = LocalBaseUITheme.current.textField
|
||||
val baseMenuTheme = LocalBaseUITheme.current.autoCompleteMenuTheme
|
||||
val resolvedTextFieldTheme = textFieldTheme ?: baseTextFieldTheme
|
||||
val resolvedMenuTheme = menuTheme ?: baseMenuTheme
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
|
||||
@ -174,19 +218,21 @@ fun <T> AutocompleteTextField(
|
||||
)
|
||||
}
|
||||
|
||||
val gradient = outlinedGradient(colors, false)
|
||||
val border = when {
|
||||
isError -> MaterialTheme.colorScheme.error
|
||||
isFocused -> MaterialTheme.colorScheme.primary
|
||||
isHovered -> colors.border.copy(alpha = 0.85f)
|
||||
else -> colors.border
|
||||
val fieldState = resolveTextFieldState(resolvedTextFieldTheme, isFocused, isHovered)
|
||||
|
||||
val resolvedBorder = if (isError) {
|
||||
BaseUITheme.Border(MaterialTheme.colorScheme.error, fieldState.border.width)
|
||||
} else {
|
||||
fieldState.border
|
||||
}
|
||||
|
||||
val contentColor = when {
|
||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
||||
else -> colors.onContainer
|
||||
!enabled -> fieldState.foreground.copy(alpha = 0.38f)
|
||||
else -> fieldState.foreground
|
||||
}
|
||||
|
||||
val cursor = cursorBrush?.let { SolidColor(it) } ?: resolvedTextFieldTheme.cursor
|
||||
|
||||
fun navigate(delta: Int) {
|
||||
if (items.isEmpty()) return
|
||||
selectedIndex = ((selectedIndex + delta) % items.size + items.size) % items.size
|
||||
@ -207,13 +253,25 @@ fun <T> AutocompleteTextField(
|
||||
.fillMaxWidth()
|
||||
.onSizeChanged { textFieldSize = it }
|
||||
.defaultMinSize(minHeight = AutocompleteTextFieldMinHeight)
|
||||
.then(textFieldShadow(AutocompleteTextFieldShape, isFocused, colors))
|
||||
.clip(AutocompleteTextFieldShape)
|
||||
.background(gradient, AutocompleteTextFieldShape)
|
||||
.border(BorderStroke(.5.dp, border), AutocompleteTextFieldShape)
|
||||
.then(
|
||||
if (fieldState.shadow.elevation > 0.dp) {
|
||||
Modifier.shadow(
|
||||
elevation = fieldState.shadow.elevation,
|
||||
shape = fieldState.shape,
|
||||
ambientColor = fieldState.shadow.ambientColor,
|
||||
spotColor = fieldState.shadow.spotColor,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.clip(fieldState.shape)
|
||||
.background(fieldState.background, fieldState.shape)
|
||||
.border(
|
||||
BorderStroke(resolvedBorder.width, resolvedBorder.color),
|
||||
fieldState.shape
|
||||
)
|
||||
.drawWithCache {
|
||||
val highlightBrush = Brush.verticalGradient(
|
||||
colors = listOf(colors.highlight, Color.Transparent),
|
||||
colors = listOf(fieldState.highlight, Color.Transparent),
|
||||
startY = 0f,
|
||||
endY = size.height * 0.5f,
|
||||
)
|
||||
@ -226,7 +284,7 @@ fun <T> AutocompleteTextField(
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
.padding(resolvedTextFieldTheme.padding),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -294,7 +352,7 @@ fun <T> AutocompleteTextField(
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle.copy(color = contentColor),
|
||||
cursorBrush = SolidColor(cursorBrush),
|
||||
cursorBrush = cursor,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = wrappedKeyboardActions,
|
||||
singleLine = singleLine,
|
||||
@ -340,9 +398,9 @@ fun <T> AutocompleteTextField(
|
||||
modifier = Modifier
|
||||
.width(textFieldWidthDp)
|
||||
.heightIn(max = menuMaxHeight)
|
||||
.shadow(menuShadowElevation, menuShape)
|
||||
.background(menuContainerColor, menuShape)
|
||||
.clip(menuShape)
|
||||
.shadow(resolvedMenuTheme.shadowElevation, resolvedMenuTheme.shape)
|
||||
.background(resolvedMenuTheme.background, resolvedMenuTheme.shape)
|
||||
.clip(resolvedMenuTheme.shape)
|
||||
.focusRequester(popupFocusRequester)
|
||||
.focusable()
|
||||
.onFocusChanged { state ->
|
||||
@ -412,70 +470,73 @@ fun <T> AutocompleteTextField(
|
||||
@Composable
|
||||
private fun AutocompleteTextFieldPreview() {
|
||||
MaterialTheme {
|
||||
androidx.compose.material3.Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
val artists = remember {
|
||||
listOf(
|
||||
"Twenty One Pilots",
|
||||
"The Beatles",
|
||||
"Adele",
|
||||
"Drake",
|
||||
"Taylor Swift",
|
||||
"Arctic Monkeys",
|
||||
"Billie Eilish",
|
||||
)
|
||||
}
|
||||
var value by remember { mutableStateOf("") }
|
||||
|
||||
AutocompleteTextField(
|
||||
value = value,
|
||||
onValueChange = {
|
||||
value = it
|
||||
},
|
||||
items = artists.filter {
|
||||
it.contains(value, ignoreCase = true) && value.isNotEmpty()
|
||||
},
|
||||
onItemSelected = { selected ->
|
||||
value = selected
|
||||
},
|
||||
placeholder = { Text("Search artists...") },
|
||||
leadingIcon = {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxSearchBroken,
|
||||
contentDescription = "Search",
|
||||
val theme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||
androidx.compose.material3.Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
val artists = remember {
|
||||
listOf(
|
||||
"Twenty One Pilots",
|
||||
"The Beatles",
|
||||
"Adele",
|
||||
"Drake",
|
||||
"Taylor Swift",
|
||||
"Arctic Monkeys",
|
||||
"Billie Eilish",
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
itemContent = { item, isSelected ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
}
|
||||
var value by remember { mutableStateOf("") }
|
||||
|
||||
AutocompleteTextField(
|
||||
value = value,
|
||||
onValueChange = {
|
||||
value = it
|
||||
},
|
||||
items = artists.filter {
|
||||
it.contains(value, ignoreCase = true) && value.isNotEmpty()
|
||||
},
|
||||
onItemSelected = { selected ->
|
||||
value = selected
|
||||
},
|
||||
placeholder = { Text("Search artists...") },
|
||||
leadingIcon = {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxSearchBroken,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
contentDescription = "Search",
|
||||
)
|
||||
Text(
|
||||
text = item,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
},
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
itemContent = { item, isSelected ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.background(
|
||||
color = if (isSelected) {
|
||||
MaterialTheme.colorScheme.onSurface.copy(alpha = 0.08f)
|
||||
} else {
|
||||
Color.Transparent
|
||||
},
|
||||
)
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxSearchBroken,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Text(
|
||||
text = item,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
@ -50,6 +51,7 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
@ -65,9 +67,28 @@ enum class CheckBoxState {
|
||||
CLEAR;
|
||||
}
|
||||
|
||||
private val CheckBoxShape = RoundedCornerShape(6.dp)
|
||||
private val CheckBoxSize = 22.dp
|
||||
|
||||
private data class ResolvedCheckBoxState(
|
||||
val colors: BaseUITheme.ButtonColors,
|
||||
val shape: Shape,
|
||||
val shadow: BaseUITheme.Shadow,
|
||||
val border: BaseUITheme.Border,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun resolveCheckBoxState(
|
||||
style: BaseUITheme.ButtonStyle,
|
||||
isPressed: Boolean,
|
||||
isHovered: Boolean,
|
||||
): ResolvedCheckBoxState {
|
||||
return when {
|
||||
isPressed -> ResolvedCheckBoxState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed)
|
||||
isHovered -> ResolvedCheckBoxState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered)
|
||||
else -> ResolvedCheckBoxState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun CheckBox(
|
||||
state: CheckBoxState = CheckBoxState.UNSELECTED,
|
||||
@ -75,69 +96,25 @@ fun CheckBox(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
theme: BaseUITheme.CheckBoxTheme? = null,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val baseTheme = LocalBaseUITheme.current.checkBox
|
||||
val checkBoxTheme = theme ?: baseTheme
|
||||
val source = interactionSource ?: remember { MutableInteractionSource() }
|
||||
val isPressed by source.collectIsPressedAsState()
|
||||
val isHovered by source.collectIsHoveredAsState()
|
||||
val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
|
||||
|
||||
val isSelected = state == CheckBoxState.SELECTED
|
||||
val isIndeterminate = state == CheckBoxState.INDETERMINATE
|
||||
val isFilled = isSelected || isIndeterminate
|
||||
|
||||
val backgroundBrush = if (isFilled) {
|
||||
primaryGradient(colors, isPressed)
|
||||
} else {
|
||||
outlinedGradient(colors, false)
|
||||
}
|
||||
val borderColor = when {
|
||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
||||
isFilled -> colors.accent
|
||||
else -> colors.border
|
||||
}
|
||||
val borderWidth = 0.5.dp
|
||||
val style = if (isFilled) checkBoxTheme.selected else checkBoxTheme.unselected
|
||||
val resolved = resolveCheckBoxState(style, isPressed, isHovered)
|
||||
val lift = if (isHovered && !isPressed && onClick != null) (-1).dp else 0.dp
|
||||
|
||||
val shadowElevation = when {
|
||||
isPressed -> 1.dp
|
||||
isFilled && isHovered -> 8.dp
|
||||
isFilled -> 6.dp
|
||||
isHovered -> 5.dp
|
||||
else -> 3.dp
|
||||
}
|
||||
val shadowAmbient = if (isFilled) {
|
||||
colors.accent.copy(
|
||||
alpha = when {
|
||||
isPressed -> 0.2f
|
||||
isHovered -> 0.4f
|
||||
else -> 0.3f
|
||||
}
|
||||
)
|
||||
} else {
|
||||
colors.shadow.copy(
|
||||
alpha = when {
|
||||
isPressed -> 0.08f
|
||||
isHovered -> 0.2f
|
||||
else -> 0.15f
|
||||
}
|
||||
)
|
||||
}
|
||||
val shadowSpot = if (isFilled) {
|
||||
colors.accent.copy(
|
||||
alpha = when {
|
||||
isPressed -> 0.25f
|
||||
isHovered -> 0.45f
|
||||
else -> 0.35f
|
||||
}
|
||||
)
|
||||
} else {
|
||||
colors.shadow.copy(
|
||||
alpha = when {
|
||||
isPressed -> 0.1f
|
||||
isHovered -> 0.24f
|
||||
else -> 0.18f
|
||||
}
|
||||
)
|
||||
val borderColor = when {
|
||||
!enabled -> resolved.colors.foreground.copy(alpha = 0.38f)
|
||||
else -> resolved.border.color
|
||||
}
|
||||
|
||||
val checkProgress by animateFloatAsState(
|
||||
@ -155,28 +132,29 @@ fun CheckBox(
|
||||
modifier = modifier
|
||||
.size(CheckBoxSize)
|
||||
.graphicsLayer { translationY = lift.toPx() }
|
||||
.shadow(
|
||||
elevation = shadowElevation,
|
||||
shape = CheckBoxShape,
|
||||
ambientColor = shadowAmbient,
|
||||
spotColor = shadowSpot,
|
||||
.then(
|
||||
if (resolved.shadow.elevation > 0.dp) {
|
||||
Modifier.shadow(
|
||||
elevation = resolved.shadow.elevation,
|
||||
shape = resolved.shape,
|
||||
ambientColor = resolved.shadow.ambientColor,
|
||||
spotColor = resolved.shadow.spotColor,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.clip(CheckBoxShape)
|
||||
.background(backgroundBrush, CheckBoxShape)
|
||||
.border(BorderStroke(borderWidth, borderColor), CheckBoxShape)
|
||||
.clip(resolved.shape)
|
||||
.background(resolved.colors.background, resolved.shape)
|
||||
.border(BorderStroke(0.5.dp, borderColor), resolved.shape)
|
||||
.drawWithCache {
|
||||
val highlight = Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
if (isFilled) Color.White.copy(alpha = 0.25f) else colors.highlight,
|
||||
Color.Transparent,
|
||||
),
|
||||
val highlightBrush = Brush.verticalGradient(
|
||||
colors = listOf(resolved.colors.highlight, Color.Transparent),
|
||||
startY = 0f,
|
||||
endY = size.height * 0.5f,
|
||||
)
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = highlight,
|
||||
brush = highlightBrush,
|
||||
topLeft = Offset.Zero,
|
||||
size = size,
|
||||
)
|
||||
@ -215,7 +193,7 @@ fun CheckBox(
|
||||
}
|
||||
drawPath(
|
||||
path = path,
|
||||
color = colors.onAccent,
|
||||
color = checkBoxTheme.checkmarkColor,
|
||||
style = Stroke(
|
||||
width = stroke,
|
||||
cap = StrokeCap.Round,
|
||||
@ -233,8 +211,8 @@ fun CheckBox(
|
||||
.background(
|
||||
brush = Brush.horizontalGradient(
|
||||
colors = listOf(
|
||||
colors.onAccent,
|
||||
colors.onAccent.copy(alpha = 0.92f),
|
||||
checkBoxTheme.checkmarkColor,
|
||||
checkBoxTheme.checkmarkColor.copy(alpha = 0.92f),
|
||||
)
|
||||
),
|
||||
),
|
||||
@ -247,38 +225,41 @@ fun CheckBox(
|
||||
@Composable
|
||||
fun CheckBoxPreview() {
|
||||
MaterialTheme {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.SELECTED, onClick = {})
|
||||
Text("Selected", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.UNSELECTED, onClick = {})
|
||||
Text("Unselected", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {})
|
||||
Text("Indeterminate", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false)
|
||||
Text("Disabled", style = MaterialTheme.typography.bodyMedium)
|
||||
val theme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||
Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.SELECTED, onClick = {})
|
||||
Text("Selected", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.UNSELECTED, onClick = {})
|
||||
Text("Unselected", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.INDETERMINATE, onClick = {})
|
||||
Text("Indeterminate", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
CheckBox(state = CheckBoxState.SELECTED, onClick = {}, enabled = false)
|
||||
Text("Disabled", style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -27,12 +27,10 @@ import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.LocalContentColor
|
||||
import androidx.compose.material3.LocalTextStyle
|
||||
import androidx.compose.material3.Text
|
||||
@ -45,57 +43,78 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
|
||||
private val ChipTabShape = RoundedCornerShape(10.dp)
|
||||
private val ChipTabMinHeight = 36.dp
|
||||
|
||||
private data class ResolvedChipState(
|
||||
val colors: BaseUITheme.ButtonColors,
|
||||
val shape: Shape,
|
||||
val shadow: BaseUITheme.Shadow,
|
||||
val border: BaseUITheme.Border,
|
||||
val padding: androidx.compose.foundation.layout.PaddingValues,
|
||||
)
|
||||
|
||||
@Composable
|
||||
private fun resolveChipState(
|
||||
style: BaseUITheme.ButtonStyle,
|
||||
isPressed: Boolean,
|
||||
isHovered: Boolean,
|
||||
): ResolvedChipState {
|
||||
return when {
|
||||
isPressed -> ResolvedChipState(style.colors.pressed, style.shape.pressed, style.shadow.pressed, style.border.pressed, style.padding.pressed)
|
||||
isHovered -> ResolvedChipState(style.colors.hovered, style.shape.hovered, style.shadow.hovered, style.border.hovered, style.padding.hovered)
|
||||
else -> ResolvedChipState(style.colors.focused, style.shape.focused, style.shadow.focused, style.border.focused, style.padding.focused)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Modifier.applyChipShadow(
|
||||
shadow: BaseUITheme.Shadow,
|
||||
shape: Shape,
|
||||
): Modifier {
|
||||
return if (shadow.elevation > 0.dp) {
|
||||
this.shadow(
|
||||
elevation = shadow.elevation,
|
||||
shape = shape,
|
||||
ambientColor = shadow.ambientColor,
|
||||
spotColor = shadow.spotColor,
|
||||
)
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChipTab(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 14.dp, vertical = 4.dp),
|
||||
theme: BaseUITheme.ChipTabTheme? = null,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val baseTheme = LocalBaseUITheme.current.chipTab
|
||||
val chipTheme = theme ?: baseTheme
|
||||
val style = if (selected) chipTheme.selected else chipTheme.unselected
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
val gradient = if (selected) {
|
||||
primaryGradient(colors, isPressed)
|
||||
} else {
|
||||
outlinedGradient(colors, isPressed)
|
||||
}
|
||||
val borderColor = if (selected) {
|
||||
colors.accent
|
||||
} else {
|
||||
colors.border.copy(alpha = if (isPressed) 0.7f else 1f)
|
||||
}
|
||||
val contentColor = if (selected) colors.onAccent else colors.onContainer
|
||||
val state = resolveChipState(style, isPressed, isHovered)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = ChipTabMinHeight)
|
||||
.hoverable(interactionSource = interactionSource, enabled = enabled)
|
||||
.then(
|
||||
buttonShadow(
|
||||
shape = ChipTabShape,
|
||||
pressed = isPressed,
|
||||
primary = selected,
|
||||
colors = colors,
|
||||
hovered = isHovered,
|
||||
)
|
||||
)
|
||||
.clip(ChipTabShape)
|
||||
.background(gradient, ChipTabShape)
|
||||
.border(BorderStroke(0.5.dp, borderColor), ChipTabShape)
|
||||
.applyChipShadow(state.shadow, state.shape)
|
||||
.clip(state.shape)
|
||||
.background(state.colors.background, state.shape)
|
||||
.border(BorderStroke(state.border.width, state.border.color), state.shape)
|
||||
.clickable(
|
||||
enabled = enabled,
|
||||
interactionSource = interactionSource,
|
||||
@ -103,13 +122,8 @@ fun ChipTab(
|
||||
onClick = onClick,
|
||||
)
|
||||
.drawWithCache {
|
||||
val highlight = if (selected) {
|
||||
Color.White.copy(alpha = 0.25f)
|
||||
} else {
|
||||
colors.highlight
|
||||
}
|
||||
val highlightBrush = Brush.verticalGradient(
|
||||
colors = listOf(highlight, Color.Transparent),
|
||||
colors = listOf(state.colors.highlight, Color.Transparent),
|
||||
startY = 0f,
|
||||
endY = size.height * 0.5f,
|
||||
)
|
||||
@ -122,11 +136,11 @@ fun ChipTab(
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(contentPadding),
|
||||
.padding(state.padding),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalContentColor provides contentColor,
|
||||
LocalContentColor provides state.colors.foreground,
|
||||
LocalTextStyle provides LocalTextStyle.current.copy(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
fontSize = 14.sp,
|
||||
@ -149,14 +163,14 @@ fun ChipTab(
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
leadingIcon: @Composable (() -> Unit)? = null,
|
||||
contentPadding: PaddingValues = PaddingValues(horizontal = 16.dp, vertical = 8.dp),
|
||||
theme: BaseUITheme.ChipTabTheme? = null,
|
||||
) {
|
||||
ChipTab(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
contentPadding = contentPadding,
|
||||
theme = theme,
|
||||
) {
|
||||
if (leadingIcon != null) {
|
||||
leadingIcon()
|
||||
@ -168,4 +182,4 @@ fun ChipTab(
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -43,6 +43,7 @@ import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableFloatStateOf
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
@ -79,8 +80,10 @@ fun Slider(
|
||||
steps: Int = 0,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
onValueChangeFinished: () -> Unit = {},
|
||||
theme: BaseUITheme.SliderTheme? = null,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val baseTheme = LocalBaseUITheme.current.slider
|
||||
val sliderTheme = theme ?: baseTheme
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val density = LocalDensity.current
|
||||
@ -89,13 +92,30 @@ fun Slider(
|
||||
val range = valueRange.endInclusive - valueRange.start
|
||||
val fraction = if (range > 0f) ((value - valueRange.start) / range).coerceIn(0f, 1f) else 0f
|
||||
|
||||
val trackActiveColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f)
|
||||
val trackInactiveColor = if (enabled) {
|
||||
colors.border.copy(alpha = 0.6f)
|
||||
} else {
|
||||
colors.onContainer.copy(alpha = 0.2f)
|
||||
val stateColor = when {
|
||||
isPressed -> sliderTheme.trackActiveColor.pressed
|
||||
isHovered -> sliderTheme.trackActiveColor.hovered
|
||||
else -> sliderTheme.trackActiveColor.focused
|
||||
}
|
||||
val thumbColor = if (enabled) colors.accent else colors.onContainer.copy(alpha = 0.38f)
|
||||
val inactiveColor = when {
|
||||
isPressed -> sliderTheme.trackInactiveColor.pressed
|
||||
isHovered -> sliderTheme.trackInactiveColor.hovered
|
||||
else -> sliderTheme.trackInactiveColor.focused
|
||||
}
|
||||
val thumbColor = when {
|
||||
isPressed -> sliderTheme.thumbColor.pressed
|
||||
isHovered -> sliderTheme.thumbColor.hovered
|
||||
else -> sliderTheme.thumbColor.focused
|
||||
}
|
||||
val thumbShadowState = when {
|
||||
isPressed -> sliderTheme.thumbShadow.pressed
|
||||
isHovered -> sliderTheme.thumbShadow.hovered
|
||||
else -> sliderTheme.thumbShadow.focused
|
||||
}
|
||||
|
||||
val trackActiveColor = if (enabled) stateColor else stateColor.copy(alpha = 0.38f)
|
||||
val trackInactiveColor = if (enabled) inactiveColor else inactiveColor.copy(alpha = 0.38f)
|
||||
val resolvedThumbColor = if (enabled) thumbColor else thumbColor.copy(alpha = 0.38f)
|
||||
|
||||
val thumbScale by animateFloatAsState(
|
||||
targetValue = when {
|
||||
@ -107,12 +127,6 @@ fun Slider(
|
||||
label = "thumbScale",
|
||||
)
|
||||
|
||||
val thumbElevation = when {
|
||||
isPressed -> 2.dp
|
||||
isHovered -> 8.dp
|
||||
else -> 5.dp
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
@ -241,9 +255,9 @@ fun Slider(
|
||||
val x = trackStart + stepSpacing * i
|
||||
drawCircle(
|
||||
color = if (x <= activeEnd) {
|
||||
colors.onAccent.copy(alpha = 0.5f)
|
||||
sliderTheme.stepActiveColor
|
||||
} else {
|
||||
colors.onContainer.copy(alpha = 0.25f)
|
||||
sliderTheme.stepInactiveColor
|
||||
},
|
||||
radius = 2.dp.toPx(),
|
||||
center = Offset(x, trackY),
|
||||
@ -267,17 +281,17 @@ fun Slider(
|
||||
scaleY = thumbScale
|
||||
}
|
||||
.shadow(
|
||||
elevation = thumbElevation,
|
||||
elevation = thumbShadowState.elevation,
|
||||
shape = CircleShape,
|
||||
ambientColor = colors.accent.copy(alpha = 0.35f),
|
||||
spotColor = colors.accent.copy(alpha = 0.5f),
|
||||
ambientColor = thumbShadowState.ambientColor,
|
||||
spotColor = thumbShadowState.spotColor,
|
||||
)
|
||||
.background(
|
||||
brush = Brush.radialGradient(
|
||||
colors = listOf(
|
||||
Color.White.copy(alpha = 0.6f),
|
||||
thumbColor.copy(alpha = 0.95f),
|
||||
thumbColor,
|
||||
resolvedThumbColor.copy(alpha = 0.95f),
|
||||
resolvedThumbColor,
|
||||
),
|
||||
center = Offset(0f, -0.55f),
|
||||
radius = 1.1f,
|
||||
@ -296,4 +310,4 @@ fun Slider(
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -34,7 +34,6 @@ import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
@ -52,6 +51,7 @@ import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.draw.shadow
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Shape
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.input.VisualTransformation
|
||||
@ -61,32 +61,28 @@ import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxSearchBroken
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxTrash
|
||||
|
||||
private val TextFieldShape = RoundedCornerShape(14.dp)
|
||||
private val TextFieldMinHeight = 44.dp
|
||||
|
||||
private data class ResolvedTextFieldState(
|
||||
val background: Brush,
|
||||
val highlight: Color,
|
||||
val shape: Shape,
|
||||
val border: BaseUITheme.Border,
|
||||
val shadow: BaseUITheme.Shadow,
|
||||
val foreground: Color,
|
||||
)
|
||||
|
||||
@Composable
|
||||
internal fun textFieldShadow(
|
||||
shape: androidx.compose.ui.graphics.Shape,
|
||||
focused: Boolean,
|
||||
colors: ButtonColors,
|
||||
): Modifier {
|
||||
val elevation = if (focused) 9.dp else 6.dp
|
||||
val ambient = if (focused) {
|
||||
colors.accent.copy(alpha = 0.2f)
|
||||
} else {
|
||||
colors.shadow.copy(alpha = 0.15f)
|
||||
private fun resolveTextFieldState(
|
||||
theme: BaseUITheme.TextFieldTheme,
|
||||
isFocused: Boolean,
|
||||
isHovered: Boolean,
|
||||
): ResolvedTextFieldState {
|
||||
return when {
|
||||
isFocused -> ResolvedTextFieldState(theme.background.focused, theme.highlight.focused, theme.shape.focused, theme.border.focused, theme.shadow.focused, theme.foreground.focused)
|
||||
isHovered -> ResolvedTextFieldState(theme.background.hovered, theme.highlight.hovered, theme.shape.hovered, theme.border.hovered, theme.shadow.hovered, theme.foreground.hovered)
|
||||
else -> ResolvedTextFieldState(theme.background.pressed, theme.highlight.pressed, theme.shape.pressed, theme.border.pressed, theme.shadow.pressed, theme.foreground.pressed)
|
||||
}
|
||||
val spot = if (focused) {
|
||||
colors.accent.copy(alpha = 0.25f)
|
||||
} else {
|
||||
colors.shadow.copy(alpha = 0.18f)
|
||||
}
|
||||
return Modifier.shadow(
|
||||
elevation = elevation,
|
||||
shape = shape,
|
||||
ambientColor = ambient,
|
||||
spotColor = spot,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ -108,25 +104,29 @@ fun TextField(
|
||||
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||
textStyle: TextStyle = TextStyle.Default,
|
||||
cursorBrush: Color = MaterialTheme.colorScheme.primary,
|
||||
cursorBrush: Color? = null,
|
||||
theme: BaseUITheme.TextFieldTheme? = null,
|
||||
) {
|
||||
val colors = rememberButtonColors()
|
||||
val baseTheme = LocalBaseUITheme.current.textField
|
||||
val textFieldTheme = theme ?: baseTheme
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
|
||||
val gradient = outlinedGradient(colors, false)
|
||||
val border = when {
|
||||
isError -> MaterialTheme.colorScheme.error
|
||||
isFocused -> MaterialTheme.colorScheme.primary
|
||||
isHovered -> colors.border.copy(alpha = 0.85f)
|
||||
else -> colors.border
|
||||
val state = resolveTextFieldState(textFieldTheme, isFocused, isHovered)
|
||||
|
||||
val resolvedBorder = if (isError) {
|
||||
BaseUITheme.Border(MaterialTheme.colorScheme.error, state.border.width)
|
||||
} else {
|
||||
state.border
|
||||
}
|
||||
|
||||
val contentColor = when {
|
||||
!enabled -> colors.onContainer.copy(alpha = 0.38f)
|
||||
else -> colors.onContainer
|
||||
!enabled -> state.foreground.copy(alpha = 0.38f)
|
||||
else -> state.foreground
|
||||
}
|
||||
|
||||
val cursor = cursorBrush?.let { SolidColor(it) } ?: textFieldTheme.cursor
|
||||
|
||||
Column(modifier = modifier) {
|
||||
AnimatedVisibility(
|
||||
visible = label != null,
|
||||
@ -139,13 +139,22 @@ fun TextField(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.defaultMinSize(minHeight = TextFieldMinHeight)
|
||||
.then(textFieldShadow(TextFieldShape, isFocused, colors))
|
||||
.clip(TextFieldShape)
|
||||
.background(gradient, TextFieldShape)
|
||||
.border(BorderStroke(0.5.dp, border), TextFieldShape)
|
||||
.then(
|
||||
if (state.shadow.elevation > 0.dp) {
|
||||
Modifier.shadow(
|
||||
elevation = state.shadow.elevation,
|
||||
shape = state.shape,
|
||||
ambientColor = state.shadow.ambientColor,
|
||||
spotColor = state.shadow.spotColor,
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.clip(state.shape)
|
||||
.background(state.background, state.shape)
|
||||
.border(BorderStroke(resolvedBorder.width, resolvedBorder.color), state.shape)
|
||||
.drawWithCache {
|
||||
val highlightBrush = Brush.verticalGradient(
|
||||
colors = listOf(colors.highlight, Color.Transparent),
|
||||
colors = listOf(state.highlight, Color.Transparent),
|
||||
startY = 0f,
|
||||
endY = size.height * 0.5f,
|
||||
)
|
||||
@ -158,7 +167,7 @@ fun TextField(
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(horizontal = 14.dp, vertical = 10.dp),
|
||||
.padding(textFieldTheme.padding),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@ -177,7 +186,7 @@ fun TextField(
|
||||
enabled = enabled,
|
||||
readOnly = readOnly,
|
||||
textStyle = textStyle.copy(color = contentColor),
|
||||
cursorBrush = SolidColor(cursorBrush),
|
||||
cursorBrush = cursor,
|
||||
keyboardOptions = keyboardOptions,
|
||||
keyboardActions = keyboardActions,
|
||||
singleLine = singleLine,
|
||||
@ -210,36 +219,39 @@ fun TextField(
|
||||
@Composable
|
||||
fun TextFieldPreview() {
|
||||
MaterialTheme {
|
||||
androidx.compose.material3.Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
value = "Twenty One Pilots",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Placeholder") },
|
||||
label = {
|
||||
Text(
|
||||
"Search Field",
|
||||
modifier = Modifier.padding(bottom = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxSearchBroken,
|
||||
contentDescription = "Search Icon",
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxTrash,
|
||||
contentDescription = "Clear Icon",
|
||||
)
|
||||
},
|
||||
)
|
||||
val theme = rememberBaseUITheme()
|
||||
CompositionLocalProvider(LocalBaseUITheme provides theme) {
|
||||
androidx.compose.material3.Surface(
|
||||
color = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.padding(24.dp),
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier.padding(top = 4.dp),
|
||||
value = "Twenty One Pilots",
|
||||
onValueChange = {},
|
||||
placeholder = { Text("Placeholder") },
|
||||
label = {
|
||||
Text(
|
||||
"Search Field",
|
||||
modifier = Modifier.padding(bottom = 6.dp),
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
},
|
||||
leadingIcon = {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxSearchBroken,
|
||||
contentDescription = "Search Icon",
|
||||
)
|
||||
},
|
||||
trailingIcon = {
|
||||
androidx.compose.material3.Icon(
|
||||
imageVector = Iconsax.IconsaxTrash,
|
||||
contentDescription = "Clear Icon",
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -21,10 +21,8 @@ import androidx.compose.animation.AnimatedContent
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.togetherWith
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
@ -63,10 +61,10 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.focus.focusModifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.platform.LocalWindowInfo
|
||||
@ -86,14 +84,13 @@ import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.common.Thumbnail
|
||||
import dev.krtirtho.plugin_interfaces.plugin_apis.metadata.track.MetadataTrack
|
||||
import dev.krtirtho.spotube.core.ui.base.ButtonGroup
|
||||
import dev.krtirtho.spotube.core.ui.base.ButtonGroupDivider
|
||||
import dev.krtirtho.spotube.core.ui.base.Card
|
||||
import dev.krtirtho.spotube.core.ui.base.CheckBox
|
||||
import dev.krtirtho.spotube.core.ui.base.CheckBoxState
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.GroupIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||
import dev.krtirtho.spotube.core.ui.base.buttonShadow
|
||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
||||
import dev.krtirtho.spotube.core.ui.misc.SkeletonTree
|
||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||
import dev.krtirtho.spotube.core.ui.misc.shimmerApply
|
||||
@ -348,30 +345,14 @@ fun TrackList(
|
||||
}
|
||||
|
||||
item(key = "track-card") {
|
||||
val colors = rememberButtonColors()
|
||||
val trackListShape = MaterialTheme.shapes.large
|
||||
|
||||
Box(
|
||||
Card(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 8.dp)
|
||||
.then(
|
||||
buttonShadow(
|
||||
trackListShape,
|
||||
pressed = false,
|
||||
primary = false,
|
||||
colors,
|
||||
hovered = false
|
||||
)
|
||||
)
|
||||
.clip(trackListShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceContainer, trackListShape)
|
||||
.border(BorderStroke(0.5.dp, colors.border), trackListShape)
|
||||
.padding(vertical = 8.dp),
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(top = 12.dp, bottom = 12.dp)
|
||||
) {
|
||||
visibleTracks.forEachIndexed { displayedIndex, track ->
|
||||
if (displayedIndex > 0) {
|
||||
@ -532,17 +513,20 @@ private fun TrackListRow(
|
||||
onArtistsOverflowClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val rowColors = rememberButtonColors()
|
||||
val rowTheme = LocalBaseUITheme.current.listRowTile
|
||||
val artworkInteractionSource = remember { MutableInteractionSource() }
|
||||
val isArtworkHovered by artworkInteractionSource.collectIsHoveredAsState()
|
||||
val rowInteractionSource = remember { MutableInteractionSource() }
|
||||
val isRowHovered by rowInteractionSource.collectIsHoveredAsState()
|
||||
val rowBackgroundColor = when {
|
||||
isSelected -> MaterialTheme.colorScheme.primaryContainer.copy(alpha = 0.3f)
|
||||
isCurrentTrack && isCurrentTrackPlaying -> MaterialTheme.colorScheme.primary.copy(alpha = 0.14f)
|
||||
isCurrentTrack -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.06f)
|
||||
else -> Color.Transparent
|
||||
isSelected -> rowTheme.background.selected
|
||||
isCurrentTrack && isCurrentTrackPlaying -> rowTheme.background.hovered
|
||||
isCurrentTrack -> rowTheme.background.focused
|
||||
else -> Brush.verticalGradient(listOf(Color.Transparent, Color.Transparent))
|
||||
}
|
||||
val highlightColor = Color.White.copy(
|
||||
alpha = if (rowTheme.foreground.hovered.luminance() > 0.5f) 0.9f else 0.06f
|
||||
)
|
||||
|
||||
Row(
|
||||
modifier = modifier
|
||||
@ -555,7 +539,7 @@ private fun TrackListRow(
|
||||
.background(rowBackgroundColor)
|
||||
.drawWithCache {
|
||||
val highlightBrush = Brush.verticalGradient(
|
||||
colors = listOf(rowColors.highlight, Color.Transparent),
|
||||
colors = listOf(highlightColor, Color.Transparent),
|
||||
startY = 0f,
|
||||
endY = size.height * 0.5f,
|
||||
)
|
||||
|
||||
@ -49,6 +49,8 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil3.compose.AsyncImage
|
||||
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.PrimaryIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||
import dev.krtirtho.spotube.core.ui.misc.TextWithShimmer
|
||||
@ -108,7 +110,11 @@ fun PlayableCard(
|
||||
SecondaryIconButton(
|
||||
onClick = onAddToQueue,
|
||||
modifier = Modifier.size(30.dp),
|
||||
shape = CircleShape,
|
||||
theme = LocalBaseUITheme.current.iconButtons.secondary.copy(
|
||||
shape = BaseUITheme.InteractionState.fromSingleValue(
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Iconsax.IconsaxAddSquare,
|
||||
@ -121,7 +127,12 @@ fun PlayableCard(
|
||||
PrimaryIconButton(
|
||||
onClick = onPlay,
|
||||
modifier = Modifier.size(30.dp),
|
||||
shape = CircleShape,
|
||||
theme = LocalBaseUITheme.current.iconButtons.secondary.copy(
|
||||
shape = BaseUITheme.InteractionState.fromSingleValue(
|
||||
CircleShape
|
||||
)
|
||||
)
|
||||
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isPlaying) {
|
||||
|
||||
@ -83,10 +83,13 @@ import dev.krtirtho.spotube.core.audioplayer.LoopState
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationCommands
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.ui.base.BaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.Slider
|
||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
||||
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||
import dev.krtirtho.spotube.core.ui.base.invertedButtonStyle
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadStatus
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
@ -466,7 +469,10 @@ fun AppExpandedPlayer(
|
||||
textAlign = TextAlign.Center,
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onQueue, shape = CircleShape) {
|
||||
IconButton(
|
||||
onClick = onQueue,
|
||||
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape)
|
||||
) {
|
||||
Icon(Iconsax.IconsaxMusicFilter, contentDescription = "Queue")
|
||||
}
|
||||
}
|
||||
@ -526,22 +532,14 @@ fun AppExpandedPlayer(
|
||||
}
|
||||
IconButton(
|
||||
onClick = ::onPlayPause,
|
||||
colors = rememberButtonColors().copy(
|
||||
containerDarker = MaterialTheme.colorScheme.onSurface,
|
||||
containerLighter = MaterialTheme.colorScheme.onSurface,
|
||||
containerPressed = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
shadow = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
border = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
theme = invertedButtonStyle().copyShape(CircleShape),
|
||||
modifier = Modifier
|
||||
.size(72.dp),
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Icon(
|
||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play",
|
||||
modifier = Modifier.size(30.dp),
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
GhostIconButton(onClick = ::onSkipNext) {
|
||||
|
||||
@ -59,7 +59,8 @@ import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.Slider
|
||||
import dev.krtirtho.spotube.core.ui.base.VariableIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.VariableIconButtonVariant
|
||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
||||
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||
import dev.krtirtho.spotube.core.ui.base.invertedButtonStyle
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadProgressIcon
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadsViewModel
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SAVED_TRACKS_COLLECTION_ID
|
||||
@ -279,21 +280,13 @@ fun AppLargePlayer(
|
||||
}
|
||||
IconButton(
|
||||
onClick = ::onPlayPause,
|
||||
colors = rememberButtonColors().copy(
|
||||
containerDarker = MaterialTheme.colorScheme.onSurface,
|
||||
containerLighter = MaterialTheme.colorScheme.onSurface,
|
||||
containerPressed = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
shadow = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
border = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
),
|
||||
theme = invertedButtonStyle().copyShape(CircleShape),
|
||||
modifier = Modifier
|
||||
.size(50.dp),
|
||||
shape = CircleShape,
|
||||
) {
|
||||
Icon(
|
||||
if (playerUiState.isPlaying) Iconsax.IconsaxPause else Iconsax.IconsaxPlay,
|
||||
contentDescription = if (playerUiState.isPlaying) "Pause" else "Play or pause",
|
||||
tint = MaterialTheme.colorScheme.surface,
|
||||
)
|
||||
}
|
||||
GhostIconButton(onClick = ::onSkipNext) {
|
||||
|
||||
@ -21,14 +21,8 @@ import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateDpAsState
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
@ -42,34 +36,26 @@ import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.drawWithCache
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.krtirtho.spotube.core.navigation.NavigationState
|
||||
import dev.krtirtho.spotube.core.navigation.Navigator
|
||||
import dev.krtirtho.spotube.core.navigation.Routes
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.OutlineButton
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryButton
|
||||
import dev.krtirtho.spotube.core.ui.base.buttonShadow
|
||||
import dev.krtirtho.spotube.core.ui.base.outlinedGradient
|
||||
import dev.krtirtho.spotube.core.ui.base.rememberButtonColors
|
||||
import dev.krtirtho.spotube.core.ui.base.copyPadding
|
||||
import dev.krtirtho.spotube.modules.downloads.DownloadBadgeIndicator
|
||||
import dev.krtirtho.spotube.modules.library.LibraryState
|
||||
import dev.krtirtho.spotube.modules.library.LibraryTab
|
||||
@ -215,66 +201,16 @@ fun SidebarItem(
|
||||
SecondaryButton(
|
||||
onClick = onClick,
|
||||
modifier = buttonModifier,
|
||||
contentPadding = contentPadding,
|
||||
theme = LocalBaseUITheme.current.buttons.secondary.copyPadding(contentPadding),
|
||||
content = itemContent,
|
||||
)
|
||||
} else {
|
||||
val colors = rememberButtonColors()
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||
val isPressed by interactionSource.collectIsPressedAsState()
|
||||
val gradient = outlinedGradient(colors, isPressed)
|
||||
val border = colors.border.copy(alpha = if (isPressed) 0.7f else 1f)
|
||||
val lift = if (isHovered && !isPressed) (-1).dp else 0.dp
|
||||
val shape = RoundedCornerShape(14.dp)
|
||||
|
||||
Box(
|
||||
modifier = buttonModifier
|
||||
.hoverable(interactionSource = interactionSource)
|
||||
.graphicsLayer { translationY = lift.toPx() }
|
||||
.clip(shape)
|
||||
.then(
|
||||
if (isHovered || isPressed) {
|
||||
buttonShadow(
|
||||
shape,
|
||||
isPressed,
|
||||
primary = false,
|
||||
colors,
|
||||
hovered = isHovered
|
||||
).background(gradient, shape)
|
||||
.border(BorderStroke(0.5.dp, border), shape)
|
||||
.drawWithCache {
|
||||
val highlightBrush = Brush.verticalGradient(
|
||||
colors = listOf(colors.highlight, Color.Transparent),
|
||||
startY = 0f,
|
||||
endY = size.height * 0.5f,
|
||||
)
|
||||
onDrawWithContent {
|
||||
drawContent()
|
||||
drawRect(
|
||||
brush = highlightBrush,
|
||||
topLeft = androidx.compose.ui.geometry.Offset.Zero,
|
||||
size = size,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.clickable(
|
||||
interactionSource = interactionSource,
|
||||
indication = ripple(),
|
||||
onClick = onClick,
|
||||
)
|
||||
.padding(contentPadding),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
content = itemContent,
|
||||
)
|
||||
}
|
||||
OutlineButton(
|
||||
onClick = onClick,
|
||||
modifier = buttonModifier,
|
||||
theme = LocalBaseUITheme.current.buttons.outline.copyPadding(contentPadding),
|
||||
hoverOnly = true,
|
||||
content = itemContent,
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -27,6 +27,8 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import dev.krtirtho.spotube.core.audioplayer.AudioPlayerQueue
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedState
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.SavedTracksViewModel
|
||||
import dev.krtirtho.spotube.modules.saved_tracks.rememberIsSavedTracks
|
||||
@ -69,7 +71,7 @@ fun PlayerHeartButton(
|
||||
IconButton(
|
||||
onClick = ::onLike,
|
||||
enabled = isSavedTrackState is SavedState.Success,
|
||||
shape = CircleShape,
|
||||
theme = LocalBaseUITheme.current.iconButtons.outline.copyShape(CircleShape),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = if (isLiked) Iconsax.IconsaxHeart2 else Iconsax.IconsaxHeart,
|
||||
|
||||
@ -62,8 +62,10 @@ import compose.icons.feathericons.MoreVertical
|
||||
import dev.krtirtho.spotube.core.audioplayer.QueueEntry
|
||||
import dev.krtirtho.spotube.core.di.rememberLogger
|
||||
import dev.krtirtho.spotube.core.ui.base.GhostIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.LocalBaseUITheme
|
||||
import dev.krtirtho.spotube.core.ui.base.SecondaryIconButton
|
||||
import dev.krtirtho.spotube.core.ui.base.TextField
|
||||
import dev.krtirtho.spotube.core.ui.base.copyShape
|
||||
import dev.krtirtho.spotube.resources.iconsax.Iconsax
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxFilterSearch
|
||||
import dev.krtirtho.spotube.resources.iconsax.IconsaxMusicSquareRemove
|
||||
@ -188,7 +190,7 @@ fun PlayerQueueContent(
|
||||
)
|
||||
SecondaryIconButton(
|
||||
onClick = viewModel::clearQueue,
|
||||
shape = MaterialTheme.shapes.small,
|
||||
theme = LocalBaseUITheme.current.iconButtons.secondary.copyShape(MaterialTheme.shapes.small),
|
||||
) {
|
||||
Icon(Iconsax.IconsaxTrash, contentDescription = "Clear Queue")
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user