Redesign LocalPlaylistScreen (#172)
This commit is contained in:
@@ -19,9 +19,4 @@ data class PlaylistWithSongs(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
val songs: List<DetailedSong>
|
val songs: List<DetailedSong>
|
||||||
) {
|
)
|
||||||
companion object {
|
|
||||||
val Empty = PlaylistWithSongs(Playlist(-1, ""), emptyList())
|
|
||||||
val NotFound = PlaylistWithSongs(Playlist(-2, "Not found"), emptyList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import androidx.compose.runtime.saveable.SaverScope
|
|||||||
import it.vfsfitvnm.vimusic.models.DetailedSong
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
|
||||||
object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
|
object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
|
||||||
override fun SaverScope.save(value: DetailedSong): List<Any?> =
|
override fun SaverScope.save(value: DetailedSong) =
|
||||||
listOf(
|
listOf(
|
||||||
value.id,
|
value.id,
|
||||||
value.title,
|
value.title,
|
||||||
@@ -18,16 +18,14 @@ object DetailedSongSaver : Saver<DetailedSong, List<Any?>> {
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
@Suppress("UNCHECKED_CAST")
|
||||||
override fun restore(value: List<Any?>): DetailedSong? {
|
override fun restore(value: List<Any?>) = DetailedSong(
|
||||||
return if (value.size == 8) DetailedSong(
|
id = value[0] as String,
|
||||||
id = value[0] as String,
|
title = value[1] as String,
|
||||||
title = value[1] as String,
|
artistsText = value[2] as String?,
|
||||||
artistsText = value[2] as String?,
|
durationText = value[3] as String,
|
||||||
durationText = value[3] as String,
|
thumbnailUrl = value[4] as String?,
|
||||||
thumbnailUrl = value[4] as String?,
|
totalPlayTimeMs = value[5] as Long,
|
||||||
totalPlayTimeMs = value[5] as Long,
|
albumId = value[6] as String?,
|
||||||
albumId = value[6] as String?,
|
artists = InfoListSaver.restore(value[7] as List<List<String>>)
|
||||||
artists = InfoListSaver.restore(value[7] as List<List<String>>)
|
)
|
||||||
) else null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import androidx.compose.runtime.saveable.Saver
|
|||||||
import androidx.compose.runtime.saveable.SaverScope
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
|
||||||
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
|
interface ListSaver<Original, Saveable : Any> : Saver<List<Original>, List<Saveable>> {
|
||||||
|
override fun SaverScope.save(value: List<Original>): List<Saveable>
|
||||||
|
override fun restore(value: List<Saveable>): List<Original>
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>): ListSaver<Original, Saveable> {
|
fun <Original, Saveable : Any> of(saver: Saver<Original, Saveable>): ListSaver<Original, Saveable> {
|
||||||
return object : ListSaver<Original, Saveable> {
|
return object : ListSaver<Original, Saveable> {
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.savers
|
||||||
|
|
||||||
|
import androidx.compose.runtime.saveable.Saver
|
||||||
|
import androidx.compose.runtime.saveable.SaverScope
|
||||||
|
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
||||||
|
|
||||||
|
object PlaylistWithSongsSaver : Saver<PlaylistWithSongs?, List<Any>> {
|
||||||
|
override fun SaverScope.save(value: PlaylistWithSongs?) = value?.let {
|
||||||
|
listOf(
|
||||||
|
with(PlaylistSaver) { save(value.playlist) },
|
||||||
|
with(DetailedSongListSaver) { save(value.songs) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun restore(value: List<Any>): PlaylistWithSongs = PlaylistWithSongs(
|
||||||
|
playlist = PlaylistSaver.restore(value[0] as List<Any?>),
|
||||||
|
songs = DetailedSongListSaver.restore(value[1] as List<List<Any?>>)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
package it.vfsfitvnm.vimusic.ui.screens
|
|
||||||
|
|
||||||
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.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
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.itemsIndexed
|
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
|
||||||
import androidx.compose.foundation.text.BasicText
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.ColorFilter
|
|
||||||
import androidx.compose.ui.res.painterResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.zIndex
|
|
||||||
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
|
||||||
import it.vfsfitvnm.reordering.animateItemPlacement
|
|
||||||
import it.vfsfitvnm.reordering.draggedItem
|
|
||||||
import it.vfsfitvnm.reordering.rememberReorderingState
|
|
||||||
import it.vfsfitvnm.reordering.reorder
|
|
||||||
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.models.DetailedSong
|
|
||||||
import it.vfsfitvnm.vimusic.models.PlaylistWithSongs
|
|
||||||
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
|
||||||
import it.vfsfitvnm.vimusic.query
|
|
||||||
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.ConfirmationDialog
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Menu
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.MenuEntry
|
|
||||||
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
|
||||||
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.views.SongItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
|
||||||
import it.vfsfitvnm.vimusic.utils.enqueue
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
|
|
||||||
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
|
|
||||||
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.flow.map
|
|
||||||
import kotlinx.coroutines.runBlocking
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
@ExperimentalFoundationApi
|
|
||||||
@ExperimentalAnimationApi
|
|
||||||
@Composable
|
|
||||||
fun LocalPlaylistScreen(playlistId: Long) {
|
|
||||||
val playlistWithSongs by remember(playlistId) {
|
|
||||||
Database.playlistWithSongs(playlistId).map { it ?: PlaylistWithSongs.NotFound }
|
|
||||||
}.collectAsState(initial = PlaylistWithSongs.Empty, context = Dispatchers.IO)
|
|
||||||
|
|
||||||
val lazyListState = rememberLazyListState()
|
|
||||||
|
|
||||||
RouteHandler(listenToGlobalEmitter = true) {
|
|
||||||
globalRoutes()
|
|
||||||
|
|
||||||
host {
|
|
||||||
val (colorPalette, typography) = LocalAppearance.current
|
|
||||||
val menuState = LocalMenuState.current
|
|
||||||
val binder = LocalPlayerServiceBinder.current
|
|
||||||
|
|
||||||
val thumbnailSize = Dimensions.thumbnails.song.px
|
|
||||||
|
|
||||||
val reorderingState = rememberReorderingState(
|
|
||||||
lazyListState = lazyListState,
|
|
||||||
key = playlistWithSongs.songs,
|
|
||||||
onDragEnd = { fromIndex, toIndex ->
|
|
||||||
query {
|
|
||||||
Database.move(playlistWithSongs.playlist.id, fromIndex, toIndex)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
extraItemCount = 1
|
|
||||||
)
|
|
||||||
|
|
||||||
var isRenaming by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isRenaming) {
|
|
||||||
TextFieldDialog(
|
|
||||||
hintText = "Enter the playlist name",
|
|
||||||
initialTextInput = playlistWithSongs.playlist.name,
|
|
||||||
onDismiss = { isRenaming = false },
|
|
||||||
onDone = { text ->
|
|
||||||
query {
|
|
||||||
Database.update(playlistWithSongs.playlist.copy(name = text))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var isDeleting by rememberSaveable {
|
|
||||||
mutableStateOf(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isDeleting) {
|
|
||||||
ConfirmationDialog(
|
|
||||||
text = "Do you really want to delete this playlist?",
|
|
||||||
onDismiss = { isDeleting = false },
|
|
||||||
onConfirm = {
|
|
||||||
query {
|
|
||||||
Database.delete(playlistWithSongs.playlist)
|
|
||||||
}
|
|
||||||
pop()
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
ReorderingLazyColumn(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
contentPadding = LocalPlayerAwarePaddingValues.current,
|
|
||||||
modifier = Modifier
|
|
||||||
.background(colorPalette.background0)
|
|
||||||
.fillMaxSize()
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
Column {
|
|
||||||
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, horizontal = 16.dp)
|
|
||||||
.size(24.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(top = 16.dp, bottom = 8.dp)
|
|
||||||
.padding(horizontal = 16.dp)
|
|
||||||
) {
|
|
||||||
BasicText(
|
|
||||||
text = playlistWithSongs.playlist.name,
|
|
||||||
style = typography.m.semiBold
|
|
||||||
)
|
|
||||||
|
|
||||||
BasicText(
|
|
||||||
text = "${playlistWithSongs.songs.size} songs",
|
|
||||||
style = typography.xxs.semiBold.secondary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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(enabled = playlistWithSongs.songs.isNotEmpty()) {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayFromBeginning(
|
|
||||||
playlistWithSongs.songs
|
|
||||||
.shuffled()
|
|
||||||
.map(DetailedSong::asMediaItem)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.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",
|
|
||||||
isEnabled = playlistWithSongs.songs.isNotEmpty(),
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
binder?.player?.enqueue(
|
|
||||||
playlistWithSongs.songs.map(
|
|
||||||
DetailedSong::asMediaItem
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.pencil,
|
|
||||||
text = "Rename",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
isRenaming = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
playlistWithSongs.playlist.browseId?.let { browseId ->
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.sync,
|
|
||||||
text = "Sync",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
transaction {
|
|
||||||
runBlocking(Dispatchers.IO) {
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
YouTube.playlist(browseId)?.map {
|
|
||||||
it.next()
|
|
||||||
}?.map { playlist ->
|
|
||||||
playlist.copy(items = playlist.items?.filter { it.info.endpoint != null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}?.getOrNull()?.let { remotePlaylist ->
|
|
||||||
Database.clearPlaylist(playlistWithSongs.playlist.id)
|
|
||||||
|
|
||||||
remotePlaylist.items?.forEachIndexed { index, song ->
|
|
||||||
song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem ->
|
|
||||||
Database.insert(mediaItem)
|
|
||||||
|
|
||||||
Database.insert(
|
|
||||||
SongPlaylistMap(
|
|
||||||
songId = mediaItem.mediaId,
|
|
||||||
playlistId = playlistId,
|
|
||||||
position = index
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
MenuEntry(
|
|
||||||
icon = R.drawable.trash,
|
|
||||||
text = "Delete",
|
|
||||||
onClick = {
|
|
||||||
menuState.hide()
|
|
||||||
isDeleting = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(horizontal = 8.dp, vertical = 8.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
itemsIndexed(
|
|
||||||
items = playlistWithSongs.songs,
|
|
||||||
key = { _, song -> song.id },
|
|
||||||
contentType = { _, song -> song },
|
|
||||||
) { index, song ->
|
|
||||||
SongItem(
|
|
||||||
song = song,
|
|
||||||
thumbnailSize = thumbnailSize,
|
|
||||||
onClick = {
|
|
||||||
binder?.stopRadio()
|
|
||||||
binder?.player?.forcePlayAtIndex(
|
|
||||||
playlistWithSongs.songs.map(
|
|
||||||
DetailedSong::asMediaItem
|
|
||||||
), index
|
|
||||||
)
|
|
||||||
},
|
|
||||||
menuContent = {
|
|
||||||
InPlaylistMediaItemMenu(
|
|
||||||
playlistId = playlistId,
|
|
||||||
positionInPlaylist = index,
|
|
||||||
song = song
|
|
||||||
)
|
|
||||||
},
|
|
||||||
trailingContent = {
|
|
||||||
Image(
|
|
||||||
painter = painterResource(R.drawable.reorder),
|
|
||||||
contentDescription = null,
|
|
||||||
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
|
||||||
modifier = Modifier
|
|
||||||
.clickable { }
|
|
||||||
.reorder(
|
|
||||||
reorderingState = reorderingState,
|
|
||||||
index = index
|
|
||||||
)
|
|
||||||
.padding(horizontal = 8.dp, vertical = 4.dp)
|
|
||||||
.size(20.dp)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.animateItemPlacement(reorderingState = reorderingState)
|
|
||||||
.draggedItem(reorderingState = reorderingState, index = index)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -13,7 +13,7 @@ import it.vfsfitvnm.vimusic.query
|
|||||||
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen
|
import it.vfsfitvnm.vimusic.ui.screens.BuiltInPlaylistScreen
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen
|
import it.vfsfitvnm.vimusic.ui.screens.IntentUriScreen
|
||||||
import it.vfsfitvnm.vimusic.ui.screens.LocalPlaylistScreen
|
import it.vfsfitvnm.vimusic.ui.screens.localplaylist.LocalPlaylistScreen
|
||||||
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.builtInPlaylistRoute
|
import it.vfsfitvnm.vimusic.ui.screens.builtInPlaylistRoute
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.localplaylist
|
||||||
|
|
||||||
|
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 LocalPlaylistScreen(playlistId: Long) {
|
||||||
|
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) {
|
||||||
|
LocalPlaylistSongList(
|
||||||
|
playlistId = playlistId,
|
||||||
|
onDelete = pop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
package it.vfsfitvnm.vimusic.ui.screens.localplaylist
|
||||||
|
|
||||||
|
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.Box
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
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.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.ColorFilter
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import it.vfsfitvnm.reordering.ReorderingLazyColumn
|
||||||
|
import it.vfsfitvnm.reordering.animateItemPlacement
|
||||||
|
import it.vfsfitvnm.reordering.draggedItem
|
||||||
|
import it.vfsfitvnm.reordering.rememberReorderingState
|
||||||
|
import it.vfsfitvnm.reordering.reorder
|
||||||
|
import it.vfsfitvnm.vimusic.Database
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
|
||||||
|
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
|
||||||
|
import it.vfsfitvnm.vimusic.R
|
||||||
|
import it.vfsfitvnm.vimusic.models.DetailedSong
|
||||||
|
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
|
||||||
|
import it.vfsfitvnm.vimusic.query
|
||||||
|
import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver
|
||||||
|
import it.vfsfitvnm.vimusic.transaction
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.Header
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
|
||||||
|
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
|
||||||
|
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.views.SongItem
|
||||||
|
import it.vfsfitvnm.vimusic.utils.asMediaItem
|
||||||
|
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.produceSaveableState
|
||||||
|
import it.vfsfitvnm.vimusic.utils.toMediaItem
|
||||||
|
import it.vfsfitvnm.youtubemusic.YouTube
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@ExperimentalAnimationApi
|
||||||
|
@ExperimentalFoundationApi
|
||||||
|
@Composable
|
||||||
|
fun LocalPlaylistSongList(
|
||||||
|
playlistId: Long,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
val (colorPalette, typography) = LocalAppearance.current
|
||||||
|
val binder = LocalPlayerServiceBinder.current
|
||||||
|
|
||||||
|
val playlistWithSongs by produceSaveableState(
|
||||||
|
initialValue = null,
|
||||||
|
stateSaver = PlaylistWithSongsSaver
|
||||||
|
) {
|
||||||
|
Database
|
||||||
|
.playlistWithSongs(playlistId)
|
||||||
|
.flowOn(Dispatchers.IO)
|
||||||
|
.collect { value = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
val reorderingState = rememberReorderingState(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
key = playlistWithSongs?.songs ?: emptyList<Any>(),
|
||||||
|
onDragEnd = { fromIndex, toIndex ->
|
||||||
|
query {
|
||||||
|
Database.move(playlistId, fromIndex, toIndex)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
extraItemCount = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
var isRenaming by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isRenaming) {
|
||||||
|
TextFieldDialog(
|
||||||
|
hintText = "Enter the playlist name",
|
||||||
|
initialTextInput = playlistWithSongs?.playlist?.name ?: "",
|
||||||
|
onDismiss = { isRenaming = false },
|
||||||
|
onDone = { text ->
|
||||||
|
query {
|
||||||
|
playlistWithSongs?.playlist?.copy(name = text)?.let(Database::update)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var isDeleting by rememberSaveable {
|
||||||
|
mutableStateOf(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDeleting) {
|
||||||
|
ConfirmationDialog(
|
||||||
|
text = "Do you really want to delete this playlist?",
|
||||||
|
onDismiss = { isDeleting = false },
|
||||||
|
onConfirm = {
|
||||||
|
query {
|
||||||
|
playlistWithSongs?.playlist?.let(Database::delete)
|
||||||
|
}
|
||||||
|
onDelete()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbnailSize = Dimensions.thumbnails.song.px
|
||||||
|
|
||||||
|
Box {
|
||||||
|
ReorderingLazyColumn(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
contentPadding = LocalPlayerAwarePaddingValues.current,
|
||||||
|
modifier = Modifier
|
||||||
|
.background(colorPalette.background0)
|
||||||
|
.fillMaxSize()
|
||||||
|
) {
|
||||||
|
item(
|
||||||
|
key = "header",
|
||||||
|
contentType = 0
|
||||||
|
) {
|
||||||
|
Header(title = playlistWithSongs?.playlist?.name ?: "Unknown") {
|
||||||
|
BasicText(
|
||||||
|
text = "Enqueue",
|
||||||
|
style = typography.xxs.medium,
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = playlistWithSongs?.songs?.isNotEmpty() == true) {
|
||||||
|
playlistWithSongs?.songs
|
||||||
|
?.map(DetailedSong::asMediaItem)
|
||||||
|
?.let { mediaItems ->
|
||||||
|
binder?.player?.enqueue(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(colorPalette.background2)
|
||||||
|
.padding(all = 8.dp)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
)
|
||||||
|
|
||||||
|
playlistWithSongs?.playlist?.browseId?.let { browseId ->
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.sync),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable {
|
||||||
|
transaction {
|
||||||
|
runBlocking(Dispatchers.IO) {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
YouTube.playlist(browseId)?.map {
|
||||||
|
it.next()
|
||||||
|
}?.map { playlist ->
|
||||||
|
playlist.copy(items = playlist.items?.filter { it.info.endpoint != null })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}?.getOrNull()?.let { remotePlaylist ->
|
||||||
|
Database.clearPlaylist(playlistId)
|
||||||
|
|
||||||
|
remotePlaylist.items?.forEachIndexed { index, song ->
|
||||||
|
song.toMediaItem(browseId, remotePlaylist)?.let { mediaItem ->
|
||||||
|
Database.insert(mediaItem)
|
||||||
|
|
||||||
|
Database.insert(
|
||||||
|
SongPlaylistMap(
|
||||||
|
songId = mediaItem.mediaId,
|
||||||
|
playlistId = playlistId,
|
||||||
|
position = index
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.pencil),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { isRenaming = true }
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.trash),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.text),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { isDeleting = true }
|
||||||
|
.padding(all = 4.dp)
|
||||||
|
.size(18.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
itemsIndexed(
|
||||||
|
items = playlistWithSongs?.songs ?: emptyList(),
|
||||||
|
key = { _, song -> song.id },
|
||||||
|
contentType = { _, song -> song },
|
||||||
|
) { index, song ->
|
||||||
|
SongItem(
|
||||||
|
song = song,
|
||||||
|
thumbnailSize = thumbnailSize,
|
||||||
|
onClick = {
|
||||||
|
playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
|
||||||
|
?.let { mediaItems ->
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayAtIndex(mediaItems, index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
menuContent = {
|
||||||
|
InPlaylistMediaItemMenu(
|
||||||
|
playlistId = playlistId,
|
||||||
|
positionInPlaylist = index,
|
||||||
|
song = song
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(R.drawable.reorder),
|
||||||
|
contentDescription = null,
|
||||||
|
colorFilter = ColorFilter.tint(colorPalette.textSecondary),
|
||||||
|
modifier = Modifier
|
||||||
|
.clickable { }
|
||||||
|
.reorder(
|
||||||
|
reorderingState = reorderingState,
|
||||||
|
index = index
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp)
|
||||||
|
.size(20.dp)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.animateItemPlacement(reorderingState = reorderingState)
|
||||||
|
.draggedItem(reorderingState = reorderingState, index = index)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(all = 16.dp)
|
||||||
|
.padding(LocalPlayerAwarePaddingValues.current)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.clickable(enabled = playlistWithSongs?.songs?.isNotEmpty() == true) {
|
||||||
|
playlistWithSongs?.songs
|
||||||
|
?.shuffled()
|
||||||
|
?.map(DetailedSong::asMediaItem)
|
||||||
|
?.let { mediaItems ->
|
||||||
|
binder?.stopRadio()
|
||||||
|
binder?.player?.forcePlayFromBeginning(mediaItems)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.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)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user