polish the themed screens: comment line, brighter mini player, art preloading

This commit is contained in:
austinried
2026-10-04 00:29:54 +00:00
parent f70d1ba27e
commit 39e2c5d09e
7 changed files with 60 additions and 6 deletions
@@ -384,6 +384,13 @@ class PlaybackController(
thumbnail: Boolean = false, thumbnail: Boolean = false,
): CoverArtRef? = sourceRepository.coverArt(item?.coverArtId, thumbnail) ): 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( private fun play(
entries: List<QueueEntry>, entries: List<QueueEntry>,
startPosition: Long, startPosition: Long,
@@ -213,6 +213,7 @@ fun HeroHeader(
topInset: Dp, topInset: Dp,
controlsModifier: Modifier = Modifier, controlsModifier: Modifier = Modifier,
thumbnailRef: CoverArtRef? = null, thumbnailRef: CoverArtRef? = null,
comment: String? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
Column( Column(
@@ -252,6 +253,17 @@ fun HeroHeader(
overflow = TextOverflow.Ellipsis, 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)) Spacer(Modifier.height(20.dp))
Row( Row(
modifier = controlsModifier, modifier = controlsModifier,
@@ -160,7 +160,7 @@ fun ArtistDetailScreen(
ref = art, ref = art,
name = artist?.name.orEmpty(), name = artist?.name.orEmpty(),
thumbnailRef = artThumbnail, thumbnailRef = artThumbnail,
showPlaceholder = false, showPlaceholder = art == null,
modifier = Modifier.fillMaxWidth().height(ART_HEIGHT), modifier = Modifier.fillMaxWidth().height(ART_HEIGHT),
) )
Text( Text(
@@ -28,6 +28,7 @@ import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.components.EmptyState import com.subtracks.ui.components.EmptyState
import com.subtracks.ui.components.LoadingState import com.subtracks.ui.components.LoadingState
import com.subtracks.ui.components.rememberViewportFill import com.subtracks.ui.components.rememberViewportFill
import com.subtracks.ui.theme.PrefetchArtworkSeeds
@Composable @Composable
fun ArtistsContent( fun ArtistsContent(
@@ -57,6 +58,7 @@ fun ArtistsContent(
items(count = items.itemCount, key = items.itemKey { it.id }) { index -> items(count = items.itemCount, key = items.itemKey { it.id }) { index ->
val artist = items[index] val artist = items[index]
if (artist != null) { if (artist != null) {
PrefetchArtworkSeeds(coverArt(artist.coverArt, true))
ListItem( ListItem(
headlineContent = { headlineContent = {
Text( Text(
@@ -65,7 +65,8 @@ fun PlaylistDetailScreen(
art = coverArt(playlist?.coverArt, false), art = coverArt(playlist?.coverArt, false),
thumbnailRef = coverArt(playlist?.coverArt, true), thumbnailRef = coverArt(playlist?.coverArt, true),
name = playlist?.name.orEmpty(), name = playlist?.name.orEmpty(),
subtitle = playlistSubtitle(playlist), subtitle = playlistSummary(playlist),
comment = playlist?.comment,
hasSongs = songs.itemCount > 0, hasSongs = songs.itemCount > 0,
onPlay = { onSongClick(0) }, onPlay = { onSongClick(0) },
onShuffle = {}, onShuffle = {},
@@ -93,12 +94,11 @@ fun PlaylistDetailScreen(
) )
} }
private fun playlistSubtitle(playlist: Playlist?): String { private fun playlistSummary(playlist: Playlist?): String {
playlist ?: return "" playlist ?: return ""
return listOfNotNull( return listOfNotNull(
"${playlist.songCount} ${if (playlist.songCount == 1L) "song" else "songs"}", "${playlist.songCount} ${if (playlist.songCount == 1L) "song" else "songs"}",
formatDuration(playlist.duration), formatDuration(playlist.duration),
playlist.comment?.takeIf { it.isNotBlank() },
).joinToString(" $DOT ") ).joinToString(" $DOT ")
} }
@@ -46,7 +46,7 @@ fun MiniPlayer(
val item = state.item ?: return val item = state.item ?: return
ArtworkTheme(artwork) { ArtworkTheme(artwork) {
Surface( Surface(
color = MaterialTheme.colorScheme.surfaceContainerHigh, color = artwork?.accents?.firstOrNull() ?: MaterialTheme.colorScheme.surfaceContainerHigh,
contentColor = MaterialTheme.colorScheme.onSurface, contentColor = MaterialTheme.colorScheme.onSurface,
modifier = modifier =
modifier.fillMaxWidth().clickable( modifier.fillMaxWidth().clickable(
@@ -1,5 +1,6 @@
package com.subtracks.ui.playback package com.subtracks.ui.playback
import android.content.Context
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.basicMarquee
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
@@ -34,6 +35,7 @@ import androidx.compose.material3.TopAppBar
import androidx.compose.material3.TopAppBarDefaults import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf 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.graphics.Color
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle 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.data.model.CoverArtRef
import com.subtracks.playback.PlaybackController import com.subtracks.playback.PlaybackController
import com.subtracks.playback.PlaybackState import com.subtracks.playback.PlaybackState
import com.subtracks.ui.components.CoverArt import com.subtracks.ui.components.CoverArt
import com.subtracks.ui.theme.ArtworkColors import com.subtracks.ui.theme.ArtworkColors
import com.subtracks.ui.theme.ArtworkSeedCache
import com.subtracks.ui.theme.ArtworkTheme import com.subtracks.ui.theme.ArtworkTheme
import com.subtracks.ui.theme.HeroGradient import com.subtracks.ui.theme.HeroGradient
import com.subtracks.ui.theme.rememberArtworkColors import com.subtracks.ui.theme.rememberArtworkColors
@@ -63,11 +69,22 @@ fun NowPlayingRoute(
controller: PlaybackController = koinInject(), controller: PlaybackController = koinInject(),
) { ) {
val state by controller.state.collectAsStateWithLifecycle() val state by controller.state.collectAsStateWithLifecycle()
val context = LocalPlatformContext.current
val art = controller.coverArt(state.item) 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( NowPlayingScreen(
state = state, state = state,
coverArt = art, coverArt = art,
artwork = rememberArtworkColors(art), thumbnailRef = thumbnail,
artwork = rememberArtworkColors(thumbnail ?: art),
onBack = onBack, onBack = onBack,
onQueue = onQueue, onQueue = onQueue,
onPlayPause = controller::togglePlayPause, 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) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun NowPlayingScreen( fun NowPlayingScreen(
@@ -90,6 +121,7 @@ fun NowPlayingScreen(
onNext: () -> Unit, onNext: () -> Unit,
onPrevious: () -> Unit, onPrevious: () -> Unit,
onSeek: (Long) -> Unit, onSeek: (Long) -> Unit,
thumbnailRef: CoverArtRef? = null,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val playButtonSize = 96.dp val playButtonSize = 96.dp
@@ -133,6 +165,7 @@ fun NowPlayingScreen(
CoverArt( CoverArt(
ref = coverArt, ref = coverArt,
name = state.item?.title.orEmpty(), name = state.item?.title.orEmpty(),
thumbnailRef = thumbnailRef,
modifier = modifier =
Modifier Modifier
.fillMaxHeight() .fillMaxHeight()