fix(music): playlist cover chip is wrap-content again, not full-cover

The previous cover used SubcomposeAsyncImage with a single content lambda
that stacked the loaded image and the chip as BoxScope siblings. Under
Coil's subcompose-layout pass the chip's wrap-content Box got measured
to the cover's full size, painting a translucent black square over the
whole artwork.

Drop SubcomposeAsyncImage. Use rememberAsyncImagePainter + a plain
Image + painter-state observation inside a normal Box. The image fills
the box (Modifier.matchParentSize) and the chip sits as a normal
BoxScope child with Modifier.align(BottomStart).padding(12.dp) —
predictable wrap-content sizing.
This commit is contained in:
Claude
2026-05-27 18:33:22 +00:00
parent 4177390591
commit ea227f1968
@@ -20,16 +20,15 @@
*/
package com.vitorpamplona.amethyst.ui.note.types
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@@ -56,8 +55,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImagePainter
import coil3.compose.SubcomposeAsyncImage
import coil3.compose.SubcomposeAsyncImageContent
import coil3.compose.rememberAsyncImagePainter
import com.vitorpamplona.amethyst.R
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
@@ -282,51 +280,46 @@ private fun MusicPlaylistCover(
.clip(imageShape)
Box(imageModifier) {
if (image != null) {
// Driving SubcomposeAsyncImage directly (rather than going through MyAsyncImage)
// so each painter state can choose its own overlay: only `Success` floats the
// chip alone over the loaded artwork; `Loading` and `Error` fall back to the
// banner with the author's avatar + chip side-by-side — same treatment as the
// no-image case below — so the chip never sits on top of the avatar.
SubcomposeAsyncImage(
model = image,
contentDescription = stringRes(R.string.preview_card_image_for, image),
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
) {
val state by painter.state.collectAsState()
when (state) {
is AsyncImagePainter.State.Success -> {
SubcomposeAsyncImageContent(imageModifier)
FloatingTrackCountChip(trackCount)
}
if (image == null) {
// No artwork at all: banner + author avatar + chip side-by-side at the bottom.
BannerWithAuthorChip(note, trackCount, imageModifier, accountViewModel, blurred = false)
} else {
// Drive the painter directly so we can layer the loaded image and the chip as
// separate siblings of the same Box — using SubcomposeAsyncImage's content-lambda
// confused its layout (the chip's wrap-content Box was stretched to the cover's
// full size). A plain Image + painter-state observation behaves predictably.
val painter = rememberAsyncImagePainter(model = image)
val state by painter.state.collectAsState()
is AsyncImagePainter.State.Error -> {
BannerWithAuthorChip(note, trackCount, imageModifier, accountViewModel, blurred = false)
}
when (state) {
is AsyncImagePainter.State.Success -> {
Image(
painter = painter,
contentDescription = stringRes(R.string.preview_card_image_for, image),
contentScale = ContentScale.Crop,
modifier = Modifier.matchParentSize(),
)
TrackCountChip(
trackCount = trackCount,
modifier =
Modifier
.align(Alignment.BottomStart)
.padding(12.dp),
)
}
else -> {
BannerWithAuthorChip(note, trackCount, imageModifier, accountViewModel, blurred = true)
}
is AsyncImagePainter.State.Error -> {
BannerWithAuthorChip(note, trackCount, imageModifier, accountViewModel, blurred = false)
}
else -> {
BannerWithAuthorChip(note, trackCount, imageModifier, accountViewModel, blurred = true)
}
}
} else {
BannerWithAuthorChip(note, trackCount, imageModifier, accountViewModel, blurred = false)
}
}
}
@Composable
private fun BoxScope.FloatingTrackCountChip(trackCount: Int) {
TrackCountChip(
trackCount = trackCount,
modifier =
Modifier
.align(Alignment.BottomStart)
.padding(12.dp),
)
}
/**
* Author-banner cover used whenever the playlist has no album artwork to show — either
* because the playlist doesn't carry an `image` tag, or because the configured URL is