Tweak code

This commit is contained in:
vfsfitvnm 2022-10-04 11:37:10 +02:00
parent b7046e3217
commit d2ce356d10
25 changed files with 1468 additions and 1210 deletions

View file

@ -0,0 +1,155 @@
package it.vfsfitvnm.vimusic.ui.items
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
@Composable
fun AlbumItem(
album: Album,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false
) {
AlbumItem(
thumbnailUrl = album.thumbnailUrl,
title = album.title,
authors = album.authorsText,
year = album.year,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
alternative = alternative,
modifier = modifier
)
}
@Composable
fun AlbumItem(
album: Innertube.AlbumItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false
) {
AlbumItem(
thumbnailUrl = album.thumbnail?.url,
title = album.info?.name,
authors = album.authors?.joinToString("") { it.name ?: "" },
year = album.year,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
alternative = alternative,
modifier = modifier
)
}
@Composable
fun AlbumItem(
thumbnailUrl: String?,
title: String?,
authors: String?,
year: String?,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false
) {
val (_, typography, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
AsyncImage(
model = thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer {
BasicText(
text = title ?: "",
style = typography.xs.semiBold,
maxLines = if (alternative) 1 else 2,
overflow = TextOverflow.Ellipsis,
)
if (!alternative) {
authors?.let {
BasicText(
text = authors,
style = typography.xs.semiBold.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
BasicText(
text = year ?: "",
style = typography.xxs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
@Composable
fun AlbumItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer {
TextPlaceholder()
if (!alternative) {
TextPlaceholder()
}
TextPlaceholder(
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}

View file

@ -0,0 +1,145 @@
package it.vfsfitvnm.vimusic.ui.items
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
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.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
@Composable
fun ArtistItem(
artist: Artist,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
ArtistItem(
thumbnailUrl = artist.thumbnailUrl,
name = artist.name,
subscribersCount = null,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
alternative = alternative
)
}
@Composable
fun ArtistItem(
artist: Innertube.ArtistItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
ArtistItem(
thumbnailUrl = artist.thumbnail?.url,
name = artist.info?.name,
subscribersCount = artist.subscribersCountText,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
alternative = alternative
)
}
@Composable
fun ArtistItem(
thumbnailUrl: String?,
name: String?,
subscribersCount: String?,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (_, typography) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
AsyncImage(
model = thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.requiredSize(thumbnailSizeDp)
)
ItemInfoContainer(
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
) {
BasicText(
text = name ?: "",
style = typography.xs.semiBold,
maxLines = if (alternative) 1 else 2,
overflow = TextOverflow.Ellipsis
)
subscribersCount?.let {
BasicText(
text = subscribersCount,
style = typography.xxs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
}
@Composable
fun ArtistItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (colorPalette) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = CircleShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer(
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
) {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}

View file

@ -0,0 +1,68 @@
package it.vfsfitvnm.vimusic.ui.items
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
@Composable
inline fun ItemContainer(
alternative: Boolean,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
content: @Composable (centeredModifier: Modifier) -> Unit
) {
if (alternative) {
Column(
horizontalAlignment = horizontalAlignment,
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.width(thumbnailSizeDp)
) {
content(
centeredModifier = Modifier
.align(Alignment.CenterHorizontally)
)
}
} else {
Row(
verticalAlignment = verticalAlignment,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.fillMaxWidth()
) {
content(
centeredModifier = Modifier
.align(Alignment.CenterVertically)
)
}
}
}
@Composable
inline fun ItemInfoContainer(
modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
Column(
horizontalAlignment = horizontalAlignment,
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
content = content
)
}

View file

@ -0,0 +1,274 @@
package it.vfsfitvnm.vimusic.ui.items
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.overlay
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@Composable
fun PlaylistItem(
@DrawableRes icon: Int,
colorTint: Color,
name: String?,
songCount: Int?,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
PlaylistItem(
thumbnailContent = {
Image(
painter = painterResource(icon),
contentDescription = null,
colorFilter = ColorFilter.tint(colorTint),
modifier = Modifier
.align(Alignment.Center)
.size(24.dp)
)
},
songCount = songCount,
name = name,
channelName = null,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
alternative = alternative
)
}
@Composable
fun PlaylistItem(
playlist: PlaylistPreview,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val thumbnails by remember {
Database.playlistThumbnailUrls(playlist.playlist.id).distinctUntilChanged().map {
it.map { url ->
url.thumbnail(thumbnailSizePx / 2)
}
}
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
PlaylistItem(
thumbnailContent = {
if (thumbnails.toSet().size == 1) {
AsyncImage(
model = thumbnails.first().thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = it
)
} else {
Box(
modifier = it
.fillMaxSize()
) {
listOf(
Alignment.TopStart,
Alignment.TopEnd,
Alignment.BottomStart,
Alignment.BottomEnd
).forEachIndexed { index, alignment ->
AsyncImage(
model = thumbnails.getOrNull(index),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.align(alignment)
.size(thumbnailSizeDp / 2)
)
}
}
}
},
songCount = playlist.songCount,
name = playlist.playlist.name,
channelName = null,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
alternative = alternative
)
}
@Composable
fun PlaylistItem(
playlist: Innertube.PlaylistItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
PlaylistItem(
thumbnailUrl = playlist.thumbnail?.url,
songCount = playlist.songCount,
name = playlist.info?.name,
channelName = playlist.channel?.name,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
alternative = alternative
)
}
@Composable
fun PlaylistItem(
thumbnailUrl: String?,
songCount: Int?,
name: String?,
channelName: String?,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
PlaylistItem(
thumbnailContent = {
AsyncImage(
model = thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = it
)
},
songCount = songCount,
name = name,
channelName = channelName,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
alternative = alternative,
)
}
@Composable
fun PlaylistItem(
thumbnailContent: @Composable BoxScope.(modifier: Modifier) -> Unit,
songCount: Int?,
name: String?,
channelName: String?,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) { centeredModifier ->
Box(
modifier = centeredModifier
.clip(thumbnailShape)
.background(color = colorPalette.background1)
.requiredSize(thumbnailSizeDp)
) {
thumbnailContent(
modifier = Modifier
.fillMaxSize()
)
songCount?.let {
BasicText(
text = "$songCount",
style = typography.xxs.medium.color(colorPalette.onOverlay),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(all = 4.dp)
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
.padding(horizontal = 4.dp, vertical = 2.dp)
.align(Alignment.BottomEnd)
)
}
}
ItemInfoContainer(
horizontalAlignment = if (alternative && channelName == null) Alignment.CenterHorizontally else Alignment.Start,
) {
BasicText(
text = name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
channelName?.let {
BasicText(
text = channelName,
style = typography.xs.semiBold.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
}
@Composable
fun PlaylistItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer(
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
) {
TextPlaceholder()
TextPlaceholder()
}
}
}

View file

@ -0,0 +1,204 @@
package it.vfsfitvnm.vimusic.ui.items
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
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.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
@Composable
fun SongItem(
song: Innertube.SongItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier
) {
SongItem(
thumbnailUrl = song.thumbnail?.size(thumbnailSizePx),
title = song.info?.name,
authors = song.authors?.joinToString("") { it.name ?: "" },
duration = song.durationText,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier,
)
}
@Composable
fun SongItem(
song: MediaItem,
thumbnailSizeDp: Dp,
thumbnailSizePx: Int,
modifier: Modifier = Modifier,
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
thumbnailUrl = song.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx)?.toString(),
title = song.mediaMetadata.title.toString(),
authors = song.mediaMetadata.artist.toString(),
duration = song.mediaMetadata.extras?.getString("durationText"),
thumbnailSizeDp = thumbnailSizeDp,
onThumbnailContent = onThumbnailContent,
trailingContent = trailingContent,
modifier = modifier,
)
}
@Composable
fun SongItem(
song: DetailedSong,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
thumbnailUrl = song.thumbnailUrl?.thumbnail(thumbnailSizePx),
title = song.title,
authors = song.artistsText,
duration = song.durationText,
thumbnailSizeDp = thumbnailSizeDp,
onThumbnailContent = onThumbnailContent,
trailingContent = trailingContent,
modifier = modifier,
)
}
@Composable
fun SongItem(
thumbnailUrl: String?,
title: String?,
authors: String?,
duration: String?,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
title = title,
authors = authors,
duration = duration,
thumbnailSizeDp = thumbnailSizeDp,
thumbnailContent = {
AsyncImage(
model = thumbnailUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(LocalAppearance.current.thumbnailShape)
.fillMaxSize()
)
onThumbnailContent?.invoke(this)
},
modifier = modifier,
trailingContent = trailingContent
)
}
@Composable
fun SongItem(
thumbnailContent: @Composable BoxScope.() -> Unit,
title: String?,
authors: String?,
duration: String?,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
trailingContent: @Composable (() -> Unit)? = null,
) {
val (_, typography) = LocalAppearance.current
ItemContainer(
alternative = false,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
Box(
modifier = Modifier
.size(thumbnailSizeDp)
) {
thumbnailContent()
}
ItemInfoContainer {
BasicText(
text = title ?: "",
style = typography.xs.semiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Row(verticalAlignment = Alignment.CenterVertically) {
BasicText(
text = authors ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Clip,
modifier = Modifier
.weight(1f)
)
duration?.let {
BasicText(
text = duration,
style = typography.xxs.secondary.medium,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
}
}
@Composable
fun SongItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = false,
thumbnailSizeDp =thumbnailSizeDp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer {
TextPlaceholder()
TextPlaceholder()
}
}
}

View file

@ -0,0 +1,149 @@
package it.vfsfitvnm.vimusic.ui.items
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
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.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.overlay
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.Innertube
@Composable
fun VideoItem(
video: Innertube.VideoItem,
thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
modifier: Modifier = Modifier
) {
VideoItem(
thumbnailUrl = video.thumbnail?.url,
duration = video.durationText,
title = video.info?.name,
uploader = video.authors?.joinToString("") { it.name ?: "" },
views = video.viewsText,
thumbnailHeightDp = thumbnailHeightDp,
thumbnailWidthDp = thumbnailWidthDp,
modifier = modifier
)
}
@Composable
fun VideoItem(
thumbnailUrl: String?,
duration: String?,
title: String?,
uploader: String?,
views: String?,
thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
modifier: Modifier = Modifier
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = false,
thumbnailSizeDp = 0.dp,
modifier = modifier
) {
Box {
AsyncImage(
model = thumbnailUrl,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
)
duration?.let {
BasicText(
text = duration,
style = typography.xxs.medium.color(colorPalette.onOverlay),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(all = 4.dp)
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
.padding(horizontal = 4.dp, vertical = 2.dp)
.align(Alignment.BottomEnd)
)
}
}
ItemInfoContainer {
BasicText(
text = title ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = uploader ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
views?.let {
BasicText(
text = views,
style = typography.xxs.medium.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
}
@Composable
fun VideoItemPlaceholder(
thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
modifier: Modifier = Modifier
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = false,
thumbnailSizeDp = 0.dp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
)
ItemInfoContainer {
TextPlaceholder()
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.padding(top = 8.dp)
)
}
}
}

View file

@ -51,8 +51,8 @@ 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.ui.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.thumbnail

View file

@ -3,6 +3,8 @@ package it.vfsfitvnm.vimusic.ui.screens.album
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
@ -11,8 +13,11 @@ import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import it.vfsfitvnm.vimusic.Database
@ -21,14 +26,15 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
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.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.ShimmerHost
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.color
@ -50,6 +56,9 @@ fun AlbumSongs(
) {
val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val songs by produceSaveableState(
initialValue = emptyList(),
@ -61,6 +70,8 @@ fun AlbumSongs(
.collect { value = it }
}
val thumbnailSizeDp = Dimensions.thumbnails.song
Box {
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
@ -94,27 +105,33 @@ fun AlbumSongs(
SongItem(
title = song.title,
authors = song.artistsText,
durationText = song.durationText,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
},
startContent = {
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(Dimensions.thumbnails.song)
.width(thumbnailSizeDp)
.align(Alignment.Center)
)
},
menuContent = {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
}
)
)
}

View file

@ -1,15 +1,20 @@
package it.vfsfitvnm.vimusic.ui.screens.artist
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
@ -18,15 +23,16 @@ import it.vfsfitvnm.vimusic.R
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.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.ShimmerHost
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.views.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
@ -35,6 +41,7 @@ import it.vfsfitvnm.vimusic.utils.produceSaveableState
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun ArtistLocalSongs(
@ -44,6 +51,9 @@ fun ArtistLocalSongs(
) {
val binder = LocalPlayerServiceBinder.current
val (colorPalette) = LocalAppearance.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val songs by produceSaveableState(
initialValue = null,
@ -55,7 +65,8 @@ fun ArtistLocalSongs(
.collect { value = it }
}
val songThumbnailSizePx = Dimensions.thumbnails.song.px
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
Box {
LazyColumn(
@ -90,17 +101,22 @@ fun ArtistLocalSongs(
) { index, song ->
SongItem(
song = song,
thumbnailSizeDp = songThumbnailSizeDp,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
}
)
},
menuContent = {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
)
}
} ?: item(key = "loading") {

View file

@ -1,8 +1,10 @@
package it.vfsfitvnm.vimusic.ui.screens.artist
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.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@ -26,17 +28,19 @@ import androidx.compose.ui.unit.dp
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.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.ShimmerHost
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
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.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.secondary
@ -44,6 +48,7 @@ import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun ArtistOverview(
@ -57,6 +62,9 @@ fun ArtistOverview(
) {
val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val songThumbnailSizeDp = Dimensions.thumbnails.song
val songThumbnailSizePx = songThumbnailSizeDp.px
@ -120,15 +128,26 @@ fun ArtistOverview(
songs.forEach { song ->
SongItem(
song = song,
thumbnailSizeDp = songThumbnailSizeDp,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
}
)
}
)
}
}

View file

@ -2,9 +2,12 @@ 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.Image
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.ColumnScope
@ -39,9 +42,15 @@ import it.vfsfitvnm.vimusic.savers.InnertubeAlbumsPageSaver
import it.vfsfitvnm.vimusic.savers.InnertubeArtistPageSaver
import it.vfsfitvnm.vimusic.savers.InnertubeSongsPageSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.Header
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.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.searchresult.ItemsPage
@ -49,10 +58,6 @@ 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.ui.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.utils.artistScreenTabIndexKey
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
@ -69,6 +74,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun ArtistScreen(browseId: String) {
@ -250,9 +256,12 @@ fun ArtistScreen(browseId: String) {
1 -> {
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
val rippleIndication = rememberRipple(bounded = true)
ItemsPage(
stateSaver = InnertubeSongsPageSaver,
headerContent = headerContent,
@ -284,12 +293,23 @@ fun ArtistScreen(browseId: String) {
itemContent = { song ->
SongItem(
song = song,
thumbnailSizeDp = thumbnailSizeDp,
thumbnailSizePx = thumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info?.endpoint)
}
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info?.endpoint)
}
)
)
},
itemPlaceholderContent = {

View file

@ -3,13 +3,17 @@ package it.vfsfitvnm.vimusic.ui.screens.builtinplaylist
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
@ -18,15 +22,16 @@ import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.enums.BuiltInPlaylist
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.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InFavoritesMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.enqueue
import it.vfsfitvnm.vimusic.utils.forcePlayAtIndex
@ -42,6 +47,9 @@ import kotlinx.coroutines.flow.map
fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
val (colorPalette) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val songs by produceSaveableState(
initialValue = emptyList(),
@ -64,7 +72,8 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
}.collect { value = it }
}
val thumbnailSize = Dimensions.thumbnails.song.px
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSize = thumbnailSizeDp.px
Box {
LazyColumn(
@ -105,21 +114,25 @@ fun BuiltInPlaylistSongs(builtInPlaylist: BuiltInPlaylist) {
) { index, song ->
SongItem(
song = song,
thumbnailSizeDp = thumbnailSizeDp,
thumbnailSizePx = thumbnailSize,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(
songs.map(DetailedSong::asMediaItem),
index
)
},
menuContent = {
when (builtInPlaylist) {
BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu(song = song)
BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(song = song)
}
},
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
when (builtInPlaylist) {
BuiltInPlaylist.Favorites -> InFavoritesMediaItemMenu(song = song)
BuiltInPlaylist.Offline -> InHistoryMediaItemMenu(song = song)
}
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(songs.map(DetailedSong::asMediaItem), index)
}
)
.animateItemPlacement()
)
}

View file

@ -10,33 +10,23 @@ import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
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.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
@ -45,6 +35,7 @@ import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Album
import it.vfsfitvnm.vimusic.savers.AlbumListSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
@ -52,9 +43,6 @@ import it.vfsfitvnm.vimusic.utils.albumSortByKey
import it.vfsfitvnm.vimusic.utils.albumSortOrderKey
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ -64,7 +52,7 @@ import kotlinx.coroutines.flow.flowOn
fun HomeAlbums(
onAlbumClick: (Album) -> Unit
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val (colorPalette) = LocalAppearance.current
var sortBy by rememberPreference(albumSortByKey, AlbumSortBy.DateAdded)
var sortOrder by rememberPreference(albumSortOrderKey, SortOrder.Descending)
@ -154,55 +142,18 @@ fun HomeAlbums(
items = items,
key = Album::id
) { album ->
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
AlbumItem(
album = album,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onAlbumClick(album) }
)
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.fillMaxWidth()
.animateItemPlacement()
) {
AsyncImage(
model = album.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
BasicText(
text = album.title ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = album.authorsText ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
album.year?.let { year ->
BasicText(
text = year,
style = typography.xxs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 8.dp)
)
}
}
}
)
}
}
}

View file

@ -11,20 +11,15 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
@ -32,13 +27,10 @@ import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.R
@ -47,16 +39,14 @@ import it.vfsfitvnm.vimusic.enums.SortOrder
import it.vfsfitvnm.vimusic.models.Artist
import it.vfsfitvnm.vimusic.savers.ArtistListSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.items.ArtistItem
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.utils.artistSortByKey
import it.vfsfitvnm.vimusic.utils.artistSortOrderKey
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.rememberPreference
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
@ -66,7 +56,7 @@ import kotlinx.coroutines.flow.flowOn
fun HomeArtistList(
onArtistClick: (Artist) -> Unit
) {
val (colorPalette, typography) = LocalAppearance.current
val (colorPalette) = LocalAppearance.current
var sortBy by rememberPreference(artistSortByKey, ArtistSortBy.DateAdded)
var sortOrder by rememberPreference(artistSortOrderKey, SortOrder.Descending)
@ -154,39 +144,21 @@ fun HomeArtistList(
}
}
items(
items = items,
key = Artist::id
) { artist ->
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
items(items = items, key = Artist::id) { artist ->
ArtistItem(
artist = artist,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
alternative = true,
modifier = Modifier
.requiredWidth(thumbnailSizeDp)
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onArtistClick(artist) }
)
// .requiredWidth(thumbnailSizeDp)
.animateItemPlacement()
) {
AsyncImage(
model = artist.thumbnailUrl?.thumbnail(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.clickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { onArtistClick(artist) }
)
.background(colorPalette.background1)
.align(Alignment.CenterHorizontally)
.requiredSize(thumbnailSizeDp),
)
BasicText(
text = artist.name ?: "",
style = typography.xxs.semiBold.center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
)
}
}
}

View file

@ -44,10 +44,10 @@ import it.vfsfitvnm.vimusic.savers.PlaylistPreviewListSaver
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.items.PlaylistItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.views.BuiltInPlaylistItem
import it.vfsfitvnm.vimusic.ui.views.PlaylistPreviewItem
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.utils.playlistSortByKey
import it.vfsfitvnm.vimusic.utils.playlistSortOrderKey
import it.vfsfitvnm.vimusic.utils.produceSaveableState
@ -100,6 +100,9 @@ fun HomePlaylists(
animationSpec = tween(durationMillis = 400, easing = LinearEasing)
)
val thumbnailSizeDp = 108.dp
val thumbnailSizePx = thumbnailSizeDp.px
LazyVerticalGrid(
columns = GridCells.Adaptive(Dimensions.thumbnails.song * 2 + Dimensions.itemsVerticalPadding * 2),
contentPadding = LocalPlayerAwarePaddingValues.current,
@ -112,11 +115,7 @@ fun HomePlaylists(
.fillMaxSize()
.background(colorPalette.background0)
) {
item(
key = "header",
contentType = 0,
span = { GridItemSpan(maxLineSpan) }
) {
item(key = "header", contentType = 0, span = { GridItemSpan(maxLineSpan) }) {
Header(title = "Playlists") {
@Composable
fun Item(
@ -178,24 +177,31 @@ fun HomePlaylists(
}
item(key = "favorites") {
BuiltInPlaylistItem(
PlaylistItem(
icon = R.drawable.heart,
colorTint = colorPalette.red,
name = "Favorites",
songCount = null,
thumbnailSizeDp = thumbnailSizeDp,
alternative = true,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onClick = { onBuiltInPlaylist(BuiltInPlaylist.Favorites) }
)
.animateItemPlacement()
)
}
item(key = "offline") {
BuiltInPlaylistItem(
PlaylistItem(
icon = R.drawable.airplane,
colorTint = colorPalette.blue,
name = "Offline",
songCount = null,
thumbnailSizeDp = thumbnailSizeDp,
alternative = true,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
@ -206,12 +212,12 @@ fun HomePlaylists(
)
}
items(
items = items,
key = { it.playlist.id }
) { playlistPreview ->
PlaylistPreviewItem(
playlistPreview = playlistPreview,
items(items = items, key = { it.playlist.id }) { playlistPreview ->
PlaylistItem(
playlist = playlistPreview,
thumbnailSizeDp = thumbnailSizeDp,
thumbnailSizePx = thumbnailSizePx,
alternative = true,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),

View file

@ -1,17 +1,16 @@
package it.vfsfitvnm.vimusic.ui.screens.home
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -24,8 +23,10 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -44,13 +45,16 @@ import it.vfsfitvnm.vimusic.enums.SongSortBy
import it.vfsfitvnm.vimusic.enums.SortOrder
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.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.ScrollToTop
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.overlay
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.color
@ -69,8 +73,12 @@ import kotlinx.coroutines.flow.flowOn
fun HomeSongs() {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val thumbnailSize = Dimensions.thumbnails.song.px
val rippleIndication = rememberRipple(bounded = true)
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
var sortBy by rememberPreference(songSortByKey, SongSortBy.DateAdded)
var sortOrder by rememberPreference(songSortOrderKey, SortOrder.Descending)
@ -162,46 +170,40 @@ fun HomeSongs() {
) { index, song ->
SongItem(
song = song,
thumbnailSizePx = thumbnailSize,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)
},
menuContent = {
InHistoryMediaItemMenu(song = song)
},
onThumbnailContent = {
AnimatedVisibility(
visible = sortBy == SongSortBy.PlayTime,
enter = fadeIn(),
exit = fadeOut(),
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
onThumbnailContent = if (sortBy == SongSortBy.PlayTime) ({
BasicText(
text = song.formattedTotalPlayTime,
style = typography.xxs.semiBold.center.color(colorPalette.onOverlay),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(Color.Transparent, colorPalette.overlay)
),
shape = thumbnailShape
)
.padding(horizontal = 8.dp, vertical = 4.dp)
.align(Alignment.BottomCenter)
) {
BasicText(
text = song.formattedTotalPlayTime,
style = typography.xxs.semiBold.center.color(Color.White),
maxLines = 2,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.fillMaxWidth()
.background(
brush = Brush.verticalGradient(
colors = listOf(
Color.Transparent,
Color.Black.copy(alpha = 0.75f)
)
),
shape = thumbnailShape
)
.padding(
horizontal = 8.dp,
vertical = 4.dp
)
)
}
},
)
}) else null,
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
InHistoryMediaItemMenu(song = song)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(items.map(DetailedSong::asMediaItem), index)
}
)
.animateItemPlacement()
)
}

View file

@ -1,8 +1,10 @@
package it.vfsfitvnm.vimusic.ui.screens.home
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.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.Column
@ -40,20 +42,21 @@ import it.vfsfitvnm.vimusic.savers.DetailedSongSaver
import it.vfsfitvnm.vimusic.savers.InnertubeRelatedPageSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.ArtistItem
import it.vfsfitvnm.vimusic.ui.items.ArtistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.PlaylistItem
import it.vfsfitvnm.vimusic.ui.items.PlaylistItemPlaceholder
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.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.ArtistItem
import it.vfsfitvnm.vimusic.ui.views.ArtistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.PlaylistItem
import it.vfsfitvnm.vimusic.ui.views.PlaylistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.forcePlay
@ -61,7 +64,6 @@ import it.vfsfitvnm.vimusic.utils.produceSaveableOneShotState
import it.vfsfitvnm.vimusic.utils.produceSaveableState
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import it.vfsfitvnm.youtubemusic.Innertube
import it.vfsfitvnm.youtubemusic.models.NavigationEndpoint
import it.vfsfitvnm.youtubemusic.models.bodies.NextBody
@ -71,6 +73,7 @@ import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun QuickPicks(
@ -80,6 +83,9 @@ fun QuickPicks(
) {
val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val trending by produceSaveableState(
initialValue = null,
@ -135,20 +141,28 @@ fun QuickPicks(
trending?.let { song ->
item {
SongItem(
thumbnailModel = song.thumbnailUrl?.thumbnail(songThumbnailSizePx),
title = song.title,
authors = song.artistsText,
durationText = null,
menuContent = { NonQueuedMediaItemMenu(mediaItem = song.asMediaItem) },
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
song = song,
thumbnailSizePx = songThumbnailSizePx,
thumbnailSizeDp = songThumbnailSizeDp,
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
}
)
.animateItemPlacement()
.width(itemInHorizontalGridWidth)
)
}
@ -161,15 +175,26 @@ fun QuickPicks(
SongItem(
song = song,
thumbnailSizePx = songThumbnailSizePx,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
thumbnailSizeDp = songThumbnailSizeDp,
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
}
)
.animateItemPlacement()
.width(itemInHorizontalGridWidth)
)
}

View file

@ -5,6 +5,8 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
@ -12,9 +14,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
@ -36,16 +40,17 @@ import it.vfsfitvnm.vimusic.query
import it.vfsfitvnm.vimusic.savers.PlaylistWithSongsSaver
import it.vfsfitvnm.vimusic.savers.nullableSaver
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.ConfirmationDialog
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InPlaylistMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.PrimaryButton
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.components.themed.TextFieldDialog
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.completed
import it.vfsfitvnm.vimusic.utils.enqueue
@ -69,6 +74,9 @@ fun LocalPlaylistSongs(
) {
val (colorPalette) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val playlistWithSongs by produceSaveableState(
initialValue = null,
@ -127,7 +135,8 @@ fun LocalPlaylistSongs(
)
}
val thumbnailSize = Dimensions.thumbnails.song.px
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
Box {
ReorderingLazyColumn(
@ -224,21 +233,8 @@ fun LocalPlaylistSongs(
) { index, song ->
SongItem(
song = song,
thumbnailSizePx = thumbnailSize,
onClick = {
playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
?.let { mediaItems ->
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
},
menuContent = {
InPlaylistMediaItemMenu(
playlistId = playlistId,
positionInPlaylist = index,
song = song
)
},
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
trailingContent = {
Image(
painter = painterResource(R.drawable.reorder),
@ -255,6 +251,26 @@ fun LocalPlaylistSongs(
)
},
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
InPlaylistMediaItemMenu(
playlistId = playlistId,
positionInPlaylist = index,
song = song
)
}
},
onClick = {
playlistWithSongs?.songs?.map(DetailedSong::asMediaItem)
?.let { mediaItems ->
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
}
)
.animateItemPlacement(reorderingState = reorderingState)
.draggedItem(reorderingState = reorderingState, index = index)
)

View file

@ -7,6 +7,7 @@ import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
@ -51,14 +52,15 @@ import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
import it.vfsfitvnm.vimusic.R
import it.vfsfitvnm.vimusic.ui.components.BottomSheet
import it.vfsfitvnm.vimusic.ui.components.BottomSheetState
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.MusicBars
import it.vfsfitvnm.vimusic.ui.components.themed.QueuedMediaItemMenu
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.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.rememberMediaItemIndex
import it.vfsfitvnm.vimusic.utils.rememberShouldBePlaying
@ -106,7 +108,12 @@ fun PlayerBottomSheet(
binder?.player ?: return@BottomSheet
val thumbnailSize = Dimensions.thumbnails.song.px
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
val mediaItemIndex by rememberMediaItemIndex(binder.player)
val windows by rememberWindows(binder.player)
@ -149,26 +156,9 @@ fun PlayerBottomSheet(
val isPlayingThisMediaItem = mediaItemIndex == window.firstPeriodIndex
SongItem(
mediaItem = window.mediaItem,
thumbnailSizePx = thumbnailSize,
onClick = {
if (isPlayingThisMediaItem) {
if (shouldBePlaying) {
binder.player.pause()
} else {
binder.player.play()
}
} else {
binder.player.playWhenReady = true
binder.player.seekToDefaultPosition(window.firstPeriodIndex)
}
},
menuContent = {
QueuedMediaItemMenu(
mediaItem = window.mediaItem,
indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex
)
},
song = window.mediaItem,
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
onThumbnailContent = {
androidx.compose.animation.AnimatedVisibility(
visible = isPlayingThisMediaItem,
@ -218,11 +208,32 @@ fun PlayerBottomSheet(
)
},
modifier = Modifier
.animateItemPlacement(reorderingState)
.draggedItem(
reorderingState = reorderingState,
index = window.firstPeriodIndex
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
QueuedMediaItemMenu(
mediaItem = window.mediaItem,
indexInQueue = if (isPlayingThisMediaItem) null else window.firstPeriodIndex
)
}
},
onClick = {
if (isPlayingThisMediaItem) {
if (shouldBePlaying) {
binder.player.pause()
} else {
binder.player.play()
}
} else {
binder.player.playWhenReady = true
binder.player.seekToDefaultPosition(window.firstPeriodIndex)
}
}
)
.animateItemPlacement(reorderingState = reorderingState)
.draggedItem(reorderingState = reorderingState, index = window.firstPeriodIndex)
)
}

View file

@ -2,9 +2,12 @@ package it.vfsfitvnm.vimusic.ui.screens.playlist
import android.content.Intent
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxWithConstraints
@ -19,15 +22,16 @@ 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.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
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.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
@ -42,17 +46,18 @@ import it.vfsfitvnm.vimusic.models.SongPlaylistMap
import it.vfsfitvnm.vimusic.savers.InnertubePlaylistOrAlbumPageSaver
import it.vfsfitvnm.vimusic.savers.resultSaver
import it.vfsfitvnm.vimusic.transaction
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.HeaderPlaceholder
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.items.SongItem
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.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.completed
@ -68,6 +73,7 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.withContext
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun PlaylistSongList(
@ -76,6 +82,9 @@ fun PlaylistSongList(
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val context = LocalContext.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val playlistPageResult by produceSaveableState(
initialValue = null,
@ -201,28 +210,25 @@ fun PlaylistSongList(
itemsIndexed(items = playlist.songsPage?.items ?: emptyList()) { index, song ->
SongItem(
title = song.info?.name,
authors = song.authors?.joinToString("") { it.name ?: "" },
durationText = song.durationText,
onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
},
startContent = {
AsyncImage(
model = song.thumbnail?.size(songThumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(Dimensions.thumbnails.song)
song = song,
thumbnailSizePx = songThumbnailSizePx,
thumbnailSizeDp = songThumbnailSizeDp,
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
playlist.songsPage?.items?.map(Innertube.SongItem::asMediaItem)?.let { mediaItems ->
binder?.stopRadio()
binder?.player?.forcePlayAtIndex(mediaItems, index)
}
}
)
},
menuContent = {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
)
}
}

View file

@ -2,13 +2,17 @@ package it.vfsfitvnm.vimusic.ui.screens.search
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.input.ImeAction
@ -19,13 +23,14 @@ import it.vfsfitvnm.vimusic.LocalPlayerAwarePaddingValues
import it.vfsfitvnm.vimusic.LocalPlayerServiceBinder
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.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.InHistoryMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.SecondaryTextButton
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.utils.align
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
@ -45,6 +50,9 @@ fun LocalSongSearch(
) {
val (colorPalette, typography) = LocalAppearance.current
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val rippleIndication = rememberRipple(bounded = true)
val items by produceSaveableState(
initialValue = emptyList(),
@ -59,7 +67,8 @@ fun LocalSongSearch(
}
}
val thumbnailSize = Dimensions.thumbnails.song.px
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
LazyColumn(
contentPadding = LocalPlayerAwarePaddingValues.current,
@ -100,17 +109,26 @@ fun LocalSongSearch(
) { song ->
SongItem(
song = song,
thumbnailSizePx = thumbnailSize,
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
},
menuContent = { InHistoryMediaItemMenu(song = song) },
thumbnailSizePx = thumbnailSizePx,
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
InHistoryMediaItemMenu(song = song)
}
},
onClick = {
val mediaItem = song.asMediaItem
binder?.stopRadio()
binder?.player?.forcePlay(mediaItem)
binder?.setupRadio(
NavigationEndpoint.Endpoint.Watch(videoId = mediaItem.mediaId)
)
}
)
.animateItemPlacement()
)
}

View file

@ -3,6 +3,7 @@ package it.vfsfitvnm.vimusic.ui.screens.searchresult
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.material.ripple.rememberRipple
@ -21,24 +22,26 @@ import it.vfsfitvnm.vimusic.savers.InnertubePlaylistItemListSaver
import it.vfsfitvnm.vimusic.savers.InnertubeSongsPageSaver
import it.vfsfitvnm.vimusic.savers.InnertubeVideoItemListSaver
import it.vfsfitvnm.vimusic.savers.innertubeItemsPageSaver
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.Header
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.Scaffold
import it.vfsfitvnm.vimusic.ui.items.AlbumItem
import it.vfsfitvnm.vimusic.ui.items.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.ArtistItem
import it.vfsfitvnm.vimusic.ui.items.ArtistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.PlaylistItem
import it.vfsfitvnm.vimusic.ui.items.PlaylistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.SongItem
import it.vfsfitvnm.vimusic.ui.items.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.items.VideoItem
import it.vfsfitvnm.vimusic.ui.items.VideoItemPlaceholder
import it.vfsfitvnm.vimusic.ui.screens.albumRoute
import it.vfsfitvnm.vimusic.ui.screens.artistRoute
import it.vfsfitvnm.vimusic.ui.screens.globalRoutes
import it.vfsfitvnm.vimusic.ui.screens.playlistRoute
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.px
import it.vfsfitvnm.vimusic.ui.views.AlbumItem
import it.vfsfitvnm.vimusic.ui.views.AlbumItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.ArtistItem
import it.vfsfitvnm.vimusic.ui.views.ArtistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.PlaylistItem
import it.vfsfitvnm.vimusic.ui.views.PlaylistItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.SongItem
import it.vfsfitvnm.vimusic.ui.views.SongItemPlaceholder
import it.vfsfitvnm.vimusic.ui.views.VideoItem
import it.vfsfitvnm.vimusic.ui.views.VideoItemPlaceholder
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.forcePlay
import it.vfsfitvnm.vimusic.utils.rememberPreference
@ -74,6 +77,8 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
val emptyItemsText = "No results found. Please try a different query or category"
val rippleIndication = rememberRipple(bounded = true)
Scaffold(
topIconButtonId = R.drawable.chevron_back,
onTopIconButtonClick = pop,
@ -92,6 +97,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
when (tabIndex) {
0 -> {
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val thumbnailSizeDp = Dimensions.thumbnails.song
val thumbnailSizePx = thumbnailSizeDp.px
@ -116,11 +122,22 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
SongItem(
song = song,
thumbnailSizePx = thumbnailSizePx,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info?.endpoint)
}
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(song.asMediaItem)
binder?.setupRadio(song.info?.endpoint)
}
)
)
},
itemPlaceholderContent = {
@ -157,7 +174,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { albumRoute(album.info?.endpoint?.browseId) }
)
@ -198,7 +215,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { artistRoute(artist.info?.endpoint?.browseId) }
)
@ -209,8 +226,10 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
}
)
}
3 -> {
val binder = LocalPlayerServiceBinder.current
val menuState = LocalMenuState.current
val thumbnailHeightDp = 72.dp
val thumbnailWidthDp = 128.dp
@ -236,11 +255,21 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
video = video,
thumbnailWidthDp = thumbnailWidthDp,
thumbnailHeightDp = thumbnailHeightDp,
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(video.asMediaItem)
binder?.setupRadio(video.info?.endpoint)
}
modifier = Modifier
.combinedClickable(
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = video.asMediaItem)
}
},
onClick = {
binder?.stopRadio()
binder?.player?.forcePlay(video.asMediaItem)
binder?.setupRadio(video.info?.endpoint)
}
)
)
},
itemPlaceholderContent = {
@ -286,7 +315,7 @@ fun SearchResultScreen(query: String, onSearchAgain: () -> Unit) {
thumbnailSizeDp = thumbnailSizeDp,
modifier = Modifier
.clickable(
indication = rememberRipple(bounded = true),
indication = rippleIndication,
interactionSource = remember { MutableInteractionSource() },
onClick = { playlistRoute(playlist.info?.endpoint?.browseId) }
)

View file

@ -1,506 +0,0 @@
package it.vfsfitvnm.vimusic.ui.views
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.components.themed.NonQueuedMediaItemMenu
import it.vfsfitvnm.vimusic.ui.components.themed.TextPlaceholder
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.ui.styling.onOverlay
import it.vfsfitvnm.vimusic.ui.styling.overlay
import it.vfsfitvnm.vimusic.ui.styling.shimmer
import it.vfsfitvnm.vimusic.utils.asMediaItem
import it.vfsfitvnm.vimusic.utils.color
import it.vfsfitvnm.vimusic.utils.medium
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.youtubemusic.Innertube
@ExperimentalAnimationApi
@Composable
fun SongItem(
song: Innertube.SongItem,
thumbnailSizePx: Int,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
SongItem(
thumbnailModel = song.thumbnail?.size(thumbnailSizePx),
title = song.info?.name,
authors = song.authors?.joinToString("") { it.name ?: "" },
durationText = song.durationText,
onClick = onClick,
menuContent = {
NonQueuedMediaItemMenu(mediaItem = song.asMediaItem)
},
modifier = modifier
)
}
@Composable
fun SongItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = Dimensions.itemsVerticalPadding)
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(thumbnailSizeDp)
)
Column {
TextPlaceholder()
TextPlaceholder()
}
}
}
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun VideoItem(
video: Innertube.VideoItem,
thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val menuState = LocalMenuState.current
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = false,
thumbnailSizeDp = 0.dp,
modifier = modifier
.combinedClickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onLongClick = {
menuState.display {
NonQueuedMediaItemMenu(mediaItem = video.asMediaItem)
}
},
onClick = onClick
)
) {
Box {
AsyncImage(
model = video.thumbnail?.url,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
)
video.durationText?.let { durationText ->
BasicText(
text = durationText,
style = typography.xxs.medium.color(colorPalette.onOverlay),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(all = 4.dp)
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
.padding(horizontal = 4.dp, vertical = 2.dp)
.align(Alignment.BottomEnd)
)
}
}
ItemInfoContainer {
BasicText(
text = video.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = video.authors?.joinToString("") { it.name ?: "" } ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
video.viewsText?.let { viewsText ->
BasicText(
text = viewsText,
style = typography.xxs.medium.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
}
@ExperimentalFoundationApi
@ExperimentalAnimationApi
@Composable
fun VideoItemPlaceholder(
thumbnailHeightDp: Dp,
thumbnailWidthDp: Dp,
modifier: Modifier = Modifier
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = false,
thumbnailSizeDp = 0.dp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(width = thumbnailWidthDp, height = thumbnailHeightDp)
)
ItemInfoContainer {
TextPlaceholder()
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.padding(top = 8.dp)
)
}
}
}
@Composable
fun PlaylistItem(
playlist: Innertube.PlaylistItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
Box {
AsyncImage(
model = playlist.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
playlist.songCount?.let { songCount ->
BasicText(
text = "$songCount",
style = typography.xxs.medium.color(colorPalette.onOverlay),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(all = 4.dp)
.background(color = colorPalette.overlay, shape = RoundedCornerShape(2.dp))
.padding(horizontal = 4.dp, vertical = 2.dp)
.align(Alignment.BottomEnd)
)
}
}
ItemInfoContainer {
BasicText(
text = playlist.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = playlist.channel?.name ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
}
@Composable
fun PlaylistItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer {
TextPlaceholder()
TextPlaceholder()
}
}
}
@Composable
fun AlbumItem(
album: Innertube.AlbumItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false
) {
val (_, typography, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
AsyncImage(
model = album.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer {
BasicText(
text = album.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = if (alternative) 1 else 2,
overflow = TextOverflow.Ellipsis,
)
if (!alternative) {
album.authors?.joinToString("") { it.name ?: "" }?.let { authorsText ->
BasicText(
text = authorsText,
style = typography.xs.semiBold.secondary,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
BasicText(
text = album.year ?: "",
style = typography.xxs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
@Composable
fun AlbumItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false
) {
val (colorPalette, _, thumbnailShape) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = thumbnailShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer {
TextPlaceholder()
if (!alternative) {
TextPlaceholder()
}
TextPlaceholder(
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
@Composable
fun ArtistItem(
artist: Innertube.ArtistItem,
thumbnailSizePx: Int,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (_, typography) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
AsyncImage(
model = artist.thumbnail?.size(thumbnailSizePx),
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer(
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
) {
BasicText(
text = artist.info?.name ?: "",
style = typography.xs.semiBold,
maxLines = if (alternative) 1 else 2,
overflow = TextOverflow.Ellipsis
)
artist.subscribersCountText?.let { subscribersCountText ->
BasicText(
text = subscribersCountText,
style = typography.xxs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
}
@Composable
fun ArtistItemPlaceholder(
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
alternative: Boolean = false,
) {
val (colorPalette) = LocalAppearance.current
ItemContainer(
alternative = alternative,
thumbnailSizeDp = thumbnailSizeDp,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
) {
Spacer(
modifier = Modifier
.background(color = colorPalette.shimmer, shape = CircleShape)
.size(thumbnailSizeDp)
)
ItemInfoContainer(
horizontalAlignment = if (alternative) Alignment.CenterHorizontally else Alignment.Start,
) {
TextPlaceholder()
TextPlaceholder(
modifier = Modifier
.padding(top = 4.dp)
)
}
}
}
@Composable
private inline fun ItemContainer(
alternative: Boolean,
thumbnailSizeDp: Dp,
modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
verticalAlignment: Alignment.Vertical = Alignment.CenterVertically,
content: @Composable () -> Unit
) {
if (alternative) {
Column(
horizontalAlignment = horizontalAlignment,
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.width(thumbnailSizeDp)
) {
content()
}
} else {
Row(
verticalAlignment = verticalAlignment,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.padding(vertical = Dimensions.itemsVerticalPadding, horizontal = 16.dp)
.fillMaxWidth()
) {
content()
}
}
}
@Composable
private inline fun ItemInfoContainer(
modifier: Modifier = Modifier,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
content: @Composable ColumnScope.() -> Unit
) {
Column(
horizontalAlignment = horizontalAlignment,
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier,
content = content
)
}

View file

@ -1,160 +0,0 @@
package it.vfsfitvnm.vimusic.ui.views
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.Database
import it.vfsfitvnm.vimusic.models.PlaylistPreview
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.center
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
@Composable
fun PlaylistPreviewItem(
playlistPreview: PlaylistPreview,
modifier: Modifier = Modifier,
thumbnailSize: Dp = Dimensions.thumbnails.song
) {
val density = LocalDensity.current
val (_, _, thumbnailShape) = LocalAppearance.current
val thumbnailSizePx = with(density) {
thumbnailSize.roundToPx()
}
val thumbnails by remember(playlistPreview.playlist.id) {
Database.playlistThumbnailUrls(playlistPreview.playlist.id).distinctUntilChanged().map {
it.map { url ->
url.thumbnail(thumbnailSizePx)
}
}
}.collectAsState(initial = emptyList(), context = Dispatchers.IO)
PlaylistItem(
name = playlistPreview.playlist.name,
thumbnailSize = thumbnailSize,
imageContent = {
if (thumbnails.toSet().size == 1) {
AsyncImage(
model = thumbnails.first().thumbnail(thumbnailSizePx * 2),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.fillMaxSize()
)
} else {
Box(
modifier = Modifier
.fillMaxSize()
) {
listOf(
Alignment.TopStart,
Alignment.TopEnd,
Alignment.BottomStart,
Alignment.BottomEnd
).forEachIndexed { index, alignment ->
AsyncImage(
model = thumbnails.getOrNull(index),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(thumbnailShape)
.align(alignment)
.size(thumbnailSize)
)
}
}
}
},
modifier = modifier
)
}
@Composable
fun BuiltInPlaylistItem(
@DrawableRes icon: Int,
colorTint: Color,
name: String,
modifier: Modifier = Modifier,
thumbnailSize: Dp = Dimensions.thumbnails.song
) {
PlaylistItem(
name = name,
thumbnailSize = thumbnailSize,
imageContent = {
Image(
painter = painterResource(icon),
contentDescription = null,
colorFilter = ColorFilter.tint(colorTint),
modifier = Modifier
.align(Alignment.Center)
.size(24.dp)
)
},
modifier = modifier,
)
}
@Composable
fun PlaylistItem(
name: String,
modifier: Modifier = Modifier,
thumbnailSize: Dp = Dimensions.thumbnails.song,
imageContent: @Composable BoxScope.() -> Unit
) {
val (colorPalette, typography, thumbnailShape) = LocalAppearance.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = modifier
.requiredWidth(thumbnailSize * 2)
) {
Box(
modifier = Modifier
.clip(thumbnailShape)
.background(colorPalette.background1)
.align(Alignment.CenterHorizontally)
.requiredSize(thumbnailSize * 2),
content = imageContent
)
BasicText(
text = name,
style = typography.xxs.semiBold.center,
maxLines = 2,
overflow = TextOverflow.Ellipsis
)
}
}

View file

@ -1,192 +0,0 @@
package it.vfsfitvnm.vimusic.ui.views
import androidx.compose.animation.ExperimentalAnimationApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.text.BasicText
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.NonRestartableComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.media3.common.MediaItem
import coil.compose.AsyncImage
import it.vfsfitvnm.vimusic.models.DetailedSong
import it.vfsfitvnm.vimusic.ui.components.LocalMenuState
import it.vfsfitvnm.vimusic.ui.styling.Dimensions
import it.vfsfitvnm.vimusic.ui.styling.LocalAppearance
import it.vfsfitvnm.vimusic.utils.secondary
import it.vfsfitvnm.vimusic.utils.semiBold
import it.vfsfitvnm.vimusic.utils.thumbnail
@ExperimentalAnimationApi
@Composable
@NonRestartableComposable
fun SongItem(
mediaItem: MediaItem,
thumbnailSizePx: Int,
onClick: () -> Unit,
menuContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
thumbnailModel = mediaItem.mediaMetadata.artworkUri.thumbnail(thumbnailSizePx),
title = mediaItem.mediaMetadata.title.toString(),
authors = mediaItem.mediaMetadata.artist.toString(),
durationText = mediaItem.mediaMetadata.extras?.getString("durationText"),
menuContent = menuContent,
onClick = onClick,
onThumbnailContent = onThumbnailContent,
trailingContent = trailingContent,
modifier = modifier,
)
}
@ExperimentalAnimationApi
@Composable
@NonRestartableComposable
fun SongItem(
song: DetailedSong,
thumbnailSizePx: Int,
onClick: () -> Unit,
menuContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
thumbnailModel = song.thumbnailUrl?.thumbnail(thumbnailSizePx),
title = song.title,
authors = song.artistsText,
durationText = song.durationText,
menuContent = menuContent,
onClick = onClick,
onThumbnailContent = onThumbnailContent,
trailingContent = trailingContent,
modifier = modifier,
)
}
@ExperimentalAnimationApi
@Composable
@NonRestartableComposable
fun SongItem(
thumbnailModel: Any?,
title: String?,
authors: String?,
durationText: String?,
onClick: () -> Unit,
menuContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
onThumbnailContent: (@Composable BoxScope.() -> Unit)? = null,
trailingContent: (@Composable () -> Unit)? = null
) {
SongItem(
title = title,
authors = authors,
durationText = durationText,
onClick = onClick,
startContent = {
Box(
modifier = Modifier
.size(Dimensions.thumbnails.song)
) {
AsyncImage(
model = thumbnailModel,
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier
.clip(LocalAppearance.current.thumbnailShape)
.fillMaxSize()
)
onThumbnailContent?.invoke(this)
}
},
menuContent = menuContent,
trailingContent = trailingContent,
modifier = modifier,
)
}
@OptIn(ExperimentalFoundationApi::class)
@ExperimentalAnimationApi
@Composable
fun SongItem(
title: String?,
authors: String?,
durationText: String?,
onClick: () -> Unit,
startContent: @Composable () -> Unit,
menuContent: @Composable () -> Unit,
modifier: Modifier = Modifier,
trailingContent: (@Composable () -> Unit)? = null
) {
val menuState = LocalMenuState.current
val (_, typography) = LocalAppearance.current
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp),
modifier = modifier
.combinedClickable(
indication = rememberRipple(bounded = true),
interactionSource = remember { MutableInteractionSource() },
onLongClick = { menuState.display(menuContent) },
onClick = onClick
)
.fillMaxWidth()
.padding(vertical = Dimensions.itemsVerticalPadding)
.padding(start = 16.dp, end = if (trailingContent == null) 16.dp else 8.dp)
.height(Dimensions.thumbnails.song)
) {
startContent()
Column(
modifier = Modifier
.weight(1f)
) {
BasicText(
text = title ?: "",
style = typography.xs.semiBold,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
BasicText(
text = authors ?: "",
style = typography.xs.semiBold.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
durationText?.let {
BasicText(
text = durationText,
style = typography.xxs.secondary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
trailingContent?.invoke()
}
}