Add "Other versions" tab in AlbumScreen
This commit is contained in:
@@ -183,6 +183,9 @@ interface Database {
|
|||||||
@Query("SELECT * FROM Album WHERE id = :id")
|
@Query("SELECT * FROM Album WHERE id = :id")
|
||||||
fun album(id: String): Flow<Album?>
|
fun album(id: String): Flow<Album?>
|
||||||
|
|
||||||
|
@Query("SELECT timestamp FROM Album WHERE id = :id")
|
||||||
|
fun albumTimestamp(id: String): Long?
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
@Query("SELECT * FROM Song JOIN SongAlbumMap ON Song.id = SongAlbumMap.songId WHERE SongAlbumMap.albumId = :albumId AND position IS NOT NULL ORDER BY position")
|
||||||
@RewriteQueriesToDropUnusedColumns
|
@RewriteQueriesToDropUnusedColumns
|
||||||
|
|||||||
@@ -28,6 +28,4 @@ object AlbumSaver : Saver<Album, List<Any?>> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val AlbumResultSaver = resultSaver(AlbumSaver)
|
|
||||||
|
|
||||||
val AlbumListSaver = listSaver(AlbumSaver)
|
val AlbumListSaver = listSaver(AlbumSaver)
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ object InnertubePlaylistOrAlbumPageSaver : Saver<Innertube.PlaylistOrAlbumPage,
|
|||||||
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } } ,
|
value.thumbnail?.let { with(InnertubeThumbnailSaver) { save(it) } } ,
|
||||||
value.url,
|
value.url,
|
||||||
value.songsPage?.let { with(InnertubeSongsPageSaver) { save(it) } },
|
value.songsPage?.let { with(InnertubeSongsPageSaver) { save(it) } },
|
||||||
|
value.otherVersions?.let { with(InnertubeAlbumItemListSaver) { save(it) } },
|
||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
@@ -22,5 +23,6 @@ object InnertubePlaylistOrAlbumPageSaver : Saver<Innertube.PlaylistOrAlbumPage,
|
|||||||
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
|
thumbnail = (value[3] as List<Any?>?)?.let(InnertubeThumbnailSaver::restore),
|
||||||
url = value[4] as String?,
|
url = value[4] as String?,
|
||||||
songsPage = (value[5] as List<Any?>?)?.let(InnertubeSongsPageSaver::restore),
|
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.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawWithContent
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.graphics.BlendMode
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
@@ -12,8 +13,12 @@ import androidx.compose.ui.graphics.graphicsLayer
|
|||||||
import com.valentinilk.shimmer.shimmer
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ShimmerHost(content: @Composable ColumnScope.() -> Unit) {
|
fun ShimmerHost(
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
content: @Composable ColumnScope.() -> Unit,
|
||||||
|
) {
|
||||||
Column(
|
Column(
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.shimmer()
|
.shimmer()
|
||||||
.graphicsLayer(alpha = 0.99f)
|
.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
|
package it.vfsfitvnm.vimusic.ui.screens.album
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
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.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.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.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
import it.vfsfitvnm.vimusic.R
|
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.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
|
||||||
//@Stable
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
//class AlbumScreenState(
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
// initialIsLoading: Boolean = false,
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
// initialError: Throwable? = null,
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
// initialAlbum: Album? = null,
|
import it.vfsfitvnm.vimusic.ui.views.AlbumItem
|
||||||
// initialYouTubeAlbum: YouTube.PlaylistOrAlbum? = null,
|
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
|
||||||
//) {
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
// var isLoading by mutableStateOf(initialIsLoading)
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
// var error by mutableStateOf(initialError)
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
// var album by mutableStateOf(initialAlbum)
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
// var youtubeAlbum by mutableStateOf(initialYouTubeAlbum)
|
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||||
//
|
import it.vfsfitvnm.youtubemusic.requests.albumPage
|
||||||
// suspend fun loadAlbum(browseId: String) {
|
import kotlinx.coroutines.Dispatchers
|
||||||
// println("loadAlbum $browseId")
|
import kotlinx.coroutines.flow.flowOn
|
||||||
// Database.album(browseId).flowOn(Dispatchers.IO).collect {
|
import kotlinx.coroutines.withContext
|
||||||
// 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) } },
|
|
||||||
// )
|
|
||||||
//}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@@ -99,21 +69,217 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
|||||||
fun AlbumScreen(browseId: String) {
|
fun AlbumScreen(browseId: String) {
|
||||||
val saveableStateHolder = rememberSaveableStateHolder()
|
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) {
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
globalRoutes()
|
globalRoutes()
|
||||||
|
|
||||||
host {
|
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(
|
Scaffold(
|
||||||
topIconButtonId = R.drawable.chevron_back,
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
onTopIconButtonClick = pop,
|
onTopIconButtonClick = pop,
|
||||||
tabIndex = 0,
|
tabIndex = tabIndex,
|
||||||
onTabChanged = { },
|
onTabChanged = onTabChanged,
|
||||||
tabColumnContent = { Item ->
|
tabColumnContent = { Item ->
|
||||||
Item(0, "Overview", R.drawable.sparkles)
|
Item(0, "Songs", R.drawable.musical_notes)
|
||||||
|
Item(1, "Other versions", R.drawable.disc)
|
||||||
}
|
}
|
||||||
) { currentTabIndex ->
|
) { currentTabIndex ->
|
||||||
saveableStateHolder.SaveableStateProvider(key = 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
|
@Composable
|
||||||
fun ArtistLocalSongsList(
|
fun ArtistLocalSongsList(
|
||||||
browseId: String,
|
browseId: String,
|
||||||
thumbnailContent: @Composable ColumnScope.() -> Unit,
|
|
||||||
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||||
|
thumbnailContent: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
val (colorPalette) = LocalAppearance.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.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
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.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
|
||||||
@@ -97,7 +97,7 @@ fun ArtistScreen(browseId: String) {
|
|||||||
if (value != null || (tabIndex == 4 && withContext(Dispatchers.IO) { Database.artistTimestamp(browseId) } != null)) return@produceSaveableState
|
if (value != null || (tabIndex == 4 && withContext(Dispatchers.IO) { Database.artistTimestamp(browseId) } != null)) return@produceSaveableState
|
||||||
|
|
||||||
withContext(Dispatchers.IO) {
|
withContext(Dispatchers.IO) {
|
||||||
Innertube.artistPage(browseId)
|
Innertube.artistPage(BrowseBody(browseId = browseId))
|
||||||
}?.onSuccess { artistPage ->
|
}?.onSuccess { artistPage ->
|
||||||
value = artistPage
|
value = artistPage
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ fun ArtistScreen(browseId: String) {
|
|||||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = InnertubeSongsPageSaver,
|
stateSaver = InnertubeSongsPageSaver,
|
||||||
headerContent = headerContent,
|
headerContent = headerContent,
|
||||||
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
||||||
@@ -301,7 +301,7 @@ fun ArtistScreen(browseId: String) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = InnertubeAlbumsPageSaver,
|
stateSaver = InnertubeAlbumsPageSaver,
|
||||||
headerContent = headerContent,
|
headerContent = headerContent,
|
||||||
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
||||||
@@ -352,7 +352,7 @@ fun ArtistScreen(browseId: String) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = InnertubeAlbumsPageSaver,
|
stateSaver = InnertubeAlbumsPageSaver,
|
||||||
headerContent = headerContent,
|
headerContent = headerContent,
|
||||||
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
itemsPageProvider = youtubeArtist?.let {({ continuation ->
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ import kotlinx.coroutines.withContext
|
|||||||
|
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
inline fun <T : Innertube.Item> ArtistContent(
|
inline fun <T : Innertube.Item> ItemsPage(
|
||||||
stateSaver: Saver<Innertube.ItemsPage<T>, List<Any?>>,
|
stateSaver: Saver<Innertube.ItemsPage<T>, List<Any?>>,
|
||||||
noinline itemsPageProvider: (suspend (String?) -> Result<Innertube.ItemsPage<T>?>?)? = null,
|
noinline itemsPageProvider: (suspend (String?) -> Result<Innertube.ItemsPage<T>?>?)? = null,
|
||||||
crossinline headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
crossinline headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = Dimensions.thumbnails.song
|
val thumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = InnertubeSongsPageSaver,
|
stateSaver = InnertubeSongsPageSaver,
|
||||||
itemsPageProvider = { continuation ->
|
itemsPageProvider = { continuation ->
|
||||||
if (continuation == null) {
|
if (continuation == null) {
|
||||||
@@ -130,7 +130,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = InnertubeAlbumsPageSaver,
|
stateSaver = InnertubeAlbumsPageSaver,
|
||||||
itemsPageProvider = { continuation ->
|
itemsPageProvider = { continuation ->
|
||||||
if (continuation == null) {
|
if (continuation == null) {
|
||||||
@@ -170,7 +170,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 64.dp
|
val thumbnailSizeDp = 64.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = innertubeItemsPageSaver(InnertubeArtistItemListSaver),
|
stateSaver = innertubeItemsPageSaver(InnertubeArtistItemListSaver),
|
||||||
itemsPageProvider = { continuation ->
|
itemsPageProvider = { continuation ->
|
||||||
if (continuation == null) {
|
if (continuation == null) {
|
||||||
@@ -209,7 +209,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailHeightDp = 72.dp
|
val thumbnailHeightDp = 72.dp
|
||||||
val thumbnailWidthDp = 128.dp
|
val thumbnailWidthDp = 128.dp
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = innertubeItemsPageSaver(InnertubeVideoItemListSaver),
|
stateSaver = innertubeItemsPageSaver(InnertubeVideoItemListSaver),
|
||||||
itemsPageProvider = { continuation ->
|
itemsPageProvider = { continuation ->
|
||||||
if (continuation == null) {
|
if (continuation == null) {
|
||||||
@@ -250,7 +250,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
ArtistContent(
|
ItemsPage(
|
||||||
stateSaver = innertubeItemsPageSaver(InnertubePlaylistItemListSaver),
|
stateSaver = innertubeItemsPageSaver(InnertubePlaylistItemListSaver),
|
||||||
itemsPageProvider = { continuation ->
|
itemsPageProvider = { continuation ->
|
||||||
if (continuation == null) {
|
if (continuation == null) {
|
||||||
|
|||||||
@@ -178,7 +178,8 @@ object Innertube {
|
|||||||
val year: String?,
|
val year: String?,
|
||||||
val thumbnail: Thumbnail?,
|
val thumbnail: Thumbnail?,
|
||||||
val url: String?,
|
val url: String?,
|
||||||
val songsPage: ItemsPage<SongItem>?
|
val songsPage: ItemsPage<SongItem>?,
|
||||||
|
val otherVersions: List<AlbumItem>?
|
||||||
)
|
)
|
||||||
|
|
||||||
data class NextPage(
|
data class NextPage(
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MusicCarouselShelfRenderer(
|
data class MusicCarouselShelfRenderer(
|
||||||
val header: Header?,
|
val header: Header?,
|
||||||
val contents: List<Content>,
|
val contents: List<Content>?,
|
||||||
) {
|
) {
|
||||||
@Serializable
|
@Serializable
|
||||||
data class Content(
|
data class Content(
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ import it.vfsfitvnm.youtubemusic.utils.findSectionByTitle
|
|||||||
import it.vfsfitvnm.youtubemusic.utils.from
|
import it.vfsfitvnm.youtubemusic.utils.from
|
||||||
import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable
|
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) {
|
val response = client.post(browse) {
|
||||||
setBody(BrowseBody(browseId = browseId))
|
setBody(body)
|
||||||
mask("contents,header")
|
mask("contents,header")
|
||||||
}.body<BrowseResponse>()
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import io.ktor.client.request.setBody
|
|||||||
import it.vfsfitvnm.youtubemusic.Innertube
|
import it.vfsfitvnm.youtubemusic.Innertube
|
||||||
import it.vfsfitvnm.youtubemusic.models.BrowseResponse
|
import it.vfsfitvnm.youtubemusic.models.BrowseResponse
|
||||||
import it.vfsfitvnm.youtubemusic.models.ContinuationResponse
|
import it.vfsfitvnm.youtubemusic.models.ContinuationResponse
|
||||||
|
import it.vfsfitvnm.youtubemusic.models.MusicCarouselShelfRenderer
|
||||||
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
|
import it.vfsfitvnm.youtubemusic.models.MusicShelfRenderer
|
||||||
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
|
||||||
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
|
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
|
||||||
@@ -15,14 +16,14 @@ import it.vfsfitvnm.youtubemusic.utils.runCatchingNonCancellable
|
|||||||
suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable {
|
suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable {
|
||||||
val response = client.post(browse) {
|
val response = client.post(browse) {
|
||||||
setBody(body)
|
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>()
|
}.body<BrowseResponse>()
|
||||||
|
|
||||||
val musicDetailHeaderRenderer = response
|
val musicDetailHeaderRenderer = response
|
||||||
.header
|
.header
|
||||||
?.musicDetailHeaderRenderer
|
?.musicDetailHeaderRenderer
|
||||||
|
|
||||||
val musicShelfRenderer = response
|
val sectionListRendererContents = response
|
||||||
.contents
|
.contents
|
||||||
?.singleColumnBrowseResultsRenderer
|
?.singleColumnBrowseResultsRenderer
|
||||||
?.tabs
|
?.tabs
|
||||||
@@ -31,9 +32,15 @@ suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable
|
|||||||
?.content
|
?.content
|
||||||
?.sectionListRenderer
|
?.sectionListRenderer
|
||||||
?.contents
|
?.contents
|
||||||
|
|
||||||
|
val musicShelfRenderer = sectionListRendererContents
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.musicShelfRenderer
|
?.musicShelfRenderer
|
||||||
|
|
||||||
|
val musicCarouselShelfRenderer = sectionListRendererContents
|
||||||
|
?.getOrNull(1)
|
||||||
|
?.musicCarouselShelfRenderer
|
||||||
|
|
||||||
Innertube.PlaylistOrAlbumPage(
|
Innertube.PlaylistOrAlbumPage(
|
||||||
title = musicDetailHeaderRenderer
|
title = musicDetailHeaderRenderer
|
||||||
?.title
|
?.title
|
||||||
@@ -60,7 +67,11 @@ suspend fun Innertube.playlistPage(body: BrowseBody) = runCatchingNonCancellable
|
|||||||
?.microformatDataRenderer
|
?.microformatDataRenderer
|
||||||
?.urlCanonical,
|
?.urlCanonical,
|
||||||
songsPage = musicShelfRenderer
|
songsPage = musicShelfRenderer
|
||||||
?.toSongsPage()
|
?.toSongsPage(),
|
||||||
|
otherVersions = musicCarouselShelfRenderer
|
||||||
|
?.contents
|
||||||
|
?.mapNotNull(MusicCarouselShelfRenderer.Content::musicTwoRowItemRenderer)
|
||||||
|
?.mapNotNull(Innertube.AlbumItem::from)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user