Dense view: date label above each specific photo that starts a new day
Each photo that is the first of its day gets a 14dp date label
('Jul 28') above its thumbnail only — within its column, not spanning
the row. Other photos in the same row get an invisible spacer so all
thumbnails stay aligned. Result: date markers appear above individual
photos exactly where the day changes.
This commit is contained in:
0
.attach_pid1471
Normal file
0
.attach_pid1471
Normal file
@@ -100,23 +100,13 @@ fun BrowseScreen(state: AppState) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dense mode: folder scroll index (folder header + date labels + photo rows)
|
// Dense mode: folder scroll index (folder header + photo rows only)
|
||||||
val denseScrollIndex = remember(folderSections, columnCount) {
|
val denseScrollIndex = remember(folderSections, columnCount) {
|
||||||
var idx = 0
|
var idx = 0
|
||||||
val zoneId = java.time.ZoneId.systemDefault()
|
|
||||||
folderSections.associate { folderSection ->
|
folderSections.associate { folderSection ->
|
||||||
val startIdx = idx
|
val startIdx = idx
|
||||||
idx += 1 // folder header
|
idx += 1 // folder header
|
||||||
val sorted = folderSection.allPhotos.sortedByDescending { it.dateTaken }
|
idx += ((folderSection.allPhotos.size + columnCount - 1) / columnCount)
|
||||||
var lastDate: java.time.LocalDate? = null
|
|
||||||
sorted.chunked(columnCount).forEach { row ->
|
|
||||||
val rowDate = row.first().dateTaken.atZone(zoneId).toLocalDate()
|
|
||||||
if (rowDate != lastDate) {
|
|
||||||
idx += 1 // date label row
|
|
||||||
lastDate = rowDate
|
|
||||||
}
|
|
||||||
idx += 1 // photo row
|
|
||||||
}
|
|
||||||
folderSection.folder.name to startIdx
|
folderSection.folder.name to startIdx
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,13 +60,14 @@ fun DensePhotoGrid(
|
|||||||
val zoneId = ZoneId.systemDefault()
|
val zoneId = ZoneId.systemDefault()
|
||||||
val dayFmt = remember { DateTimeFormatter.ofPattern("MMM d") }
|
val dayFmt = remember { DateTimeFormatter.ofPattern("MMM d") }
|
||||||
|
|
||||||
// Build items: FOLDER_HEADER | DATE_LABEL | PHOTO_ROW
|
// Build items: FOLDER_HEADER | PHOTO_ROW
|
||||||
data class PhotoRow(val photos: List<PhonePhoto>)
|
// Each photo carries its own dateLabel if it's the first of a new day
|
||||||
|
data class LabeledPhoto(val photo: PhonePhoto, val dateLabel: String?)
|
||||||
|
data class PhotoRow(val photos: List<LabeledPhoto>)
|
||||||
data class Item(
|
data class Item(
|
||||||
val type: GridItemType,
|
val type: GridItemType,
|
||||||
val folderSection: FolderSection? = null,
|
val folderSection: FolderSection? = null,
|
||||||
val photoRow: PhotoRow? = null,
|
val photoRow: PhotoRow? = null,
|
||||||
val dateLabel: String? = null,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
val items: List<Item> = buildList {
|
val items: List<Item> = buildList {
|
||||||
@@ -77,13 +78,14 @@ fun DensePhotoGrid(
|
|||||||
val sorted = section.allPhotos.sortedByDescending { it.dateTaken }
|
val sorted = section.allPhotos.sortedByDescending { it.dateTaken }
|
||||||
var lastDate: LocalDate? = null
|
var lastDate: LocalDate? = null
|
||||||
|
|
||||||
sorted.chunked(columnCount).forEach { rowPhotos ->
|
// Tag each photo with a date label if it's the first of its day
|
||||||
val rowDate = rowPhotos.first().dateTaken.atZone(zoneId).toLocalDate()
|
val labeled: List<LabeledPhoto> = sorted.map { photo ->
|
||||||
if (rowDate != lastDate) {
|
val date = photo.dateTaken.atZone(zoneId).toLocalDate()
|
||||||
lastDate = rowDate
|
val label = if (date != lastDate) { lastDate = date; dayFmt.format(date) } else null
|
||||||
// Slim date label above every day's first row (including first row of folder)
|
LabeledPhoto(photo, label)
|
||||||
add(Item(GridItemType.DAY_BOUNDARY, dateLabel = dayFmt.format(rowDate)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
labeled.chunked(columnCount).forEach { rowPhotos ->
|
||||||
add(Item(GridItemType.PHOTO_ROW, photoRow = PhotoRow(rowPhotos)))
|
add(Item(GridItemType.PHOTO_ROW, photoRow = PhotoRow(rowPhotos)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,8 +102,7 @@ fun DensePhotoGrid(
|
|||||||
val item = items[idx]
|
val item = items[idx]
|
||||||
when (item.type) {
|
when (item.type) {
|
||||||
GridItemType.FOLDER_HEADER -> "hdr-${item.folderSection!!.folder.name}"
|
GridItemType.FOLDER_HEADER -> "hdr-${item.folderSection!!.folder.name}"
|
||||||
GridItemType.DAY_BOUNDARY -> "date-${item.dateLabel}-${idx}"
|
else -> "row-${item.photoRow!!.photos.first().photo.devicePath}"
|
||||||
else -> "row-${item.photoRow!!.photos.first().devicePath}"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
) { idx ->
|
) { idx ->
|
||||||
@@ -119,42 +120,24 @@ fun DensePhotoGrid(
|
|||||||
onDeselectAll = { onDeselectAllFolder(section.allPhotos) },
|
onDeselectAll = { onDeselectAllFolder(section.allPhotos) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
GridItemType.DAY_BOUNDARY -> {
|
|
||||||
// Slim full-width date label — 18dp tall, no wasted space
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(horizontal = 8.dp)
|
|
||||||
.padding(top = 4.dp, bottom = 1.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
item.dateLabel!!,
|
|
||||||
fontSize = 10.sp,
|
|
||||||
fontWeight = FontWeight.Medium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
HorizontalDivider(
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
thickness = 0.5.dp,
|
|
||||||
color = MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> {
|
else -> {
|
||||||
val row = item.photoRow!!
|
val row = item.photoRow!!
|
||||||
|
// Check if any photo in this row has a date label — if so, add top padding
|
||||||
|
// to the row so label text has room above the thumbnail
|
||||||
|
val hasLabel = row.photos.any { it.dateLabel != null }
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
|
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp)
|
||||||
|
.padding(top = if (hasLabel) 0.dp else 0.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
) {
|
) {
|
||||||
row.photos.forEach { photo ->
|
row.photos.forEach { labeled ->
|
||||||
DenseThumb(
|
DenseThumb(
|
||||||
photo = photo,
|
photo = labeled.photo,
|
||||||
isSelected = photo in selectedPhotos,
|
isSelected = labeled.photo in selectedPhotos,
|
||||||
device = device,
|
device = device,
|
||||||
adbBin = adbBin,
|
adbBin = adbBin,
|
||||||
onToggle = { onTogglePhoto(photo) },
|
onToggle = { onTogglePhoto(labeled.photo) },
|
||||||
|
dateLabel = labeled.dateLabel,
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -174,6 +157,7 @@ private fun DenseThumb(
|
|||||||
device: DeviceInfo?,
|
device: DeviceInfo?,
|
||||||
adbBin: String,
|
adbBin: String,
|
||||||
onToggle: () -> Unit,
|
onToggle: () -> Unit,
|
||||||
|
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) }
|
||||||
@@ -194,6 +178,68 @@ private fun DenseThumb(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
// Date label above this specific column — 16dp tall, always present to keep rows aligned
|
||||||
|
if (dateLabel != null) {
|
||||||
|
Text(
|
||||||
|
dateLabel,
|
||||||
|
fontSize = 10.sp,
|
||||||
|
fontWeight = FontWeight.Medium,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(bottom = 2.dp, start = 1.dp).height(14.dp),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(1f)
|
||||||
|
.clickable(onClick = onToggle)
|
||||||
|
.then(if (isSelected) Modifier.border(2.dp, MaterialTheme.colorScheme.primary) else Modifier),
|
||||||
|
) {
|
||||||
|
if (thumbnail != null) {
|
||||||
|
Image(bitmap = thumbnail!!, contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop, modifier = Modifier.fillMaxSize())
|
||||||
|
} else {
|
||||||
|
Box(Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceVariant),
|
||||||
|
contentAlignment = Alignment.Center) {
|
||||||
|
Icon(Icons.Default.Image, contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.outline, modifier = Modifier.size(20.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (photo.mediaType == MediaType.VIDEO) {
|
||||||
|
Badge(containerColor = MaterialTheme.colorScheme.tertiary,
|
||||||
|
modifier = Modifier.align(Alignment.BottomStart).padding(2.dp)) {
|
||||||
|
Text("▶", style = MaterialTheme.typography.labelSmall)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (isSelected) {
|
||||||
|
Icon(Icons.Default.CheckCircle, contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.align(Alignment.TopEnd).padding(2.dp).size(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var thumbnail by remember(photo.devicePath) { mutableStateOf<ImageBitmap?>(null) }
|
||||||
|
var thumbFailed by remember(photo.devicePath) { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(photo.devicePath) {
|
||||||
|
if (device?.adbSerial != null && !thumbFailed && thumbnail == null) {
|
||||||
|
val bytes = withContext(Dispatchers.IO) {
|
||||||
|
ThumbnailCache.get(adbBin, device.adbSerial, photo.devicePath, photo.filename)
|
||||||
|
}
|
||||||
|
if (bytes != null) {
|
||||||
|
runCatching {
|
||||||
|
thumbnail = SkiaImage.makeFromEncoded(bytes).toComposeImageBitmap()
|
||||||
|
}.onFailure { thumbFailed = true }
|
||||||
|
} else {
|
||||||
|
thumbFailed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
|
|||||||
Reference in New Issue
Block a user