From d2ce356d10feabdde0a5bec1d9e01074da966599 Mon Sep 17 00:00:00 2001 From: vfsfitvnm Date: Tue, 4 Oct 2022 11:37:10 +0200 Subject: [PATCH] Tweak code --- .../vfsfitvnm/vimusic/ui/items/AlbumItem.kt | 155 ++++++ .../vfsfitvnm/vimusic/ui/items/ArtistItem.kt | 145 +++++ .../vimusic/ui/items/ItemContainer.kt | 68 +++ .../vimusic/ui/items/PlaylistItem.kt | 274 ++++++++++ .../it/vfsfitvnm/vimusic/ui/items/SongItem.kt | 204 +++++++ .../vfsfitvnm/vimusic/ui/items/VideoItem.kt | 149 ++++++ .../vimusic/ui/screens/album/AlbumScreen.kt | 4 +- .../vimusic/ui/screens/album/AlbumSongs.kt | 47 +- .../ui/screens/artist/ArtistLocalSongs.kt | 40 +- .../ui/screens/artist/ArtistOverview.kt | 41 +- .../vimusic/ui/screens/artist/ArtistScreen.kt | 38 +- .../builtinplaylist/BuiltInPlaylistSongs.kt | 43 +- .../vimusic/ui/screens/home/HomeAlbums.kt | 63 +-- .../vimusic/ui/screens/home/HomeArtists.kt | 58 +- .../vimusic/ui/screens/home/HomePlaylists.kt | 36 +- .../vimusic/ui/screens/home/HomeSongs.kt | 86 +-- .../vimusic/ui/screens/home/QuickPicks.kt | 85 +-- .../localplaylist/LocalPlaylistSongs.kt | 50 +- .../ui/screens/player/PlayerBottomSheet.kt | 65 ++- .../ui/screens/playlist/PlaylistSongList.kt | 52 +- .../ui/screens/search/LocalSongSearch.kt | 42 +- .../searchresult/SearchResultScreen.kt | 75 ++- .../vimusic/ui/views/InnertubeItems.kt | 506 ------------------ .../vimusic/ui/views/PlaylistPreviewItem.kt | 160 ------ .../it/vfsfitvnm/vimusic/ui/views/SongItem.kt | 192 ------- 25 files changed, 1468 insertions(+), 1210 deletions(-) create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/AlbumItem.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ArtistItem.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ItemContainer.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/PlaylistItem.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt create mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/VideoItem.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt delete mode 100644 app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/AlbumItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/AlbumItem.kt new file mode 100644 index 0000000..911540a --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/AlbumItem.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ArtistItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ArtistItem.kt new file mode 100644 index 0000000..64c2892 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ArtistItem.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ItemContainer.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ItemContainer.kt new file mode 100644 index 0000000..fe16910 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/ItemContainer.kt @@ -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 + ) +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/PlaylistItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/PlaylistItem.kt new file mode 100644 index 0000000..91ad1a5 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/PlaylistItem.kt @@ -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() + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt new file mode 100644 index 0000000..89503e9 --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/SongItem.kt @@ -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() + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/VideoItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/VideoItem.kt new file mode 100644 index 0000000..06aa30b --- /dev/null +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/items/VideoItem.kt @@ -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) + ) + } + } +} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt index 03202df..77dc3d5 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumScreen.kt @@ -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 diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt index 2d0656e..dc37d3a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/album/AlbumSongs.kt @@ -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) + } + ) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt index db2ae28..93b13c4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistLocalSongs.kt @@ -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") { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt index 03b2902..4f6e95a 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistOverview.kt @@ -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) + ) + } ) - } ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt index ab56d2c..c78faef 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/artist/ArtistScreen.kt @@ -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 = { diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt index 40604e8..cffa71c 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/builtinplaylist/BuiltInPlaylistSongs.kt @@ -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() ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt index a5d6b97..543f2b1 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeAlbums.kt @@ -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) - ) - } - } - } + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt index 8ada7f0..bd4b2cd 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeArtists.kt @@ -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 - ) - } + ) } } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt index cb2fb04..2a52ad4 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomePlaylists.kt @@ -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), diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt index 49c22bc..8165568 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/HomeSongs.kt @@ -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() ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt index 26145c8..6f50348 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/home/QuickPicks.kt @@ -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) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt index a6a8862..ecc5b02 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/localplaylist/LocalPlaylistSongs.kt @@ -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) ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt index 87046aa..86b285d 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/player/PlayerBottomSheet.kt @@ -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) ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt index 5d7a5d7..6bce133 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/playlist/PlaylistSongList.kt @@ -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) - } ) } } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt index 849b64e..8447f61 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/search/LocalSongSearch.kt @@ -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() ) } diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt index f3452a4..0977c14 100644 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt +++ b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/screens/searchresult/SearchResultScreen.kt @@ -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) } ) diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt deleted file mode 100644 index 314c4f7..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/InnertubeItems.kt +++ /dev/null @@ -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 - ) -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt deleted file mode 100644 index ddc99c9..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/PlaylistPreviewItem.kt +++ /dev/null @@ -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 - ) - } -} diff --git a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt b/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt deleted file mode 100644 index c9f589a..0000000 --- a/app/src/main/kotlin/it/vfsfitvnm/vimusic/ui/views/SongItem.kt +++ /dev/null @@ -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() - } -}