Add "Other versions" tab in AlbumScreen
This commit is contained in:
parent
663ec33e3b
commit
d0e9c7e6b9
15 changed files with 440 additions and 439 deletions
|
@ -183,6 +183,9 @@ interface Database {
|
|||
@Query("SELECT * FROM Album WHERE id = :id")
|
||||
fun album(id: String): Flow<Album?>
|
||||
|
||||
@Query("SELECT timestamp FROM Album WHERE id = :id")
|
||||
fun albumTimestamp(id: String): Long?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
||||
@RewriteQueriesToDropUnusedColumns
|
||||
|
|
|
@ -28,6 +28,4 @@ object AlbumSaver : Saver<Album, List<Any?>> {
|
|||
)
|
||||
}
|
||||
|
||||
val AlbumResultSaver = resultSaver(AlbumSaver)
|
||||
|
||||
val AlbumListSaver = listSaver(AlbumSaver)
|
||||
|
|
|
@ -7,11 +7,12 @@ import it.vfsfitvnm.youtubemusic.Innertube
|
|||
object InnertubePlaylistOrAlbumPageSaver : Saver<Innertube.PlaylistOrAlbumPage, List<Any?>> {
|
||||
override fun SaverScope.save(value: Innertube.PlaylistOrAlbumPage): List<Any?> = listOf(
|
||||
value.title,
|
||||
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } } ,
|
||||
value.authors?.let { with(InnertubeBrowseInfoListSaver) { save(it) } },
|
||||
value.year,
|
||||
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } } ,
|
||||
value.url,
|
||||
value.songsPage?.let { with(InnertubeSongsPageSaver) { save(it) } },
|
||||
value.otherVersions?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
|
||||
)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
@ -22,5 +23,6 @@ object InnertubePlaylistOrAlbumPageSaver : Saver<Innertube.PlaylistOrAlbumPage,
|
|||
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
|
||||
url = value[4] as String?,
|
||||
songsPage = (value[5] as List<Any?>?)?.let(InnertubeSongsPageSaver::restore),
|
||||
otherVersions = (value[6] as List<List<Any?>>?)?.let(InnertubeAlbumItemListSaver::restore),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ package it.vfsfitvnm.vimusic.ui.components.themed
|
|||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.graphics.BlendMode
|
||||
|
@ -12,8 +13,12 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||
import com.valentinilk.shimmer.shimmer
|
||||
|
||||
@Composable
|
||||
fun ShimmerHost(content: @Composable ColumnScope.() -> Unit) {
|
||||
fun ShimmerHost(
|
||||
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = horizontalAlignment,
|
||||
modifier = Modifier
|
||||
.shimmer()
|
||||
.graphicsLayer(alpha = 0.99f)
|
||||
|
|
|
@ -1,328 +0,0 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.models.Album
|
||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.savers.AlbumResultSaver
|
||||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
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.px
|
||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||
import it.vfsfitvnm.vimusic.utils.secondary
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||
import it.vfsfitvnm.youtubemusic.Innertube
|
||||
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||
import it.vfsfitvnm.youtubemusic.requests.albumPage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun AlbumOverview(
|
||||
browseId: String,
|
||||
) {
|
||||
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
val context = LocalContext.current
|
||||
|
||||
val albumResult by produceSaveableState(
|
||||
initialValue = null,
|
||||
stateSaver = AlbumResultSaver,
|
||||
) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Database.album(browseId).collect { album ->
|
||||
if (album?.timestamp == null) {
|
||||
Innertube.albumPage(BrowseBody(browseId = browseId))?.onSuccess { albumPage ->
|
||||
Database.upsert(
|
||||
Album(
|
||||
id = browseId,
|
||||
title = albumPage.title,
|
||||
thumbnailUrl = albumPage.thumbnail?.url,
|
||||
year = albumPage.year,
|
||||
authorsText = albumPage.authors?.joinToString("") { it.name ?: "" },
|
||||
shareUrl = albumPage.url,
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
albumPage
|
||||
.songsPage
|
||||
?.items
|
||||
?.map(Innertube.SongItem::asMediaItem)
|
||||
?.onEach(Database::insert)
|
||||
?.mapIndexed { position, mediaItem ->
|
||||
SongAlbumMap(
|
||||
songId = mediaItem.mediaId,
|
||||
albumId = browseId,
|
||||
position = position
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}?.onFailure { throwable ->
|
||||
value = Result.failure(throwable)
|
||||
}
|
||||
} else {
|
||||
value = Result.success(album)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val songs by produceSaveableState(
|
||||
initialValue = emptyList(),
|
||||
stateSaver = DetailedSongListSaver
|
||||
) {
|
||||
Database
|
||||
.albumSongs(browseId)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { value = it }
|
||||
}
|
||||
|
||||
BoxWithConstraints {
|
||||
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth
|
||||
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||
|
||||
albumResult?.getOrNull()?.let { album ->
|
||||
LazyColumn(
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Column {
|
||||
Header(title = album.title ?: "Unknown") {
|
||||
SecondaryTextButton(
|
||||
text = "Enqueue",
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
||||
}
|
||||
)
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(
|
||||
if (album.bookmarkedAt == null) {
|
||||
R.drawable.bookmark_outline
|
||||
} else {
|
||||
R.drawable.bookmark
|
||||
}
|
||||
),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
query {
|
||||
Database.update(
|
||||
album.copy(
|
||||
bookmarkedAt = if (album.bookmarkedAt == null) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.share_social),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
album.shareUrl?.let { url ->
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
|
||||
context.startActivity(
|
||||
Intent.createChooser(
|
||||
sendIntent,
|
||||
null
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
}
|
||||
|
||||
AsyncImage(
|
||||
model = album.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(all = 16.dp)
|
||||
.clip(thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = songs,
|
||||
key = { _, song -> song.id }
|
||||
) { index, song ->
|
||||
SongItem(
|
||||
title = song.title,
|
||||
authors = song.artistsText ?: album.authorsText,
|
||||
durationText = song.durationText,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
songs.map(DetailedSong::asMediaItem),
|
||||
index
|
||||
)
|
||||
},
|
||||
startContent = {
|
||||
BasicText(
|
||||
text = "${index + 1}",
|
||||
style = typography.s.semiBold.center.color(colorPalette.textDisabled),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.width(Dimensions.thumbnails.song)
|
||||
)
|
||||
},
|
||||
menuContent = {
|
||||
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
)
|
||||
} ?: albumResult?.exceptionOrNull()?.let {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
BasicText(
|
||||
text = "An error has occurred.",
|
||||
style = typography.s.secondary.center,
|
||||
modifier = Modifier
|
||||
.align(Alignment.Center)
|
||||
)
|
||||
}
|
||||
} ?: Column(
|
||||
modifier = Modifier
|
||||
.padding(LocalPlayerAwarePaddingValues.current)
|
||||
.shimmer()
|
||||
.fillMaxSize()
|
||||
) {
|
||||
HeaderPlaceholder()
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(all = 16.dp)
|
||||
.clip(thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
.background(colorPalette.shimmer)
|
||||
)
|
||||
|
||||
repeat(3) { index ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
modifier = Modifier
|
||||
.alpha(1f - index * 0.25f)
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
||||
.height(Dimensions.thumbnails.song)
|
||||
) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||
.size(Dimensions.thumbnails.song)
|
||||
)
|
||||
|
||||
Column {
|
||||
TextPlaceholder()
|
||||
TextPlaceholder()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,97 +1,67 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
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.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.valentinilk.shimmer.shimmer
|
||||
import it.vfsfitvnm.route.RouteHandler
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.models.Album
|
||||
import it.vfsfitvnm.vimusic.models.SongAlbumMap
|
||||
import it.vfsfitvnm.vimusic.query
|
||||
import it.vfsfitvnm.vimusic.savers.AlbumSaver
|
||||
import it.vfsfitvnm.vimusic.savers.InnertubeAlbumsPageSaver
|
||||
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver
|
||||
import it.vfsfitvnm.vimusic.savers.nullableSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||
|
||||
//@Stable
|
||||
//class AlbumScreenState(
|
||||
// initialIsLoading: Boolean = false,
|
||||
// initialError: Throwable? = null,
|
||||
// initialAlbum: Album? = null,
|
||||
// initialYouTubeAlbum: YouTube.PlaylistOrAlbum? = null,
|
||||
//) {
|
||||
// var isLoading by mutableStateOf(initialIsLoading)
|
||||
// var error by mutableStateOf(initialError)
|
||||
// var album by mutableStateOf(initialAlbum)
|
||||
// var youtubeAlbum by mutableStateOf(initialYouTubeAlbum)
|
||||
//
|
||||
// suspend fun loadAlbum(browseId: String) {
|
||||
// println("loadAlbum $browseId")
|
||||
// Database.album(browseId).flowOn(Dispatchers.IO).collect {
|
||||
// if (it == null) {
|
||||
// loadYouTubeAlbum(browseId)
|
||||
// } else {
|
||||
// album = it
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// suspend fun loadYouTubeAlbum(browseId: String) {
|
||||
// println("loadYouTubeAlbum $browseId")
|
||||
// if (youtubeAlbum == null) {
|
||||
// isLoading = true
|
||||
// withContext(Dispatchers.IO) {
|
||||
// YouTube.album(browseId)
|
||||
// }?.onSuccess {
|
||||
// youtubeAlbum = it
|
||||
// isLoading = false
|
||||
//
|
||||
// query {
|
||||
// Database.upsert(
|
||||
// Album(
|
||||
// id = browseId,
|
||||
// title = it.title,
|
||||
// thumbnailUrl = it.thumbnail?.url,
|
||||
// year = it.year,
|
||||
// authorsText = it.authors?.joinToString(
|
||||
// "",
|
||||
// transform = YouTube.Info<NavigationEndpoint.Endpoint.Browse>::name
|
||||
// ),
|
||||
// shareUrl = it.url,
|
||||
// timestamp = System.currentTimeMillis()
|
||||
// ),
|
||||
// it.items?.mapIndexedNotNull { position, albumItem ->
|
||||
// albumItem.toMediaItem(browseId, it)?.let { mediaItem ->
|
||||
// Database.insert(mediaItem)
|
||||
// SongAlbumMap(
|
||||
// songId = mediaItem.mediaId,
|
||||
// albumId = browseId,
|
||||
// position = position
|
||||
// )
|
||||
// }
|
||||
// } ?: emptyList()
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// }?.onFailure {
|
||||
// error = it
|
||||
// isLoading = false
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//object AlbumScreenStateSaver : Saver<AlbumScreenState, List<Any?>> {
|
||||
// override fun restore(value: List<Any?>) = AlbumScreenState(
|
||||
// initialIsLoading = value[0] as Boolean,
|
||||
// initialError = value[1] as Throwable?,
|
||||
// initialAlbum = (value[1] as List<Any?>?)?.let(AlbumSaver::restore),
|
||||
// )
|
||||
//
|
||||
// override fun SaverScope.save(value: AlbumScreenState): List<Any?> =
|
||||
// listOf(
|
||||
// value.isLoading,
|
||||
// value.error,
|
||||
// value.album?.let { with(AlbumSaver) { save(it) } },
|
||||
//// value.youtubeAlbum?.let { with(YouTubeAlbumSaver) { save(it) } },
|
||||
// )
|
||||
//}
|
||||
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
|
||||
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.styling.shimmer
|
||||
import it.vfsfitvnm.vimusic.ui.views.AlbumItem
|
||||
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||
import it.vfsfitvnm.youtubemusic.Innertube
|
||||
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||
import it.vfsfitvnm.youtubemusic.requests.albumPage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@ExperimentalAnimationApi
|
||||
|
@ -99,21 +69,217 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
|||
fun AlbumScreen(browseId: String) {
|
||||
val saveableStateHolder = rememberSaveableStateHolder()
|
||||
|
||||
val (tabIndex, onTabChanged) = rememberSaveable {
|
||||
mutableStateOf(0)
|
||||
}
|
||||
|
||||
val album by produceSaveableState(
|
||||
initialValue = null,
|
||||
stateSaver = nullableSaver(AlbumSaver),
|
||||
) {
|
||||
Database
|
||||
.album(browseId)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { value = it }
|
||||
}
|
||||
|
||||
val innertubeAlbum by produceSaveableState(
|
||||
initialValue = null,
|
||||
stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver),
|
||||
tabIndex > 0
|
||||
) {
|
||||
if (value != null || (tabIndex == 0 && withContext(Dispatchers.IO) { Database.albumTimestamp(browseId) } != null)) return@produceSaveableState
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Innertube.albumPage(BrowseBody(browseId = browseId))
|
||||
}?.onSuccess { albumPage ->
|
||||
value = albumPage
|
||||
|
||||
query {
|
||||
Database.upsert(
|
||||
Album(
|
||||
id = browseId,
|
||||
title = albumPage.title,
|
||||
thumbnailUrl = albumPage.thumbnail?.url,
|
||||
year = albumPage.year,
|
||||
authorsText = albumPage.authors?.joinToString("") { it.name ?: "" },
|
||||
shareUrl = albumPage.url,
|
||||
timestamp = System.currentTimeMillis()
|
||||
),
|
||||
albumPage
|
||||
.songsPage
|
||||
?.items
|
||||
?.map(Innertube.SongItem::asMediaItem)
|
||||
?.onEach(Database::insert)
|
||||
?.mapIndexed { position, mediaItem ->
|
||||
SongAlbumMap(
|
||||
songId = mediaItem.mediaId,
|
||||
albumId = browseId,
|
||||
position = position
|
||||
)
|
||||
} ?: emptyList()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RouteHandler(listenToGlobalEmitter = true) {
|
||||
globalRoutes()
|
||||
|
||||
host {
|
||||
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = { textButton ->
|
||||
if (album?.timestamp == null) {
|
||||
HeaderPlaceholder(
|
||||
modifier = Modifier
|
||||
.shimmer()
|
||||
)
|
||||
} else {
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
val context = LocalContext.current
|
||||
|
||||
Header(title = album?.title ?: "Unknown") {
|
||||
textButton?.invoke()
|
||||
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(
|
||||
if (album?.bookmarkedAt == null) {
|
||||
R.drawable.bookmark_outline
|
||||
} else {
|
||||
R.drawable.bookmark
|
||||
}
|
||||
),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
val bookmarkedAt =
|
||||
if (album?.bookmarkedAt == null) System.currentTimeMillis() else null
|
||||
|
||||
query {
|
||||
album
|
||||
?.copy(bookmarkedAt = bookmarkedAt)
|
||||
?.let(Database::update)
|
||||
}
|
||||
}
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
|
||||
Image(
|
||||
painter = painterResource(R.drawable.share_social),
|
||||
contentDescription = null,
|
||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||
modifier = Modifier
|
||||
.clickable {
|
||||
album?.shareUrl?.let { url ->
|
||||
val sendIntent = Intent().apply {
|
||||
action = Intent.ACTION_SEND
|
||||
type = "text/plain"
|
||||
putExtra(Intent.EXTRA_TEXT, url)
|
||||
}
|
||||
|
||||
context.startActivity(Intent.createChooser(sendIntent, null))
|
||||
}
|
||||
}
|
||||
.padding(all = 4.dp)
|
||||
.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val thumbnailContent: @Composable ColumnScope.() -> Unit = {
|
||||
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
|
||||
|
||||
if (album?.timestamp == null) {
|
||||
Spacer(
|
||||
modifier = Modifier
|
||||
.shimmer()
|
||||
.align(Alignment.CenterHorizontally)
|
||||
.padding(all = 16.dp)
|
||||
.clip(CircleShape)
|
||||
.fillMaxWidth()
|
||||
.aspectRatio(1f)
|
||||
.background(colorPalette.shimmer)
|
||||
)
|
||||
} else {
|
||||
BoxWithConstraints(
|
||||
modifier = Modifier
|
||||
.align(Alignment.CenterHorizontally)
|
||||
) {
|
||||
val thumbnailSizeDp = maxWidth - Dimensions.navigationRailWidth
|
||||
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||
|
||||
AsyncImage(
|
||||
model = album?.thumbnailUrl?.thumbnail(thumbnailSizePx),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.padding(all = 16.dp)
|
||||
.clip(thumbnailShape)
|
||||
.size(thumbnailSizeDp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
topIconButtonId = R.drawable.chevron_back,
|
||||
onTopIconButtonClick = pop,
|
||||
tabIndex = 0,
|
||||
onTabChanged = { },
|
||||
tabIndex = tabIndex,
|
||||
onTabChanged = onTabChanged,
|
||||
tabColumnContent = { Item ->
|
||||
Item(0, "Overview", R.drawable.sparkles)
|
||||
Item(0, "Songs", R.drawable.musical_notes)
|
||||
Item(1, "Other versions", R.drawable.disc)
|
||||
}
|
||||
) { currentTabIndex ->
|
||||
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||
AlbumOverview(browseId = browseId)
|
||||
when (currentTabIndex) {
|
||||
0 -> AlbumSongs(
|
||||
browseId = browseId,
|
||||
headerContent = headerContent,
|
||||
thumbnailContent = thumbnailContent,
|
||||
)
|
||||
1 -> {
|
||||
val thumbnailSizeDp = 108.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ItemsPage(
|
||||
stateSaver = InnertubeAlbumsPageSaver,
|
||||
headerContent = headerContent,
|
||||
itemsPageProvider = innertubeAlbum?.let {
|
||||
({
|
||||
Result.success(
|
||||
Innertube.ItemsPage(
|
||||
items = innertubeAlbum?.otherVersions,
|
||||
continuation = null
|
||||
)
|
||||
)
|
||||
})
|
||||
},
|
||||
itemContent = { album ->
|
||||
AlbumItem(
|
||||
album = album,
|
||||
thumbnailSizePx = thumbnailSizePx,
|
||||
thumbnailSizeDp = thumbnailSizeDp,
|
||||
modifier = Modifier
|
||||
.clickable(
|
||||
indication = rememberRipple(bounded = true),
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
onClick = { albumRoute(album.key) }
|
||||
)
|
||||
)
|
||||
},
|
||||
itemPlaceholderContent = {
|
||||
AlbumItemPlaceholder(thumbnailSizeDp = thumbnailSizeDp)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,143 @@
|
|||
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.text.BasicText
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import it.vfsfitvnm.vimusic.Database
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||
import it.vfsfitvnm.vimusic.R
|
||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
|
||||
import it.vfsfitvnm.vimusic.ui.components.themed.ShimmerHost
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
|
||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||
import it.vfsfitvnm.vimusic.utils.center
|
||||
import it.vfsfitvnm.vimusic.utils.color
|
||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
|
||||
@ExperimentalAnimationApi
|
||||
@ExperimentalFoundationApi
|
||||
@Composable
|
||||
fun AlbumSongs(
|
||||
browseId: String,
|
||||
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||
thumbnailContent: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val (colorPalette, typography) = LocalAppearance.current
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
|
||||
val songs by produceSaveableState(
|
||||
initialValue = emptyList(),
|
||||
stateSaver = DetailedSongListSaver
|
||||
) {
|
||||
Database
|
||||
.albumSongs(browseId)
|
||||
.flowOn(Dispatchers.IO)
|
||||
.collect { value = it }
|
||||
}
|
||||
|
||||
Box {
|
||||
LazyColumn(
|
||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||
modifier = Modifier
|
||||
.background(colorPalette.background0)
|
||||
.fillMaxSize()
|
||||
) {
|
||||
item(
|
||||
key = "header",
|
||||
contentType = 0
|
||||
) {
|
||||
Column {
|
||||
headerContent {
|
||||
SecondaryTextButton(
|
||||
text = "Enqueue",
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
thumbnailContent()
|
||||
}
|
||||
}
|
||||
|
||||
itemsIndexed(
|
||||
items = songs,
|
||||
key = { _, song -> song.id }
|
||||
) { index, song ->
|
||||
SongItem(
|
||||
title = song.title,
|
||||
authors = song.artistsText,
|
||||
durationText = song.durationText,
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayAtIndex(
|
||||
songs.map(DetailedSong::asMediaItem),
|
||||
index
|
||||
)
|
||||
},
|
||||
startContent = {
|
||||
BasicText(
|
||||
text = "${index + 1}",
|
||||
style = typography.s.semiBold.center.color(colorPalette.textDisabled),
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier
|
||||
.width(Dimensions.thumbnails.song)
|
||||
)
|
||||
},
|
||||
menuContent = {
|
||||
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (songs.isEmpty()) {
|
||||
item(key = "loading") {
|
||||
ShimmerHost {
|
||||
repeat(4) {
|
||||
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PrimaryButton(
|
||||
iconId = R.drawable.shuffle,
|
||||
isEnabled = songs.isNotEmpty(),
|
||||
onClick = {
|
||||
binder?.stopRadio()
|
||||
binder?.player?.forcePlayFromBeginning(
|
||||
songs.shuffled().map(DetailedSong::asMediaItem)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -39,8 +39,8 @@ import kotlinx.coroutines.flow.flowOn
|
|||
@Composable
|
||||
fun ArtistLocalSongsList(
|
||||
browseId: String,
|
||||
thumbnailContent: @Composable ColumnScope.() -> Unit,
|
||||
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||
thumbnailContent: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val binder = LocalPlayerServiceBinder.current
|
||||
val (colorPalette) = LocalAppearance.current
|
||||
|
|
|
@ -44,7 +44,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
|||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ArtistContent
|
||||
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
|
||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||
|
@ -97,7 +97,7 @@ fun ArtistScreen(browseId: String) {
|
|||
if (value != null || (tabIndex == 4 && withContext(Dispatchers.IO) { Database.artistTimestamp(browseId) } != null)) return@produceSaveableState
|
||||
|
||||
withContext(Dispatchers.IO) {
|
||||
Innertube.artistPage(browseId)
|
||||
Innertube.artistPage(BrowseBody(browseId = browseId))
|
||||
}?.onSuccess { artistPage ->
|
||||
value = artistPage
|
||||
|
||||
|
@ -252,7 +252,7 @@ fun ArtistScreen(browseId: String) {
|
|||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = InnertubeSongsPageSaver,
|
||||
headerContent = headerContent,
|
||||
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
||||
|
@ -301,7 +301,7 @@ fun ArtistScreen(browseId: String) {
|
|||
val thumbnailSizeDp = 108.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = InnertubeAlbumsPageSaver,
|
||||
headerContent = headerContent,
|
||||
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
||||
|
@ -352,7 +352,7 @@ fun ArtistScreen(browseId: String) {
|
|||
val thumbnailSizeDp = 108.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = InnertubeAlbumsPageSaver,
|
||||
headerContent = headerContent,
|
||||
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
||||
|
|
|
@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
|
|||
|
||||
@ExperimentalAnimationApi
|
||||
@Composable
|
||||
inline fun <T : Innertube.Item> ArtistContent(
|
||||
inline fun <T : Innertube.Item> ItemsPage(
|
||||
stateSaver: Saver<Innertube.ItemsPage<T>, List<Any?>>,
|
||||
noinline itemsPageProvider: (suspend (String?) -> Result<Innertube.ItemsPage<T>?>?)? = null,
|
||||
crossinline headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||
|
|
|
@ -93,7 +93,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = InnertubeSongsPageSaver,
|
||||
itemsPageProvider = { continuation ->
|
||||
if (continuation == null) {
|
||||
|
@ -130,7 +130,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
val thumbnailSizeDp = 108.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = InnertubeAlbumsPageSaver,
|
||||
itemsPageProvider = { continuation ->
|
||||
if (continuation == null) {
|
||||
|
@ -170,7 +170,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
val thumbnailSizeDp = 64.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = innertubeItemsPageSaver(InnertubeArtistItemListSaver),
|
||||
itemsPageProvider = { continuation ->
|
||||
if (continuation == null) {
|
||||
|
@ -209,7 +209,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
val thumbnailHeightDp = 72.dp
|
||||
val thumbnailWidthDp = 128.dp
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = innertubeItemsPageSaver(InnertubeVideoItemListSaver),
|
||||
itemsPageProvider = { continuation ->
|
||||
if (continuation == null) {
|
||||
|
@ -250,7 +250,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||
val thumbnailSizeDp = 108.dp
|
||||
val thumbnailSizePx = thumbnailSizeDp.px
|
||||
|
||||
ArtistContent(
|
||||
ItemsPage(
|
||||
stateSaver = innertubeItemsPageSaver(InnertubePlaylistItemListSaver),
|
||||
itemsPageProvider = { continuation ->
|
||||
if (continuation == null) {
|
||||
|
|
|
@ -178,7 +178,8 @@ object Innertube {
|
|||
val year: String?,
|
||||
val thumbnail: Thumbnail?,
|
||||
val url: String?,
|
||||
val songsPage: ItemsPage<SongItem>?
|
||||
val songsPage: ItemsPage<SongItem>?,
|
||||
val otherVersions: List<AlbumItem>?
|
||||
)
|
||||
|
||||
data class NextPage(
|
||||
|
|
|
@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class MusicCarouselShelfRenderer(
|
||||
val header: Header?,
|
||||
val contents: List<Content>,
|
||||
val contents: List<Content>?,
|
||||
) {
|
||||
@Serializable
|
||||
data class Content(
|
||||
|
|
|
@ -13,9 +13,9 @@ import it.vfsfitvnm.youtubemusic.utils.findSectionByTitle
|
|||
import it.vfsfitvnm.youtubemusic.utils.from
|
||||
import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable
|
||||
|
||||
suspend fun Innertube.artistPage(browseId: String): Result<Innertube.ArtistPage>? = runCatchingNonCancellable {
|
||||
suspend fun Innertube.artistPage(body: BrowseBody): Result<Innertube.ArtistPage>? = runCatchingNonCancellable {
|
||||
val response = client.post(browse) {
|
||||
setBody(BrowseBody(browseId = browseId))
|
||||
setBody(body)
|
||||
mask("contents,header")
|
||||
}.body<BrowseResponse>()
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import io.ktor.client.request.setBody
|
|||
import it.vfsfitvnm.youtubemusic.Innertube
|
||||
import it.vfsfitvnm.youtubemusic.models.BrowseResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.ContinuationResponse
|
||||
import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
|
||||
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
|
||||
|
@ -15,14 +16,14 @@ import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable
|
|||
suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable {
|
||||
val response = client.post(browse) {
|
||||
setBody(body)
|
||||
mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents.musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),header.musicDetailHeaderRenderer(title,subtitle,thumbnail),microformat")
|
||||
mask("contents.singleColumnBrowseResultsRenderer.tabs.tabRenderer.content.sectionListRenderer.contents(musicPlaylistShelfRenderer(continuations,contents.$musicResponsiveListItemRendererMask),musicCarouselShelfRenderer.contents.$musicTwoRowItemRendererMask),header.musicDetailHeaderRenderer(title,subtitle,thumbnail),microformat")
|
||||
}.body<BrowseResponse>()
|
||||
|
||||
val musicDetailHeaderRenderer = response
|
||||
.header
|
||||
?.musicDetailHeaderRenderer
|
||||
|
||||
val musicShelfRenderer = response
|
||||
val sectionListRendererContents = response
|
||||
.contents
|
||||
?.singleColumnBrowseResultsRenderer
|
||||
?.tabs
|
||||
|
@ -31,9 +32,15 @@ suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable
|
|||
?.content
|
||||
?.sectionListRenderer
|
||||
?.contents
|
||||
|
||||
val musicShelfRenderer = sectionListRendererContents
|
||||
?.firstOrNull()
|
||||
?.musicShelfRenderer
|
||||
|
||||
val musicCarouselShelfRenderer = sectionListRendererContents
|
||||
?.getOrNull(1)
|
||||
?.musicCarouselShelfRenderer
|
||||
|
||||
Innertube.PlaylistOrAlbumPage(
|
||||
title = musicDetailHeaderRenderer
|
||||
?.title
|
||||
|
@ -60,7 +67,11 @@ suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable
|
|||
?.microformatDataRenderer
|
||||
?.urlCanonical,
|
||||
songsPage = musicShelfRenderer
|
||||
?.toSongsPage()
|
||||
?.toSongsPage(),
|
||||
otherVersions = musicCarouselShelfRenderer
|
||||
?.contents
|
||||
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||
?.mapNotNull(Innertube.AlbumItem::from)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue