diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt index 8805946..b72cfcf 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt @@ -94,6 +94,10 @@ class AppState(private val configStore: ConfigStore) { private val _selectedDevice = MutableStateFlow(null) val selectedDevice: StateFlow = _selectedDevice.asStateFlow() + /** Incremented after each thumbnail batch completes — DenseThumb observes this to retry */ + private val _thumbnailVersion = MutableStateFlow(0) + val thumbnailVersion: StateFlow = _thumbnailVersion.asStateFlow() + fun scanForDevices() { scope.launch { _deviceScan.value = DeviceScanState.Scanning @@ -240,6 +244,10 @@ class AppState(private val configStore: ConfigStore) { } } loaded += batchLoaded + if (batchLoaded > 0) { + // Notify UI that new thumbnails are available + _thumbnailVersion.value++ + } log.info("iOS thumbnails: $loaded / ${needed.size} loaded (batch ${batchIdx + 1}/${(needed.size + batchSize - 1) / batchSize})") } catch (e: Exception) { log.warn("iOS thumbnail batch ${batchIdx + 1} failed: ${e.message}") diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt index 1a9ccf3..abb91d8 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt @@ -92,6 +92,7 @@ fun BrowseScreen(state: AppState) { VerticalDivider() + val thumbnailVersion by state.thumbnailVersion.collectAsState() DensePhotoGrid( folderSections = folderSections, selectedPhotos = selectedPhotos, @@ -101,6 +102,7 @@ fun BrowseScreen(state: AppState) { state.connectorFor(d).pullToFile(d, devicePath, localPath) } ?: false }, + thumbnailVersion = thumbnailVersion, columnCount = columnCount, listState = listState, onTogglePhoto = { state.togglePhotoSelection(it) }, diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt index fca147e..52b70b3 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/DensePhotoGrid.kt @@ -43,6 +43,7 @@ fun DensePhotoGrid( selectedPhotos: Set, cacheKey: String, pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean, + thumbnailVersion: Int = 0, columnCount: Int, listState: LazyListState, onTogglePhoto: (PhonePhoto) -> Unit, @@ -140,6 +141,7 @@ fun DensePhotoGrid( isSelected = labeled.photo in selectedPhotos, cacheKey = cacheKey, pullFn = pullFn, + thumbnailVersion = thumbnailVersion, onToggle = { onTogglePhoto(labeled.photo) }, dateLabel = labeled.dateLabel, modifier = Modifier.weight(1f), @@ -162,15 +164,16 @@ private fun DenseThumb( isSelected: Boolean, cacheKey: String, pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean, + thumbnailVersion: Int, onToggle: () -> Unit, dateLabel: String?, modifier: Modifier = Modifier, ) { var thumbnail by remember(photo.devicePath) { mutableStateOf(null) } - var thumbFailed by remember(photo.devicePath) { mutableStateOf(false) } - LaunchedEffect(photo.devicePath) { - if (!thumbFailed && thumbnail == null) { + // Re-run when thumbnailVersion increments (new batch cached) or on first composition + LaunchedEffect(photo.devicePath, thumbnailVersion) { + if (thumbnail == null) { val bytes = withContext(Dispatchers.IO) { ThumbnailCache.get( cacheKey = cacheKey, @@ -182,9 +185,7 @@ private fun DenseThumb( if (bytes != null) { runCatching { thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap() - }.onFailure { thumbFailed = true } - } else { - thumbFailed = true + } } } }