Split PlaylistOrAlbumScreen
This commit is contained in:
parent
d07d3f23b2
commit
c8d5753046
18 changed files with 1054 additions and 495 deletions
|
@ -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(
|
||||||
|
|
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 ->
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Reference in a new issue