From 78c44988d78d72345a09b977f099754a9dcf5ee1 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Thu, 6 Oct 2022 11:30:43 +0200 Subject: [PATCH] Improve UI in landscape mode --- .../themed/LayoutWithAdaptiveThumbnail.kt | 71 ++++ .../ui/components/themed/NavigationRail.kt | 6 +- .../vimusic/ui/screens/album/AlbumScreen.kt | 157 ++++---- .../vimusic/ui/screens/album/AlbumSongs.kt | 166 +++++---- .../ui/screens/artist/ArtistLocalSongs.kt | 138 +++---- .../ui/screens/artist/ArtistOverview.kt | 351 +++++++++--------- .../vimusic/ui/screens/artist/ArtistScreen.kt | 50 +-- .../ui/screens/playlist/PlaylistSongList.kt | 264 ++++++------- .../vfsfitvnm/vimusic/utils/Configuration.kt | 11 + 9 files changed, 596 insertions(+), 618 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LayoutWithAdaptiveThumbnail.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Configuration.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LayoutWithAdaptiveThumbnail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LayoutWithAdaptiveThumbnail.kt new file mode 100644 index 0000000..be6a99d --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/LayoutWithAdaptiveThumbnail.kt @@ -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 + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt index c3a34d8..e6bfaf3 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/components/themed/NavigationRail.kt @@ -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, diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index d6e8f36..6401e4f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -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 diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt index 5401d55..4828f06 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt @@ -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) + ) + } + ) + } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt index 916f6b5..39dbb6f 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt @@ -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) + ) + } + ) + } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt index df59160..d14e058 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -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) + } + ) + } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index bb616a6..4d60a23 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -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 -> diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 85a72bd..54addbc 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -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() - } - } - } } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Configuration.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Configuration.kt new file mode 100644 index 0000000..6011a8f --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/utils/Configuration.kt @@ -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