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.dismissedAnchor
import it.vfsfitvnm.vimusic.ui.components.expandedAnchor import it.vfsfitvnm.vimusic.ui.components.expandedAnchor
import it.vfsfitvnm.vimusic.ui.components.rememberBottomSheetState 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.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.Appearance
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.colorPaletteOf
import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf import it.vfsfitvnm.vimusic.ui.styling.dynamicColorPaletteOf
import it.vfsfitvnm.vimusic.ui.styling.typographyOf 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.colorPaletteModeKey
import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey import it.vfsfitvnm.vimusic.utils.colorPaletteNameKey
import it.vfsfitvnm.vimusic.utils.getEnum 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.animation.ExperimentalAnimationApi
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyItemScope import androidx.compose.foundation.lazy.LazyItemScope
@ -22,11 +23,10 @@ import it.vfsfitvnm.youtubemusic.YouTube
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
inline fun <I : YouTube.Item> ItemSearchResultTab( inline fun <I : YouTube.Item> ItemSearchResult(
query: String, query: String,
filter: String, filter: String,
crossinline onSearchAgain: () -> Unit, crossinline onSearchAgain: () -> Unit,
isArtists: Boolean = false,
viewModel: ItemSearchResultViewModel<I> = viewModel( viewModel: ItemSearchResultViewModel<I> = viewModel(
key = query + filter, key = query + filter,
factory = object : ViewModelProvider.Factory { 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( LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current, contentPadding = LocalPlayerAwarePaddingValues.current,
@ -45,7 +46,7 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
) { ) {
item( item(
key = "header", key = "header",
contentType = 0 contentType = 0,
) { ) {
Header( Header(
title = query, title = query,
@ -60,6 +61,7 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
items( items(
items = viewModel.items, items = viewModel.items,
key = { it.key!! },
itemContent = itemContent itemContent = itemContent
) )
@ -73,7 +75,8 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
item { item {
SearchResultLoadingOrError( SearchResultLoadingOrError(
errorMessage = throwable.javaClass.canonicalName, errorMessage = throwable.javaClass.canonicalName,
onRetry = viewModel::fetch onRetry = viewModel::fetch,
shimmerContent = {}
) )
} }
} ?: viewModel.continuationResult?.let { } ?: viewModel.continuationResult?.let {
@ -88,7 +91,7 @@ inline fun <I : YouTube.Item> ItemSearchResultTab(
} ?: item(key = "loading") { } ?: item(key = "loading") {
SearchResultLoadingOrError( SearchResultLoadingOrError(
itemCount = if (viewModel.items.isEmpty()) 8 else 3, 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 package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -12,8 +11,11 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
class ItemSearchResultViewModel<T : YouTube.Item>(private val query: String, private val filter: String) : ViewModel() { class ItemSearchResultViewModel<T : YouTube.Item>(
val items = mutableStateListOf<T>() private val query: String,
private val filter: String
) : ViewModel() {
var items by mutableStateOf(listOf<T>())
var continuationResult by mutableStateOf<Result<String?>?>(null) 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) YouTube.search(query, filter, token)
}?.map { searchResult -> }?.map { searchResult ->
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
items.addAll(searchResult.items as List<T>) items = items.plus(searchResult.items as List<T>).distinctBy(YouTube.Item::key)
searchResult.continuation searchResult.continuation
} }
} }

View file

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

View file

@ -1,8 +1,13 @@
package it.vfsfitvnm.vimusic.ui.views package it.vfsfitvnm.vimusic.ui.views
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background 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.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer 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.padding
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.alpha 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 androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage 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.LoadingOrError
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance 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.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.asMediaItem 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.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
@ -44,6 +56,8 @@ fun SmallSongItemShimmer(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
) { ) {
Spacer( Spacer(
modifier = Modifier 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 @ExperimentalAnimationApi
@Composable @Composable
fun SmallSongItem( fun SmallSongItem(
@ -102,74 +93,80 @@ fun SmallSongItem(
) )
} }
@ExperimentalFoundationApi
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun SmallVideoItem( fun VideoItem(
video: YouTube.Item.Video, video: YouTube.Item.Video,
thumbnailSizePx: Int, thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
onClick: () -> Unit, onClick: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
SongItem( val menuState = LocalMenuState.current
thumbnailModel = video.thumbnail?.size(thumbnailSizePx), val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
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
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier 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( Box {
model = playlist.thumbnail?.size(thumbnailSizePx), AsyncImage(
contentDescription = null, model = video.thumbnail?.url,
contentScale = ContentScale.Crop, contentDescription = null,
modifier = Modifier contentScale = ContentScale.Crop,
.clip(ThumbnailRoundness.shape) modifier = Modifier
.size(thumbnailSizeDp) .clip(thumbnailShape)
) .size(width = thumbnailWidthDp, height = thumbnailHeightDp)
)
Column( video.durationText?.let { durationText ->
modifier = Modifier BasicText(
.weight(1f) 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( BasicText(
text = playlist.info.name, text = video.info.name,
style = typography.xs.semiBold, style = typography.xs.semiBold,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
BasicText( BasicText(
text = playlist.channel?.name ?: "", text = video.authors.joinToString("") { it.name },
style = typography.xs.semiBold.secondary, style = typography.xs.semiBold.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
}
playlist.songCount?.let { songCount ->
BasicText( BasicText(
text = "$songCount songs", text = video.views.firstOrNull()?.name ?: "",
style = typography.xxs.secondary, style = typography.xxs.medium.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
@ -177,60 +174,208 @@ fun SmallPlaylistItem(
} }
} }
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable @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, album: YouTube.Item.Album,
thumbnailSizePx: Int, thumbnailSizePx: Int,
thumbnailSizeDp: Dp, thumbnailSizeDp: Dp,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val (_, typography) = LocalAppearance.current val (_, typography, thumbnailShape) = LocalAppearance.current
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier modifier = modifier
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.fillMaxWidth()
) { ) {
AsyncImage( AsyncImage(
model = album.thumbnail?.size(thumbnailSizePx), model = album.thumbnail?.size(thumbnailSizePx),
contentDescription = null, contentDescription = null,
contentScale = ContentScale.Crop, contentScale = ContentScale.Crop,
modifier = Modifier modifier = Modifier
.clip(ThumbnailRoundness.shape) .clip(thumbnailShape)
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) )
Column( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
modifier = Modifier
.weight(1f)
) {
BasicText( BasicText(
text = album.info.name, text = album.info.name,
style = typography.xs.semiBold, style = typography.xs.semiBold,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
BasicText( BasicText(
text = album.authors?.joinToString("") { it.name } ?: "", text = album.authors?.joinToString("") { it.name } ?: "",
style = typography.xs.semiBold.secondary, style = typography.xs.semiBold.secondary,
maxLines = 1, maxLines = 2,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) )
}
album.year?.let { year -> album.year?.let { year ->
BasicText( BasicText(
text = year, text = year,
style = typography.xxs.secondary, style = typography.xxs.semiBold.secondary,
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
) modifier = Modifier
.padding(top = 8.dp)
)
}
} }
} }
} }
@Composable @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, artist: YouTube.Item.Artist,
thumbnailSizePx: Int, thumbnailSizePx: Int,
thumbnailSizeDp: Dp, thumbnailSizeDp: Dp,
@ -240,8 +385,10 @@ fun SmallArtistItem(
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier modifier = modifier
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.fillMaxWidth()
) { ) {
AsyncImage( AsyncImage(
model = artist.thumbnail?.size(thumbnailSizePx), model = artist.thumbnail?.size(thumbnailSizePx),
@ -251,23 +398,49 @@ fun SmallArtistItem(
.size(thumbnailSizeDp) .size(thumbnailSizeDp)
) )
BasicText( Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
text = artist.info.name, BasicText(
style = typography.xs.semiBold, text = artist.info.name,
maxLines = 1, style = typography.xs.semiBold,
overflow = TextOverflow.Ellipsis, 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 modifier = Modifier
.weight(1f) .background(color = colorPalette.shimmer, shape = CircleShape)
.size(thumbnailSizeDp)
) )
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
TextPlaceholder()
}
} }
} }
@Composable @Composable
fun SearchResultLoadingOrError( fun SearchResultLoadingOrError(
itemCount: Int = 0, itemCount: Int = 0,
isLoadingArtists: Boolean = false,
errorMessage: String? = null, errorMessage: String? = null,
onRetry: (() -> Unit)? = null onRetry: (() -> Unit)? = null,
shimmerContent: @Composable BoxScope.() -> Unit,
) { ) {
LoadingOrError( LoadingOrError(
errorMessage = errorMessage, errorMessage = errorMessage,
@ -275,23 +448,28 @@ fun SearchResultLoadingOrError(
horizontalAlignment = Alignment.CenterHorizontally horizontalAlignment = Alignment.CenterHorizontally
) { ) {
repeat(itemCount) { index -> repeat(itemCount) { index ->
if (isLoadingArtists) { Box(
SmallArtistItemShimmer( modifier = Modifier
thumbnailSizeDp = Dimensions.thumbnails.song, .alpha(1f - index * 0.125f),
modifier = Modifier content = shimmerContent
.alpha(1f - index * 0.125f) )
.fillMaxWidth() // if (isLoadingArtists) {
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) // SmallArtistItemShimmer(
) // thumbnailSizeDp = Dimensions.thumbnails.song,
} else { // modifier = Modifier
SmallSongItemShimmer( // .alpha(1f - index * 0.125f)
thumbnailSizeDp = Dimensions.thumbnails.song, // .fillMaxWidth()
modifier = Modifier // .padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.alpha(1f - index * 0.125f) // )
.fillMaxWidth() // } else {
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp) // 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 { sealed class Item {
abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? abstract val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
abstract val key: String?
data class Song( data class Song(
val info: Info<NavigationEndpoint.Endpoint.Watch>, val info: Info<NavigationEndpoint.Endpoint.Watch>,
@ -185,6 +186,9 @@ object YouTube {
val durationText: String?, val durationText: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() { ) : Item() {
override val key: String?
get() = info.endpoint?.videoId
companion object : FromMusicShelfRendererContent<Song> { companion object : FromMusicShelfRendererContent<Song> {
val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D") val Filter = Filter("EgWKAQIIAWoKEAkQBRAKEAMQBA%3D%3D")
@ -232,6 +236,9 @@ object YouTube {
val durationText: String?, val durationText: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() { ) : Item() {
override val key: String?
get() = info.endpoint?.videoId
val isOfficialMusicVideo: Boolean val isOfficialMusicVideo: Boolean
get() = info get() = info
.endpoint .endpoint
@ -278,6 +285,9 @@ object YouTube {
val year: String?, val year: String?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() { ) : Item() {
override val key: String?
get() = info.endpoint?.browseId
companion object : FromMusicShelfRendererContent<Album> { companion object : FromMusicShelfRendererContent<Album> {
val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D") val Filter = Filter("EgWKAQIYAWoKEAkQChAFEAMQBA%3D%3D")
@ -312,6 +322,9 @@ object YouTube {
val info: Info<NavigationEndpoint.Endpoint.Browse>, val info: Info<NavigationEndpoint.Endpoint.Browse>,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() { ) : Item() {
override val key: String?
get() = info.endpoint?.browseId
companion object : FromMusicShelfRendererContent<Artist> { companion object : FromMusicShelfRendererContent<Artist> {
val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D") val Filter = Filter("EgWKAQIgAWoKEAkQChAFEAMQBA%3D%3D")
@ -341,6 +354,9 @@ object YouTube {
val songCount: Int?, val songCount: Int?,
override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail? override val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?
) : Item() { ) : Item() {
override val key: String?
get() = info.endpoint?.browseId
companion object : FromMusicShelfRendererContent<Playlist> { companion object : FromMusicShelfRendererContent<Playlist> {
override fun from(content: MusicShelfRenderer.Content): Playlist { override fun from(content: MusicShelfRenderer.Content): Playlist {
val (mainRuns, otherRuns) = content.runs val (mainRuns, otherRuns) = content.runs