diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt index 4207968..6dbba4c 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/ChannelsScreen.kt @@ -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), diff --git a/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoCard.kt b/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoCard.kt index 0a1c3a2..e910f5b 100644 --- a/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoCard.kt +++ b/app/src/main/java/net/jeena/jtubetv/ui/screens/list/VideoCard.kt @@ -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) - // 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() + // 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,