Improve UI in landscape mode

This commit is contained in:
vfsfitvnm
2022-10-06 11:30:43 +02:00
parent 8fd402b5ce
commit 78c44988d7
9 changed files with 596 additions and 618 deletions

View File

@@ -0,0 +1,71 @@
package it.vfsfitvnm.vimusic.ui.components.themed
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
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.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.thumbnail
@Composable
inline fun LayoutWithAdaptiveThumbnail(
thumbnailContent: @Composable () -> Unit,
content: @Composable () -> Unit
) {
val isLandscape = isLandscape
if (isLandscape) {
Row(verticalAlignment = Alignment.CenterVertically) {
thumbnailContent()
content()
}
} else {
content()
}
}
fun adaptiveThumbnailContent(
isLoading: Boolean,
url: String?,
shape: Shape? = null
): @Composable () -> Unit = {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
BoxWithConstraints(contentAlignment = Alignment.Center) {
val size = if (isLandscape) maxHeight else maxWidth
val thumbnailSizeDp = size - 64.dp
val thumbnailSizePx = thumbnailSizeDp.px
val modifier = Modifier
.padding(all = 16.dp)
.clip(shape ?: thumbnailShape)
.size(thumbnailSizeDp)
if (isLoading) {
Spacer(
modifier = modifier
.shimmer()
.background(colorPalette.shimmer)
)
} else {
AsyncImage(
model = url?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = modifier
)
}
}
}

View File

@@ -1,6 +1,5 @@
package it.vfsfitvnm.vimusic.ui.components.themed package it.vfsfitvnm.vimusic.ui.components.themed
import android.content.res.Configuration
import androidx.compose.animation.animateColor import androidx.compose.animation.animateColor
import androidx.compose.animation.core.animateFloat import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.updateTransition import androidx.compose.animation.core.updateTransition
@@ -27,12 +26,12 @@ import androidx.compose.ui.draw.rotate
import androidx.compose.ui.graphics.ColorFilter import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.layout import androidx.compose.ui.layout.layout
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
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.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
@Composable @Composable
@@ -45,9 +44,8 @@ fun NavigationRail(
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val configuration = LocalConfiguration.current
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE val isLandscape = isLandscape
Column( Column(
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,

View File

@@ -3,25 +3,16 @@ package it.vfsfitvnm.vimusic.ui.screens.album
import android.content.Intent import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
@@ -38,6 +29,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
import it.vfsfitvnm.vimusic.ui.items.AlbumItem import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.screens.albumRoute import it.vfsfitvnm.vimusic.ui.screens.albumRoute
@@ -45,10 +37,8 @@ import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
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
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.albumPage import it.vfsfitvnm.youtubemusic.requests.albumPage
@@ -81,7 +71,11 @@ fun AlbumScreen(browseId: String) {
stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver), stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver),
tabIndex > 0 tabIndex > 0
) { ) {
if (value != null || (tabIndex == 0 && withContext(Dispatchers.IO) { Database.albumTimestamp(browseId) } != null)) return@produceSaveableState if (value != null || (tabIndex == 0 && withContext(Dispatchers.IO) {
Database.albumTimestamp(
browseId
)
} != null)) return@produceSaveableState
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
Innertube.albumPage(BrowseBody(browseId = browseId)) Innertube.albumPage(BrowseBody(browseId = browseId))
@@ -121,94 +115,70 @@ fun AlbumScreen(browseId: String) {
globalRoutes() globalRoutes()
host { host {
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = { textButton -> val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit =
if (album?.timestamp == null) { { textButton ->
HeaderPlaceholder(
modifier = Modifier
.shimmer()
)
} else {
val (colorPalette) = LocalAppearance.current
val context = LocalContext.current
Header(title = album?.title ?: "Unknown") {
textButton?.invoke()
Spacer(
modifier = Modifier
.weight(1f)
)
HeaderIconButton(
icon = if (album?.bookmarkedAt == null) {
R.drawable.bookmark_outline
} else {
R.drawable.bookmark
},
color = colorPalette.accent,
onClick = {
val bookmarkedAt =
if (album?.bookmarkedAt == null) System.currentTimeMillis() else null
query {
album
?.copy(bookmarkedAt = bookmarkedAt)
?.let(Database::update)
}
}
)
HeaderIconButton(
icon = R.drawable.share_social,
color = colorPalette.text,
onClick = {
album?.shareUrl?.let { url ->
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
context.startActivity(Intent.createChooser(sendIntent, null))
}
}
)
}
}
}
val thumbnailContent: @Composable ColumnScope.() -> Unit = {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
) {
val thumbnailSizeDp = maxWidth - 64.dp
val thumbnailSizePx = thumbnailSizeDp.px
if (album?.timestamp == null) { if (album?.timestamp == null) {
Spacer( HeaderPlaceholder(
modifier = Modifier modifier = Modifier
.padding(all = 16.dp)
.shimmer() .shimmer()
.clip(thumbnailShape)
.size(thumbnailSizeDp)
.background(colorPalette.shimmer)
) )
} else { } else {
AsyncImage( val (colorPalette) = LocalAppearance.current
model = album?.thumbnailUrl?.thumbnail(thumbnailSizePx), val context = LocalContext.current
contentDescription = null,
modifier = Modifier Header(title = album?.title ?: "Unknown") {
.padding(all = 16.dp) textButton?.invoke()
.clip(thumbnailShape)
.size(thumbnailSizeDp) Spacer(
) modifier = Modifier
.weight(1f)
)
HeaderIconButton(
icon = if (album?.bookmarkedAt == null) {
R.drawable.bookmark_outline
} else {
R.drawable.bookmark
},
color = colorPalette.accent,
onClick = {
val bookmarkedAt =
if (album?.bookmarkedAt == null) System.currentTimeMillis() else null
query {
album
?.copy(bookmarkedAt = bookmarkedAt)
?.let(Database::update)
}
}
)
HeaderIconButton(
icon = R.drawable.share_social,
color = colorPalette.text,
onClick = {
album?.shareUrl?.let { url ->
val sendIntent = Intent().apply {
action = Intent.ACTION_SEND
type = "text/plain"
putExtra(Intent.EXTRA_TEXT, url)
}
context.startActivity(
Intent.createChooser(
sendIntent,
null
)
)
}
}
)
}
} }
} }
}
val thumbnailContent =
adaptiveThumbnailContent(album?.timestamp == null, album?.thumbnailUrl)
Scaffold( Scaffold(
topIconButtonId = R.drawable.chevron_back, topIconButtonId = R.drawable.chevron_back,
@@ -227,6 +197,7 @@ fun AlbumScreen(browseId: String) {
headerContent = headerContent, headerContent = headerContent,
thumbnailContent = thumbnailContent, thumbnailContent = thumbnailContent,
) )
1 -> { 1 -> {
val thumbnailSizeDp = 108.dp val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px val thumbnailSizePx = thumbnailSizeDp.px

View File

@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@@ -24,10 +23,11 @@ import it.vfsfitvnm.vimusic.R
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.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
@@ -38,6 +38,7 @@ import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.semiBold import it.vfsfitvnm.vimusic.utils.semiBold
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -49,7 +50,7 @@ import kotlinx.coroutines.flow.flowOn
fun AlbumSongs( fun AlbumSongs(
browseId: String, browseId: String,
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit, headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
thumbnailContent: @Composable ColumnScope.() -> Unit, thumbnailContent: @Composable () -> Unit,
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
@@ -67,93 +68,100 @@ fun AlbumSongs(
val thumbnailSizeDp = Dimensions.thumbnails.song val thumbnailSizeDp = Dimensions.thumbnails.song
Box { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
LazyColumn( Box {
contentPadding = LocalPlayerAwarePaddingValues.current, LazyColumn(
modifier = Modifier contentPadding = LocalPlayerAwarePaddingValues.current,
.background(colorPalette.background0) modifier = Modifier
.fillMaxSize() .background(colorPalette.background0)
) { .fillMaxSize()
item(
key = "header",
contentType = 0
) { ) {
Column { item(
headerContent { key = "header",
SecondaryTextButton( contentType = 0
text = "Enqueue", ) {
isEnabled = songs.isNotEmpty(), Column(horizontalAlignment = Alignment.CenterHorizontally) {
onClick = { headerContent {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem)) SecondaryTextButton(
} text = "Enqueue",
) isEnabled = songs.isNotEmpty(),
onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
}
)
}
if (!isLandscape) {
thumbnailContent()
}
} }
thumbnailContent()
} }
}
itemsIndexed( itemsIndexed(
items = songs, items = songs,
key = { _, song -> song.id } key = { _, song -> song.id }
) { index, song -> ) { index, song ->
SongItem( SongItem(
title = song.title, title = song.title,
authors = song.artistsText, authors = song.artistsText,
duration = song.durationText, duration = song.durationText,
thumbnailSizeDp = thumbnailSizeDp, thumbnailSizeDp = thumbnailSizeDp,
thumbnailContent = { thumbnailContent = {
BasicText( BasicText(
text = "${index + 1}", text = "${index + 1}",
style = typography.s.semiBold.center.color(colorPalette.textDisabled), style = typography.s.semiBold.center.color(colorPalette.textDisabled),
maxLines = 1, maxLines = 1,
overflow = TextOverflow.Ellipsis, overflow = TextOverflow.Ellipsis,
modifier = Modifier modifier = Modifier
.width(thumbnailSizeDp) .width(thumbnailSizeDp)
.align(Alignment.Center) .align(Alignment.Center)
) )
}, },
modifier = Modifier modifier = Modifier
.combinedClickable( .combinedClickable(
onLongClick = { onLongClick = {
menuState.display { menuState.display {
NonQueuedMediaItemMenu( NonQueuedMediaItemMenu(
onDismiss = menuState::hide, onDismiss = menuState::hide,
mediaItem = song.asMediaItem, mediaItem = song.asMediaItem,
)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
) )
} }
}, )
onClick = { )
binder?.stopRadio() }
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
}
)
)
}
if (songs.isEmpty()) { if (songs.isEmpty()) {
item(key = "loading") { item(key = "loading") {
ShimmerHost( ShimmerHost(
modifier = Modifier modifier = Modifier
.fillParentMaxSize() .fillParentMaxSize()
) { ) {
repeat(4) { repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song) SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
}
} }
} }
} }
} }
}
PrimaryButton( PrimaryButton(
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = songs.isNotEmpty(), isEnabled = songs.isNotEmpty(),
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem) songs.shuffled().map(DetailedSong::asMediaItem)
) )
} }
) )
}
} }
} }

View File

@@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
@@ -21,10 +20,11 @@ import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver import it.vfsfitvnm.vimusic.savers.DetailedSongListSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
@@ -44,7 +44,7 @@ import kotlinx.coroutines.flow.flowOn
fun ArtistLocalSongs( fun ArtistLocalSongs(
browseId: String, browseId: String,
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit, headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
thumbnailContent: @Composable ColumnScope.() -> Unit, thumbnailContent: @Composable () -> Unit,
) { ) {
val binder = LocalPlayerServiceBinder.current val binder = LocalPlayerServiceBinder.current
val (colorPalette) = LocalAppearance.current val (colorPalette) = LocalAppearance.current
@@ -63,79 +63,81 @@ fun ArtistLocalSongs(
val songThumbnailSizeDp = Dimensions.thumbnails.song val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px val songThumbnailSizePx = songThumbnailSizeDp.px
Box { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
LazyColumn( Box {
contentPadding = LocalPlayerAwarePaddingValues.current, LazyColumn(
modifier = Modifier contentPadding = LocalPlayerAwarePaddingValues.current,
.background(colorPalette.background0) modifier = Modifier
.fillMaxSize() .background(colorPalette.background0)
) { .fillMaxSize()
item(
key = "header",
contentType = 0
) { ) {
Column { item(
headerContent { key = "header",
SecondaryTextButton( contentType = 0
text = "Enqueue", ) {
isEnabled = !songs.isNullOrEmpty(), Column {
onClick = { headerContent {
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem)) SecondaryTextButton(
} text = "Enqueue",
) isEnabled = !songs.isNullOrEmpty(),
}
thumbnailContent()
}
}
songs?.let { songs ->
itemsIndexed(
items = songs,
key = { _, song -> song.id }
) { index, song ->
SongItem(
song = song,
thumbnailSizeDp = songThumbnailSizeDp,
thumbnailSizePx = songThumbnailSizePx,
modifier = Modifier
.combinedClickable(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
)
}
},
onClick = { onClick = {
binder?.stopRadio() binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
} }
) )
) }
thumbnailContent()
}
} }
} ?: item(key = "loading") {
ShimmerHost { songs?.let { songs ->
repeat(4) { itemsIndexed(
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song) items = songs,
key = { _, song -> song.id }
) { index, song ->
SongItem(
song = song,
thumbnailSizeDp = songThumbnailSizeDp,
thumbnailSizePx = songThumbnailSizePx,
modifier = Modifier
.combinedClickable(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
}
)
)
}
} ?: item(key = "loading") {
ShimmerHost {
repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
}
} }
} }
} }
}
PrimaryButton( PrimaryButton(
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = !songs.isNullOrEmpty(), isEnabled = !songs.isNullOrEmpty(),
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning( binder?.player?.forcePlayFromBeginning(
songs!!.shuffled().map(DetailedSong::asMediaItem) songs!!.shuffled().map(DetailedSong::asMediaItem)
) )
} }
) )
}
} }
} }

View File

@@ -8,7 +8,6 @@ import androidx.compose.foundation.combinedClickable
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.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@@ -26,10 +25,11 @@ import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.items.AlbumItem import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
@@ -54,7 +54,7 @@ fun ArtistOverview(
onViewAllAlbumsClick: () -> Unit, onViewAllAlbumsClick: () -> Unit,
onViewAllSinglesClick: () -> Unit, onViewAllSinglesClick: () -> Unit,
onAlbumClick: (String) -> Unit, onAlbumClick: (String) -> Unit,
thumbnailContent: @Composable ColumnScope.() -> Unit, thumbnailContent: @Composable () -> Unit,
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit, headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
) { ) {
val (colorPalette, typography) = LocalAppearance.current val (colorPalette, typography) = LocalAppearance.current
@@ -70,199 +70,202 @@ fun ArtistOverview(
.padding(horizontal = 16.dp) .padding(horizontal = 16.dp)
.padding(top = 24.dp, bottom = 8.dp) .padding(top = 24.dp, bottom = 8.dp)
Box { LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Column( Box {
modifier = Modifier Column(
.background(colorPalette.background0) horizontalAlignment = Alignment.CenterHorizontally,
.fillMaxSize() modifier = Modifier
.verticalScroll(rememberScrollState()) .background(colorPalette.background0)
.padding(LocalPlayerAwarePaddingValues.current) .fillMaxSize()
) { .verticalScroll(rememberScrollState())
headerContent { .padding(LocalPlayerAwarePaddingValues.current)
youtubeArtistPage?.radioEndpoint?.let { radioEndpoint -> ) {
SecondaryTextButton( headerContent {
text = "Start radio", youtubeArtistPage?.radioEndpoint?.let { radioEndpoint ->
onClick = { SecondaryTextButton(
binder?.stopRadio() text = "Start radio",
binder?.playRadio(radioEndpoint) onClick = {
} binder?.stopRadio()
) binder?.playRadio(radioEndpoint)
} }
}
thumbnailContent()
if (youtubeArtistPage != null) {
youtubeArtistPage.songs?.let { songs ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
) {
BasicText(
text = "Songs",
style = typography.m.semiBold,
modifier = sectionTextModifier
) )
youtubeArtistPage.songsEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
modifier = sectionTextModifier
.clickable(onClick = onViewAllSongsClick),
)
}
} }
}
songs.forEach { song -> thumbnailContent()
SongItem(
song = song, if (youtubeArtistPage != null) {
thumbnailSizeDp = songThumbnailSizeDp, youtubeArtistPage.songs?.let { songs ->
thumbnailSizePx = songThumbnailSizePx, Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier modifier = Modifier
.combinedClickable( .fillMaxSize()
onLongClick = { ) {
menuState.display { BasicText(
NonQueuedMediaItemMenu( text = "Songs",
onDismiss = menuState::hide, style = typography.m.semiBold,
mediaItem = song.asMediaItem, modifier = sectionTextModifier
)
youtubeArtistPage.songsEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
modifier = sectionTextModifier
.clickable(onClick = onViewAllSongsClick),
)
}
}
songs.forEach { song ->
SongItem(
song = song,
thumbnailSizeDp = songThumbnailSizeDp,
thumbnailSizePx = songThumbnailSizePx,
modifier = Modifier
.combinedClickable(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
)
}
},
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
) )
} }
}, )
onClick = { )
val mediaItem = song.asMediaItem }
binder?.stopRadio() }
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio( youtubeArtistPage.albums?.let { albums ->
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId) Row(
) verticalAlignment = Alignment.Bottom,
} horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
) {
BasicText(
text = "Albums",
style = typography.m.semiBold,
modifier = sectionTextModifier
)
youtubeArtistPage.albumsEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
modifier = sectionTextModifier
.clickable(onClick = onViewAllAlbumsClick),
) )
) }
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
) {
items(
items = albums,
key = Innertube.AlbumItem::key
) { album ->
AlbumItem(
album = album,
thumbnailSizePx = albumThumbnailSizePx,
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true,
modifier = Modifier
.clickable(onClick = { onAlbumClick(album.key) })
)
}
}
} }
}
youtubeArtistPage.albums?.let { albums -> youtubeArtistPage.singles?.let { singles ->
Row( Row(
verticalAlignment = Alignment.Bottom, verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
) { ) {
BasicText(
text = "Albums",
style = typography.m.semiBold,
modifier = sectionTextModifier
)
youtubeArtistPage.albumsEndpoint?.let {
BasicText( BasicText(
text = "View all", text = "Singles",
style = typography.xs.secondary, style = typography.m.semiBold,
modifier = sectionTextModifier modifier = sectionTextModifier
.clickable(onClick = onViewAllAlbumsClick),
) )
youtubeArtistPage.singlesEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
modifier = sectionTextModifier
.clickable(onClick = onViewAllSinglesClick),
)
}
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
) {
items(
items = singles,
key = Innertube.AlbumItem::key
) { album ->
AlbumItem(
album = album,
thumbnailSizePx = albumThumbnailSizePx,
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true,
modifier = Modifier
.clickable(onClick = { onAlbumClick(album.key) })
)
}
} }
} }
} else {
LazyRow( ShimmerHost {
modifier = Modifier
.fillMaxWidth()
) {
items(
items = albums,
key = Innertube.AlbumItem::key
) { album ->
AlbumItem(
album = album,
thumbnailSizePx = albumThumbnailSizePx,
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true,
modifier = Modifier
.clickable(onClick = { onAlbumClick(album.key) })
)
}
}
}
youtubeArtistPage.singles?.let { singles ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
) {
BasicText(
text = "Singles",
style = typography.m.semiBold,
modifier = sectionTextModifier
)
youtubeArtistPage.singlesEndpoint?.let {
BasicText(
text = "View all",
style = typography.xs.secondary,
modifier = sectionTextModifier
.clickable(onClick = onViewAllSinglesClick),
)
}
}
LazyRow(
modifier = Modifier
.fillMaxWidth()
) {
items(
items = singles,
key = Innertube.AlbumItem::key
) { album ->
AlbumItem(
album = album,
thumbnailSizePx = albumThumbnailSizePx,
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true,
modifier = Modifier
.clickable(onClick = { onAlbumClick(album.key) })
)
}
}
}
} else {
ShimmerHost {
TextPlaceholder(modifier = sectionTextModifier)
repeat(5) {
SongItemPlaceholder(
thumbnailSizeDp = songThumbnailSizeDp,
)
}
repeat(2) {
TextPlaceholder(modifier = sectionTextModifier) TextPlaceholder(modifier = sectionTextModifier)
Row { repeat(5) {
repeat(2) { SongItemPlaceholder(
AlbumItemPlaceholder( thumbnailSizeDp = songThumbnailSizeDp,
thumbnailSizeDp = albumThumbnailSizeDp, )
alternative = true }
)
repeat(2) {
TextPlaceholder(modifier = sectionTextModifier)
Row {
repeat(2) {
AlbumItemPlaceholder(
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true
)
}
} }
} }
} }
} }
} }
}
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint -> youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
PrimaryButton( PrimaryButton(
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
onClick = { onClick = {
binder?.stopRadio() binder?.stopRadio()
binder?.playRadio(shuffleEndpoint) binder?.playRadio(shuffleEndpoint)
} }
) )
}
} }
} }
} }

View File

@@ -3,25 +3,16 @@ package it.vfsfitvnm.vimusic.ui.screens.artist
import android.content.Intent import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveableStateHolder import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
@@ -40,6 +31,7 @@ import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
import it.vfsfitvnm.vimusic.ui.items.AlbumItem import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
@@ -50,13 +42,11 @@ import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
import it.vfsfitvnm.vimusic.ui.styling.Dimensions import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.rememberPreference import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
@@ -123,38 +113,12 @@ fun ArtistScreen(browseId: String) {
globalRoutes() globalRoutes()
host { host {
val thumbnailContent: @Composable ColumnScope.() -> Unit = { val thumbnailContent =
BoxWithConstraints( adaptiveThumbnailContent(
contentAlignment = Alignment.Center, artist?.timestamp == null,
modifier = Modifier artist?.thumbnailUrl,
.fillMaxWidth() CircleShape
) { )
val thumbnailSizeDp = maxWidth - 64.dp
val thumbnailSizePx = thumbnailSizeDp.px
if (artist?.timestamp == null) {
val (colorPalette) = LocalAppearance.current
Spacer(
modifier = Modifier
.padding(all = 16.dp)
.shimmer()
.clip(CircleShape)
.size(thumbnailSizeDp)
.background(colorPalette.shimmer)
)
} else {
AsyncImage(
model = artist?.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.padding(all = 16.dp)
.clip(CircleShape)
.size(thumbnailSizeDp)
)
}
}
}
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit =
{ textButton -> { textButton ->

View File

@@ -5,30 +5,18 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.combinedClickable
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.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize 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.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.autoSaver import androidx.compose.runtime.saveable.autoSaver
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.vimusic.Database import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
@@ -37,29 +25,30 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Playlist import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver
import it.vfsfitvnm.vimusic.savers.resultSaver import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.transaction import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.Header import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton import it.vfsfitvnm.vimusic.ui.components.themed.HeaderIconButton
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
import it.vfsfitvnm.vimusic.ui.components.themed.LayoutWithAdaptiveThumbnail
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder import it.vfsfitvnm.vimusic.ui.components.themed.adaptiveThumbnailContent
import it.vfsfitvnm.vimusic.ui.items.SongItem import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
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
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.asMediaItem import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.completed import it.vfsfitvnm.vimusic.utils.completed
import it.vfsfitvnm.vimusic.utils.enqueue import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.produceSaveableState import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.Innertube import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.playlistPage import it.vfsfitvnm.youtubemusic.requests.playlistPage
@@ -78,14 +67,14 @@ fun PlaylistSongList(
val context = LocalContext.current val context = LocalContext.current
val menuState = LocalMenuState.current val menuState = LocalMenuState.current
val playlistPageResult by produceSaveableState( val playlistPage by produceSaveableState(
initialValue = null, initialValue = null,
stateSaver = resultSaver(InnertubePlaylistOrAlbumPageSaver), stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver),
) { ) {
if (value != null && value?.getOrNull()?.songsPage?.continuation == null) return@produceSaveableState if (value != null && value?.songsPage?.continuation == null) return@produceSaveableState
value = withContext(Dispatchers.IO) { value = withContext(Dispatchers.IO) {
Innertube.playlistPage(BrowseBody(browseId = browseId))?.completed() Innertube.playlistPage(BrowseBody(browseId = browseId))?.completed()?.getOrNull()
} }
} }
@@ -99,17 +88,82 @@ fun PlaylistSongList(
.collect { value = it } .collect { value = it }
} }
BoxWithConstraints( val songThumbnailSizeDp = Dimensions.thumbnails.song
modifier = Modifier val songThumbnailSizePx = songThumbnailSizeDp.px
.fillMaxWidth()
) {
val thumbnailSizeDp = maxWidth - 64.dp
val thumbnailSizePx = thumbnailSizeDp.px
val songThumbnailSizeDp = Dimensions.thumbnails.song val headerContent: @Composable () -> Unit = {
val songThumbnailSizePx = songThumbnailSizeDp.px if (playlistPage == null) {
HeaderPlaceholder(
modifier = Modifier
.shimmer()
)
} else {
Header(title = playlistPage?.title ?: "Unknown") {
SecondaryTextButton(
text = "Enqueue",
isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true,
onClick = {
playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.player?.enqueue(mediaItems)
}
}
)
playlistPageResult?.getOrNull()?.let { playlist -> Spacer(
modifier = Modifier
.weight(1f)
)
HeaderIconButton(
icon = if (isImported == true) R.drawable.bookmark else R.drawable.bookmark_outline,
color = colorPalette.accent,
onClick = {
transaction {
val playlistId =
Database.insert(
Playlist(
name = playlistPage?.title ?: "Unknown",
browseId = browseId
)
)
playlistPage?.songsPage?.items
?.map(Innertube.SongItem::asMediaItem)
?.onEach(Database::insert)
?.mapIndexed { index, mediaItem ->
SongPlaylistMap(
songId = mediaItem.mediaId,
playlistId = playlistId,
position = index
)
}?.let(Database::insertSongPlaylistMaps)
}
}
)
HeaderIconButton(
icon = R.drawable.share_social,
color = colorPalette.text,
onClick = {
(playlistPage?.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))
}
}
)
}
}
}
val thumbnailContent = adaptiveThumbnailContent(playlistPage == null, playlistPage?.thumbnail?.url)
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box {
LazyColumn( LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current, contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier modifier = Modifier
@@ -120,79 +174,13 @@ fun PlaylistSongList(
key = "header", key = "header",
contentType = 0 contentType = 0
) { ) {
Column { Column(horizontalAlignment = Alignment.CenterHorizontally) {
Header(title = playlist.title ?: "Unknown") { headerContent()
SecondaryTextButton( if (!isLandscape) thumbnailContent()
text = "Enqueue",
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true,
onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.player?.enqueue(mediaItems)
}
}
)
Spacer(
modifier = Modifier
.weight(1f)
)
HeaderIconButton(
icon = if (isImported == true) R.drawable.bookmark else R.drawable.bookmark_outline,
color = colorPalette.accent,
onClick = {
transaction {
val playlistId =
Database.insert(
Playlist(
name = playlist.title ?: "Unknown",
browseId = browseId
)
)
playlist.songsPage?.items
?.map(Innertube.SongItem::asMediaItem)
?.onEach(Database::insert)
?.mapIndexed { index, mediaItem ->
SongPlaylistMap(
songId = mediaItem.mediaId,
playlistId = playlistId,
position = index
)
}?.let(Database::insertSongPlaylistMaps)
}
}
)
HeaderIconButton(
icon = R.drawable.share_social,
color = colorPalette.text,
onClick = {
(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))
}
}
)
}
AsyncImage(
model = playlist.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.padding(all = 16.dp)
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
} }
} }
itemsIndexed(items = playlist.songsPage?.items ?: emptyList()) { index, song -> itemsIndexed(items = playlistPage?.songsPage?.items ?: emptyList()) { index, song ->
SongItem( SongItem(
song = song, song = song,
thumbnailSizePx = songThumbnailSizePx, thumbnailSizePx = songThumbnailSizePx,
@@ -202,13 +190,13 @@ fun PlaylistSongList(
onLongClick = { onLongClick = {
menuState.display { menuState.display {
NonQueuedMediaItemMenu( NonQueuedMediaItemMenu(
onDismiss = menuState::hide, onDismiss = menuState::hide,
mediaItem = song.asMediaItem, mediaItem = song.asMediaItem,
) )
} }
}, },
onClick = { onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index) binder?.player?.forcePlayAtIndex(mediaItems, index)
} }
@@ -216,69 +204,31 @@ fun PlaylistSongList(
) )
) )
} }
if (playlistPage == null) {
item(key = "loading") {
ShimmerHost(
modifier = Modifier
.fillParentMaxSize()
) {
repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = songThumbnailSizeDp)
}
}
}
}
} }
PrimaryButton( PrimaryButton(
iconId = R.drawable.shuffle, iconId = R.drawable.shuffle,
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true, isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true,
onClick = { onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems -> playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio() binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(mediaItems.shuffled()) binder?.player?.forcePlayFromBeginning(mediaItems.shuffled())
} }
} }
) )
} ?: playlistPageResult?.exceptionOrNull()?.let {
Box(
modifier = Modifier
.align(Alignment.Center)
.fillMaxSize()
) {
BasicText(
text = "An error has occurred.\nTap to retry",
style = typography.s.secondary.center,
modifier = Modifier
.align(Alignment.Center)
)
}
} ?: Column(
modifier = Modifier
.padding(LocalPlayerAwarePaddingValues.current)
.shimmer()
.fillMaxSize()
) {
HeaderPlaceholder()
Spacer(
modifier = Modifier
.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()
}
}
}
} }
} }
} }

View File

@@ -0,0 +1,11 @@
package it.vfsfitvnm.vimusic.utils
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.platform.LocalConfiguration
val isLandscape
@Composable
@ReadOnlyComposable
get() = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE