feat(android): covers on AlbumDetail header + MiniPlayer + NowPlaying

Three more "shows the placeholder icon when a cover exists" spots
fixed. Same patterns as Phase 124 (AlbumCard) and earlier track-row
covers — uses the existing displayCoverUrl / TrackRef.coverUrl
properties that route through BaseUrlInterceptor + Coil.

Modified:
  - library/ui/AlbumDetailScreen.kt — AlbumCover() branches on
    `album.id.isEmpty()` instead of `coverUrl.isEmpty()`, paints
    via album.displayCoverUrl. Same cached-only-album story as
    AlbumCard.
  - player/ui/MiniPlayer.kt — drops the "placeholder until 5.x"
    comment, renders track.coverUrl via AsyncImage with the
    Lucide.Music fallback. Cover box gets a surfaceVariant
    background so failed loads degrade to a tinted square. Picks
    up cover for the now-playing track in the persistent mini bar.
  - player/ui/NowPlayingScreen.kt — renames CoverPlaceholder() →
    NowPlayingCover(coverUrl, contentDescription). The full-screen
    player's large 320dp cover now shows actual art instead of a
    96dp music glyph. Same surfaceVariant + fallback pattern.

Search track results stay the lone remaining cover-less surface;
splitting Search's generic TextRow for per-track covers is the
biggest remaining polish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 08:22:42 -04:00
parent 283706c4f8
commit f3ee182cd7
3 changed files with 46 additions and 20 deletions
@@ -222,7 +222,7 @@ private fun AlbumCover(album: AlbumRef) {
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
if (album.coverUrl.isEmpty()) {
if (album.id.isEmpty()) {
Icon(
imageVector = Lucide.Disc3,
contentDescription = null,
@@ -231,7 +231,7 @@ private fun AlbumCover(album: AlbumRef) {
)
} else {
AsyncImage(
model = album.coverUrl,
model = album.displayCoverUrl,
contentDescription = album.title,
modifier = Modifier.fillMaxSize(),
)
@@ -1,5 +1,6 @@
package com.fabledsword.minstrel.player.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -21,10 +22,12 @@ import androidx.compose.runtime.getValue
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.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil3.compose.AsyncImage
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music
import com.composables.icons.lucide.Pause
@@ -41,9 +44,9 @@ private const val COVER_SIZE_DP = 48
* fresh install. Tapping the body navigates to the full
* NowPlayingScreen via [onExpandClick].
*
* Cover art is a placeholder Lucide icon for now — TrackRef carries
* no coverUrl; covers come from the AlbumRef join we'll add in a
* later 5.x slice. AsyncImage / Coil wire-up lands then.
* Cover comes from TrackRef.coverUrl (derived from albumId via the
* BaseUrlInterceptor placeholder); falls back to the Lucide.Music
* icon when the album binding is missing.
*/
@Composable
fun MiniPlayer(
@@ -69,14 +72,24 @@ fun MiniPlayer(
Box(
modifier = Modifier
.size(COVER_SIZE_DP.dp)
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)),
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Lucide.Music,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
if (track.coverUrl.isEmpty()) {
Icon(
imageVector = Lucide.Music,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
} else {
AsyncImage(
model = track.coverUrl,
contentDescription = track.title,
modifier = Modifier.size(COVER_SIZE_DP.dp),
contentScale = ContentScale.Crop,
)
}
}
Spacer(Modifier.width(12.dp))
Column(modifier = Modifier.weight(1f)) {
@@ -1,5 +1,6 @@
package com.fabledsword.minstrel.player.ui
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -24,12 +25,14 @@ import androidx.compose.runtime.getValue
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.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController
import coil3.compose.AsyncImage
import com.composables.icons.lucide.ListMusic
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Music
@@ -75,7 +78,7 @@ fun NowPlayingScreen(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
CoverPlaceholder()
NowPlayingCover(coverUrl = track.coverUrl, contentDescription = track.title)
Spacer(Modifier.height(24.dp))
TrackHeader(
title = track.title,
@@ -112,21 +115,31 @@ private fun ViewQueueButton(onClick: () -> Unit) {
}
@Composable
private fun CoverPlaceholder() {
private fun NowPlayingCover(coverUrl: String, contentDescription: String) {
Box(
modifier = Modifier
.widthIn(max = COVER_MAX_WIDTH_DP.dp)
.fillMaxWidth()
.aspectRatio(1f)
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusLg)),
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusLg))
.background(MaterialTheme.colorScheme.surfaceVariant),
contentAlignment = Alignment.Center,
) {
Icon(
imageVector = Lucide.Music,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(96.dp),
)
if (coverUrl.isEmpty()) {
Icon(
imageVector = Lucide.Music,
contentDescription = null,
tint = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(96.dp),
)
} else {
AsyncImage(
model = coverUrl,
contentDescription = contentDescription,
modifier = Modifier.fillMaxSize(),
contentScale = ContentScale.Crop,
)
}
}
}