images: rearm the loading shimmer only when the image changes
ShimmerAsyncImage keyed its loaded-state on the passed model, and the callers built a fresh coil ImageRequest on every recomposition. ImageRequest has no value equality, so the key changed every time: the loaded flag reset, the shimmer overlay was re-added and the image load restarted with each recomposition — an infinite animation per card that never disposed. On the TV this showed as a general fps collapse wherever thumbnails render (issue #28's feature shipped with this trap). The requests are now remembered under stable keys (video/channel id and the URL strings), so the sweep stops for real once the image (or an error) lands. Addresses #28. Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
afb3a0c871
commit
96c53c6c89
2 changed files with 33 additions and 15 deletions
|
|
@ -574,13 +574,19 @@ fun ChannelCard(
|
|||
val coverUrl = banner?.let { absoluteBanner(it) }
|
||||
?: stats?.latestThumbUrl
|
||||
if (coverUrl != null) {
|
||||
ShimmerAsyncImage(
|
||||
model = coil.request.ImageRequest.Builder(
|
||||
androidx.compose.ui.platform.LocalContext.current,
|
||||
)
|
||||
// The request must be remembered: ImageRequest has no value equality,
|
||||
// a fresh request per recomposition reset the shimmer (and retried
|
||||
// the load) every frame — the fps regression on the TV.
|
||||
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||
val coverKey = remember(channel.name, channel.host) { channel.name + "@" + channel.host }
|
||||
val coverReq = remember(coverKey, coverUrl) {
|
||||
coil.request.ImageRequest.Builder(ctx)
|
||||
.data(coverUrl)
|
||||
.size(480, 270) // same key as prefetch
|
||||
.build(),
|
||||
.build()
|
||||
}
|
||||
ShimmerAsyncImage(
|
||||
model = coverReq,
|
||||
contentDescription = channel.displayName,
|
||||
contentScale = ContentScale.Crop,
|
||||
modifier = Modifier.fillMaxWidth().aspectRatio(16f / 9f),
|
||||
|
|
@ -588,13 +594,18 @@ fun ChannelCard(
|
|||
}
|
||||
val avatar = channel.avatars.maxByOrNull { it.width }
|
||||
if (avatar != null) {
|
||||
ShimmerAsyncImage(
|
||||
model = coil.request.ImageRequest.Builder(
|
||||
androidx.compose.ui.platform.LocalContext.current,
|
||||
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||
val avatarUrl = remember(avatar.path, avatar.fileUrl) { absoluteAvatar(avatar) }
|
||||
val avatarReq = remember(avatarUrl) {
|
||||
coil.request.ImageRequest.Builder(
|
||||
ctx,
|
||||
)
|
||||
.data(absoluteAvatar(avatar))
|
||||
.data(avatarUrl)
|
||||
.size(80, 80)
|
||||
.build(),
|
||||
.build()
|
||||
}
|
||||
ShimmerAsyncImage(
|
||||
model = avatarReq,
|
||||
contentDescription = null,
|
||||
contentScale = ContentScale.Crop,
|
||||
base = Color(0xFF333333),
|
||||
|
|
|
|||
|
|
@ -124,11 +124,18 @@ fun VideoCard(
|
|||
.background(Color(0xFF222222)),
|
||||
) {
|
||||
video.thumbnailPath?.takeIf { it.isNotBlank() }?.let { path ->
|
||||
val req = coil.request.ImageRequest.Builder(androidx.compose.ui.platform.LocalContext.current)
|
||||
// The request object must be remembered: ImageRequest has no value
|
||||
// equality, and a fresh request per recomposition reset the
|
||||
// shimmer's loaded state (and retried the load) on every frame —
|
||||
// the fps regression on the TV.
|
||||
val ctx = androidx.compose.ui.platform.LocalContext.current
|
||||
val req = remember(video.id, video.thumbnailUrl) {
|
||||
coil.request.ImageRequest.Builder(ctx)
|
||||
// search-index results carry an absolute URL for their own instance
|
||||
.data(video.thumbnailUrl ?: AppGraph.repository.absoluteUrl(path))
|
||||
.size(480, 270) // ~2x the drawn thumb; avoids decoding 1080p+ jpegs
|
||||
.build()
|
||||
}
|
||||
ShimmerAsyncImage(
|
||||
model = req,
|
||||
contentDescription = video.name,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue