Start working on QuickPicks screen

This commit is contained in:
vfsfitvnm
2022-09-28 21:46:56 +02:00
parent 7a3c0ca110
commit 33778b33dd
37 changed files with 1354 additions and 272 deletions

View File

@@ -1,5 +1,6 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import android.content.Intent
import android.text.format.DateUtils
import androidx.compose.animation.AnimatedContentScope
import androidx.compose.animation.ExperimentalAnimationApi
@@ -57,7 +58,6 @@ import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.shareAsYouTubeSong
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOf
@@ -241,7 +241,13 @@ fun BaseMediaItemMenu(
onGoToAlbum = albumRoute::global,
onGoToArtist = artistRoute::global,
onShare = {
context.shareAsYouTubeSong(mediaItem)
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, "https://music.youtube.com/watch?v=${mediaItem.mediaId}")
}
context.startActivity(Intent.createChooser(sendIntent, null))
},
modifier = modifier
)

View File

@@ -95,7 +95,7 @@ fun AlbumOverview(
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
authorsText = youtubeAlbum.authors?.joinToString("") { it.name ?: "" },
shareUrl = youtubeAlbum.url,
timestamp = System.currentTimeMillis()
),

View File

@@ -271,7 +271,7 @@ fun ArtistScreen2(browseId: String) {
) { index, song ->
SongItem(
song = song,
thumbnailSize = songThumbnailSizePx,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
@@ -351,7 +351,7 @@ private suspend fun fetchArtist(browseId: String): Result<Artist>? {
?.map { youtubeArtist ->
Artist(
id = browseId,
name = youtubeArtist.name,
name = youtubeArtist.name ?: "",
thumbnailUrl = youtubeArtist.thumbnail?.url,
info = youtubeArtist.description,
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,

View File

@@ -103,7 +103,7 @@ fun BuiltInPlaylistSongList(builtInPlaylist: BuiltInPlaylist) {
) { index, song ->
SongItem(
song = song,
thumbnailSize = thumbnailSize,
thumbnailSizePx = thumbnailSize,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(

View File

@@ -58,8 +58,8 @@ import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi
@Composable
fun HomePlaylistList(
onBuiltInPlaylistClicked: (BuiltInPlaylist) -> Unit,
onPlaylistClicked: (Playlist) -> Unit,
onBuiltInPlaylist: (BuiltInPlaylist) -> Unit,
onPlaylistClick: (Playlist) -> Unit,
) {
val (colorPalette) = LocalAppearance.current
@@ -186,7 +186,7 @@ fun HomePlaylistList(
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Favorites) }
onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) }
)
)
}
@@ -200,7 +200,7 @@ fun HomePlaylistList(
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { onBuiltInPlaylistClicked(BuiltInPlaylist.Offline) }
onClick = { onBuiltInPlaylist(BuiltInPlaylist.Offline) }
)
.animateItemPlacement()
)
@@ -216,7 +216,7 @@ fun HomePlaylistList(
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { onPlaylistClicked(playlistPreview.playlist) }
onClick = { onPlaylistClick(playlistPreview.playlist) }
)
.animateItemPlacement()
)

View File

@@ -86,30 +86,27 @@ fun HomeScreen(onPlaylistUrl: (String) -> Unit) {
tabIndex = tabIndex,
onTabChanged = onTabChanged,
tabColumnContent = { Item ->
Item(0, "Songs", R.drawable.musical_notes)
Item(1, "Playlists", R.drawable.playlist)
Item(2, "Artists", R.drawable.person)
Item(3, "Albums", R.drawable.disc)
Item(0, "Quick picks", R.drawable.sparkles)
Item(1, "Songs", R.drawable.musical_notes)
Item(2, "Playlists", R.drawable.playlist)
Item(3, "Artists", R.drawable.person)
Item(4, "Albums", R.drawable.disc)
},
primaryIconButtonId = R.drawable.search,
onPrimaryIconButtonClick = { searchRoute("") }
) { currentTabIndex ->
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
when (currentTabIndex) {
1 -> HomePlaylistList(
onBuiltInPlaylistClicked = { builtInPlaylistRoute(it) },
onPlaylistClicked = { localPlaylistRoute(it.id) }
0 -> QuickPicks(
onAlbumClick = { albumRoute(it) },
)
2 -> HomeArtistList(
onArtistClick = { artistRoute(it.id) }
1 -> HomeSongList()
2 -> HomePlaylistList(
onBuiltInPlaylist = { builtInPlaylistRoute(it) },
onPlaylistClick = { localPlaylistRoute(it.id) }
)
3 -> HomeAlbumList(
onAlbumClick = { albumRoute(it.id) }
)
else -> HomeSongList()
3 -> HomeArtistList(onArtistClick = { artistRoute(it.id) })
4 -> HomeAlbumList(onAlbumClick = { albumRoute(it.id) })
}
}
}

View File

@@ -162,7 +162,7 @@ fun HomeSongList() {
) { index, song ->
SongItem(
song = song,
thumbnailSize = thumbnailSize,
thumbnailSizePx = thumbnailSize,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)

View File

@@ -0,0 +1,214 @@
package it.vfsfitvnm.vimusic.ui.screens.home
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
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.LazyHorizontalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
import it.vfsfitvnm.vimusic.savers.YouTubeRelatedSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.SmallSongItem
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.YouTube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
@ExperimentalAnimationApi
@Composable
fun QuickPicks(
onAlbumClick: (String) -> Unit
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val trending by produceSaveableState(
initialValue = null,
stateSaver = nullableSaver(DetailedSongSaver),
) {
Database.trending()
.flowOn(Dispatchers.IO)
.filterNotNull()
.distinctUntilChanged()
.collect { value = it }
}
val relatedResult by produceSaveableOneShotState(
initialValue = null,
stateSaver = resultSaver(nullableSaver(YouTubeRelatedSaver)),
trending?.id
) {
println("trendingVideoId: ${trending?.id}")
trending?.id?.let { trendingVideoId ->
value = YouTube.related(trendingVideoId)?.map { related ->
related?.copy(
albums = related.albums?.map { album ->
album.copy(
authors = trending?.artists?.map { info ->
YouTube.Info(
name = info.name,
endpoint = NavigationEndpoint.Endpoint.Browse(
browseId = info.id,
params = null,
browseEndpointContextSupportedConfigs = null
)
)
}
)
}
)
}
}
}
val songThumbnailSizePx = Dimensions.thumbnails.song.px
val albumThumbnailSizeDp = 108.dp
val albumThumbnailSizePx = albumThumbnailSizeDp.px
// val itemInHorizontalGridWidth = (LocalConfiguration.current.screenWidthDp.dp) * 0.8f
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
) {
item(
key = "header",
contentType = 0
) {
Header(title = "Quick picks")
}
trending?.let { song ->
item(key = song.id) {
SongItem(
song = song,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
menuContent = {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
)
}
}
relatedResult?.getOrNull()?.let { related ->
items(
items = related.songs?.take(6) ?: emptyList(),
key = YouTube.Item::key
) { song ->
SmallSongItem(
song = song,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
)
}
item(
key = "albums",
contentType = "LazyRow"
) {
LazyRow {
items(
items = related.albums ?: emptyList(),
key = YouTube.Item::key
) { album ->
AlbumItem(
album = album,
thumbnailSizePx = albumThumbnailSizePx,
thumbnailSizeDp = albumThumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { onAlbumClick(album.key) }
)
.fillMaxWidth()
)
}
}
}
items(
items = related.songs?.drop(6) ?: emptyList(),
key = YouTube.Item::key
) { song ->
SmallSongItem(
song = song,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
)
}
}
}
}

View File

@@ -165,11 +165,7 @@ fun LocalPlaylistSongList(
transaction {
runBlocking(Dispatchers.IO) {
withContext(Dispatchers.IO) {
YouTube.playlist(browseId)?.map {
it.next()
}?.map { playlist ->
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
}
YouTube.playlist(browseId)?.map { it.next() }
}
}?.getOrNull()?.let { remotePlaylist ->
Database.clearPlaylist(playlistId)
@@ -222,7 +218,7 @@ fun LocalPlaylistSongList(
) { index, song ->
SongItem(
song = song,
thumbnailSize = thumbnailSize,
thumbnailSizePx = thumbnailSize,
onClick = {
playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
?.let { mediaItems ->

View File

@@ -135,7 +135,7 @@ fun Lyrics(
)?.map { it?.value }
} else {
YouTube.next(mediaId, null)
?.map { nextResult -> nextResult.lyrics?.text()?.getOrNull() }
?.map { nextResult -> nextResult.lyrics()?.getOrNull() }
}?.map { newLyrics ->
onLyricsUpdate(isShowingSynchronizedLyrics, mediaId, newLyrics ?: "")
state = state.copy(isLoading = false)

View File

@@ -149,7 +149,7 @@ fun PlayerBottomSheet(
SongItem(
mediaItem = window.mediaItem,
thumbnailSize = thumbnailSize,
thumbnailSizePx = thumbnailSize,
onClick = {
if (isPlayingThisMediaItem) {
if (shouldBePlaying) {

View File

@@ -81,11 +81,7 @@ fun PlaylistSongList(
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
) {
value = withContext(Dispatchers.IO) {
YouTube.playlist(browseId)?.map {
it.next()
}?.map { playlist ->
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
}
YouTube.playlist(browseId)?.map { it.next() }
}
}
@@ -202,8 +198,8 @@ fun PlaylistSongList(
itemsIndexed(items = playlist.songs ?: emptyList()) { index, song ->
SongItem(
title = song.info.name,
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name },
title = song.info?.name,
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name ?: "" },
durationText = song.durationText,
onClick = {
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->

View File

@@ -100,7 +100,7 @@ fun LocalSongSearch(
) { song ->
SongItem(
song = song,
thumbnailSize = thumbnailSize,
thumbnailSizePx = thumbnailSize,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()

View File

@@ -92,7 +92,7 @@ inline fun <T : YouTube.Item> SearchResult(
items(
items = items,
key = { it.key!! },
key = YouTube.Item::key,
itemContent = itemContent
)

View File

@@ -102,7 +102,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info.endpoint)
binder?.setupRadio(song.info?.endpoint)
}
)
},
@@ -130,7 +130,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { albumRoute(album.info.endpoint?.browseId) }
onClick = { albumRoute(album.info?.endpoint?.browseId) }
)
)
@@ -159,7 +159,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { artistRoute(artist.info.endpoint?.browseId) }
onClick = { artistRoute(artist.info?.endpoint?.browseId) }
)
)
},
@@ -186,7 +186,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(video.asMediaItem)
binder?.setupRadio(video.info.endpoint)
binder?.setupRadio(video.info?.endpoint)
}
)
},
@@ -217,7 +217,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { playlistRoute(playlist.info.endpoint?.browseId) }
onClick = { playlistRoute(playlist.info?.endpoint?.browseId) }
)
)
},

View File

@@ -40,7 +40,7 @@ import it.vfsfitvnm.vimusic.utils.thumbnail
@NonRestartableComposable
fun SongItem(
mediaItem: MediaItem,
thumbnailSize: Int,
thumbnailSizePx: Int,
onClick: () -> Unit,
menuContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
@@ -48,7 +48,7 @@ fun SongItem(
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSize),
thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
title = mediaItem.mediaMetadata.title!!.toString(),
authors = mediaItem.mediaMetadata.artist.toString(),
durationText = mediaItem.mediaMetadata.extras?.getString("durationText") ?: "?",
@@ -65,7 +65,7 @@ fun SongItem(
@NonRestartableComposable
fun SongItem(
song: DetailedSong,
thumbnailSize: Int,
thumbnailSizePx: Int,
onClick: () -> Unit,
menuContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
@@ -73,7 +73,7 @@ fun SongItem(
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSize),
thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSizePx),
title = song.title,
authors = song.artistsText ?: "",
durationText = song.durationText,
@@ -90,8 +90,8 @@ fun SongItem(
@NonRestartableComposable
fun SongItem(
thumbnailModel: Any?,
title: String,
authors: String,
title: String?,
authors: String?,
durationText: String?,
onClick: () -> Unit,
menuContent: @Composable () -> Unit,
@@ -131,7 +131,7 @@ fun SongItem(
@ExperimentalAnimationApi
@Composable
fun SongItem(
title: String,
title: String?,
authors: String?,
durationText: String?,
onClick: () -> Unit,
@@ -167,7 +167,7 @@ fun SongItem(
.weight(1f)
) {
BasicText(
text = title,
text = title ?: "",
style = typography.xs.semiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,

View File

@@ -79,8 +79,8 @@ fun SmallSongItem(
) {
SongItem(
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
title = song.info.name,
authors = song.authors?.joinToString("") { it.name } ?: "",
title = song.info?.name,
authors = song.authors?.joinToString("") { it.name ?: "" },
durationText = song.durationText,
onClick = onClick,
menuContent = {
@@ -148,14 +148,14 @@ fun VideoItem(
Column {
BasicText(
text = video.info.name,
text = video.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = video.authors?.joinToString("") { it.name } ?: "",
text = video.authors?.joinToString("") { it.name ?: "" } ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
@@ -252,7 +252,7 @@ fun PlaylistItem(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
BasicText(
text = playlist.info.name,
text = playlist.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@@ -322,14 +322,14 @@ fun AlbumItem(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
BasicText(
text = album.info.name,
text = album.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = album.authors?.joinToString("") { it.name } ?: "",
text = album.authors?.joinToString("") { it.name ?: "" } ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
@@ -406,7 +406,7 @@ fun ArtistItem(
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
BasicText(
text = artist.info.name,
text = artist.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis