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