Improve SearchResultScreen UI

This commit is contained in:
vfsfitvnm 2022-09-24 13:02:52 +02:00
parent 8db6f7a13e
commit e71e34c0d7
6 changed files with 450 additions and 231 deletions

View file

@ -63,15 +63,15 @@ import it.vfsfitvnm.vimusic.ui.components.collapsedAnchor
import it.vfsfitvnm.vimusic.ui.components.dismissedAnchor
import it.vfsfitvnm.vimusic.ui.components.expandedAnchor
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState
import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen
import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen
import it.vfsfitvnm.vimusic.ui.screens.home.HomeScreen
import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView
import it.vfsfitvnm.vimusic.ui.styling.Appearance
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf
import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf
import it.vfsfitvnm.vimusic.ui.styling.typographyOf
import it.vfsfitvnm.vimusic.ui.screens.player.PlayerView
import it.vfsfitvnm.vimusic.utils.colorPaletteModeKey
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
import it.vfsfitvnm.vimusic.utils.getEnum

View file

@ -2,6 +2,7 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope
@ -22,11 +23,10 @@ import it.vfsfitvnm.youtubemusic.YouTube
@ExperimentalAnimationApi
@Composable
inline fun <I : YouTube.Item> ItemSearchResultTab(
inline fun <I : YouTube.Item> ItemSearchResult(
query: String,
filter: String,
crossinline onSearchAgain: () -> Unit,
isArtists: Boolean = false,
viewModel: ItemSearchResultViewModel<I> = viewModel(
key = query + filter,
factory = object : ViewModelProvider.Factory {
@ -36,7 +36,8 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
}
}
),
crossinline itemContent: @Composable (LazyItemScope.(I) -> Unit)
crossinline itemContent: @Composable LazyItemScope.(I) -> Unit,
noinline itemShimmer: @Composable BoxScope.() -> Unit,
) {
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
@ -45,7 +46,7 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
) {
item(
key = "header",
contentType = 0
contentType = 0,
) {
Header(
title = query,
@ -60,6 +61,7 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
items(
items = viewModel.items,
key = { it.key!! },
itemContent = itemContent
)
@ -73,7 +75,8 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
item {
SearchResultLoadingOrError(
errorMessage = throwable.javaClass.canonicalName,
onRetry = viewModel::fetch
onRetry = viewModel::fetch,
shimmerContent = {}
)
}
} ?: viewModel.continuationResult?.let {
@ -88,7 +91,7 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
} ?: item(key = "loading") {
SearchResultLoadingOrError(
itemCount = if (viewModel.items.isEmpty()) 8 else 3,
isLoadingArtists = isArtists
shimmerContent = itemShimmer
)
}
}

View file

@ -1,7 +1,6 @@
package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
@ -12,8 +11,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ItemSearchResultViewModel<T : YouTube.Item>(private val query: String, private val filter: String) : ViewModel() {
val items = mutableStateListOf<T>()
class ItemSearchResultViewModel<T : YouTube.Item>(
private val query: String,
private val filter: String
) : ViewModel() {
var items by mutableStateOf(listOf<T>())
var continuationResult by mutableStateOf<Result<String?>?>(null)
@ -35,7 +37,7 @@ class ItemSearchResultViewModel<T : YouTube.Item>(private val query: String, pri
YouTube.search(query, filter, token)
}?.map { searchResult ->
@Suppress("UNCHECKED_CAST")
items.addAll(searchResult.items as List<T>)
items = items.plus(searchResult.items as List<T>).distinctBy(YouTube.Item::key)
searchResult.continuation
}
}

View file

@ -1,9 +1,9 @@
package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.padding
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
@ -21,17 +21,23 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SmallAlbumItem
import it.vfsfitvnm.vimusic.ui.views.SmallArtistItem
import it.vfsfitvnm.vimusic.ui.views.SmallPlaylistItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemShimmer
import it.vfsfitvnm.vimusic.ui.views.ArtistItem
import it.vfsfitvnm.vimusic.ui.views.ArtistItemShimmer
import it.vfsfitvnm.vimusic.ui.views.PlaylistItem
import it.vfsfitvnm.vimusic.ui.views.PlaylistItemShimmer
import it.vfsfitvnm.vimusic.ui.views.SmallSongItem
import it.vfsfitvnm.vimusic.ui.views.SmallVideoItem
import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer
import it.vfsfitvnm.vimusic.ui.views.VideoItem
import it.vfsfitvnm.vimusic.ui.views.VideoItemShimmer
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.searchResultScreenTabIndexKey
import it.vfsfitvnm.youtubemusic.YouTube
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
@ -76,125 +82,139 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
when (tabIndex) {
0 -> {
val binder = LocalPlayerServiceBinder.current
val thumbnailSizePx = Dimensions.thumbnails.song.px
ItemSearchResultTab<YouTube.Item.Song>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain
) { song ->
SmallSongItem(
song = song,
thumbnailSizePx = thumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info.endpoint)
}
)
}
}
1 -> {
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
ItemSearchResultTab<YouTube.Item.Album>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain
) { album ->
SmallAlbumItem(
album = album,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { albumRoute(album.info.endpoint?.browseId) }
)
.padding(
vertical = Dimensions.itemsVerticalPadding,
horizontal = 16.dp
)
)
}
}
2 -> {
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
ItemSearchResultTab<YouTube.Item.Artist>(
ItemSearchResult<YouTube.Item.Song>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain,
isArtists = true
) { artist ->
SmallArtistItem(
artist = artist,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { artistRoute(artist.info.endpoint?.browseId) }
)
.padding(
vertical = Dimensions.itemsVerticalPadding,
horizontal = 16.dp
)
)
}
itemContent = { song ->
SmallSongItem(
song = song,
thumbnailSizePx = thumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info.endpoint)
}
)
},
itemShimmer = {
SmallSongItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
}
)
}
1 -> {
val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px
ItemSearchResult<YouTube.Item.Album>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain,
itemContent = { album ->
AlbumItem(
album = album,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { albumRoute(album.info.endpoint?.browseId) }
)
)
},
itemShimmer = {
AlbumItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
}
)
}
2 -> {
val thumbnailSizeDp = 64.dp
val thumbnailSizePx = thumbnailSizeDp.px
ItemSearchResult<YouTube.Item.Artist>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain,
itemContent = { artist ->
ArtistItem(
artist = artist,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { artistRoute(artist.info.endpoint?.browseId) }
)
)
},
itemShimmer = {
ArtistItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
}
)
}
3 -> {
val binder = LocalPlayerServiceBinder.current
val thumbnailSizePx = Dimensions.thumbnails.song.px
val thumbnailHeightDp = 72.dp
val thumbnailWidthDp = 128.dp
ItemSearchResultTab<YouTube.Item.Video>(
ItemSearchResult<YouTube.Item.Video>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain
) { video ->
SmallVideoItem(
video = video,
thumbnailSizePx = thumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(video.asMediaItem)
binder?.setupRadio(video.info.endpoint)
}
)
}
onSearchAgain = onSearchAgain,
itemContent = { video ->
VideoItem(
video = video,
thumbnailWidthDp = thumbnailWidthDp,
thumbnailHeightDp = thumbnailHeightDp,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(video.asMediaItem)
binder?.setupRadio(video.info.endpoint)
}
)
},
itemShimmer = {
VideoItemShimmer(
thumbnailHeightDp = thumbnailHeightDp,
thumbnailWidthDp = thumbnailWidthDp
)
}
)
}
4, 5 -> {
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px
ItemSearchResultTab<YouTube.Item.Playlist>(
ItemSearchResult<YouTube.Item.Playlist>(
query = query,
filter = searchFilter,
onSearchAgain = onSearchAgain
) { playlist ->
SmallPlaylistItem(
playlist = playlist,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { playlistRoute(playlist.info.endpoint?.browseId) }
)
.padding(
vertical = Dimensions.itemsVerticalPadding,
horizontal = 16.dp
)
)
}
onSearchAgain = onSearchAgain,
itemContent = { playlist ->
PlaylistItem(
playlist = playlist,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { playlistRoute(playlist.info.endpoint?.browseId) }
)
)
},
itemShimmer = {
PlaylistItemShimmer(thumbnailSizeDp = thumbnailSizeDp)
}
)
}
}
}

View file

@ -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)
// )
// }
}
}
}

View file

@ -177,6 +177,7 @@ object YouTube {
sealed class Item {
abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
abstract val key: String?
data class Song(
val info: Info<NavigationEndpoint.Endpoint.Watch>,
@ -185,6 +186,9 @@ object YouTube {
val durationText: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() {
override val key: String?
get() = info.endpoint?.videoId
companion object : FromMusicShelfRendererContent<Song> {
val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
@ -232,6 +236,9 @@ object YouTube {
val durationText: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() {
override val key: String?
get() = info.endpoint?.videoId
val isOfficialMusicVideo: Boolean
get() = info
.endpoint
@ -278,6 +285,9 @@ object YouTube {
val year: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() {
override val key: String?
get() = info.endpoint?.browseId
companion object : FromMusicShelfRendererContent<Album> {
val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
@ -312,6 +322,9 @@ object YouTube {
val info: Info<NavigationEndpoint.Endpoint.Browse>,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() {
override val key: String?
get() = info.endpoint?.browseId
companion object : FromMusicShelfRendererContent<Artist> {
val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
@ -341,6 +354,9 @@ object YouTube {
val songCount: Int?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() {
override val key: String?
get() = info.endpoint?.browseId
companion object : FromMusicShelfRendererContent<Playlist> {
override fun from(content: MusicShelfRenderer.Content): Playlist {
val (mainRuns, otherRuns) = content.runs