diff --git a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt index 9896adef..a0c01a09 100644 --- a/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt +++ b/app/src/main/kotlin/com/subtracks/playback/PlaybackController.kt @@ -384,6 +384,13 @@ class PlaybackController( thumbnail: Boolean = false, ): CoverArtRef? = sourceRepository.coverArt(item?.coverArtId, thumbnail) + suspend fun upcomingCoverArt(thumbnail: Boolean = false): CoverArtRef? { + val snap = snapshot ?: return null + val position = currentPosition() ?: return null + val next = queueRepository.itemAt(snap, position + 1) ?: return null + return sourceRepository.coverArt(next.toQueueItem().coverArtId, thumbnail) + } + private fun play( entries: List, startPosition: Long, diff --git a/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt b/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt index add04abc..2fd58b7f 100644 --- a/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt +++ b/app/src/main/kotlin/com/subtracks/ui/components/HeroDetailScaffold.kt @@ -213,6 +213,7 @@ fun HeroHeader( topInset: Dp, controlsModifier: Modifier = Modifier, thumbnailRef: CoverArtRef? = null, + comment: String? = null, modifier: Modifier = Modifier, ) { Column( @@ -252,6 +253,17 @@ fun HeroHeader( overflow = TextOverflow.Ellipsis, ) } + if (!comment.isNullOrBlank()) { + Spacer(Modifier.height(4.dp)) + Text( + text = comment, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + textAlign = TextAlign.Center, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + ) + } Spacer(Modifier.height(20.dp)) Row( modifier = controlsModifier, diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt index cf91d86f..f4cebf50 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistDetailScreen.kt @@ -160,7 +160,7 @@ fun ArtistDetailScreen( ref = art, name = artist?.name.orEmpty(), thumbnailRef = artThumbnail, - showPlaceholder = false, + showPlaceholder = art == null, modifier = Modifier.fillMaxWidth().height(ART_HEIGHT), ) Text( diff --git a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt index 0af9850b..21253e92 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/ArtistsContent.kt @@ -28,6 +28,7 @@ import com.subtracks.ui.components.CoverArt import com.subtracks.ui.components.EmptyState import com.subtracks.ui.components.LoadingState import com.subtracks.ui.components.rememberViewportFill +import com.subtracks.ui.theme.PrefetchArtworkSeeds @Composable fun ArtistsContent( @@ -57,6 +58,7 @@ fun ArtistsContent( items(count = items.itemCount, key = items.itemKey { it.id }) { index -> val artist = items[index] if (artist != null) { + PrefetchArtworkSeeds(coverArt(artist.coverArt, true)) ListItem( headlineContent = { Text( diff --git a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt index 9cdc0eb7..87cf0c85 100644 --- a/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/library/PlaylistDetailScreen.kt @@ -65,7 +65,8 @@ fun PlaylistDetailScreen( art = coverArt(playlist?.coverArt, false), thumbnailRef = coverArt(playlist?.coverArt, true), name = playlist?.name.orEmpty(), - subtitle = playlistSubtitle(playlist), + subtitle = playlistSummary(playlist), + comment = playlist?.comment, hasSongs = songs.itemCount > 0, onPlay = { onSongClick(0) }, onShuffle = {}, @@ -93,12 +94,11 @@ fun PlaylistDetailScreen( ) } -private fun playlistSubtitle(playlist: Playlist?): String { +private fun playlistSummary(playlist: Playlist?): String { playlist ?: return "" return listOfNotNull( "${playlist.songCount} ${if (playlist.songCount == 1L) "song" else "songs"}", formatDuration(playlist.duration), - playlist.comment?.takeIf { it.isNotBlank() }, ).joinToString(" $DOT ") } diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt b/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt index eeed9bdc..9af2a45f 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/MiniPlayer.kt @@ -46,7 +46,7 @@ fun MiniPlayer( val item = state.item ?: return ArtworkTheme(artwork) { Surface( - color = MaterialTheme.colorScheme.surfaceContainerHigh, + color = artwork?.accents?.firstOrNull() ?: MaterialTheme.colorScheme.surfaceContainerHigh, contentColor = MaterialTheme.colorScheme.onSurface, modifier = modifier.fillMaxWidth().clickable( diff --git a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt index 960a736e..2f2080f5 100644 --- a/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt +++ b/app/src/main/kotlin/com/subtracks/ui/playback/NowPlayingScreen.kt @@ -1,5 +1,6 @@ package com.subtracks.ui.playback +import android.content.Context import androidx.compose.foundation.background import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement @@ -34,6 +35,7 @@ import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableStateOf @@ -45,11 +47,15 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil3.SingletonImageLoader +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest import com.subtracks.data.model.CoverArtRef import com.subtracks.playback.PlaybackController import com.subtracks.playback.PlaybackState import com.subtracks.ui.components.CoverArt import com.subtracks.ui.theme.ArtworkColors +import com.subtracks.ui.theme.ArtworkSeedCache import com.subtracks.ui.theme.ArtworkTheme import com.subtracks.ui.theme.HeroGradient import com.subtracks.ui.theme.rememberArtworkColors @@ -63,11 +69,22 @@ fun NowPlayingRoute( controller: PlaybackController = koinInject(), ) { val state by controller.state.collectAsStateWithLifecycle() + val context = LocalPlatformContext.current val art = controller.coverArt(state.item) + val thumbnail = controller.coverArt(state.item, thumbnail = true) + LaunchedEffect(state.item?.id, state.hasNext) { + if (!state.hasNext) return@LaunchedEffect + controller.upcomingCoverArt(thumbnail = true)?.let { + ArtworkSeedCache.prefetch(context, it) + prefetchImage(context, it) + } + controller.upcomingCoverArt()?.let { prefetchImage(context, it) } + } NowPlayingScreen( state = state, coverArt = art, - artwork = rememberArtworkColors(art), + thumbnailRef = thumbnail, + artwork = rememberArtworkColors(thumbnail ?: art), onBack = onBack, onQueue = onQueue, onPlayPause = controller::togglePlayPause, @@ -78,6 +95,20 @@ fun NowPlayingRoute( ) } +private fun prefetchImage( + context: Context, + ref: CoverArtRef, +) { + SingletonImageLoader.get(context).enqueue( + ImageRequest + .Builder(context) + .data(ref.url) + .memoryCacheKey(ref.cacheKey) + .diskCacheKey(ref.cacheKey) + .build(), + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable fun NowPlayingScreen( @@ -90,6 +121,7 @@ fun NowPlayingScreen( onNext: () -> Unit, onPrevious: () -> Unit, onSeek: (Long) -> Unit, + thumbnailRef: CoverArtRef? = null, modifier: Modifier = Modifier, ) { val playButtonSize = 96.dp @@ -133,6 +165,7 @@ fun NowPlayingScreen( CoverArt( ref = coverArt, name = state.item?.title.orEmpty(), + thumbnailRef = thumbnailRef, modifier = Modifier .fillMaxHeight()