diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/AlbumCard.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/AlbumCard.kt index 3837dd0f..37de12e0 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/AlbumCard.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/AlbumCard.kt @@ -1,23 +1,17 @@ package com.fabledsword.minstrel.library.widgets -import androidx.compose.foundation.background import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -26,8 +20,7 @@ import com.composables.icons.lucide.Lucide import androidx.compose.material3.Icon import com.fabledsword.minstrel.models.AlbumRef import com.fabledsword.minstrel.nav.LocalDetailSeedCache -import com.fabledsword.minstrel.shared.widgets.ServerImage -import com.fabledsword.minstrel.theme.FabledSwordFlatTokens +import com.fabledsword.minstrel.shared.widgets.CoverTile /** * One album tile. Mirrors the Flutter `AlbumCard` (~176dp wide, 144dp @@ -53,31 +46,20 @@ fun AlbumCard( color = Color.Transparent, ) { Column(modifier = Modifier.padding(horizontal = 8.dp)) { - Box( - modifier = Modifier - .size(144.dp) - .clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)) - .background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center, - ) { - if (album.id.isEmpty()) { + CoverTile( + url = album.displayCoverUrl, + contentDescription = album.title, + size = 144.dp, + background = MaterialTheme.colorScheme.surfaceVariant, + fallback = { Icon( imageVector = Lucide.Disc3, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(56.dp), ) - } else { - // displayCoverUrl yields /api/albums/{id}/cover — - // relative on fresh fetches, placeholder-absolute from - // cache. ServerImage resolves both to a loadable URL. - ServerImage( - url = album.displayCoverUrl, - contentDescription = album.title, - modifier = Modifier.fillMaxSize(), - ) - } - } + }, + ) Spacer(Modifier.height(8.dp)) Text( text = album.title, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/ArtistCard.kt b/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/ArtistCard.kt index fd99b137..87c87aa9 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/ArtistCard.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/library/widgets/ArtistCard.kt @@ -1,10 +1,8 @@ package com.fabledsword.minstrel.library.widgets import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -15,9 +13,7 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text 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.graphics.Color import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextOverflow @@ -26,7 +22,7 @@ import com.composables.icons.lucide.Lucide import com.composables.icons.lucide.User import com.fabledsword.minstrel.models.ArtistRef import com.fabledsword.minstrel.nav.LocalDetailSeedCache -import com.fabledsword.minstrel.shared.widgets.ServerImage +import com.fabledsword.minstrel.shared.widgets.CoverTile /** * One artist tile. Circular cover; name centered beneath. Tap fires @@ -54,25 +50,20 @@ fun ArtistCard( modifier = Modifier.padding(horizontal = 8.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Box( - modifier = Modifier - .size(128.dp) - .clip(CircleShape), - contentAlignment = Alignment.Center, - ) { - ServerImage( - url = artist.displayCoverUrl, - contentDescription = artist.name, - modifier = Modifier.fillMaxSize(), - ) { + CoverTile( + url = artist.displayCoverUrl, + contentDescription = artist.name, + size = 128.dp, + shape = CircleShape, + fallback = { Icon( imageVector = Lucide.User, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(56.dp), ) - } - } + }, + ) Spacer(Modifier.height(8.dp)) Text( text = artist.name, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt b/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt index d44f5c69..cae8abe5 100644 --- a/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt +++ b/android/app/src/main/java/com/fabledsword/minstrel/playlists/widgets/PlaylistCard.kt @@ -1,10 +1,8 @@ package com.fabledsword.minstrel.playlists.widgets import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -17,7 +15,6 @@ import androidx.compose.material3.Text 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.graphics.Color import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp @@ -25,7 +22,7 @@ import com.composables.icons.lucide.ListMusic import com.composables.icons.lucide.Lucide import com.fabledsword.minstrel.models.PlaylistRef import com.fabledsword.minstrel.nav.LocalDetailSeedCache -import com.fabledsword.minstrel.shared.widgets.ServerImage +import com.fabledsword.minstrel.shared.widgets.CoverTile import com.fabledsword.minstrel.theme.FabledSwordFlatTokens /** @@ -54,33 +51,29 @@ fun PlaylistCard( color = Color.Transparent, ) { Column(modifier = Modifier.padding(horizontal = 8.dp)) { - Box( - modifier = Modifier - .size(144.dp) - .clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)), - contentAlignment = Alignment.Center, - ) { - ServerImage( - url = playlist.coverUrl, - contentDescription = playlist.name, - modifier = Modifier.fillMaxSize(), - ) { + CoverTile( + url = playlist.coverUrl, + contentDescription = playlist.name, + size = 144.dp, + fallback = { Icon( imageVector = Lucide.ListMusic, contentDescription = null, tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(56.dp), ) - } - if (playlist.isSystem) { - VariantPill( - label = systemLabelFor(playlist.systemVariant), - modifier = Modifier - .align(Alignment.TopStart) - .padding(6.dp), - ) - } - } + }, + overlay = { + if (playlist.isSystem) { + VariantPill( + label = systemLabelFor(playlist.systemVariant), + modifier = Modifier + .align(Alignment.TopStart) + .padding(6.dp), + ) + } + }, + ) Spacer(Modifier.height(8.dp)) Text( text = playlist.name, diff --git a/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/CoverTile.kt b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/CoverTile.kt new file mode 100644 index 00000000..b51ef75f --- /dev/null +++ b/android/app/src/main/java/com/fabledsword/minstrel/shared/widgets/CoverTile.kt @@ -0,0 +1,56 @@ +package com.fabledsword.minstrel.shared.widgets + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import com.fabledsword.minstrel.theme.FabledSwordFlatTokens + +/** + * Shared cover artwork tile: clipped, optionally backgrounded `Box` + * with a [ServerImage] inside and a per-caller [fallback] icon. Used + * by `AlbumCard`, `ArtistCard`, `PlaylistCard` — they vary on size / + * shape / fallback icon / background, but the surrounding Box + + * ServerImage + fallback structure was identical at all three sites. + * + * [overlay] is a `BoxScope` slot for things drawn on top of the cover + * (e.g. the `VariantPill` system-playlist label) — callers can use + * `Modifier.align(...)` inside it. + */ +@Composable +fun CoverTile( + url: String?, + contentDescription: String?, + size: Dp, + fallback: @Composable () -> Unit, + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(FabledSwordFlatTokens.radiusSm), + background: Color = Color.Transparent, + overlay: @Composable BoxScope.() -> Unit = {}, +) { + Box( + modifier = modifier + .size(size) + .clip(shape) + .background(background), + contentAlignment = Alignment.Center, + ) { + ServerImage( + url = url, + contentDescription = contentDescription, + modifier = Modifier.fillMaxSize(), + ) { + fallback() + } + overlay() + } +}