Redesign PlaylistScreen (#172)
This commit is contained in:
@@ -285,6 +285,9 @@ interface Database {
|
|||||||
@Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query")
|
@Query("SELECT * FROM Song WHERE title LIKE :query OR artistsText LIKE :query")
|
||||||
fun search(query: String): Flow<List<DetailedSong>>
|
fun search(query: String): Flow<List<DetailedSong>>
|
||||||
|
|
||||||
|
@Query("SELECT EXISTS(SELECT 1 FROM Playlist WHERE browseId = :browseId)")
|
||||||
|
fun isImportedPlaylist(browseId: String): Flow<Boolean>
|
||||||
|
|
||||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
fun insert(format: Format)
|
fun insert(format: Format)
|
||||||
|
|
||||||
@@ -315,6 +318,9 @@ interface Database {
|
|||||||
@Insert(onConflict = OnConflictStrategy.ABORT)
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
fun insert(queuedMediaItems: List<QueuedMediaItem>)
|
fun insert(queuedMediaItems: List<QueuedMediaItem>)
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
fun insertSongPlaylistMaps(songPlaylistMaps: List<SongPlaylistMap>)
|
||||||
|
|
||||||
@Transaction
|
@Transaction
|
||||||
fun insert(mediaItem: MediaItem, block: (Song) -> Song = { it }) {
|
fun insert(mediaItem: MediaItem, block: (Song) -> Song = { it }) {
|
||||||
val song = Song(
|
val song = Song(
|
||||||
|
|||||||
@@ -22,11 +22,4 @@ enum class ThumbnailRoundness {
|
|||||||
Heavy -> RoundedCornerShape(8.dp)
|
Heavy -> RoundedCornerShape(8.dp)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
|
||||||
val shape: Shape
|
|
||||||
@Composable
|
|
||||||
@ReadOnlyComposable
|
|
||||||
get() = LocalAppearance.current.thumbnailShape
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
package it.vfsfitvnm.vimusic.savers
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
val AlbumResultSaver = ResultSaver.of(AlbumSaver)
|
val AlbumResultSaver = resultSaver(AlbumSaver)
|
||||||
|
|||||||
@@ -3,16 +3,14 @@ package it.vfsfitvnm.vimusic.savers
|
|||||||
import androidx.compose.runtime.saveable.Saver
|
import androidx.compose.runtime.saveable.Saver
|
||||||
import androidx.compose.runtime.saveable.SaverScope
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
|
||||||
interface ResultSaver<Original, Saveable> : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
|
interface ResultSaver<Original, Saveable> : Saver<Result<Original>?, Pair<Saveable?, Throwable?>>
|
||||||
companion object {
|
|
||||||
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>) =
|
|
||||||
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
|
|
||||||
override fun restore(value: Pair<Saveable?, Throwable?>) =
|
|
||||||
value.first?.let(saver::restore)?.let(Result.Companion::success)
|
|
||||||
?: value.second?.let(Result.Companion::failure)
|
|
||||||
|
|
||||||
override fun SaverScope.save(value: Result<Original>?) =
|
fun <Original, Saveable : Any> resultSaver(saver: Saver<Original, Saveable>) =
|
||||||
with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull()
|
object : Saver<Result<Original>?, Pair<Saveable?, Throwable?>> {
|
||||||
}
|
override fun restore(value: Pair<Saveable?, Throwable?>) =
|
||||||
|
value.first?.let(saver::restore)?.let(Result.Companion::success)
|
||||||
|
?: value.second?.let(Result.Companion::failure)
|
||||||
|
|
||||||
|
override fun SaverScope.save(value: Result<Original>?) =
|
||||||
|
with(saver) { value?.getOrNull()?.let { save(it) } } to value?.exceptionOrNull()
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ package it.vfsfitvnm.vimusic.savers
|
|||||||
|
|
||||||
import androidx.compose.runtime.saveable.autoSaver
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
|
|
||||||
val StringListResultSaver = ResultSaver.of(autoSaver<List<String>?>())
|
val StringListResultSaver = resultSaver(autoSaver<List<String>?>())
|
||||||
|
|||||||
@@ -2,4 +2,4 @@ package it.vfsfitvnm.vimusic.savers
|
|||||||
|
|
||||||
import androidx.compose.runtime.saveable.autoSaver
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
|
|
||||||
val StringResultSaver = ResultSaver.of(autoSaver<String?>())
|
val StringResultSaver = resultSaver(autoSaver<String?>())
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
|
||||||
|
object YouTubePlaylistOrAlbumSaver : Saver<YouTube.PlaylistOrAlbum, List<Any?>> {
|
||||||
|
override fun SaverScope.save(value: YouTube.PlaylistOrAlbum): List<Any?> = listOf(
|
||||||
|
value.title,
|
||||||
|
value.authors?.let { with(YouTubeBrowseInfoListSaver) { save(it) } } ,
|
||||||
|
value.year,
|
||||||
|
value.thumbnail?.let { with(YouTubeThumbnailSaver) { save(it) } } ,
|
||||||
|
value.songs?.let { with(YouTubeSongListSaver) { save(it) } },
|
||||||
|
value.url
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any?>) = YouTube.PlaylistOrAlbum(
|
||||||
|
title = value[0] as String?,
|
||||||
|
authors = (value[1] as List<List<Any?>>?)?.let(YouTubeBrowseInfoListSaver::restore),
|
||||||
|
year = value[2] as String?,
|
||||||
|
thumbnail = (value[3] as List<Any?>?)?.let(YouTubeThumbnailSaver::restore),
|
||||||
|
songs = (value[4] as List<List<Any?>>?)?.let(YouTubeSongListSaver::restore),
|
||||||
|
url = value[5] as String?,
|
||||||
|
continuation = null
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package it.vfsfitvnm.vimusic.ui.screens
|
|||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.animation.ExperimentalAnimationApi
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -40,6 +41,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextCard
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
@@ -53,6 +55,7 @@ import it.vfsfitvnm.youtubemusic.YouTube
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun IntentUriScreen(uri: Uri) {
|
fun IntentUriScreen(uri: Uri) {
|
||||||
|
|||||||
@@ -1,457 +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.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.IntrinsicSize
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.layout.size
|
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.draw.alpha
|
|
||||||
import androidx.compose.ui.draw.clip
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import coil.compose.AsyncImage
|
|
||||||
import it.vfsfitvnm.route.RouteHandler
|
|
||||||
import it.vfsfitvnm.vimusic.Database
|
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
|
||||||
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.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.transaction
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.TopAppBar
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.LoadingOrError
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.bold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.center
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
import it.vfsfitvnm.vimusic.utils.relaunchableEffect
|
|
||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun PlaylistScreen(browseId: String) {
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val context = LocalContext.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
|
|
||||||
val thumbnailSizePx = Dimensions.thumbnails.playlist.px
|
|
||||||
val songThumbnailSizePx = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
var playlist by remember {
|
|
||||||
mutableStateOf<Result<YouTube.PlaylistOrAlbum>?>(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
val onLoad = relaunchableEffect(Unit) {
|
|
||||||
playlist = withContext(Dispatchers.IO) {
|
|
||||||
YouTube.playlist(browseId)?.map {
|
|
||||||
it.next()
|
|
||||||
}?.map { playlist ->
|
|
||||||
playlist.copy(items = playlist.items?.filter { it.info.endpoint != null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LazyColumn(
|
|
||||||
state = lazyListState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.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)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
item {
|
|
||||||
playlist?.getOrNull()?.let { playlist ->
|
|
||||||
Column {
|
|
||||||
Row(
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(IntrinsicSize.Max)
|
|
||||||
.padding(vertical = 8.dp, horizontal = 16.dp)
|
|
||||||
.padding(bottom = 8.dp)
|
|
||||||
) {
|
|
||||||
AsyncImage(
|
|
||||||
model = playlist.thumbnail?.size(thumbnailSizePx),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier
|
|
||||||
.clip(ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.playlist)
|
|
||||||
)
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.SpaceEvenly,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
Column {
|
|
||||||
BasicText(
|
|
||||||
text = playlist.title ?: "Unknown",
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = playlist.authors?.joinToString("") { it.name }
|
|
||||||
?: "",
|
|
||||||
style = typography.xs.secondary.semiBold,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
playlist.year?.let { year ->
|
|
||||||
BasicText(
|
|
||||||
text = year,
|
|
||||||
style = typography.xs.secondary,
|
|
||||||
maxLines = 1,
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 8.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.End,
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.zIndex(1f)
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
) {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.shuffle),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
binder?.stopRadio()
|
|
||||||
playlist.items
|
|
||||||
?.shuffled()
|
|
||||||
?.mapNotNull { song ->
|
|
||||||
song.toMediaItem(browseId, playlist)
|
|
||||||
}
|
|
||||||
?.let { mediaItems ->
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
mediaItems
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.ellipsis_horizontal),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.text),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable {
|
|
||||||
menuState.display {
|
|
||||||
Menu {
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.enqueue,
|
|
||||||
text = "Enqueue",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
playlist.items
|
|
||||||
?.mapNotNull { song ->
|
|
||||||
song.toMediaItem(
|
|
||||||
browseId,
|
|
||||||
playlist
|
|
||||||
)
|
|
||||||
}
|
|
||||||
?.let { mediaItems ->
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
mediaItems
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.playlist,
|
|
||||||
text = "Import",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
transaction {
|
|
||||||
val playlistId =
|
|
||||||
Database.insert(
|
|
||||||
Playlist(
|
|
||||||
name = playlist.title
|
|
||||||
?: "Unknown",
|
|
||||||
browseId = browseId
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
playlist.items?.forEachIndexed { index, song ->
|
|
||||||
song
|
|
||||||
.toMediaItem(
|
|
||||||
browseId,
|
|
||||||
playlist
|
|
||||||
)
|
|
||||||
?.let { mediaItem ->
|
|
||||||
Database.insert(
|
|
||||||
mediaItem
|
|
||||||
)
|
|
||||||
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.share_social,
|
|
||||||
text = "Share",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
|
|
||||||
(playlist.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 = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} ?: playlist?.exceptionOrNull()?.let { throwable ->
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = throwable.javaClass.canonicalName,
|
|
||||||
onRetry = onLoad
|
|
||||||
)
|
|
||||||
} ?: LoadingOrError()
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = playlist?.getOrNull()?.items ?: emptyList(),
|
|
||||||
contentType = { _, song -> song }
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
title = song.info.name,
|
|
||||||
authors = (song.authors
|
|
||||||
?: playlist?.getOrNull()?.authors)?.joinToString("") { it.name },
|
|
||||||
durationText = song.durationText,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
playlist?.getOrNull()?.items?.mapNotNull { song ->
|
|
||||||
song.toMediaItem(browseId, playlist?.getOrNull()!!)
|
|
||||||
}?.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(Dimensions.thumbnails.song)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
NonQueuedMediaItemMenu(
|
|
||||||
mediaItem = song.toMediaItem(
|
|
||||||
browseId,
|
|
||||||
playlist?.getOrNull()!!
|
|
||||||
)
|
|
||||||
?: return@SongItem,
|
|
||||||
onDismiss = menuState::hide,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun LoadingOrError(
|
|
||||||
errorMessage: String? = null,
|
|
||||||
onRetry: (() -> Unit)? = null
|
|
||||||
) {
|
|
||||||
val (colorPalette) = LocalAppearance.current
|
|
||||||
|
|
||||||
LoadingOrError(
|
|
||||||
errorMessage = errorMessage,
|
|
||||||
onRetry = onRetry
|
|
||||||
) {
|
|
||||||
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.shimmer, shape = ThumbnailRoundness.shape)
|
|
||||||
.size(Dimensions.thumbnails.playlist)
|
|
||||||
)
|
|
||||||
|
|
||||||
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(Dimensions.thumbnails.song)
|
|
||||||
.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 = Color.Black, shape = CircleShape)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
|
||||||
) {
|
|
||||||
TextPlaceholder()
|
|
||||||
|
|
||||||
TextPlaceholder(
|
|
||||||
modifier = Modifier
|
|
||||||
.alpha(0.7f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -6,7 +6,6 @@ import androidx.compose.foundation.ExperimentalFoundationApi
|
|||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.gestures.detectTapGestures
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
@@ -30,7 +29,6 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
@@ -67,7 +65,6 @@ import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
|||||||
import it.vfsfitvnm.vimusic.utils.secondary
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
import it.vfsfitvnm.vimusic.utils.semiBold
|
import it.vfsfitvnm.vimusic.utils.semiBold
|
||||||
import it.vfsfitvnm.vimusic.utils.thumbnail
|
import it.vfsfitvnm.vimusic.utils.thumbnail
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
@@ -101,16 +98,16 @@ fun AlbumOverview(
|
|||||||
shareUrl = youtubeAlbum.url,
|
shareUrl = youtubeAlbum.url,
|
||||||
timestamp = System.currentTimeMillis()
|
timestamp = System.currentTimeMillis()
|
||||||
),
|
),
|
||||||
youtubeAlbum.items?.mapIndexedNotNull { position, albumItem ->
|
youtubeAlbum.songs
|
||||||
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
|
?.map(YouTube.Item.Song::asMediaItem)
|
||||||
Database.insert(mediaItem)
|
?.onEach(Database::insert)
|
||||||
|
?.mapIndexed { position, mediaItem ->
|
||||||
SongAlbumMap(
|
SongAlbumMap(
|
||||||
songId = mediaItem.mediaId,
|
songId = mediaItem.mediaId,
|
||||||
albumId = browseId,
|
albumId = browseId,
|
||||||
position = position
|
position = position
|
||||||
)
|
)
|
||||||
}
|
} ?: emptyList()
|
||||||
} ?: emptyList()
|
|
||||||
)
|
)
|
||||||
|
|
||||||
null
|
null
|
||||||
@@ -298,11 +295,6 @@ fun AlbumOverview(
|
|||||||
} ?: albumResult?.exceptionOrNull()?.let {
|
} ?: albumResult?.exceptionOrNull()?.let {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.pointerInput(Unit) {
|
|
||||||
detectTapGestures {
|
|
||||||
// viewModel.fetch(browseId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.align(Alignment.Center)
|
.align(Alignment.Center)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
|||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
import it.vfsfitvnm.vimusic.enums.SongSortBy
|
||||||
import it.vfsfitvnm.vimusic.enums.SortOrder
|
import it.vfsfitvnm.vimusic.enums.SortOrder
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
@@ -68,7 +67,7 @@ import kotlinx.coroutines.flow.flowOn
|
|||||||
@ExperimentalAnimationApi
|
@ExperimentalAnimationApi
|
||||||
@Composable
|
@Composable
|
||||||
fun HomeSongList() {
|
fun HomeSongList() {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
val binder = LocalPlayerServiceBinder.current
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
@@ -193,7 +192,7 @@ fun HomeSongList() {
|
|||||||
Color.Black.copy(alpha = 0.75f)
|
Color.Black.copy(alpha = 0.75f)
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
shape = ThumbnailRoundness.shape
|
shape = thumbnailShape
|
||||||
)
|
)
|
||||||
.padding(
|
.padding(
|
||||||
horizontal = 8.dp,
|
horizontal = 8.dp,
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
|
||||||
import it.vfsfitvnm.youtubemusic.YouTube
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
@@ -177,25 +176,22 @@ fun LocalPlaylistSongList(
|
|||||||
YouTube.playlist(browseId)?.map {
|
YouTube.playlist(browseId)?.map {
|
||||||
it.next()
|
it.next()
|
||||||
}?.map { playlist ->
|
}?.map { playlist ->
|
||||||
playlist.copy(items = playlist.items?.filter { it.info.endpoint != null })
|
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}?.getOrNull()?.let { remotePlaylist ->
|
}?.getOrNull()?.let { remotePlaylist ->
|
||||||
Database.clearPlaylist(playlistId)
|
Database.clearPlaylist(playlistId)
|
||||||
|
|
||||||
remotePlaylist.items?.forEachIndexed { index, song ->
|
remotePlaylist.songs
|
||||||
song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem ->
|
?.map(YouTube.Item.Song::asMediaItem)
|
||||||
Database.insert(mediaItem)
|
?.onEach(Database::insert)
|
||||||
|
?.mapIndexed { position, mediaItem ->
|
||||||
Database.insert(
|
SongPlaylistMap(
|
||||||
SongPlaylistMap(
|
songId = mediaItem.mediaId,
|
||||||
songId = mediaItem.mediaId,
|
playlistId = playlistId,
|
||||||
playlistId = playlistId,
|
position = position
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
}
|
}?.let(Database::insertSongPlaylistMaps)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,21 @@ import androidx.compose.foundation.Image
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -35,16 +49,15 @@ import it.vfsfitvnm.reordering.rememberReorderingState
|
|||||||
import it.vfsfitvnm.reordering.reorder
|
import it.vfsfitvnm.reordering.reorder
|
||||||
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
import it.vfsfitvnm.vimusic.R
|
import it.vfsfitvnm.vimusic.R
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
|
||||||
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
|
||||||
import it.vfsfitvnm.vimusic.ui.components.MusicBars
|
import it.vfsfitvnm.vimusic.ui.components.MusicBars
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer
|
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SmallSongItemShimmer
|
||||||
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||||
import it.vfsfitvnm.vimusic.utils.medium
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
||||||
@@ -63,7 +76,7 @@ fun PlayerBottomSheet(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
|
||||||
BottomSheet(
|
BottomSheet(
|
||||||
state = layoutState,
|
state = layoutState,
|
||||||
@@ -168,7 +181,7 @@ fun PlayerBottomSheet(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.background(
|
.background(
|
||||||
color = Color.Black.copy(alpha = 0.25f),
|
color = Color.Black.copy(alpha = 0.25f),
|
||||||
shape = ThumbnailRoundness.shape
|
shape = thumbnailShape
|
||||||
)
|
)
|
||||||
.size(Dimensions.thumbnails.song)
|
.size(Dimensions.thumbnails.song)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ import it.vfsfitvnm.vimusic.service.LoginRequiredException
|
|||||||
import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException
|
import it.vfsfitvnm.vimusic.service.PlayableFormatNotFoundException
|
||||||
import it.vfsfitvnm.vimusic.service.UnplayableException
|
import it.vfsfitvnm.vimusic.service.UnplayableException
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberError
|
import it.vfsfitvnm.vimusic.utils.rememberError
|
||||||
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
|
||||||
@@ -99,7 +100,7 @@ fun Thumbnail(
|
|||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
.clip(ThumbnailRoundness.shape)
|
.clip(LocalAppearance.current.thumbnailShape)
|
||||||
.size(thumbnailSizeDp)
|
.size(thumbnailSizeDp)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.playlist
|
||||||
|
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
|
||||||
|
import it.vfsfitvnm.route.RouteHandler
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@Composable
|
||||||
|
fun PlaylistScreen(browseId: String) {
|
||||||
|
val saveableStateHolder = rememberSaveableStateHolder()
|
||||||
|
|
||||||
|
RouteHandler(listenToGlobalEmitter = true) {
|
||||||
|
globalRoutes()
|
||||||
|
|
||||||
|
host {
|
||||||
|
Scaffold(
|
||||||
|
topIconButtonId = R.drawable.chevron_back,
|
||||||
|
onTopIconButtonClick = pop,
|
||||||
|
tabIndex = 0,
|
||||||
|
onTabChanged = { },
|
||||||
|
tabColumnContent = { Item ->
|
||||||
|
Item(0, "Songs", R.drawable.musical_notes)
|
||||||
|
}
|
||||||
|
) { currentTabIndex ->
|
||||||
|
saveableStateHolder.SaveableStateProvider(key = currentTabIndex) {
|
||||||
|
PlaylistSongList(
|
||||||
|
browseId = browseId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,317 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.playlist
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.animation.ExperimentalAnimationApi
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.saveable.autoSaver
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.Playlist
|
||||||
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
|
import it.vfsfitvnm.vimusic.savers.YouTubePlaylistOrAlbumSaver
|
||||||
|
import it.vfsfitvnm.vimusic.savers.resultSaver
|
||||||
|
import it.vfsfitvnm.vimusic.transaction
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
|
import it.vfsfitvnm.vimusic.ui.styling.shimmer
|
||||||
|
import it.vfsfitvnm.vimusic.ui.views.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.center
|
||||||
|
import it.vfsfitvnm.vimusic.utils.enqueue
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
||||||
|
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
||||||
|
import it.vfsfitvnm.vimusic.utils.medium
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.secondary
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun PlaylistSongList(
|
||||||
|
browseId: String,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val playlistResult by produceSaveableOneShotState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = resultSaver(YouTubePlaylistOrAlbumSaver),
|
||||||
|
) {
|
||||||
|
value = withContext(Dispatchers.IO) {
|
||||||
|
YouTube.playlist(browseId)?.map {
|
||||||
|
it.next()
|
||||||
|
}?.map { playlist ->
|
||||||
|
playlist.copy(songs = playlist.songs?.filter { it.info.endpoint != null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val isImported by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = autoSaver<Boolean?>(),
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.isImportedPlaylist(browseId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
BoxWithConstraints {
|
||||||
|
val thumbnailSizeDp = maxWidth - Dimensions.verticalBarWidth
|
||||||
|
val thumbnailSizePx = (thumbnailSizeDp - 32.dp).px
|
||||||
|
|
||||||
|
val songThumbnailSizeDp = Dimensions.thumbnails.song
|
||||||
|
val songThumbnailSizePx = songThumbnailSizeDp.px
|
||||||
|
|
||||||
|
playlistResult?.getOrNull()?.let { playlist ->
|
||||||
|
LazyColumn(
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Header(title = playlist.title ?: "Unknown") {
|
||||||
|
if (playlist.songs?.isNotEmpty() == true) {
|
||||||
|
BasicText(
|
||||||
|
text = "Enqueue",
|
||||||
|
style = typography.xxs.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable {
|
||||||
|
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
||||||
|
binder?.player?.enqueue(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(
|
||||||
|
if (isImported == true) R.drawable.bookmark else R.drawable.bookmark_outline
|
||||||
|
),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.accent),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable(enabled = isImported == false) {
|
||||||
|
transaction {
|
||||||
|
val playlistId =
|
||||||
|
Database.insert(
|
||||||
|
Playlist(
|
||||||
|
name = playlist.title ?: "Unknown",
|
||||||
|
browseId = browseId
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
playlist.songs
|
||||||
|
?.map(YouTube.Item.Song::asMediaItem)
|
||||||
|
?.onEach(Database::insert)
|
||||||
|
?.mapIndexed { index, mediaItem ->
|
||||||
|
SongPlaylistMap(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
playlistId = playlistId,
|
||||||
|
position = index
|
||||||
|
)
|
||||||
|
}?.let(Database::insertSongPlaylistMaps)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.share_social),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
(playlist.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(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AsyncImage(
|
||||||
|
model = playlist.thumbnail?.size(thumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(items = playlist.songs ?: emptyList()) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
title = song.info.name,
|
||||||
|
authors = (song.authors ?: playlist.authors)?.joinToString("") { it.name },
|
||||||
|
durationText = song.durationText,
|
||||||
|
onClick = {
|
||||||
|
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startContent = {
|
||||||
|
AsyncImage(
|
||||||
|
model = song.thumbnail?.size(songThumbnailSizePx),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(Dimensions.thumbnails.song)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
menuContent = {
|
||||||
|
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = playlist.songs?.isNotEmpty() == true) {
|
||||||
|
playlist.songs?.map(YouTube.Item.Song::asMediaItem)?.let { mediaItems ->
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.size(62.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.shuffle),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: playlistResult?.exceptionOrNull()?.let {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = "An error has occurred.\nTap to retry",
|
||||||
|
style = typography.s.medium.secondary.center,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} ?: Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
.shimmer()
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
HeaderPlaceholder()
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.clip(thumbnailShape)
|
||||||
|
.size(thumbnailSizeDp)
|
||||||
|
.background(colorPalette.shimmer)
|
||||||
|
)
|
||||||
|
|
||||||
|
repeat(3) { index ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
modifier = Modifier
|
||||||
|
.alpha(1f - index * 0.25f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
|
||||||
|
.height(Dimensions.thumbnails.song)
|
||||||
|
) {
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = colorPalette.shimmer, shape = thumbnailShape)
|
||||||
|
.size(Dimensions.thumbnails.song)
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
TextPlaceholder()
|
||||||
|
TextPlaceholder()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,10 +19,10 @@ import it.vfsfitvnm.vimusic.savers.YouTubePlaylistListSaver
|
|||||||
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
|
import it.vfsfitvnm.vimusic.savers.YouTubeSongListSaver
|
||||||
import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver
|
import it.vfsfitvnm.vimusic.savers.YouTubeVideoListSaver
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.PlaylistScreen
|
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
|
||||||
|
import it.vfsfitvnm.vimusic.ui.screens.playlist.PlaylistScreen
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
import it.vfsfitvnm.vimusic.ui.styling.px
|
import it.vfsfitvnm.vimusic.ui.styling.px
|
||||||
@@ -173,7 +173,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailHeightDp = 72.dp
|
val thumbnailHeightDp = 72.dp
|
||||||
val thumbnailWidthDp = 128.dp
|
val thumbnailWidthDp = 128.dp
|
||||||
|
|
||||||
SearchResult<YouTube.Item.Video>(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
stateSaver = YouTubeVideoListSaver,
|
stateSaver = YouTubeVideoListSaver,
|
||||||
@@ -203,7 +203,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
|
|||||||
val thumbnailSizeDp = 108.dp
|
val thumbnailSizeDp = 108.dp
|
||||||
val thumbnailSizePx = thumbnailSizeDp.px
|
val thumbnailSizePx = thumbnailSizeDp.px
|
||||||
|
|
||||||
SearchResult<YouTube.Item.Playlist>(
|
SearchResult(
|
||||||
query = query,
|
query = query,
|
||||||
filter = searchFilter,
|
filter = searchFilter,
|
||||||
stateSaver = YouTubePlaylistListSaver,
|
stateSaver = YouTubePlaylistListSaver,
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.media3.common.MediaItem
|
import androidx.media3.common.MediaItem
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import it.vfsfitvnm.vimusic.enums.ThumbnailRoundness
|
|
||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
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.styling.Dimensions
|
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
|
||||||
@@ -115,7 +114,7 @@ fun SongItem(
|
|||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
contentScale = ContentScale.Crop,
|
contentScale = ContentScale.Crop,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.clip(ThumbnailRoundness.shape)
|
.clip(LocalAppearance.current.thumbnailShape)
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,30 @@ fun <T> produceSaveableState(
|
|||||||
return state
|
return state
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun <T> produceSaveableOneShotState(
|
||||||
|
initialValue: T,
|
||||||
|
stateSaver: Saver<T, out Any>,
|
||||||
|
@BuilderInference producer: suspend ProduceStateScope<T>.() -> Unit
|
||||||
|
): State<T> {
|
||||||
|
val state = rememberSaveable(stateSaver = stateSaver) {
|
||||||
|
mutableStateOf(initialValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
var produced by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (!produced) {
|
||||||
|
ProduceSaveableStateScope(state, coroutineContext).producer()
|
||||||
|
produced = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun <T> produceSaveableOneShotState(
|
fun <T> produceSaveableOneShotState(
|
||||||
initialValue: T,
|
initialValue: T,
|
||||||
|
|||||||
@@ -89,37 +89,6 @@ val DetailedSong.asMediaItem: MediaItem
|
|||||||
.setCustomCacheKey(id)
|
.setCustomCacheKey(id)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
fun YouTube.PlaylistOrAlbum.Item.toMediaItem(
|
|
||||||
albumId: String,
|
|
||||||
playlistOrAlbum: YouTube.PlaylistOrAlbum
|
|
||||||
): MediaItem? {
|
|
||||||
val isFromAlbum = thumbnail == null
|
|
||||||
|
|
||||||
return MediaItem.Builder()
|
|
||||||
.setMediaMetadata(
|
|
||||||
MediaMetadata.Builder()
|
|
||||||
.setTitle(info.name)
|
|
||||||
.setArtist((authors ?: playlistOrAlbum.authors)?.joinToString("") { it.name })
|
|
||||||
.setAlbumTitle(if (isFromAlbum) playlistOrAlbum.title else album?.name)
|
|
||||||
.setArtworkUri(if (isFromAlbum) playlistOrAlbum.thumbnail?.url?.toUri() else thumbnail?.url?.toUri())
|
|
||||||
.setExtras(
|
|
||||||
bundleOf(
|
|
||||||
"videoId" to info.endpoint?.videoId,
|
|
||||||
"playlistId" to info.endpoint?.playlistId,
|
|
||||||
"albumId" to (if (isFromAlbum) albumId else album?.endpoint?.browseId),
|
|
||||||
"durationText" to durationText,
|
|
||||||
"artistNames" to (authors ?: playlistOrAlbum.authors)?.filter { it.endpoint != null }?.map { it.name },
|
|
||||||
"artistIds" to (authors ?: playlistOrAlbum.authors)?.mapNotNull { it.endpoint?.browseId }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
.setMediaId(info.endpoint?.videoId ?: return null)
|
|
||||||
.setUri(info.endpoint?.videoId ?: return null)
|
|
||||||
.setCustomCacheKey(info.endpoint?.videoId ?: return null)
|
|
||||||
.build()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun String?.thumbnail(size: Int): String? {
|
fun String?.thumbnail(size: Int): String? {
|
||||||
return when {
|
return when {
|
||||||
this?.startsWith("https://lh3.googleusercontent.com") == true -> "$this-w$size-h$size"
|
this?.startsWith("https://lh3.googleusercontent.com") == true -> "$this-w$size-h$size"
|
||||||
|
|||||||
@@ -226,6 +226,49 @@ object YouTube {
|
|||||||
.thumbnail
|
.thumbnail
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun from(renderer: MusicResponsiveListItemRenderer): Song? {
|
||||||
|
return Song(
|
||||||
|
info = renderer
|
||||||
|
.flexColumns
|
||||||
|
.getOrNull(0)
|
||||||
|
?.musicResponsiveListItemFlexColumnRenderer
|
||||||
|
?.text
|
||||||
|
?.runs
|
||||||
|
?.getOrNull(0)
|
||||||
|
?.let { Info.from(it) } ?: return 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()
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -817,63 +860,10 @@ object YouTube {
|
|||||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
||||||
val year: String?,
|
val year: String?,
|
||||||
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
||||||
val items: List<Item>?,
|
val songs: List<Item.Song>?,
|
||||||
val url: String?,
|
val url: String?,
|
||||||
val continuation: String?,
|
val continuation: String?,
|
||||||
) {
|
) {
|
||||||
data class Item(
|
|
||||||
val info: Info<NavigationEndpoint.Endpoint.Watch>,
|
|
||||||
val authors: List<Info<NavigationEndpoint.Endpoint.Browse>>?,
|
|
||||||
val durationText: String?,
|
|
||||||
val album: Info<NavigationEndpoint.Endpoint.Browse>?,
|
|
||||||
val thumbnail: ThumbnailRenderer.MusicThumbnailRenderer.Thumbnail.Thumbnail?,
|
|
||||||
) {
|
|
||||||
companion object {
|
|
||||||
fun from(renderer: MusicResponsiveListItemRenderer): Item? {
|
|
||||||
return Item(
|
|
||||||
info = renderer
|
|
||||||
.flexColumns
|
|
||||||
.getOrNull(0)
|
|
||||||
?.musicResponsiveListItemFlexColumnRenderer
|
|
||||||
?.text
|
|
||||||
?.runs
|
|
||||||
?.getOrNull(0)
|
|
||||||
?.let { Info.from(it) } ?: return 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()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suspend fun next(): PlaylistOrAlbum {
|
suspend fun next(): PlaylistOrAlbum {
|
||||||
return continuation?.let {
|
return continuation?.let {
|
||||||
runCatching {
|
runCatching {
|
||||||
@@ -885,12 +875,12 @@ object YouTube {
|
|||||||
parameter("continuation", continuation)
|
parameter("continuation", continuation)
|
||||||
}.body<ContinuationResponse>().let { continuationResponse ->
|
}.body<ContinuationResponse>().let { continuationResponse ->
|
||||||
copy(
|
copy(
|
||||||
items = items?.plus(continuationResponse
|
songs = songs?.plus(continuationResponse
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
.musicShelfContinuation
|
||||||
?.contents
|
?.contents
|
||||||
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
?.mapNotNull(Item.Companion::from) ?: emptyList()),
|
?.mapNotNull(Item.Song.Companion::from) ?: emptyList()),
|
||||||
continuation = continuationResponse
|
continuation = continuationResponse
|
||||||
.continuationContents
|
.continuationContents
|
||||||
.musicShelfContinuation
|
.musicShelfContinuation
|
||||||
@@ -909,9 +899,28 @@ object YouTube {
|
|||||||
return playlistOrAlbum(browseId)?.map { album ->
|
return playlistOrAlbum(browseId)?.map { album ->
|
||||||
album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
|
album.url?.let { Url(it).parameters["list"] }?.let { playlistId ->
|
||||||
playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist ->
|
playlistOrAlbum("VL$playlistId")?.getOrNull()?.let { playlist ->
|
||||||
album.copy(items = playlist.items)
|
album.copy(songs = playlist.songs)
|
||||||
}
|
}
|
||||||
} ?: album
|
} ?: album
|
||||||
|
}?.map { album ->
|
||||||
|
val albumInfo = Info(
|
||||||
|
name = album.title ?: "",
|
||||||
|
endpoint = NavigationEndpoint.Endpoint.Browse(
|
||||||
|
browseId = browseId,
|
||||||
|
params = null,
|
||||||
|
browseEndpointContextSupportedConfigs = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
album.copy(
|
||||||
|
songs = album.songs?.map { song ->
|
||||||
|
song.copy(
|
||||||
|
authors = song.authors ?: album.authors,
|
||||||
|
album = albumInfo,
|
||||||
|
thumbnail = album.thumbnail
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -950,7 +959,7 @@ object YouTube {
|
|||||||
?.getOrNull(2)
|
?.getOrNull(2)
|
||||||
?.firstOrNull()
|
?.firstOrNull()
|
||||||
?.text,
|
?.text,
|
||||||
items = body
|
songs = body
|
||||||
.contents
|
.contents
|
||||||
.singleColumnBrowseResultsRenderer
|
.singleColumnBrowseResultsRenderer
|
||||||
?.tabs
|
?.tabs
|
||||||
@@ -963,7 +972,7 @@ object YouTube {
|
|||||||
?.musicShelfRenderer
|
?.musicShelfRenderer
|
||||||
?.contents
|
?.contents
|
||||||
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
?.map(MusicShelfRenderer.Content::musicResponsiveListItemRenderer)
|
||||||
?.mapNotNull(PlaylistOrAlbum.Item.Companion::from)
|
?.mapNotNull(Item.Song.Companion::from)
|
||||||
// ?.filter { it.info.endpoint != null }
|
// ?.filter { it.info.endpoint != null }
|
||||||
,
|
,
|
||||||
url = body
|
url = body
|
||||||
|
|||||||
Reference in New Issue
Block a user