Add quick picks tab (#172)

This commit is contained in:
vfsfitvnm
2022-09-29 16:40:30 +02:00
parent d07af511eb
commit 6f222ac564
6 changed files with 212 additions and 90 deletions

View File

@@ -276,7 +276,7 @@ fun AlbumOverview(
) { ) {
BasicText( BasicText(
text = "An error has occurred.", text = "An error has occurred.",
style = typography.s.medium.secondary.center, style = typography.s.secondary.center,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
) )

View File

@@ -17,6 +17,7 @@ import it.vfsfitvnm.vimusic.ui.screens.builtinplaylist.BuiltInPlaylistScreen
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.localPlaylistRoute
import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen import it.vfsfitvnm.vimusic.ui.screens.search.SearchScreen
import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute import it.vfsfitvnm.vimusic.ui.screens.searchResultRoute
import it.vfsfitvnm.vimusic.ui.screens.searchRoute import it.vfsfitvnm.vimusic.ui.screens.searchRoute
@@ -77,7 +78,7 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
host { host {
val (tabIndex, onTabChanged) = rememberPreference( val (tabIndex, onTabChanged) = rememberPreference(
homeScreenTabIndexKey, homeScreenTabIndexKey,
defaultValue = 0 defaultValue = 1
) )
Scaffold( Scaffold(
@@ -99,6 +100,8 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
when (currentTabIndex) { when (currentTabIndex) {
0 -> QuickPicks( 0 -> QuickPicks(
onAlbumClick = { albumRoute(it) }, onAlbumClick = { albumRoute(it) },
onArtistClick = { artistRoute(it) },
onPlaylistClick = { playlistRoute(it) },
) )
1 -> HomeSongList() 1 -> HomeSongList()
2 -> HomePlaylistList( 2 -> HomePlaylistList(

View File

@@ -1,24 +1,19 @@
package it.vfsfitvnm.vimusic.ui.screens.home package it.vfsfitvnm.vimusic.ui.screens.home
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background import androidx.compose.foundation.background
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.Arrangement import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
@@ -28,12 +23,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember 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.clip import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.graphics.BlendMode
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
@@ -43,19 +39,27 @@ import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.screens.albumRoute 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.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.AlbumItem 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.SmallSongItem
import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer
import it.vfsfitvnm.vimusic.ui.views.SongItem import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
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.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.YouTube import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -66,9 +70,11 @@ import kotlinx.coroutines.flow.flowOn
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun QuickPicks( fun QuickPicks(
onAlbumClick: (String) -> Unit onAlbumClick: (String) -> Unit,
onArtistClick: (String) -> Unit,
onPlaylistClick: (String) -> Unit,
) { ) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val trending by produceSaveableState( val trending by produceSaveableState(
@@ -87,7 +93,6 @@ fun QuickPicks(
stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)), stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)),
trending?.id trending?.id
) { ) {
println("trendingVideoId: ${trending?.id}")
trending?.id?.let { trendingVideoId -> trending?.id?.let { trendingVideoId ->
value = YouTube.related(trendingVideoId)?.map { related -> value = YouTube.related(trendingVideoId)?.map { related ->
related?.copy( related?.copy(
@@ -110,29 +115,46 @@ fun QuickPicks(
} }
} }
val songThumbnailSizePx = Dimensions.thumbnails.song.px val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
val albumThumbnailSizeDp = 108.dp val albumThumbnailSizeDp = 108.dp
val albumThumbnailSizePx = albumThumbnailSizeDp.px val albumThumbnailSizePx = albumThumbnailSizeDp.px
// val itemInHorizontalGridWidth = (LocalConfiguration.current.screenWidthDp.dp) * 0.8f val artistThumbnailSizeDp = 64.dp
val artistThumbnailSizePx = artistThumbnailSizeDp.px
val playlistThumbnailSizeDp = 108.dp
val playlistThumbnailSizePx = playlistThumbnailSizeDp.px
LazyColumn( val sectionTextModifier = Modifier
contentPadding = LocalPlayerAwarePaddingValues.current, .padding(horizontal = 16.dp)
.padding(top = 24.dp, bottom = 8.dp)
BoxWithConstraints {
val itemInHorizontalGridWidth = maxWidth * 0.9f
Column(
modifier = Modifier modifier = Modifier
.background(colorPalette.background0) .background(colorPalette.background0)
.fillMaxSize() .fillMaxSize()
) { .verticalScroll(rememberScrollState())
item( .padding(LocalPlayerAwarePaddingValues.current)
key = "header",
contentType = 0
) { ) {
Header(title = "Quick picks") Header(title = "Quick picks")
}
relatedResult?.getOrNull()?.let { related ->
LazyHorizontalGrid(
rows = GridCells.Fixed(4),
modifier = Modifier
.fillMaxWidth()
.height((songThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 4)
) {
trending?.let { song -> trending?.let { song ->
item(key = song.id) { item(key = song.id) {
SongItem( SongItem(
song = song, thumbnailModel = song.thumbnailUrl?.thumbnail(songThumbnailSizePx),
thumbnailSizePx = songThumbnailSizePx, title = song.title,
authors = song.artistsText,
durationText = null,
menuContent = { NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) },
onClick = { onClick = {
val mediaItem = song.asMediaItem val mediaItem = song.asMediaItem
binder?.stopRadio() binder?.stopRadio()
@@ -141,17 +163,15 @@ fun QuickPicks(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
) )
}, },
menuContent = { modifier = Modifier
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) .width(itemInHorizontalGridWidth)
}
) )
} }
} }
relatedResult?.getOrNull()?.let { related ->
items( items(
items = related.songs?.take(6) ?: emptyList(), items = related.songs ?: emptyList(),
key = YouTube.Item::key key = YouTube.Item.Song::key
) { song -> ) { song ->
SmallSongItem( SmallSongItem(
song = song, song = song,
@@ -164,17 +184,27 @@ fun QuickPicks(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
) )
}, },
modifier = Modifier
.width(itemInHorizontalGridWidth)
) )
} }
}
item( BasicText(
key = "albums", text = "Related albums",
contentType = "LazyRow" style = typography.m.semiBold,
modifier = sectionTextModifier
)
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = Modifier
.fillMaxWidth()
.height((albumThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 2)
) { ) {
LazyRow {
items( items(
items = related.albums ?: emptyList(), items = related.albums ?: emptyList(),
key = YouTube.Item::key key = YouTube.Item.Album::key
) { album -> ) { album ->
AlbumItem( AlbumItem(
album = album, album = album,
@@ -186,29 +216,118 @@ fun QuickPicks(
interactionSource = remember { MutableInteractionSource() }, interactionSource = remember { MutableInteractionSource() },
onClick = { onAlbumClick(album.key) } onClick = { onAlbumClick(album.key) }
) )
.fillMaxWidth() .width(itemInHorizontalGridWidth)
) )
} }
} }
}
BasicText(
text = "Similar artists",
style = typography.m.semiBold,
modifier = sectionTextModifier
)
LazyHorizontalGrid(
rows = GridCells.Fixed(1),
modifier = Modifier
.fillMaxWidth()
.height((artistThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2))
) {
items( items(
items = related.songs?.drop(6) ?: emptyList(), items = related.artists ?: emptyList(),
key = YouTube.Item::key key = YouTube.Item.Artist::key,
) { song -> ) { artist ->
SmallSongItem( ArtistItem(
song = song, artist = artist,
thumbnailSizePx = songThumbnailSizePx, thumbnailSizePx = artistThumbnailSizePx,
onClick = { thumbnailSizeDp = artistThumbnailSizeDp,
val mediaItem = song.asMediaItem modifier = Modifier
binder?.stopRadio() .clickable(
binder?.player?.forcePlay(mediaItem) indication = rememberRipple(bounded = true),
binder?.setupRadio( interactionSource = remember { MutableInteractionSource() },
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) onClick = { onArtistClick(artist.key) }
) )
}, .width(itemInHorizontalGridWidth)
) )
} }
} }
BasicText(
text = "Playlists you might like",
style = typography.m.semiBold,
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 24.dp, bottom = 8.dp)
)
LazyHorizontalGrid(
rows = GridCells.Fixed(2),
modifier = Modifier
.fillMaxWidth()
.height((playlistThumbnailSizeDp + Dimensions.itemsVerticalPadding * 2) * 2)
) {
items(
items = related.playlists ?: emptyList(),
key = YouTube.Item.Playlist::key,
) { playlist ->
PlaylistItem(
playlist = playlist,
thumbnailSizePx = playlistThumbnailSizePx,
thumbnailSizeDp = playlistThumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { onPlaylistClick(playlist.key) }
)
.width(itemInHorizontalGridWidth)
)
}
}
} ?: relatedResult?.exceptionOrNull()?.let {
BasicText(
text = "An error has occurred",
style = typography.s.secondary.center,
modifier = Modifier
.align(Alignment.CenterHorizontally)
.padding(all = 16.dp)
)
} ?: Column(
modifier = Modifier
.shimmer()
.graphicsLayer(alpha = 0.99f)
.drawWithContent {
drawContent()
drawRect(
brush = Brush.verticalGradient(
listOf(Color.Black, Color.Transparent)
),
blendMode = BlendMode.DstIn
)
}
) {
repeat(4) {
SmallSongItemShimmer(
thumbnailSizeDp = songThumbnailSizeDp,
)
}
TextPlaceholder(modifier = sectionTextModifier)
repeat(2) {
AlbumItemShimmer(thumbnailSizeDp = albumThumbnailSizeDp)
}
TextPlaceholder(modifier = sectionTextModifier)
ArtistItemShimmer(thumbnailSizeDp = artistThumbnailSizeDp)
TextPlaceholder(modifier = sectionTextModifier)
repeat(2) {
PlaylistItemShimmer(thumbnailSizeDp = playlistThumbnailSizeDp)
}
}
}
} }
} }

View File

@@ -242,7 +242,7 @@ fun PlaylistSongList(
) { ) {
BasicText( BasicText(
text = "An error has occurred.\nTap to retry", text = "An error has occurred.\nTap to retry",
style = typography.s.medium.secondary.center, style = typography.s.secondary.center,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
) )

View File

@@ -287,7 +287,7 @@ fun OnlineSearch(
) { ) {
BasicText( BasicText(
text = "An error has occurred.", text = "An error has occurred.",
style = typography.s.medium.secondary.center, style = typography.s.secondary.center,
modifier = Modifier modifier = Modifier
.align(Alignment.Center) .align(Alignment.Center)
) )

View File

@@ -1,7 +1,7 @@
package it.vfsfitvnm.vimusic.utils package it.vfsfitvnm.vimusic.utils
class RingBuffer<T>(val size: Int, init: (index: Int) -> T) { class RingBuffer<T>(val size: Int, init: (index: Int) -> T) {
private val list = MutableList(2, init) private val list = MutableList(size, init)
private var index = 0 private var index = 0