mirror of
https://github.com/bbara04/Purefin.git
synced 2026-07-24 03:36:51 +00:00
feat(player): enhance UI responsiveness with animated color states for buttons and track selection
This commit is contained in:
@@ -54,6 +54,18 @@ internal fun TvIconButton(
|
||||
targetValue = if (isFocused) scheme.primary else Color.Transparent,
|
||||
label = "border"
|
||||
)
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = if (isFocused) {
|
||||
scheme.primaryContainer
|
||||
} else {
|
||||
scheme.surfaceContainerHigh.copy(alpha = 0.72f)
|
||||
},
|
||||
label = "background"
|
||||
)
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = if (isFocused) scheme.onPrimaryContainer else scheme.onSurface,
|
||||
label = "content"
|
||||
)
|
||||
val shape = RoundedCornerShape(50)
|
||||
val iconSize = (size - 24).coerceAtLeast(0).dp
|
||||
|
||||
@@ -69,10 +81,7 @@ internal fun TvIconButton(
|
||||
shape = shape
|
||||
)
|
||||
.clip(shape)
|
||||
.background(
|
||||
if (isFocused) scheme.primary.copy(alpha = 0.5f)
|
||||
else scheme.background.copy(alpha = 0.65f)
|
||||
)
|
||||
.background(backgroundColor)
|
||||
.focusProperties { canFocus = enabled }
|
||||
.semantics {
|
||||
if (!enabled) {
|
||||
@@ -86,7 +95,7 @@ internal fun TvIconButton(
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = scheme.onBackground,
|
||||
tint = contentColor,
|
||||
modifier = buttonModifier
|
||||
.size(size.dp)
|
||||
.padding(12.dp)
|
||||
@@ -104,12 +113,12 @@ internal fun TvIconButton(
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = contentDescription,
|
||||
tint = scheme.onBackground,
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(iconSize)
|
||||
)
|
||||
Text(
|
||||
text = label,
|
||||
color = scheme.onBackground,
|
||||
color = contentColor,
|
||||
style = MaterialTheme.typography.labelLarge,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package hu.bbara.purefin.ui.screen.player.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
@@ -7,14 +9,19 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.PlayArrow
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -25,11 +32,14 @@ import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.withFrameNanos
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
@@ -82,24 +92,6 @@ internal fun TvPlayerQueuePanel(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
color = scheme.surface.copy(alpha = 0.94f)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(horizontal = 22.dp, vertical = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(
|
||||
text = "Playlist",
|
||||
color = scheme.onSurface,
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
Text(
|
||||
text = queueCountLabel,
|
||||
color = scheme.onSurfaceVariant,
|
||||
style = MaterialTheme.typography.bodyMedium
|
||||
)
|
||||
}
|
||||
|
||||
if (uiState.queue.isEmpty()) {
|
||||
Text(
|
||||
text = "Add something to the queue to browse it here.",
|
||||
@@ -113,7 +105,7 @@ internal fun TvPlayerQueuePanel(
|
||||
.testTag(TvPlayerPlaylistRowTag),
|
||||
state = listState,
|
||||
horizontalArrangement = Arrangement.spacedBy(14.dp),
|
||||
contentPadding = PaddingValues(end = 4.dp)
|
||||
contentPadding = PaddingValues(16.dp)
|
||||
) {
|
||||
itemsIndexed(uiState.queue, key = { _, item -> item.id }) { index, item ->
|
||||
val isEntryItem = index == entryIndex
|
||||
@@ -155,7 +147,6 @@ internal fun TvPlayerQueuePanel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvQueueRowCard(
|
||||
@@ -169,25 +160,49 @@ private fun TvQueueRowCard(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val borderColor = when {
|
||||
val scale by animateFloatAsState(
|
||||
targetValue = if (isFocused) 1.06f else 1f,
|
||||
label = "queueCardScale"
|
||||
)
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isFocused -> scheme.primary
|
||||
isCurrent -> scheme.primary.copy(alpha = 0.55f)
|
||||
else -> scheme.outlineVariant.copy(alpha = 0.35f)
|
||||
}
|
||||
val backgroundColor = when {
|
||||
isFocused -> scheme.primary.copy(alpha = 0.18f)
|
||||
isCurrent -> scheme.surfaceVariant.copy(alpha = 0.78f)
|
||||
else -> scheme.surfaceVariant.copy(alpha = 0.62f)
|
||||
isCurrent -> scheme.tertiary
|
||||
else -> scheme.outlineVariant.copy(alpha = 0.5f)
|
||||
},
|
||||
label = "queueCardBorder"
|
||||
)
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isFocused -> scheme.primaryContainer.copy(alpha = 0.96f)
|
||||
isCurrent -> scheme.tertiaryContainer.copy(alpha = 0.86f)
|
||||
else -> scheme.surfaceContainerHigh.copy(alpha = 0.72f)
|
||||
},
|
||||
label = "queueCardBackground"
|
||||
)
|
||||
val contentColor = when {
|
||||
isFocused -> scheme.onPrimaryContainer
|
||||
isCurrent -> scheme.onTertiaryContainer
|
||||
else -> scheme.onSurface
|
||||
}
|
||||
val shape = RoundedCornerShape(18.dp)
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.graphicsLayer {
|
||||
scaleX = scale
|
||||
scaleY = scale
|
||||
}
|
||||
.border(
|
||||
width = if (isFocused) 2.dp else 1.dp,
|
||||
width = when {
|
||||
isFocused -> 3.dp
|
||||
isCurrent -> 2.dp
|
||||
else -> 1.dp
|
||||
},
|
||||
color = borderColor,
|
||||
shape = RoundedCornerShape(18.dp)
|
||||
shape = shape
|
||||
)
|
||||
.clip(RoundedCornerShape(18.dp))
|
||||
.clip(shape)
|
||||
.background(backgroundColor)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.onPreviewKeyEvent { event ->
|
||||
@@ -207,37 +222,34 @@ private fun TvQueueRowCard(
|
||||
}
|
||||
}
|
||||
.clickable { onClick() }
|
||||
.padding(12.dp),
|
||||
.padding(4.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
TvQueueArtwork(
|
||||
artworkUrl = item.artworkUrl,
|
||||
isCurrent = isCurrent,
|
||||
badgeColor = if (isFocused) scheme.primary else scheme.tertiary,
|
||||
badgeContentColor = if (isFocused) scheme.onPrimary else scheme.onTertiary,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
if (isCurrent) {
|
||||
Text(
|
||||
text = "Current",
|
||||
color = scheme.primary,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
fontWeight = FontWeight.SemiBold
|
||||
)
|
||||
}
|
||||
Text(
|
||||
text = item.title,
|
||||
color = scheme.onSurface,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
color = contentColor,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
fontWeight = if (isCurrent || isFocused) FontWeight.Bold else FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun TvQueueArtwork(
|
||||
artworkUrl: String?,
|
||||
isCurrent: Boolean,
|
||||
badgeColor: Color,
|
||||
badgeContentColor: Color,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
@@ -268,5 +280,30 @@ private fun TvQueueArtwork(
|
||||
.background(scheme.surfaceContainerHigh)
|
||||
)
|
||||
}
|
||||
if (isCurrent) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.align(Alignment.TopStart)
|
||||
.padding(8.dp)
|
||||
.clip(RoundedCornerShape(50))
|
||||
.background(badgeColor)
|
||||
.padding(horizontal = 9.dp, vertical = 5.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.PlayArrow,
|
||||
contentDescription = null,
|
||||
tint = badgeContentColor,
|
||||
modifier = Modifier.size(14.dp)
|
||||
)
|
||||
Text(
|
||||
text = "Now playing",
|
||||
color = badgeContentColor,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
package hu.bbara.purefin.ui.screen.player.components
|
||||
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Check
|
||||
import androidx.compose.material.icons.outlined.ClosedCaption
|
||||
import androidx.compose.material.icons.outlined.HighQuality
|
||||
import androidx.compose.material.icons.outlined.Language
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
@@ -211,23 +216,42 @@ private fun TvTrackOptionRow(
|
||||
) {
|
||||
val scheme = MaterialTheme.colorScheme
|
||||
var isFocused by remember { mutableStateOf(false) }
|
||||
val backgroundColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isFocused -> scheme.primaryContainer
|
||||
selected -> scheme.secondaryContainer.copy(alpha = 0.9f)
|
||||
else -> scheme.surfaceContainerHigh.copy(alpha = 0.72f)
|
||||
},
|
||||
label = "trackOptionBackground"
|
||||
)
|
||||
val contentColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isFocused -> scheme.onPrimaryContainer
|
||||
selected -> scheme.onSecondaryContainer
|
||||
else -> scheme.onSurface
|
||||
},
|
||||
label = "trackOptionContent"
|
||||
)
|
||||
val borderColor by animateColorAsState(
|
||||
targetValue = when {
|
||||
isFocused -> scheme.primary
|
||||
selected -> scheme.secondary
|
||||
else -> scheme.outlineVariant.copy(alpha = 0.45f)
|
||||
},
|
||||
label = "trackOptionBorder"
|
||||
)
|
||||
val shape = RoundedCornerShape(12.dp)
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (isFocused) {
|
||||
Modifier.border(2.dp, scheme.primary, RoundedCornerShape(12.dp))
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(
|
||||
if (isFocused) scheme.primary.copy(alpha = 0.3f)
|
||||
else if (selected) scheme.primary.copy(alpha = 0.15f)
|
||||
else scheme.surfaceVariant.copy(alpha = 0.6f)
|
||||
.border(
|
||||
width = if (isFocused) 3.dp else if (selected) 2.dp else 1.dp,
|
||||
color = borderColor,
|
||||
shape = shape
|
||||
)
|
||||
.clip(shape)
|
||||
.background(backgroundColor)
|
||||
.onFocusChanged { isFocused = it.isFocused }
|
||||
.onPreviewKeyEvent { event ->
|
||||
if (event.type != KeyEventType.KeyDown) {
|
||||
@@ -243,12 +267,26 @@ private fun TvTrackOptionRow(
|
||||
}
|
||||
.clickable { onClick() }
|
||||
.padding(horizontal = 20.dp, vertical = 14.dp)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
color = scheme.onSurface,
|
||||
color = contentColor,
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Normal
|
||||
fontWeight = if (selected || isFocused) FontWeight.SemiBold else FontWeight.Normal
|
||||
)
|
||||
if (selected) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Check,
|
||||
contentDescription = null,
|
||||
tint = contentColor,
|
||||
modifier = Modifier.size(22.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user