Fix iOS thumbnails: thumbnailVersion triggers DenseThumb retry
When the background batch pre-fetch writes new thumbnails to cache, thumbnailVersion increments. DenseThumb uses it as a LaunchedEffect key so cells retry loading from cache after each batch completes. Also removed thumbFailed permanent gate — thumbnails retry on each thumbnailVersion change instead of giving up after one failure.
This commit is contained in:
@@ -94,6 +94,10 @@ class AppState(private val configStore: ConfigStore) {
|
||||
private val _selectedDevice = MutableStateFlow<DeviceInfo?>(null)
|
||||
val selectedDevice: StateFlow<DeviceInfo?> = _selectedDevice.asStateFlow()
|
||||
|
||||
/** Incremented after each thumbnail batch completes — DenseThumb observes this to retry */
|
||||
private val _thumbnailVersion = MutableStateFlow(0)
|
||||
val thumbnailVersion: StateFlow<Int> = _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}")
|
||||
|
||||
@@ -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) },
|
||||
|
||||
@@ -43,6 +43,7 @@ fun DensePhotoGrid(
|
||||
selectedPhotos: Set<PhonePhoto>,
|
||||
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<ImageBitmap?>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user