Compare commits

...

2 Commits

Author SHA1 Message Date
Kyle Bolen
fbd228b768 Add decode failure logging to DenseThumb to diagnose sparse thumbnails 2026-07-30 03:30:47 +00:00
Kyle Bolen
309031aed7 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.
2026-07-30 03:28:31 +00:00
3 changed files with 22 additions and 7 deletions

View File

@@ -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}")

View File

@@ -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) },

View File

@@ -31,6 +31,8 @@ import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
private val thumbLog = org.slf4j.LoggerFactory.getLogger("DenseThumb")
/**
* Dense grid: folder headers + flat photo grid per folder.
* Each photo that is the first of its day gets a small date label
@@ -43,6 +45,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 +143,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 +166,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,
@@ -179,12 +184,12 @@ private fun DenseThumb(
pullFn = pullFn,
)
}
if (bytes != null) {
if (bytes != null && bytes.isNotEmpty()) {
runCatching {
thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
}.onFailure { thumbFailed = true }
} else {
thumbFailed = true
}.onFailure { e ->
thumbLog.warn("Decode failed for ${photo.filename} (${bytes.size} bytes): ${e.message}")
}
}
}
}