Split PlaylistOrAlbumScreen

This commit is contained in:
vfsfitvnm 2022-06-30 13:49:30 +02:00
parent d07d3f23b2
commit c8d5753046
18 changed files with 1054 additions and 495 deletions

View file

@ -24,7 +24,7 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute import it.vfsfitvnm.vimusic.ui.screens.rememberCreatePlaylistRoute
import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute import it.vfsfitvnm.vimusic.ui.screens.rememberAlbumRoute
import it.vfsfitvnm.vimusic.utils.* import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@ -202,7 +202,7 @@ fun BaseMediaItemMenu(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
MediaItemMenu( MediaItemMenu(

View file

@ -0,0 +1,884 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.*
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.draw.shadow
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
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.route.RouteHandler
import it.vfsfitvnm.vimusic.*
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongInPlaylist
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Composable
fun AlbumScreen(
browseId: String
) {
val lazyListState = rememberLazyListState()
val albumResult by remember(browseId) {
Database.album(browseId).map { album ->
album?.takeIf {
album.thumbnailUrl != null
}?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId)
.map { youtubeAlbum ->
Album(
id = browseId,
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
shareUrl = youtubeAlbum.url
).also(Database::update)
}
}.distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO)
val songs by remember(browseId) {
Database.artistSongs(browseId)
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute()
RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId ->
AlbumScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
artistRoute { browseId ->
ArtistScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
host {
val context = LocalContext.current
val density = LocalDensity.current
val binder = LocalPlayerServiceBinder.current
val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current
val menuState = LocalMenuState.current
val (thumbnailSizeDp, thumbnailSizePx) = remember {
density.run {
128.dp to 128.dp.roundToPx()
}
}
val (songThumbnailSizeDp, songThumbnailSizePx) = remember {
density.run {
54.dp to 54.dp.roundToPx()
}
}
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(bottom = 72.dp),
modifier = Modifier
.background(colorPalette.background)
.fillMaxSize()
) {
item {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(vertical = 8.dp)
.padding(horizontal = 16.dp)
.size(24.dp)
)
Image(
painter = painterResource(R.drawable.ellipsis_horizontal),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
menuState.display {
Menu {
MenuCloseButton(onClick = menuState::hide)
MenuEntry(
icon = R.drawable.time,
text = "Enqueue",
onClick = {
menuState.hide()
albumResult
?.getOrNull()
?.let { album ->
// album.items
// ?.mapNotNull { song ->
// song.toMediaItem(browseId, album)
// }
// ?.let { mediaItems ->
// binder?.player?.enqueue(
// mediaItems
// )
// }
}
}
)
MenuEntry(
icon = R.drawable.share_social,
text = "Share",
onClick = {
menuState.hide()
albumResult?.getOrNull()?.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(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
}
item {
albumResult?.getOrNull()?.let { album ->
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
AsyncImage(
model = album.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxSize()
) {
Column {
BasicText(
text = album.title ?: "Unknown",
style = typography.m.semiBold
)
BasicText(
text = buildString {
append(album.authorsText)
if (album.authorsText?.isNotEmpty() == true && album.year != null) {
append("")
}
append(album.year)
},
style = typography.xs.secondary.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 16.dp)
) {
Image(
painter = painterResource(R.drawable.shuffle),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.stopRadio()
// playlistOrAlbum.items
// ?.shuffled()
// ?.mapNotNull { song ->
// song.toMediaItem(browseId, playlistOrAlbum)
// }
// ?.let { mediaItems ->
// binder?.player?.forcePlayFromBeginning(
// mediaItems
// )
// }
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
color = colorPalette.elevatedBackground,
shape = CircleShape
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.size(20.dp)
)
Image(
painter = painterResource(R.drawable.play),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.stopRadio()
// playlistOrAlbum.items
// ?.mapNotNull { song ->
// song.toMediaItem(browseId, playlistOrAlbum)
// }
// ?.let { mediaItems ->
// binder?.player?.forcePlayFromBeginning(
// mediaItems
// )
// }
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
color = colorPalette.elevatedBackground,
shape = CircleShape
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.size(20.dp)
)
}
}
}
} ?: albumResult?.exceptionOrNull()?.let { throwable ->
LoadingOrError(
errorMessage = throwable.javaClass.canonicalName,
onRetry = {
query {
runBlocking {
Database.album(browseId).first()?.let(Database::update)
}
}
}
)
} ?: Loading()
}
// itemsIndexed(
// items = playlistOrAlbum.valueOrNull?.items ?: emptyList(),
// contentType = { _, song -> song }
// ) { index, song ->
// SongItem(
// title = song.info.name,
// authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name },
// durationText = song.durationText,
// onClick = {
// binder?.stopRadio()
// playlistOrAlbum.valueOrNull?.items?.mapNotNull { song ->
// song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
// }?.let { mediaItems ->
// binder?.player?.forcePlayAtIndex(mediaItems, index)
// }
// },
// startContent = {
// if (song.thumbnail == null) {
// BasicText(
// text = "${index + 1}",
// style = typography.xs.secondary.bold.center,
// maxLines = 1,
// overflow = TextOverflow.Ellipsis,
// modifier = Modifier
// .width(36.dp)
// )
// } else {
// AsyncImage(
// model = song.thumbnail!!.size(songThumbnailSizePx),
// contentDescription = null,
// contentScale = ContentScale.Crop,
// modifier = Modifier
// .clip(ThumbnailRoundness.shape)
// .size(songThumbnailSizeDp)
// )
// }
// },
// menuContent = {
// NonQueuedMediaItemMenu(
// mediaItem = song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
// ?: return@SongItem,
// onDismiss = menuState::hide,
// )
// }
// )
// }
}
}
}
}
@ExperimentalAnimationApi
@Composable
fun PlaylistScreen(
browseId: String,
) {
val lazyListState = rememberLazyListState()
val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute()
RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId ->
AlbumScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
artistRoute { browseId ->
ArtistScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
host {
val context = LocalContext.current
val density = LocalDensity.current
val binder = LocalPlayerServiceBinder.current
val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current
val menuState = LocalMenuState.current
val (thumbnailSizeDp, thumbnailSizePx) = remember {
density.run {
128.dp to 128.dp.roundToPx()
}
}
val (songThumbnailSizeDp, songThumbnailSizePx) = remember {
density.run {
54.dp to 54.dp.roundToPx()
}
}
var playlistOrAlbum by remember {
mutableStateOf<Outcome<YouTube.PlaylistOrAlbum>>(Outcome.Loading)
}
val onLoad = relaunchableEffect(Unit) {
playlistOrAlbum = withContext(Dispatchers.IO) {
YouTube.playlistOrAlbum2(browseId)
}
}
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(bottom = 72.dp),
modifier = Modifier
.background(colorPalette.background)
.fillMaxSize()
) {
item {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(vertical = 8.dp)
.padding(horizontal = 16.dp)
.size(24.dp)
)
Image(
painter = painterResource(R.drawable.ellipsis_horizontal),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
menuState.display {
Menu {
MenuCloseButton(onClick = menuState::hide)
MenuEntry(
icon = R.drawable.time,
text = "Enqueue",
onClick = {
menuState.hide()
playlistOrAlbum.valueOrNull?.let { album ->
album.items
?.mapNotNull { song ->
song.toMediaItem(browseId, album)
}
?.let { mediaItems ->
binder?.player?.enqueue(
mediaItems
)
}
}
}
)
MenuEntry(
icon = R.drawable.list,
text = "Import as playlist",
onClick = {
menuState.hide()
playlistOrAlbum.valueOrNull?.let { album ->
transaction {
val playlistId =
Database.insert(
Playlist(
name = album.title
?: "Unknown"
)
)
album.items?.forEachIndexed { index, song ->
song
.toMediaItem(browseId, album)
?.let { mediaItem ->
Database.insert(mediaItem)
Database.insert(
SongInPlaylist(
songId = mediaItem.mediaId,
playlistId = playlistId,
position = index
)
)
}
}
}
}
}
)
MenuEntry(
icon = R.drawable.share_social,
text = "Share",
onClick = {
menuState.hide()
(playlistOrAlbum.valueOrNull?.url
?: "https://music.youtube.com/playlist?list=${
browseId.removePrefix(
"VL"
)
}").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(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
}
item {
OutcomeItem(
outcome = playlistOrAlbum,
onRetry = onLoad,
onLoading = {
Loading()
}
) { playlistOrAlbum ->
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
AsyncImage(
model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxSize()
) {
Column {
BasicText(
text = playlistOrAlbum.title ?: "Unknown",
style = typography.m.semiBold
)
BasicText(
text = buildString {
val authors = playlistOrAlbum.authors?.joinToString("") { it.name }
append(authors)
if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) {
append("")
}
append(playlistOrAlbum.year)
},
style = typography.xs.secondary.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 16.dp)
) {
Image(
painter = painterResource(R.drawable.shuffle),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.stopRadio()
playlistOrAlbum.items
?.shuffled()
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}
?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning(
mediaItems
)
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
color = colorPalette.elevatedBackground,
shape = CircleShape
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.size(20.dp)
)
Image(
painter = painterResource(R.drawable.play),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.stopRadio()
playlistOrAlbum.items
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}
?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning(
mediaItems
)
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
color = colorPalette.elevatedBackground,
shape = CircleShape
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.size(20.dp)
)
}
}
}
}
}
itemsIndexed(
items = playlistOrAlbum.valueOrNull?.items ?: emptyList(),
contentType = { _, song -> song }
) { index, song ->
SongItem(
title = song.info.name,
authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name },
durationText = song.durationText,
onClick = {
binder?.stopRadio()
playlistOrAlbum.valueOrNull?.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
}?.let { mediaItems ->
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
},
startContent = {
if (song.thumbnail == null) {
BasicText(
text = "${index + 1}",
style = typography.xs.secondary.bold.center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.width(36.dp)
)
} else {
AsyncImage(
model = song.thumbnail!!.size(songThumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.size(songThumbnailSizeDp)
)
}
},
menuContent = {
NonQueuedMediaItemMenu(
mediaItem = song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
?: return@SongItem,
onDismiss = menuState::hide,
)
}
)
}
}
}
}
}
@Composable
private fun Loading() {
val colorPalette = LocalColorPalette.current
Column(
modifier = Modifier
.shimmer()
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape)
.size(128.dp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
) {
Column {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
repeat(3) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.alpha(0.6f - it * 0.1f)
.height(54.dp)
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(36.dp)
) {
Spacer(
modifier = Modifier
.size(8.dp)
.background(color = colorPalette.darkGray, shape = CircleShape)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
}
}
@Composable
private fun LoadingOrError(
errorMessage: String? = null,
onRetry: (() -> Unit)? = null
) {
val colorPalette = LocalColorPalette.current
Box {
Column(
modifier = Modifier
.alpha(if (errorMessage == null) 1f else 0f)
.shimmer()
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape)
.size(128.dp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
) {
Column {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
repeat(3) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.alpha(0.6f - it * 0.1f)
.height(54.dp)
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(36.dp)
) {
Spacer(
modifier = Modifier
.size(8.dp)
.background(color = colorPalette.darkGray, shape = CircleShape)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
}
errorMessage?.let {
TextCard(
icon = R.drawable.alert_circle,
onClick = onRetry,
modifier = Modifier
.align(Alignment.Center)
) {
Title(text = onRetry?.let { "Tap to retry" } ?: "Error")
Text(text = "An error has occurred:\n$errorMessage")
}
}
}
}

View file

@ -57,12 +57,12 @@ fun ArtistScreen(
) { ) {
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -66,7 +66,7 @@ fun HomeScreen() {
val playlistRoute = rememberLocalPlaylistRoute() val playlistRoute = rememberLocalPlaylistRoute()
val searchRoute = rememberSearchRoute() val searchRoute = rememberSearchRoute()
val searchResultRoute = rememberSearchResultRoute() val searchResultRoute = rememberSearchResultRoute()
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val playlistPreviews by remember { val playlistPreviews by remember {
@ -128,7 +128,7 @@ fun HomeScreen() {
} }
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen(browseId = browseId ?: error("browseId cannot be null")) AlbumScreen(browseId = browseId ?: error("browseId cannot be null"))
} }
artistRoute { browseId -> artistRoute { browseId ->

View file

@ -45,14 +45,14 @@ import kotlinx.coroutines.withContext
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun IntentUriScreen(uri: Uri) { fun IntentUriScreen(uri: Uri) {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -51,12 +51,12 @@ fun LocalPlaylistScreen(
val lazyListState = rememberLazyListState() val lazyListState = rememberLazyListState()
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -1,448 +0,0 @@
package it.vfsfitvnm.vimusic.ui.screens
import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.*
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.draw.shadow
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
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.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongInPlaylist
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.OutcomeItem
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.*
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.*
import it.vfsfitvnm.youtubemusic.Outcome
import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ExperimentalAnimationApi
@Composable
fun PlaylistOrAlbumScreen(
browseId: String,
) {
val lazyListState = rememberLazyListState()
var playlistOrAlbum by remember {
mutableStateOf<Outcome<YouTube.PlaylistOrAlbum>>(Outcome.Loading)
}
val onLoad = relaunchableEffect(Unit) {
playlistOrAlbum = withContext(Dispatchers.IO) {
YouTube.playlistOrAlbum(browseId)
}
}
val albumRoute = rememberPlaylistOrAlbumRoute()
val artistRoute = rememberArtistRoute()
RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId ->
PlaylistOrAlbumScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
artistRoute { browseId ->
ArtistScreen(
browseId = browseId ?: error("browseId cannot be null")
)
}
host {
val context = LocalContext.current
val density = LocalDensity.current
val binder = LocalPlayerServiceBinder.current
val colorPalette = LocalColorPalette.current
val typography = LocalTypography.current
val menuState = LocalMenuState.current
val (thumbnailSizeDp, thumbnailSizePx) = remember {
density.run {
128.dp to 128.dp.roundToPx()
}
}
val (songThumbnailSizeDp, songThumbnailSizePx) = remember {
density.run {
54.dp to 54.dp.roundToPx()
}
}
val coroutineScope = rememberCoroutineScope()
LazyColumn(
state = lazyListState,
contentPadding = PaddingValues(bottom = 72.dp),
modifier = Modifier
.background(colorPalette.background)
.fillMaxSize()
) {
item {
TopAppBar(
modifier = Modifier
.height(52.dp)
) {
Image(
painter = painterResource(R.drawable.chevron_back),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable(onClick = pop)
.padding(vertical = 8.dp)
.padding(horizontal = 16.dp)
.size(24.dp)
)
Image(
painter = painterResource(R.drawable.ellipsis_horizontal),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
menuState.display {
Menu {
MenuCloseButton(onClick = menuState::hide)
MenuEntry(
icon = R.drawable.time,
text = "Enqueue",
onClick = {
menuState.hide()
playlistOrAlbum.valueOrNull?.let { album ->
album.items
?.mapNotNull { song ->
song.toMediaItem(browseId, album)
}
?.let { mediaItems ->
binder?.player?.enqueue(
mediaItems
)
}
}
}
)
MenuEntry(
icon = R.drawable.list,
text = "Import as playlist",
onClick = {
menuState.hide()
playlistOrAlbum.valueOrNull?.let { album ->
transaction {
val playlistId =
Database.insert(Playlist(name = album.title ?: "Unknown"))
album.items?.forEachIndexed { index, song ->
song
.toMediaItem(browseId, album)
?.let { mediaItem ->
Database.insert(mediaItem)
Database.insert(
SongInPlaylist(
songId = mediaItem.mediaId,
playlistId = playlistId,
position = index
)
)
}
}
}
}
}
)
MenuEntry(
icon = R.drawable.share_social,
text = "Share",
onClick = {
menuState.hide()
(playlistOrAlbum.valueOrNull?.url
?: "https://music.youtube.com/playlist?list=${browseId.removePrefix("VL")}").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(horizontal = 16.dp, vertical = 8.dp)
.size(24.dp)
)
}
}
item {
OutcomeItem(
outcome = playlistOrAlbum,
onRetry = onLoad,
onLoading = {
Loading()
}
) { playlistOrAlbum ->
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxWidth()
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
AsyncImage(
model = playlistOrAlbum.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.size(thumbnailSizeDp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxSize()
) {
Column {
BasicText(
text = playlistOrAlbum.title ?: "Unknown",
style = typography.m.semiBold
)
BasicText(
text = buildString {
val authors = playlistOrAlbum.authors?.joinToString("") { it.name }
append(authors)
if (authors?.isNotEmpty() == true && playlistOrAlbum.year != null) {
append("")
}
append(playlistOrAlbum.year)
},
style = typography.xs.secondary.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.align(Alignment.End)
.padding(horizontal = 16.dp)
) {
Image(
painter = painterResource(R.drawable.shuffle),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.stopRadio()
playlistOrAlbum.items
?.shuffled()
?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning(mediaItems)
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
color = colorPalette.elevatedBackground,
shape = CircleShape
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.size(20.dp)
)
Image(
painter = painterResource(R.drawable.play),
contentDescription = null,
colorFilter = ColorFilter.tint(colorPalette.text),
modifier = Modifier
.clickable {
binder?.stopRadio()
playlistOrAlbum.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum)
}?.let { mediaItems ->
binder?.player?.forcePlayFromBeginning(mediaItems)
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
color = colorPalette.elevatedBackground,
shape = CircleShape
)
.padding(horizontal = 16.dp, vertical = 16.dp)
.size(20.dp)
)
}
}
}
}
}
itemsIndexed(
items = playlistOrAlbum.valueOrNull?.items ?: emptyList(),
contentType = { _, song -> song }
) { index, song ->
SongItem(
title = song.info.name,
authors = (song.authors ?: playlistOrAlbum.valueOrNull?.authors)?.joinToString("") { it.name },
durationText = song.durationText,
onClick = {
binder?.stopRadio()
playlistOrAlbum.valueOrNull?.items?.mapNotNull { song ->
song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
}?.let { mediaItems ->
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
},
startContent = {
if (song.thumbnail == null) {
BasicText(
text = "${index + 1}",
style = typography.xs.secondary.bold.center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.width(36.dp)
)
} else {
AsyncImage(
model = song.thumbnail!!.size(songThumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(ThumbnailRoundness.shape)
.size(songThumbnailSizeDp)
)
}
},
menuContent = {
NonQueuedMediaItemMenu(
mediaItem = song.toMediaItem(browseId, playlistOrAlbum.valueOrNull!!)
?: return@SongItem,
onDismiss = menuState::hide,
)
}
)
}
}
}
}
}
@Composable
private fun Loading() {
val colorPalette = LocalColorPalette.current
Column(
modifier = Modifier
.shimmer()
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.height(IntrinsicSize.Max)
.padding(vertical = 8.dp, horizontal = 16.dp)
.padding(bottom = 16.dp)
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.darkGray, shape = ThumbnailRoundness.shape)
.size(128.dp)
)
Column(
verticalArrangement = Arrangement.SpaceEvenly,
modifier = Modifier
.fillMaxHeight()
) {
Column {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
repeat(3) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.alpha(0.6f - it * 0.1f)
.height(54.dp)
.fillMaxWidth()
.padding(vertical = 4.dp, horizontal = 16.dp)
) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.size(36.dp)
) {
Spacer(
modifier = Modifier
.size(8.dp)
.background(color = colorPalette.darkGray, shape = CircleShape)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.alpha(0.7f)
)
}
}
}
}
}

View file

@ -90,14 +90,14 @@ fun SearchResultScreen(
} }
} }
val playlistOrAlbumRoute = rememberPlaylistOrAlbumRoute() val playlistOrAlbumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
RouteHandler( RouteHandler(
listenToGlobalEmitter = true listenToGlobalEmitter = true
) { ) {
playlistOrAlbumRoute { browseId -> playlistOrAlbumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: "browseId cannot be null" browseId = browseId ?: "browseId cannot be null"
) )
} }

View file

@ -87,12 +87,12 @@ fun SearchScreen(
} }
}.collectAsState(initial = null, context = Dispatchers.IO) }.collectAsState(initial = null, context = Dispatchers.IO)
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -29,7 +29,7 @@ import it.vfsfitvnm.vimusic.utils.*
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun SettingsScreen() { fun SettingsScreen() {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val appearanceSettingsRoute = rememberAppearanceSettingsRoute() val appearanceSettingsRoute = rememberAppearanceSettingsRoute()
val playerSettingsRoute = rememberPlayerSettingsRoute() val playerSettingsRoute = rememberPlayerSettingsRoute()
@ -53,7 +53,7 @@ fun SettingsScreen() {
} }
) { ) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -19,12 +19,22 @@ fun rememberIntentUriRoute(): Route1<Uri?> {
} }
@Composable @Composable
fun rememberPlaylistOrAlbumRoute(): Route1<String?> { fun rememberPlaylistRoute(): Route1<String?> {
val browseId = rememberSaveable { val browseId = rememberSaveable {
mutableStateOf<String?>(null) mutableStateOf<String?>(null)
} }
return remember { return remember {
Route1("PlaylistOrAlbumRoute", browseId) Route1("PlaylistRoute", browseId)
}
}
@Composable
fun rememberAlbumRoute(): Route1<String?> {
val browseId = rememberSaveable {
mutableStateOf<String?>(null)
}
return remember {
Route1("AlbumRoute", browseId)
} }
} }

View file

@ -17,9 +17,9 @@ import it.vfsfitvnm.vimusic.BuildConfig
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.TopAppBar import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen
import it.vfsfitvnm.vimusic.ui.screens.PlaylistOrAlbumScreen import it.vfsfitvnm.vimusic.ui.screens.AlbumScreen
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute import it.vfsfitvnm.vimusic.ui.screens.rememberAlbumRoute
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.utils.bold import it.vfsfitvnm.vimusic.utils.bold
@ -29,14 +29,14 @@ import it.vfsfitvnm.vimusic.utils.semiBold
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun AboutScreen() { fun AboutScreen() {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -21,14 +21,14 @@ import it.vfsfitvnm.vimusic.utils.semiBold
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun AppearanceSettingsScreen() { fun AppearanceSettingsScreen() {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -27,12 +27,11 @@ import it.vfsfitvnm.vimusic.ui.components.TopAppBar
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen import it.vfsfitvnm.vimusic.ui.screens.ArtistScreen
import it.vfsfitvnm.vimusic.ui.screens.PlaylistOrAlbumScreen import it.vfsfitvnm.vimusic.ui.screens.AlbumScreen
import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute import it.vfsfitvnm.vimusic.ui.screens.rememberArtistRoute
import it.vfsfitvnm.vimusic.ui.screens.rememberPlaylistOrAlbumRoute import it.vfsfitvnm.vimusic.ui.screens.rememberAlbumRoute
import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette import it.vfsfitvnm.vimusic.ui.styling.LocalColorPalette
import it.vfsfitvnm.vimusic.ui.styling.LocalTypography import it.vfsfitvnm.vimusic.ui.styling.LocalTypography
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
@ -44,14 +43,14 @@ import kotlin.system.exitProcess
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun BackupAndRestoreScreen() { fun BackupAndRestoreScreen() {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -33,14 +33,14 @@ import kotlinx.coroutines.launch
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun OtherSettingsScreen() { fun OtherSettingsScreen() {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -37,14 +37,14 @@ import kotlinx.coroutines.flow.flowOf
@ExperimentalAnimationApi @ExperimentalAnimationApi
@Composable @Composable
fun PlayerSettingsScreen() { fun PlayerSettingsScreen() {
val albumRoute = rememberPlaylistOrAlbumRoute() val albumRoute = rememberAlbumRoute()
val artistRoute = rememberArtistRoute() val artistRoute = rememberArtistRoute()
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
RouteHandler(listenToGlobalEmitter = true) { RouteHandler(listenToGlobalEmitter = true) {
albumRoute { browseId -> albumRoute { browseId ->
PlaylistOrAlbumScreen( AlbumScreen(
browseId = browseId ?: error("browseId cannot be null") browseId = browseId ?: error("browseId cannot be null")
) )
} }

View file

@ -28,27 +28,35 @@ fun Database.insert(mediaItem: MediaItem): Song {
return@runInTransaction it return@runInTransaction it
} }
val album = mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId ->
Album(
id = albumId,
title = mediaItem.mediaMetadata.albumTitle!!.toString(),
year = null,
authorsText = null,
thumbnailUrl = null
).also(::insert)
}
val song = Song( val song = Song(
id = mediaItem.mediaId, id = mediaItem.mediaId,
title = mediaItem.mediaMetadata.title!!.toString(), title = mediaItem.mediaMetadata.title!!.toString(),
artistsText = mediaItem.mediaMetadata.artist!!.toString(), artistsText = mediaItem.mediaMetadata.artist!!.toString(),
albumId = album?.id,
durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!, durationText = mediaItem.mediaMetadata.extras?.getString("durationText")!!,
thumbnailUrl = mediaItem.mediaMetadata.artworkUri!!.toString(), thumbnailUrl = mediaItem.mediaMetadata.artworkUri!!.toString(),
loudnessDb = mediaItem.mediaMetadata.extras?.getFloatOrNull("loudnessDb"), loudnessDb = mediaItem.mediaMetadata.extras?.getFloatOrNull("loudnessDb"),
contentLength = mediaItem.mediaMetadata.extras?.getLongOrNull("contentLength"), contentLength = mediaItem.mediaMetadata.extras?.getLongOrNull("contentLength"),
).also(::insert) ).also(::insert)
mediaItem.mediaMetadata.extras?.getString("albumId")?.let { albumId ->
Album(
id = albumId,
title = mediaItem.mediaMetadata.albumTitle!!.toString(),
year = null,
authorsText = null,
thumbnailUrl = null,
shareUrl = null,
).also(::insert)
insert(
SongAlbumMap(
songId = song.id,
albumId = albumId,
position = null
)
)
}
mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames -> mediaItem.mediaMetadata.extras?.getStringArrayList("artistNames")?.let { artistNames ->
mediaItem.mediaMetadata.extras!!.getStringArrayList("artistIds")?.let { artistIds -> mediaItem.mediaMetadata.extras!!.getStringArrayList("artistIds")?.let { artistIds ->
artistNames.mapIndexed { index, artistName -> artistNames.mapIndexed { index, artistName ->
@ -125,12 +133,11 @@ val DetailedSong.asMediaItem: MediaItem
MediaMetadata.Builder() MediaMetadata.Builder()
.setTitle(song.title) .setTitle(song.title)
.setArtist(song.artistsText) .setArtist(song.artistsText)
.setAlbumTitle(album?.title)
.setArtworkUri(song.thumbnailUrl?.toUri()) .setArtworkUri(song.thumbnailUrl?.toUri())
.setExtras( .setExtras(
bundleOf( bundleOf(
"videoId" to song.id, "videoId" to song.id,
"albumId" to album?.id, "albumId" to albumId,
"artistNames" to artists?.map { it.name }, "artistNames" to artists?.map { it.name },
"artistIds" to artists?.map { it.id }, "artistIds" to artists?.map { it.id },
"durationText" to song.durationText, "durationText" to song.durationText,

View file

@ -723,7 +723,114 @@ object YouTube {
) )
} }
suspend fun playlistOrAlbum(browseId: String): Outcome<PlaylistOrAlbum> { suspend fun playlistOrAlbum(browseId: String): Result<PlaylistOrAlbum> {
return browse2(browseId).map { body ->
PlaylistOrAlbum(
title = body
.header
?.musicDetailHeaderRenderer
?.title
?.text,
thumbnail = body
.header
?.musicDetailHeaderRenderer
?.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull(),
authors = body
.header
?.musicDetailHeaderRenderer
?.subtitle
?.splitBySeparator()
?.getOrNull(1)
?.map { Info.from(it) },
year = body
.header
?.musicDetailHeaderRenderer
?.subtitle
?.splitBySeparator()
?.getOrNull(2)
?.firstOrNull()
?.text,
items = body
.contents
.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.contents
?.firstOrNull()
?.musicShelfRenderer
?.contents
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
?.mapNotNull { renderer ->
PlaylistOrAlbum.Item(
info = renderer
.flexColumns
.getOrNull(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.getOrNull(0)
?.let { Info.from(it) } ?: return@mapNotNull null,
authors = renderer
.flexColumns
.getOrNull(1)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.map { Info.from<NavigationEndpoint.Endpoint.Browse>(it) }
?.takeIf { it.isNotEmpty() },
durationText = renderer
.fixedColumns
?.getOrNull(0)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.getOrNull(0)
?.text,
album = renderer
.flexColumns
.getOrNull(2)
?.musicResponsiveListItemFlexColumnRenderer
?.text
?.runs
?.firstOrNull()
?.let { Info.from(it) },
thumbnail = renderer
.thumbnail
?.musicThumbnailRenderer
?.thumbnail
?.thumbnails
?.firstOrNull()
)
}
?.filter { it.info.endpoint != null },
url = body
.microformat
?.microformatDataRenderer
?.urlCanonical,
continuation = body
.contents
.singleColumnBrowseResultsRenderer
?.tabs
?.firstOrNull()
?.tabRenderer
?.content
?.sectionListRenderer
?.continuations
?.firstOrNull()
?.nextRadioContinuationData
?.continuation
)
}
}
suspend fun playlistOrAlbum2(browseId: String): Outcome<PlaylistOrAlbum> {
return browse(browseId).map { body -> return browse(browseId).map { body ->
PlaylistOrAlbum( PlaylistOrAlbum(
title = body title = body