refactor(android): extract shared CoverTile from Album/Artist/Playlist cards
The three cards shared an identical Box + clip + background + ServerImage + fallback structure around their cover (only size, shape, fallback icon, and overlay differ). Extract a single CoverTile composable; each card now passes its own size, shape, background, fallback icon, and optional BoxScope overlay (used by PlaylistCard for the VariantPill). Pure DRY consolidation; no behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
+18
-25
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user