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

View file

@ -3,25 +3,16 @@ package it.vfsfitvnm.vimusic.ui.screens.album
import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
@ -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.HeaderPlaceholder
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.AlbumItemPlaceholder
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.styling.LocalAppearance
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.produceSaveableState
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.albumPage
@ -81,7 +71,11 @@ fun AlbumScreen(browseId: String) {
stateSaver = nullableSaver(InnertubePlaylistOrAlbumPageSaver),
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) {
Innertube.albumPage(BrowseBody(browseId = browseId))
@ -121,94 +115,70 @@ fun AlbumScreen(browseId: String) {
globalRoutes()
host {
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit = { textButton ->
if (album?.timestamp == null) {
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
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit =
{ textButton ->
if (album?.timestamp == null) {
Spacer(
HeaderPlaceholder(
modifier = Modifier
.padding(all = 16.dp)
.shimmer()
.clip(thumbnailShape)
.size(thumbnailSizeDp)
.background(colorPalette.shimmer)
)
} else {
AsyncImage(
model = album?.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.padding(all = 16.dp)
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
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 =
adaptiveThumbnailContent(album?.timestamp == null, album?.thumbnailUrl)
Scaffold(
topIconButtonId = R.drawable.chevron_back,
@ -227,6 +197,7 @@ fun AlbumScreen(browseId: String) {
headerContent = headerContent,
thumbnailContent = thumbnailContent,
)
1 -> {
val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px

View file

@ -6,7 +6,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
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.savers.DetailedSongListSaver
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.PrimaryButton
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.SongItemPlaceholder
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.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.semiBold
import kotlinx.coroutines.Dispatchers
@ -49,7 +50,7 @@ import kotlinx.coroutines.flow.flowOn
fun AlbumSongs(
browseId: String,
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
thumbnailContent: @Composable ColumnScope.() -> Unit,
thumbnailContent: @Composable () -> Unit,
) {
val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
@ -67,93 +68,100 @@ fun AlbumSongs(
val thumbnailSizeDp = Dimensions.thumbnails.song
Box {
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
) {
item(
key = "header",
contentType = 0
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box {
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
) {
Column {
headerContent {
SecondaryTextButton(
text = "Enqueue",
isEnabled = songs.isNotEmpty(),
onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
}
)
item(
key = "header",
contentType = 0
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
headerContent {
SecondaryTextButton(
text = "Enqueue",
isEnabled = songs.isNotEmpty(),
onClick = {
binder?.player?.enqueue(songs.map(DetailedSong::asMediaItem))
}
)
}
if (!isLandscape) {
thumbnailContent()
}
}
thumbnailContent()
}
}
itemsIndexed(
items = songs,
key = { _, song -> song.id }
) { index, song ->
SongItem(
title = song.title,
authors = song.artistsText,
duration = song.durationText,
thumbnailSizeDp = thumbnailSizeDp,
thumbnailContent = {
BasicText(
text = "${index + 1}",
style = typography.s.semiBold.center.color(colorPalette.textDisabled),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.width(thumbnailSizeDp)
.align(Alignment.Center)
)
},
modifier = Modifier
.combinedClickable(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
itemsIndexed(
items = songs,
key = { _, song -> song.id }
) { index, song ->
SongItem(
title = song.title,
authors = song.artistsText,
duration = song.durationText,
thumbnailSizeDp = thumbnailSizeDp,
thumbnailContent = {
BasicText(
text = "${index + 1}",
style = typography.s.semiBold.center.color(colorPalette.textDisabled),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.width(thumbnailSizeDp)
.align(Alignment.Center)
)
},
modifier = Modifier
.combinedClickable(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
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()) {
item(key = "loading") {
ShimmerHost(
modifier = Modifier
.fillParentMaxSize()
) {
repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
if (songs.isEmpty()) {
item(key = "loading") {
ShimmerHost(
modifier = Modifier
.fillParentMaxSize()
) {
repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
}
}
}
}
}
}
PrimaryButton(
iconId = R.drawable.shuffle,
isEnabled = songs.isNotEmpty(),
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(
songs.shuffled().map(DetailedSong::asMediaItem)
)
}
)
PrimaryButton(
iconId = R.drawable.shuffle,
isEnabled = songs.isNotEmpty(),
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(
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.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
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.nullableSaver
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.PrimaryButton
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.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
@ -44,7 +44,7 @@ import kotlinx.coroutines.flow.flowOn
fun ArtistLocalSongs(
browseId: String,
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
thumbnailContent: @Composable ColumnScope.() -> Unit,
thumbnailContent: @Composable () -> Unit,
) {
val binder = LocalPlayerServiceBinder.current
val (colorPalette) = LocalAppearance.current
@ -63,79 +63,81 @@ fun ArtistLocalSongs(
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
Box {
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
) {
item(
key = "header",
contentType = 0
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box {
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
) {
Column {
headerContent {
SecondaryTextButton(
text = "Enqueue",
isEnabled = !songs.isNullOrEmpty(),
onClick = {
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
}
)
}
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,
)
}
},
item(
key = "header",
contentType = 0
) {
Column {
headerContent {
SecondaryTextButton(
text = "Enqueue",
isEnabled = !songs.isNullOrEmpty(),
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
binder?.player?.enqueue(songs!!.map(DetailedSong::asMediaItem))
}
)
)
}
thumbnailContent()
}
}
} ?: item(key = "loading") {
ShimmerHost {
repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
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 = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
}
)
)
}
} ?: item(key = "loading") {
ShimmerHost {
repeat(4) {
SongItemPlaceholder(thumbnailSizeDp = Dimensions.thumbnails.song)
}
}
}
}
}
PrimaryButton(
iconId = R.drawable.shuffle,
isEnabled = !songs.isNullOrEmpty(),
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(
songs!!.shuffled().map(DetailedSong::asMediaItem)
)
}
)
PrimaryButton(
iconId = R.drawable.shuffle,
isEnabled = !songs.isNullOrEmpty(),
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayFromBeginning(
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.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@ -26,10 +25,11 @@ import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
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.PrimaryButton
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.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
@ -54,7 +54,7 @@ fun ArtistOverview(
onViewAllAlbumsClick: () -> Unit,
onViewAllSinglesClick: () -> Unit,
onAlbumClick: (String) -> Unit,
thumbnailContent: @Composable ColumnScope.() -> Unit,
thumbnailContent: @Composable () -> Unit,
headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit,
) {
val (colorPalette, typography) = LocalAppearance.current
@ -70,199 +70,202 @@ fun ArtistOverview(
.padding(horizontal = 16.dp)
.padding(top = 24.dp, bottom = 8.dp)
Box {
Column(
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
headerContent {
youtubeArtistPage?.radioEndpoint?.let { radioEndpoint ->
SecondaryTextButton(
text = "Start radio",
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
LayoutWithAdaptiveThumbnail(thumbnailContent = thumbnailContent) {
Box {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.background(colorPalette.background0)
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(LocalPlayerAwarePaddingValues.current)
) {
headerContent {
youtubeArtistPage?.radioEndpoint?.let { radioEndpoint ->
SecondaryTextButton(
text = "Start radio",
onClick = {
binder?.stopRadio()
binder?.playRadio(radioEndpoint)
}
)
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,
thumbnailContent()
if (youtubeArtistPage != null) {
youtubeArtistPage.songs?.let { songs ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.combinedClickable(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
.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 ->
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(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
}
)
)
}
}
youtubeArtistPage.albums?.let { albums ->
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 ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
) {
BasicText(
text = "Albums",
style = typography.m.semiBold,
modifier = sectionTextModifier
)
youtubeArtistPage.albumsEndpoint?.let {
youtubeArtistPage.singles?.let { singles ->
Row(
verticalAlignment = Alignment.Bottom,
horizontalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.fillMaxSize()
) {
BasicText(
text = "View all",
style = typography.xs.secondary,
text = "Singles",
style = typography.m.semiBold,
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) })
)
}
}
}
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.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) {
} else {
ShimmerHost {
TextPlaceholder(modifier = sectionTextModifier)
Row {
repeat(2) {
AlbumItemPlaceholder(
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true
)
repeat(5) {
SongItemPlaceholder(
thumbnailSizeDp = songThumbnailSizeDp,
)
}
repeat(2) {
TextPlaceholder(modifier = sectionTextModifier)
Row {
repeat(2) {
AlbumItemPlaceholder(
thumbnailSizeDp = albumThumbnailSizeDp,
alternative = true
)
}
}
}
}
}
}
}
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
PrimaryButton(
iconId = R.drawable.shuffle,
onClick = {
binder?.stopRadio()
binder?.playRadio(shuffleEndpoint)
}
)
youtubeArtistPage?.shuffleEndpoint?.let { shuffleEndpoint ->
PrimaryButton(
iconId = R.drawable.shuffle,
onClick = {
binder?.stopRadio()
binder?.playRadio(shuffleEndpoint)
}
)
}
}
}
}

View file

@ -3,25 +3,16 @@ package it.vfsfitvnm.vimusic.ui.screens.artist
import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.saveable.rememberSaveableStateHolder
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.valentinilk.shimmer.shimmer
import it.vfsfitvnm.route.RouteHandler
import it.vfsfitvnm.vimusic.Database
@ -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.NonQueuedMediaItemMenu
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.AlbumItemPlaceholder
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.LocalAppearance
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.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.models.bodies.ContinuationBody
@ -123,38 +113,12 @@ fun ArtistScreen(browseId: String) {
globalRoutes()
host {
val thumbnailContent: @Composable ColumnScope.() -> Unit = {
BoxWithConstraints(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
) {
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 thumbnailContent =
adaptiveThumbnailContent(
artist?.timestamp == null,
artist?.thumbnailUrl,
CircleShape
)
val headerContent: @Composable (textButton: (@Composable () -> Unit)?) -> Unit =
{ textButton ->

View file

@ -5,30 +5,18 @@ import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
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.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.platform.LocalContext
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
@ -37,29 +25,30 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.models.Playlist
import it.vfsfitvnm.vimusic.models.SongPlaylistMap
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.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.HeaderIconButton
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.PrimaryButton
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.SongItemPlaceholder
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.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.completed
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
import it.vfsfitvnm.vimusic.utils.forcePlayFromBeginning
import it.vfsfitvnm.vimusic.utils.isLandscape
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.bodies.BrowseBody
import it.vfsfitvnm.youtubemusic.requests.playlistPage
@ -78,14 +67,14 @@ fun PlaylistSongList(
val context = LocalContext.current
val menuState = LocalMenuState.current
val playlistPageResult by produceSaveableState(
val playlistPage by produceSaveableState(
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) {
Innertube.playlistPage(BrowseBody(browseId = browseId))?.completed()
Innertube.playlistPage(BrowseBody(browseId = browseId))?.completed()?.getOrNull()
}
}
@ -99,17 +88,82 @@ fun PlaylistSongList(
.collect { value = it }
}
BoxWithConstraints(
modifier = Modifier
.fillMaxWidth()
) {
val thumbnailSizeDp = maxWidth - 64.dp
val thumbnailSizePx = thumbnailSizeDp.px
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
val headerContent: @Composable () -> Unit = {
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(
contentPadding = LocalPlayerAwarePaddingValues.current,
modifier = Modifier
@ -120,79 +174,13 @@ fun PlaylistSongList(
key = "header",
contentType = 0
) {
Column {
Header(title = playlist.title ?: "Unknown") {
SecondaryTextButton(
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)
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
headerContent()
if (!isLandscape) thumbnailContent()
}
}
itemsIndexed(items = playlist.songsPage?.items ?: emptyList()) { index, song ->
itemsIndexed(items = playlistPage?.songsPage?.items ?: emptyList()) { index, song ->
SongItem(
song = song,
thumbnailSizePx = songThumbnailSizePx,
@ -202,13 +190,13 @@ fun PlaylistSongList(
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
)
onDismiss = menuState::hide,
mediaItem = song.asMediaItem,
)
}
},
onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio()
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(
iconId = R.drawable.shuffle,
isEnabled = playlist.songsPage?.items?.isNotEmpty() == true,
isEnabled = playlistPage?.songsPage?.items?.isNotEmpty() == true,
onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
playlistPage?.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio()
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