Dense view: slim date label row above each day, including first day of folder

Each new day gets a 18dp full-width row: 'Jul 28 ────────────'
that appears above the first row of photos for that date.
First day of each folder section now also gets a date row (previously
skipped). No photo overlays. denseScrollIndex updated to count date rows.
This commit is contained in:
Kyle Bolen
2026-07-28 06:33:40 +00:00
parent a6b489f133
commit 361eab792b
2 changed files with 45 additions and 30 deletions

View File

@@ -100,13 +100,23 @@ fun BrowseScreen(state: AppState) {
} }
} }
// Dense mode: folder scroll index (photo rows only, no day boundary items) // Dense mode: folder scroll index (folder header + date labels + photo rows)
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
idx += ((folderSection.allPhotos.size + columnCount - 1) / columnCount) // photo rows val sorted = folderSection.allPhotos.sortedByDescending { it.dateTaken }
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
} }
} }

View File

@@ -60,16 +60,13 @@ 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 | PHOTO_ROW (with optional dateLabel on first photo) // Build items: FOLDER_HEADER | DATE_LABEL | PHOTO_ROW
// dateLabel is set on the first photo of each new day within a folder data class PhotoRow(val photos: List<PhonePhoto>)
data class PhotoRow(
val photos: List<PhonePhoto>,
val dateLabel: String?, // non-null on first row of a new day
)
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 {
@@ -82,11 +79,12 @@ fun DensePhotoGrid(
sorted.chunked(columnCount).forEach { rowPhotos -> sorted.chunked(columnCount).forEach { rowPhotos ->
val rowDate = rowPhotos.first().dateTaken.atZone(zoneId).toLocalDate() val rowDate = rowPhotos.first().dateTaken.atZone(zoneId).toLocalDate()
val label = if (rowDate != lastDate) { if (rowDate != lastDate) {
lastDate = rowDate lastDate = rowDate
dayFmt.format(rowDate) // Slim date label above every day's first row (including first row of folder)
} else null add(Item(GridItemType.DAY_BOUNDARY, dateLabel = dayFmt.format(rowDate)))
add(Item(GridItemType.PHOTO_ROW, photoRow = PhotoRow(rowPhotos, label))) }
add(Item(GridItemType.PHOTO_ROW, photoRow = PhotoRow(rowPhotos)))
} }
} }
} }
@@ -102,6 +100,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().devicePath}" else -> "row-${item.photoRow!!.photos.first().devicePath}"
} }
}, },
@@ -120,21 +119,42 @@ 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!!
Row( Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp),
horizontalArrangement = Arrangement.spacedBy(2.dp), horizontalArrangement = Arrangement.spacedBy(2.dp),
) { ) {
row.photos.forEachIndexed { colIdx, photo -> row.photos.forEach { photo ->
DenseThumb( DenseThumb(
photo = photo, photo = photo,
isSelected = photo in selectedPhotos, isSelected = photo in selectedPhotos,
device = device, device = device,
adbBin = adbBin, adbBin = adbBin,
onToggle = { onTogglePhoto(photo) }, onToggle = { onTogglePhoto(photo) },
// Show date label only on first photo of the row that starts a new day
dateLabel = if (colIdx == 0) row.dateLabel else null,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
@@ -154,7 +174,6 @@ 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) }
@@ -199,20 +218,6 @@ private fun DenseThumb(
} }
} }
// Date label overlaid on top-left — only on first photo of a new day
if (dateLabel != null) {
Text(
dateLabel,
fontSize = 9.sp,
fontWeight = FontWeight.SemiBold,
color = Color.White,
modifier = Modifier
.align(Alignment.TopStart)
.background(Color.Black.copy(alpha = 0.55f))
.padding(horizontal = 4.dp, vertical = 2.dp),
)
}
// Video badge // Video badge
if (photo.mediaType == MediaType.VIDEO) { if (photo.mediaType == MediaType.VIDEO) {
Badge( Badge(