Improve SearchResultScreen UI
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
package it.vfsfitvnm.vimusic.ui.views
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -10,8 +15,11 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
@@ -21,14 +29,18 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||
import it.vfsfitvnm.vimusic.ui.styling.overlay
|
||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
import it.vfsfitvnm.vimusic.utils.medium
|
||||
import it.vfsfitvnm.vimusic.utils.secondary
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.youtubemusic.YouTube
|
||||
@@ -44,6 +56,8 @@ fun SmallSongItemShimmer(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
@@ -58,29 +72,6 @@ fun SmallSongItemShimmer(
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SmallArtistItemShimmer(
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.background(color = colorPalette.shimmer, shape = CircleShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
TextPlaceholder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SmallSongItem(
|
||||
@@ -102,74 +93,80 @@ fun SmallSongItem(
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SmallVideoItem(
|
||||
fun VideoItem(
|
||||
video: YouTube.Item.Video,
|
||||
thumbnailSizePx: Int,
|
||||
thumbnailHeightDp: Dp,
|
||||
thumbnailWidthDp: Dp,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
SongItem(
|
||||
thumbnailModel = video.thumbnail?.size(thumbnailSizePx),
|
||||
title = video.info.name,
|
||||
authors = (if (video.isOfficialMusicVideo) video.authors else video.views)
|
||||
.joinToString("") { it.name },
|
||||
durationText = video.durationText,
|
||||
onClick = onClick,
|
||||
menuContent = {
|
||||
NonQueuedMediaItemMenu(mediaItem = video.asMediaItem)
|
||||
},
|
||||
modifier = modifier
|
||||
)
|
||||
}
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SmallPlaylistItem(
|
||||
playlist: YouTube.Item.Playlist,
|
||||
thumbnailSizePx: Int,
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (_, typography) = LocalAppearance.current
|
||||
val menuState = LocalMenuState.current
|
||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.combinedClickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onLongClick = {
|
||||
menuState.display {
|
||||
NonQueuedMediaItemMenu(mediaItem = video.asMediaItem)
|
||||
}
|
||||
},
|
||||
onClick = onClick
|
||||
)
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
) {
|
||||
AsyncImage(
|
||||
model = playlist.thumbnail?.size(thumbnailSizePx),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.clip(ThumbnailRoundness.shape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
Box {
|
||||
AsyncImage(
|
||||
model = video.thumbnail?.url,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.clip(thumbnailShape)
|
||||
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
video.durationText?.let { durationText ->
|
||||
BasicText(
|
||||
text = durationText,
|
||||
style = typography.xxs.medium.color(colorPalette.onOverlay),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(all = 4.dp)
|
||||
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column {
|
||||
BasicText(
|
||||
text = playlist.info.name,
|
||||
text = video.info.name,
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 1,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = playlist.channel?.name ?: "",
|
||||
text = video.authors.joinToString("") { it.name },
|
||||
style = typography.xs.semiBold.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
playlist.songCount?.let { songCount ->
|
||||
BasicText(
|
||||
text = "$songCount songs",
|
||||
style = typography.xxs.secondary,
|
||||
text = video.views.firstOrNull()?.name ?: "",
|
||||
style = typography.xxs.medium.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
@@ -177,60 +174,208 @@ fun SmallPlaylistItem(
|
||||
}
|
||||
}
|
||||
|
||||
@ExperimentalFoundationApi
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
fun SmallAlbumItem(
|
||||
fun VideoItemShimmer(
|
||||
thumbnailHeightDp: Dp,
|
||||
thumbnailWidthDp: Dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
|
||||
)
|
||||
|
||||
Column {
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaylistItem(
|
||||
playlist: YouTube.Item.Playlist,
|
||||
thumbnailSizePx: Int,
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Box {
|
||||
AsyncImage(
|
||||
model = playlist.thumbnail?.size(thumbnailSizePx),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.clip(thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
playlist.songCount?.let { songCount ->
|
||||
BasicText(
|
||||
text = "$songCount",
|
||||
style = typography.xxs.medium.color(colorPalette.onOverlay),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(all = 4.dp)
|
||||
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
|
||||
.padding(horizontal = 4.dp, vertical = 2.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
BasicText(
|
||||
text = playlist.info.name,
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = playlist.channel?.name ?: "",
|
||||
style = typography.xs.semiBold.secondary,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PlaylistItemShimmer(
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AlbumItem(
|
||||
album: YouTube.Item.Album,
|
||||
thumbnailSizePx: Int,
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (_, typography) = LocalAppearance.current
|
||||
val (_, typography, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AsyncImage(
|
||||
model = album.thumbnail?.size(thumbnailSizePx),
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier
|
||||
.clip(ThumbnailRoundness.shape)
|
||||
.clip(thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
BasicText(
|
||||
text = album.info.name,
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 1,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = album.authors?.joinToString("") { it.name } ?: "",
|
||||
style = typography.xs.semiBold.secondary,
|
||||
maxLines = 1,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
|
||||
album.year?.let { year ->
|
||||
BasicText(
|
||||
text = year,
|
||||
style = typography.xxs.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
album.year?.let { year ->
|
||||
BasicText(
|
||||
text = year,
|
||||
style = typography.xxs.semiBold.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.padding(top = 8.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SmallArtistItem(
|
||||
fun AlbumItemShimmer(
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ArtistItem(
|
||||
artist: YouTube.Item.Artist,
|
||||
thumbnailSizePx: Int,
|
||||
thumbnailSizeDp: Dp,
|
||||
@@ -240,8 +385,10 @@ fun SmallArtistItem(
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
AsyncImage(
|
||||
model = artist.thumbnail?.size(thumbnailSizePx),
|
||||
@@ -251,23 +398,49 @@ fun SmallArtistItem(
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
BasicText(
|
||||
text = artist.info.name,
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
BasicText(
|
||||
text = artist.info.name,
|
||||
style = typography.xs.semiBold,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ArtistItemShimmer(
|
||||
thumbnailSizeDp: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = modifier
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.background(color = colorPalette.shimmer, shape = CircleShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
TextPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SearchResultLoadingOrError(
|
||||
itemCount: Int = 0,
|
||||
isLoadingArtists: Boolean = false,
|
||||
errorMessage: String? = null,
|
||||
onRetry: (() -> Unit)? = null
|
||||
onRetry: (() -> Unit)? = null,
|
||||
shimmerContent: @Composable BoxScope.() -> Unit,
|
||||
) {
|
||||
LoadingOrError(
|
||||
errorMessage = errorMessage,
|
||||
@@ -275,23 +448,28 @@ fun SearchResultLoadingOrError(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
repeat(itemCount) { index ->
|
||||
if (isLoadingArtists) {
|
||||
SmallArtistItemShimmer(
|
||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
||||
modifier = Modifier
|
||||
.alpha(1f - index * 0.125f)
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
)
|
||||
} else {
|
||||
SmallSongItemShimmer(
|
||||
thumbnailSizeDp = Dimensions.thumbnails.song,
|
||||
modifier = Modifier
|
||||
.alpha(1f - index * 0.125f)
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
)
|
||||
}
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.alpha(1f - index * 0.125f),
|
||||
content = shimmerContent
|
||||
)
|
||||
// if (isLoadingArtists) {
|
||||
// SmallArtistItemShimmer(
|
||||
// thumbnailSizeDp = Dimensions.thumbnails.song,
|
||||
// modifier = Modifier
|
||||
// .alpha(1f - index * 0.125f)
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
// )
|
||||
// } else {
|
||||
// SmallSongItemShimmer(
|
||||
// thumbnailSizeDp = Dimensions.thumbnails.song,
|
||||
// modifier = Modifier
|
||||
// .alpha(1f - index * 0.125f)
|
||||
// .fillMaxWidth()
|
||||
// .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
|
||||
// )
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user