Compare commits
2 Commits
eef4f61050
...
fbd228b768
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fbd228b768 | ||
|
|
309031aed7 |
@@ -94,6 +94,10 @@ class AppState(private val configStore: ConfigStore) {
|
|||||||
private val _selectedDevice = MutableStateFlow<DeviceInfo?>(null)
|
private val _selectedDevice = MutableStateFlow<DeviceInfo?>(null)
|
||||||
val selectedDevice: StateFlow<DeviceInfo?> = _selectedDevice.asStateFlow()
|
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() {
|
fun scanForDevices() {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
_deviceScan.value = DeviceScanState.Scanning
|
_deviceScan.value = DeviceScanState.Scanning
|
||||||
@@ -240,6 +244,10 @@ class AppState(private val configStore: ConfigStore) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
loaded += batchLoaded
|
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})")
|
log.info("iOS thumbnails: $loaded / ${needed.size} loaded (batch ${batchIdx + 1}/${(needed.size + batchSize - 1) / batchSize})")
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
log.warn("iOS thumbnail batch ${batchIdx + 1} failed: ${e.message}")
|
log.warn("iOS thumbnail batch ${batchIdx + 1} failed: ${e.message}")
|
||||||
|
|||||||
@@ -92,6 +92,7 @@ fun BrowseScreen(state: AppState) {
|
|||||||
|
|
||||||
VerticalDivider()
|
VerticalDivider()
|
||||||
|
|
||||||
|
val thumbnailVersion by state.thumbnailVersion.collectAsState()
|
||||||
DensePhotoGrid(
|
DensePhotoGrid(
|
||||||
folderSections = folderSections,
|
folderSections = folderSections,
|
||||||
selectedPhotos = selectedPhotos,
|
selectedPhotos = selectedPhotos,
|
||||||
@@ -101,6 +102,7 @@ fun BrowseScreen(state: AppState) {
|
|||||||
state.connectorFor(d).pullToFile(d, devicePath, localPath)
|
state.connectorFor(d).pullToFile(d, devicePath, localPath)
|
||||||
} ?: false
|
} ?: false
|
||||||
},
|
},
|
||||||
|
thumbnailVersion = thumbnailVersion,
|
||||||
columnCount = columnCount,
|
columnCount = columnCount,
|
||||||
listState = listState,
|
listState = listState,
|
||||||
onTogglePhoto = { state.togglePhotoSelection(it) },
|
onTogglePhoto = { state.togglePhotoSelection(it) },
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ import java.time.LocalDate
|
|||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.time.format.DateTimeFormatter
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
private val thumbLog = org.slf4j.LoggerFactory.getLogger("DenseThumb")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dense grid: folder headers + flat photo grid per folder.
|
* Dense grid: folder headers + flat photo grid per folder.
|
||||||
* Each photo that is the first of its day gets a small date label
|
* Each photo that is the first of its day gets a small date label
|
||||||
@@ -43,6 +45,7 @@ fun DensePhotoGrid(
|
|||||||
selectedPhotos: Set<PhonePhoto>,
|
selectedPhotos: Set<PhonePhoto>,
|
||||||
cacheKey: String,
|
cacheKey: String,
|
||||||
pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean,
|
pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean,
|
||||||
|
thumbnailVersion: Int = 0,
|
||||||
columnCount: Int,
|
columnCount: Int,
|
||||||
listState: LazyListState,
|
listState: LazyListState,
|
||||||
onTogglePhoto: (PhonePhoto) -> Unit,
|
onTogglePhoto: (PhonePhoto) -> Unit,
|
||||||
@@ -140,6 +143,7 @@ fun DensePhotoGrid(
|
|||||||
isSelected = labeled.photo in selectedPhotos,
|
isSelected = labeled.photo in selectedPhotos,
|
||||||
cacheKey = cacheKey,
|
cacheKey = cacheKey,
|
||||||
pullFn = pullFn,
|
pullFn = pullFn,
|
||||||
|
thumbnailVersion = thumbnailVersion,
|
||||||
onToggle = { onTogglePhoto(labeled.photo) },
|
onToggle = { onTogglePhoto(labeled.photo) },
|
||||||
dateLabel = labeled.dateLabel,
|
dateLabel = labeled.dateLabel,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
@@ -162,15 +166,16 @@ private fun DenseThumb(
|
|||||||
isSelected: Boolean,
|
isSelected: Boolean,
|
||||||
cacheKey: String,
|
cacheKey: String,
|
||||||
pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean,
|
pullFn: suspend (devicePath: String, localDestPath: Path) -> Boolean,
|
||||||
|
thumbnailVersion: Int,
|
||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
dateLabel: String?,
|
dateLabel: String?,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
var thumbnail by remember(photo.devicePath) { mutableStateOf<ImageBitmap?>(null) }
|
var thumbnail by remember(photo.devicePath) { mutableStateOf<ImageBitmap?>(null) }
|
||||||
var thumbFailed by remember(photo.devicePath) { mutableStateOf(false) }
|
|
||||||
|
|
||||||
LaunchedEffect(photo.devicePath) {
|
// Re-run when thumbnailVersion increments (new batch cached) or on first composition
|
||||||
if (!thumbFailed && thumbnail == null) {
|
LaunchedEffect(photo.devicePath, thumbnailVersion) {
|
||||||
|
if (thumbnail == null) {
|
||||||
val bytes = withContext(Dispatchers.IO) {
|
val bytes = withContext(Dispatchers.IO) {
|
||||||
ThumbnailCache.get(
|
ThumbnailCache.get(
|
||||||
cacheKey = cacheKey,
|
cacheKey = cacheKey,
|
||||||
@@ -179,12 +184,12 @@ private fun DenseThumb(
|
|||||||
pullFn = pullFn,
|
pullFn = pullFn,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (bytes != null) {
|
if (bytes != null && bytes.isNotEmpty()) {
|
||||||
runCatching {
|
runCatching {
|
||||||
thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
|
thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||||
}.onFailure { thumbFailed = true }
|
}.onFailure { e ->
|
||||||
} else {
|
thumbLog.warn("Decode failed for ${photo.filename} (${bytes.size} bytes): ${e.message}")
|
||||||
thumbFailed = true
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user