mirror of
https://github.com/KRTirtho/spotube.git
synced 2026-08-05 19:59:51 +00:00
feat: add GhostIconButton component and enhance button interactions in TrackList
This commit is contained in:
parent
6d8b9a575f
commit
cb27514b9b
@ -237,6 +237,7 @@ fun OutlineButton(
|
|||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
contentPadding: PaddingValues = PaddingValues(horizontal = 20.dp, vertical = 10.dp),
|
||||||
|
hoverOnly: Boolean = false,
|
||||||
content: @Composable RowScope.() -> Unit,
|
content: @Composable RowScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = rememberButtonColors()
|
val colors = rememberButtonColors()
|
||||||
@ -252,17 +253,27 @@ fun OutlineButton(
|
|||||||
.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))
|
.then(
|
||||||
|
if (hoverOnly && !isHovered) Modifier else buttonShadow(
|
||||||
|
shape,
|
||||||
|
isPressed,
|
||||||
|
primary = false,
|
||||||
|
colors,
|
||||||
|
hovered = isHovered
|
||||||
|
)
|
||||||
|
)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(gradient, shape)
|
.then(
|
||||||
.border(BorderStroke(0.5.dp, border), shape)
|
if (hoverOnly && !isHovered) Modifier else Modifier.background(gradient, shape)
|
||||||
|
.border(BorderStroke(0.5.dp, border), shape)
|
||||||
|
)
|
||||||
.clickable(
|
.clickable(
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
interactionSource = interactionSource,
|
interactionSource = interactionSource,
|
||||||
indication = ripple(),
|
indication = ripple(),
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
)
|
)
|
||||||
.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(colors.highlight, Color.Transparent),
|
||||||
startY = 0f,
|
startY = 0f,
|
||||||
@ -276,7 +287,7 @@ fun OutlineButton(
|
|||||||
size = size,
|
size = size,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
.padding(contentPadding),
|
.padding(contentPadding),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
@ -427,6 +438,26 @@ fun IconButton(
|
|||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@Composable
|
||||||
|
|
||||||
|
fun GhostIconButton(
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
shape: androidx.compose.ui.graphics.Shape = ButtonShape,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
OutlineButton(
|
||||||
|
onClick = onClick,
|
||||||
|
modifier = modifier,
|
||||||
|
enabled = enabled,
|
||||||
|
shape = shape,
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
hoverOnly = true,
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun PrimaryIconButton(
|
fun PrimaryIconButton(
|
||||||
@ -579,7 +610,15 @@ fun ButtonGroup(
|
|||||||
Surface(
|
Surface(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.defaultMinSize(minHeight = ButtonMinHeight)
|
.defaultMinSize(minHeight = ButtonMinHeight)
|
||||||
.then(buttonShadow(GroupShape, pressed = false, primary = false, colors, hovered = false)),
|
.then(
|
||||||
|
buttonShadow(
|
||||||
|
GroupShape,
|
||||||
|
pressed = false,
|
||||||
|
primary = false,
|
||||||
|
colors,
|
||||||
|
hovered = false
|
||||||
|
)
|
||||||
|
),
|
||||||
shape = GroupShape,
|
shape = GroupShape,
|
||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
border = BorderStroke(0.5.dp, colors.border),
|
border = BorderStroke(0.5.dp, colors.border),
|
||||||
|
|||||||
@ -21,13 +21,16 @@ 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
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
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
|
||||||
@ -41,12 +44,13 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
|
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.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
@ -58,11 +62,16 @@ import androidx.compose.runtime.setValue
|
|||||||
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
|
||||||
|
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.Color
|
||||||
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
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
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
|
||||||
@ -79,9 +88,12 @@ 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.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.TextField
|
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.IconButton
|
||||||
|
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
|
||||||
@ -171,12 +183,15 @@ fun TrackList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
val shouldLoadMore = remember {
|
val density = LocalDensity.current
|
||||||
|
val shouldLoadMore = remember(density) {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
val lastVisibleItem = listState.layoutInfo.visibleItemsInfo.lastOrNull()
|
val loadMoreThreshold = with(density) { 200.dp.toPx() }
|
||||||
// Note: totalItemsCount includes headers/footers, so this safely triggers
|
val cardItem = listState.layoutInfo.visibleItemsInfo.find { it.key == "track-card" }
|
||||||
// a few items before the absolute bottom without index-shifting bugs.
|
// Trigger when the bottom of the track card is close to/inside the viewport,
|
||||||
lastVisibleItem != null && lastVisibleItem.index >= listState.layoutInfo.totalItemsCount - 5
|
// which means the user has scrolled through (or is about to scroll through)
|
||||||
|
// all loaded tracks.
|
||||||
|
cardItem != null && cardItem.offset + cardItem.size <= listState.layoutInfo.viewportEndOffset + loadMoreThreshold
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,7 +202,6 @@ fun TrackList(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val windowInfo = LocalWindowInfo.current
|
val windowInfo = LocalWindowInfo.current
|
||||||
val density = LocalDensity.current
|
|
||||||
val maxWidth = with(density) { windowInfo.containerSize.width.toDp() }
|
val maxWidth = with(density) { windowInfo.containerSize.width.toDp() }
|
||||||
val isCompact = maxWidth < CompactTrackListBreakpoint
|
val isCompact = maxWidth < CompactTrackListBreakpoint
|
||||||
val isDesktop = remember { getPlatform().isDesktop() }
|
val isDesktop = remember { getPlatform().isDesktop() }
|
||||||
@ -333,90 +347,139 @@ fun TrackList(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsIndexed(
|
item(key = "track-card") {
|
||||||
items = visibleTracks,
|
val colors = rememberButtonColors()
|
||||||
key = { _, indexedTrack -> "${indexedTrack.id}-${indexedTrack.title}-${indexedTrack.album?.title ?: ""}" },
|
val trackListShape = MaterialTheme.shapes.large
|
||||||
) { displayedIndex, track ->
|
|
||||||
TrackListRow(
|
Box(
|
||||||
index = displayedIndex + 1,
|
modifier = Modifier
|
||||||
track = track,
|
.fillMaxWidth()
|
||||||
showIndex = showIndex,
|
.padding(vertical = 8.dp)
|
||||||
showAlbum = showAlbum,
|
.then(
|
||||||
useDropdownForOptions = useDropdownForOptions,
|
buttonShadow(
|
||||||
isCurrentTrack = track.id == currentTrackId,
|
trackListShape,
|
||||||
isCurrentTrackPlaying = isCurrentTrackPlaying,
|
pressed = false,
|
||||||
isSelectionMode = isSelectionMode,
|
primary = false,
|
||||||
isSelected = selectedTrackIds.contains(track.id),
|
colors,
|
||||||
onTrackClick = {
|
hovered = false
|
||||||
if (isSelectionMode) {
|
)
|
||||||
selectedTrackIds = if (selectedTrackIds.contains(track.id)) {
|
)
|
||||||
selectedTrackIds - track.id
|
.clip(trackListShape)
|
||||||
} else {
|
.background(MaterialTheme.colorScheme.surfaceContainer, trackListShape)
|
||||||
selectedTrackIds + track.id
|
.border(BorderStroke(0.5.dp, colors.border), trackListShape)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 12.dp, bottom = 12.dp)
|
||||||
|
) {
|
||||||
|
visibleTracks.forEachIndexed { displayedIndex, track ->
|
||||||
|
if (displayedIndex > 0) {
|
||||||
|
HorizontalDivider(
|
||||||
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
} else {
|
TrackListRow(
|
||||||
onTrackClick(track)
|
index = displayedIndex + 1,
|
||||||
|
track = track,
|
||||||
|
showIndex = showIndex,
|
||||||
|
showAlbum = showAlbum,
|
||||||
|
useDropdownForOptions = useDropdownForOptions,
|
||||||
|
isCurrentTrack = track.id == currentTrackId,
|
||||||
|
isCurrentTrackPlaying = isCurrentTrackPlaying,
|
||||||
|
isSelectionMode = isSelectionMode,
|
||||||
|
isSelected = selectedTrackIds.contains(track.id),
|
||||||
|
onTrackClick = {
|
||||||
|
if (isSelectionMode) {
|
||||||
|
selectedTrackIds =
|
||||||
|
if (selectedTrackIds.contains(track.id)) {
|
||||||
|
selectedTrackIds - track.id
|
||||||
|
} else {
|
||||||
|
selectedTrackIds + track.id
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
onTrackClick(track)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onLongClick = {
|
||||||
|
if (!useDropdownForOptions && !isSelectionMode) {
|
||||||
|
isSelectionMode = true
|
||||||
|
selectedTrackIds = setOf(track.id)
|
||||||
|
} else if (!useDropdownForOptions) {
|
||||||
|
selectedTrackForOptions = track
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onSelectionToggle = { checked ->
|
||||||
|
isSelectionMode = true
|
||||||
|
selectedTrackIds = if (checked) {
|
||||||
|
selectedTrackIds + track.id
|
||||||
|
} else {
|
||||||
|
selectedTrackIds - track.id
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onTrackOptionsAction = { action ->
|
||||||
|
onTrackOptionsAction(
|
||||||
|
track,
|
||||||
|
action
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trackOptionsState = trackOptionsState(track),
|
||||||
|
onShowOptionsClick = { selectedTrackForOptions = track },
|
||||||
|
onArtistClick = onArtistClick,
|
||||||
|
onAlbumClick = onAlbumClick,
|
||||||
|
onArtistsOverflowClick = { onArtistsOverflowClick(track) },
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onLongClick = {
|
if (isLoading && tracks.isEmpty()) {
|
||||||
if (!useDropdownForOptions && !isSelectionMode) {
|
repeat(ShimmerRowCount) { shimmerIndex ->
|
||||||
isSelectionMode = true
|
if (visibleTracks.isNotEmpty() || shimmerIndex > 0) {
|
||||||
selectedTrackIds = setOf(track.id)
|
HorizontalDivider(
|
||||||
} else if (!useDropdownForOptions) {
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
selectedTrackForOptions = track
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ShimmerTrackListRow(
|
||||||
|
index = shimmerIndex + 1,
|
||||||
|
showIndex = showIndex,
|
||||||
|
showAlbum = showAlbum,
|
||||||
|
useDropdownForOptions = useDropdownForOptions,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onSelectionToggle = { checked ->
|
if (error != null) {
|
||||||
isSelectionMode = true
|
if (visibleTracks.isNotEmpty()) {
|
||||||
selectedTrackIds = if (checked) {
|
HorizontalDivider(
|
||||||
selectedTrackIds + track.id
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
} else {
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||||
selectedTrackIds - track.id
|
)
|
||||||
|
}
|
||||||
|
TrackListFeedbackRow(
|
||||||
|
message = error,
|
||||||
|
isError = true,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
},
|
|
||||||
onTrackOptionsAction = { action -> onTrackOptionsAction(track, action) },
|
|
||||||
trackOptionsState = trackOptionsState(track),
|
|
||||||
onShowOptionsClick = { selectedTrackForOptions = track },
|
|
||||||
onArtistClick = onArtistClick,
|
|
||||||
onAlbumClick = onAlbumClick,
|
|
||||||
onArtistsOverflowClick = { onArtistsOverflowClick(track) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading && tracks.isEmpty()) {
|
if (showEmptyMessage && !isLoading && visibleTracks.isEmpty() && error == null) {
|
||||||
items(ShimmerRowCount) { shimmerIndex ->
|
TrackListFeedbackRow("No tracks found")
|
||||||
ShimmerTrackListRow(
|
}
|
||||||
index = shimmerIndex + 1,
|
|
||||||
showIndex = showIndex,
|
|
||||||
showAlbum = showAlbum,
|
|
||||||
useDropdownForOptions = useDropdownForOptions,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error != null) {
|
if (isLoadingNextPage) {
|
||||||
item {
|
if (visibleTracks.isNotEmpty()) {
|
||||||
TrackListFeedbackRow(
|
HorizontalDivider(
|
||||||
message = error,
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
isError = true,
|
color = MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
ShimmerTrackListRow(
|
||||||
|
index = visibleTracks.size + 1,
|
||||||
if (showEmptyMessage && !isLoading && visibleTracks.isEmpty() && error == null) {
|
showIndex = showIndex,
|
||||||
item {
|
showAlbum = showAlbum,
|
||||||
TrackListFeedbackRow("No tracks found")
|
useDropdownForOptions = useDropdownForOptions,
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (isLoadingNextPage) {
|
|
||||||
item {
|
|
||||||
ShimmerTrackListRow(
|
|
||||||
index = visibleTracks.size + 1,
|
|
||||||
showIndex = showIndex,
|
|
||||||
showAlbum = showAlbum,
|
|
||||||
useDropdownForOptions = useDropdownForOptions,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -469,6 +532,7 @@ private fun TrackListRow(
|
|||||||
onArtistsOverflowClick: () -> Unit,
|
onArtistsOverflowClick: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val rowColors = rememberButtonColors()
|
||||||
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() }
|
||||||
@ -483,14 +547,32 @@ private fun TrackListRow(
|
|||||||
Row(
|
Row(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(MaterialTheme.shapes.small)
|
|
||||||
.background(rowBackgroundColor)
|
|
||||||
.hoverable(rowInteractionSource)
|
.hoverable(rowInteractionSource)
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onTrackClick,
|
onClick = onTrackClick,
|
||||||
onLongClick = onLongClick,
|
onLongClick = onLongClick,
|
||||||
)
|
)
|
||||||
.padding(horizontal = 8.dp, vertical = 6.dp),
|
.background(rowBackgroundColor)
|
||||||
|
.drawWithCache {
|
||||||
|
val highlightBrush = Brush.verticalGradient(
|
||||||
|
colors = listOf(rowColors.highlight, Color.Transparent),
|
||||||
|
startY = 0f,
|
||||||
|
endY = size.height * 0.5f,
|
||||||
|
)
|
||||||
|
onDrawWithContent {
|
||||||
|
drawContent()
|
||||||
|
if (isRowHovered || isSelected || (isCurrentTrack && isCurrentTrackPlaying)) {
|
||||||
|
drawRect(
|
||||||
|
brush = highlightBrush,
|
||||||
|
topLeft = Offset.Zero,
|
||||||
|
size = size,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 6.dp)
|
||||||
|
.padding(end = 6.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
horizontalArrangement = Arrangement.spacedBy(10.dp),
|
||||||
) {
|
) {
|
||||||
@ -587,16 +669,47 @@ private fun TrackListRow(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (showAlbum && track.album != null) {
|
if (showAlbum && track.album != null) {
|
||||||
TextWithShimmer(
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
text = track.album!!.title,
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
color = MaterialTheme.colorScheme.primary,
|
val colorScheme = MaterialTheme.colorScheme
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
Row(
|
||||||
modifier = Modifier
|
modifier = Modifier.weight(1f)
|
||||||
.weight(1f)
|
) {
|
||||||
.clickable { onAlbumClick(track.album!!) },
|
TextWithShimmer(
|
||||||
)
|
text = track.album!!.title,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.hoverable(interactionSource)
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
) { onAlbumClick(track.album!!) }
|
||||||
|
// Hide ripple and show underline on hover/click
|
||||||
|
.then(
|
||||||
|
if (isHovered || isPressed) {
|
||||||
|
Modifier.drawWithCache {
|
||||||
|
val underlineHeight = 1.dp.toPx()
|
||||||
|
onDrawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawLine(
|
||||||
|
color = colorScheme.primary,
|
||||||
|
start = Offset(0f, size.height - underlineHeight),
|
||||||
|
end = Offset(size.width, size.height - underlineHeight),
|
||||||
|
strokeWidth = underlineHeight,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TextWithShimmer(
|
TextWithShimmer(
|
||||||
@ -615,7 +728,7 @@ private fun TrackListRow(
|
|||||||
onAlbumClick = { track.album?.let { onAlbumClick(it) } },
|
onAlbumClick = { track.album?.let { onAlbumClick(it) } },
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
IconButton(onClick = onShowOptionsClick) {
|
GhostIconButton(onClick = onShowOptionsClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.Iconsax3DotsMore,
|
imageVector = Iconsax.Iconsax3DotsMore,
|
||||||
contentDescription = "Track options",
|
contentDescription = "Track options",
|
||||||
@ -637,15 +750,35 @@ private fun ArtistChips(
|
|||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
modifier = Modifier.fillMaxWidth(),
|
modifier = Modifier.fillMaxWidth(),
|
||||||
) {
|
) {
|
||||||
visibleArtists.forEach { artist ->
|
visibleArtists.forEachIndexed { index, artist ->
|
||||||
TextWithShimmer(
|
Row(
|
||||||
text = artist.name,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
style = MaterialTheme.typography.bodySmall,
|
) {
|
||||||
color = MaterialTheme.colorScheme.primary,
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
maxLines = 1,
|
val isHovered by interactionSource.collectIsHoveredAsState()
|
||||||
overflow = TextOverflow.Ellipsis,
|
val isPressed by interactionSource.collectIsPressedAsState()
|
||||||
modifier = Modifier.clickable { onArtistClick(artist) },
|
|
||||||
)
|
TextWithShimmer(
|
||||||
|
text = artist.name,
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
textDecoration = if (isHovered || isPressed) TextDecoration.Underline else null,
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = interactionSource
|
||||||
|
) { onArtistClick(artist) }
|
||||||
|
,
|
||||||
|
)
|
||||||
|
if (index < visibleArtists.size - 1)
|
||||||
|
Text(
|
||||||
|
",",
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
style = MaterialTheme.typography.bodySmall
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val remaining = artists.size - visibleArtists.size
|
val remaining = artists.size - visibleArtists.size
|
||||||
|
|||||||
@ -38,6 +38,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil3.compose.AsyncImage
|
import coil3.compose.AsyncImage
|
||||||
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.GhostIconButton
|
||||||
import dev.krtirtho.spotube.core.ui.base.IconButton
|
import dev.krtirtho.spotube.core.ui.base.IconButton
|
||||||
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
|
||||||
@ -86,7 +87,7 @@ fun TrackOptions(
|
|||||||
onAlbumClick = onAlbumClick,
|
onAlbumClick = onAlbumClick,
|
||||||
),
|
),
|
||||||
trigger = { onClick ->
|
trigger = { onClick ->
|
||||||
IconButton(onClick = onClick) {
|
GhostIconButton(onClick = onClick) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Iconsax.Iconsax3DotsMore,
|
imageVector = Iconsax.Iconsax3DotsMore,
|
||||||
contentDescription = "Track options",
|
contentDescription = "Track options",
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user