Improve UI in landscape mode
This commit is contained in:
parent
8fd402b5ce
commit
78c44988d7
9 changed files with 596 additions and 618 deletions
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ->
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
Loading…
Reference in a new issue