diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt index 413061d..7dab1dd 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt @@ -53,6 +53,11 @@ fun DensePhotoGrid( onDeselectAllFolder: (List) -> Unit, modifier: Modifier = Modifier, ) { + // In-memory bitmap cache — survives cell recycling during scroll + // keyed on cacheKey so it resets when device changes + val bitmapCache = remember(cacheKey) { + mutableMapOf() + } if (folderSections.isEmpty() || folderSections.all { it.allPhotos.isEmpty() }) { Box(modifier.fillMaxSize(), contentAlignment = Alignment.Center) { Column( @@ -144,6 +149,7 @@ fun DensePhotoGrid( cacheKey = cacheKey, pullFn = pullFn, thumbnailVersion = thumbnailVersion, + bitmapCache = bitmapCache, onToggle = { onTogglePhoto(labeled.photo) }, dateLabel = labeled.dateLabel, modifier = Modifier.weight(1f), @@ -167,13 +173,16 @@ private fun DenseThumb( cacheKey: String, pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean, thumbnailVersion: Int, + bitmapCache: MutableMap, onToggle: () -> Unit, dateLabel: String?, modifier: Modifier = Modifier, ) { - var thumbnail by remember(photo.devicePath) { mutableStateOf(null) } + // Check in-memory cache first (survives Compose cell recycling during scroll) + var thumbnail by remember(photo.devicePath) { + mutableStateOf(bitmapCache[photo.devicePath]) + } - // Re-run when thumbnailVersion increments (new batch cached) or on first composition LaunchedEffect(photo.devicePath, thumbnailVersion) { if (thumbnail == null) { val bytes = withContext(Dispatchers.IO) { @@ -186,7 +195,9 @@ private fun DenseThumb( } if (bytes != null && bytes.isNotEmpty()) { runCatching { - thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap() + val bitmap = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap() + bitmapCache[photo.devicePath] = bitmap // store in memory cache + thumbnail = bitmap }.onFailure { e -> thumbLog.warn("Decode failed for ${photo.filename} (${bytes.size} bytes): ${e.message}") }