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
|
package com.fabledsword.minstrel.library.widgets
|
||||||
|
|
||||||
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.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
@@ -26,8 +20,7 @@ import com.composables.icons.lucide.Lucide
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import com.fabledsword.minstrel.models.AlbumRef
|
import com.fabledsword.minstrel.models.AlbumRef
|
||||||
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
|
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
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* One album tile. Mirrors the Flutter `AlbumCard` (~176dp wide, 144dp
|
* One album tile. Mirrors the Flutter `AlbumCard` (~176dp wide, 144dp
|
||||||
@@ -53,31 +46,20 @@ fun AlbumCard(
|
|||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
Box(
|
CoverTile(
|
||||||
modifier = Modifier
|
url = album.displayCoverUrl,
|
||||||
.size(144.dp)
|
contentDescription = album.title,
|
||||||
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm))
|
size = 144.dp,
|
||||||
.background(MaterialTheme.colorScheme.surfaceVariant),
|
background = MaterialTheme.colorScheme.surfaceVariant,
|
||||||
contentAlignment = Alignment.Center,
|
fallback = {
|
||||||
) {
|
|
||||||
if (album.id.isEmpty()) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Lucide.Disc3,
|
imageVector = Lucide.Disc3,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(56.dp),
|
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))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = album.title,
|
text = album.title,
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package com.fabledsword.minstrel.library.widgets
|
package com.fabledsword.minstrel.library.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -15,9 +13,7 @@ import androidx.compose.material3.MaterialTheme
|
|||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
@@ -26,7 +22,7 @@ import com.composables.icons.lucide.Lucide
|
|||||||
import com.composables.icons.lucide.User
|
import com.composables.icons.lucide.User
|
||||||
import com.fabledsword.minstrel.models.ArtistRef
|
import com.fabledsword.minstrel.models.ArtistRef
|
||||||
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
|
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
|
* One artist tile. Circular cover; name centered beneath. Tap fires
|
||||||
@@ -54,25 +50,20 @@ fun ArtistCard(
|
|||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
) {
|
) {
|
||||||
Box(
|
CoverTile(
|
||||||
modifier = Modifier
|
url = artist.displayCoverUrl,
|
||||||
.size(128.dp)
|
contentDescription = artist.name,
|
||||||
.clip(CircleShape),
|
size = 128.dp,
|
||||||
contentAlignment = Alignment.Center,
|
shape = CircleShape,
|
||||||
) {
|
fallback = {
|
||||||
ServerImage(
|
|
||||||
url = artist.displayCoverUrl,
|
|
||||||
contentDescription = artist.name,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Lucide.User,
|
imageVector = Lucide.User,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
}
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = artist.name,
|
text = artist.name,
|
||||||
|
|||||||
+18
-25
@@ -1,10 +1,8 @@
|
|||||||
package com.fabledsword.minstrel.playlists.widgets
|
package com.fabledsword.minstrel.playlists.widgets
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -17,7 +15,6 @@ import androidx.compose.material3.Text
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
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
|
||||||
@@ -25,7 +22,7 @@ import com.composables.icons.lucide.ListMusic
|
|||||||
import com.composables.icons.lucide.Lucide
|
import com.composables.icons.lucide.Lucide
|
||||||
import com.fabledsword.minstrel.models.PlaylistRef
|
import com.fabledsword.minstrel.models.PlaylistRef
|
||||||
import com.fabledsword.minstrel.nav.LocalDetailSeedCache
|
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
|
import com.fabledsword.minstrel.theme.FabledSwordFlatTokens
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,33 +51,29 @@ fun PlaylistCard(
|
|||||||
color = Color.Transparent,
|
color = Color.Transparent,
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
Column(modifier = Modifier.padding(horizontal = 8.dp)) {
|
||||||
Box(
|
CoverTile(
|
||||||
modifier = Modifier
|
url = playlist.coverUrl,
|
||||||
.size(144.dp)
|
contentDescription = playlist.name,
|
||||||
.clip(RoundedCornerShape(FabledSwordFlatTokens.radiusSm)),
|
size = 144.dp,
|
||||||
contentAlignment = Alignment.Center,
|
fallback = {
|
||||||
) {
|
|
||||||
ServerImage(
|
|
||||||
url = playlist.coverUrl,
|
|
||||||
contentDescription = playlist.name,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Lucide.ListMusic,
|
imageVector = Lucide.ListMusic,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
modifier = Modifier.size(56.dp),
|
modifier = Modifier.size(56.dp),
|
||||||
)
|
)
|
||||||
}
|
},
|
||||||
if (playlist.isSystem) {
|
overlay = {
|
||||||
VariantPill(
|
if (playlist.isSystem) {
|
||||||
label = systemLabelFor(playlist.systemVariant),
|
VariantPill(
|
||||||
modifier = Modifier
|
label = systemLabelFor(playlist.systemVariant),
|
||||||
.align(Alignment.TopStart)
|
modifier = Modifier
|
||||||
.padding(6.dp),
|
.align(Alignment.TopStart)
|
||||||
)
|
.padding(6.dp),
|
||||||
}
|
)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
Spacer(Modifier.height(8.dp))
|
Spacer(Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = playlist.name,
|
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