From e4e619695e6dc779710f6a5479a1a348ac40f11d Mon Sep 17 00:00:00 2001 From: tapframe <85391825+tapframe@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:05:10 +0530 Subject: [PATCH] feat: implement image caching for cast avatars in detail screens --- .gitignore | 3 +- .../features/details/PersonDetailScreen.kt | 34 +++++++++++++++++-- .../details/components/DetailCastSection.kt | 20 ++++++++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8bf4c7a6..84b9a289 100644 --- a/.gitignore +++ b/.gitignore @@ -21,4 +21,5 @@ logs/ Docs keystore/ scripts/build-distribution.sh -asset \ No newline at end of file +asset +scripts/scrape_android_compose_animation_docs.py diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/PersonDetailScreen.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/PersonDetailScreen.kt index f8f90588..5a6b77e1 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/PersonDetailScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/PersonDetailScreen.kt @@ -54,6 +54,8 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.lerp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest import com.nuvio.app.features.details.components.DetailPosterRailSection import com.nuvio.app.features.home.MetaPreview import com.nuvio.app.features.tmdb.TmdbMetadataService @@ -297,6 +299,20 @@ private fun HeroSection( val heroScale = 1f - (collapseProgress * 0.12f) val heroAlpha = 1f - (collapseProgress * 0.35f) val avatarUrl = person.profilePhoto?.takeIf { it.isNotBlank() } ?: fallbackProfilePhoto + val avatarCacheKey = castAvatarSharedTransitionKey(person.tmdbId) + val platformContext = LocalPlatformContext.current + val avatarRequest = if (!avatarUrl.isNullOrBlank()) { + remember(platformContext, avatarUrl, avatarCacheKey) { + ImageRequest.Builder(platformContext) + .data(avatarUrl) + .memoryCacheKey(avatarCacheKey) + .placeholderMemoryCacheKey(avatarCacheKey) + .diskCacheKey(avatarUrl) + .build() + } + } else { + null + } val avatarSharedElementModifier = if (sharedTransitionScope != null && animatedVisibilityScope != null) { with(sharedTransitionScope) { Modifier.sharedElement( @@ -333,7 +349,7 @@ private fun HeroSection( ) { if (!avatarUrl.isNullOrBlank()) { AsyncImage( - model = avatarUrl, + model = avatarRequest ?: avatarUrl, contentDescription = person.name, modifier = Modifier.matchParentSize(), contentScale = ContentScale.Crop, @@ -422,6 +438,20 @@ private fun PersonDetailSkeleton( animatedVisibilityScope: AnimatedVisibilityScope? = null, ) { val accentColor = MaterialTheme.colorScheme.primary + val avatarCacheKey = castAvatarSharedTransitionKey(personId) + val platformContext = LocalPlatformContext.current + val avatarRequest = if (!profilePhoto.isNullOrBlank()) { + remember(platformContext, profilePhoto, avatarCacheKey) { + ImageRequest.Builder(platformContext) + .data(profilePhoto) + .memoryCacheKey(avatarCacheKey) + .placeholderMemoryCacheKey(avatarCacheKey) + .diskCacheKey(profilePhoto) + .build() + } + } else { + null + } val accentGradient = remember(accentColor) { Brush.verticalGradient( colorStops = arrayOf( @@ -480,7 +510,7 @@ private fun PersonDetailSkeleton( ) { if (!profilePhoto.isNullOrBlank()) { AsyncImage( - model = profilePhoto, + model = avatarRequest ?: profilePhoto, contentDescription = personName, modifier = Modifier.matchParentSize(), contentScale = ContentScale.Crop, diff --git a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt index d2b315b9..00e33d68 100644 --- a/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt +++ b/composeApp/src/commonMain/kotlin/com/nuvio/app/features/details/components/DetailCastSection.kt @@ -19,6 +19,7 @@ import androidx.compose.ui.draw.clip import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -29,6 +30,8 @@ import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil3.compose.AsyncImage +import coil3.compose.LocalPlatformContext +import coil3.request.ImageRequest import com.nuvio.app.features.details.MetaPerson import com.nuvio.app.features.details.castAvatarSharedTransitionKey @@ -86,6 +89,21 @@ private fun CastItem( animatedVisibilityScope: AnimatedVisibilityScope? = null, onClick: (() -> Unit)? = null, ) { + val avatarCacheKey = person.tmdbId?.takeIf { it > 0 }?.let(::castAvatarSharedTransitionKey) + val platformContext = LocalPlatformContext.current + val avatarRequest = if (!person.photo.isNullOrBlank() && !avatarCacheKey.isNullOrBlank()) { + remember(platformContext, person.photo, avatarCacheKey) { + ImageRequest.Builder(platformContext) + .data(person.photo) + .memoryCacheKey(avatarCacheKey) + .placeholderMemoryCacheKey(avatarCacheKey) + .diskCacheKey(person.photo) + .build() + } + } else { + null + } + val avatarSharedElementModifier = if ( sharedTransitionScope != null && animatedVisibilityScope != null && @@ -124,7 +142,7 @@ private fun CastItem( ) { if (person.photo != null) { AsyncImage( - model = person.photo, + model = avatarRequest ?: person.photo, contentDescription = person.name, modifier = Modifier.matchParentSize(), contentScale = ContentScale.Crop,