diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt index cc29fd0..4e4fc61 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/BrowseScreen.kt @@ -1,44 +1,22 @@ package com.bolenpad.photophetch.ui -import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.CheckCircle -import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.filled.RadioButtonUnchecked import androidx.compose.material.icons.filled.Warning import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.drawWithContent -import androidx.compose.ui.graphics.Brush -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.toComposeImageBitmap -import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import com.bolenpad.photophetch.model.DeviceInfo -import com.bolenpad.photophetch.model.MediaType import com.bolenpad.photophetch.model.PhonePhoto -import com.bolenpad.photophetch.util.ThumbnailCache -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import org.jetbrains.skia.Image as SkiaImage -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -enum class ViewMode { GROUPED, DENSE } +import java.time.ZoneId @Composable fun BrowseScreen(state: AppState) { @@ -49,18 +27,15 @@ fun BrowseScreen(state: AppState) { Column(modifier = Modifier.fillMaxSize()) { var columnCount by remember { mutableStateOf(4) } - var viewMode by remember { mutableStateOf(ViewMode.GROUPED) } BrowseTopBar( deviceName = device?.displayName ?: "Browse Photos", selectedCount = selectedPhotos.size, columnCount = columnCount, - viewMode = viewMode, onBack = { state.navigateTo(Screen.CONNECT) }, onSelectNone = { state.selectNone() }, onImport = { state.prepareImport() }, onColumnChange = { columnCount = (columnCount + it).coerceIn(2, 8) }, - onToggleView = { viewMode = if (viewMode == ViewMode.GROUPED) ViewMode.DENSE else ViewMode.GROUPED }, ) if (state.shootingInHeic && !config.heicWarningDismissed) { @@ -78,62 +53,38 @@ fun BrowseScreen(state: AppState) { } } is PhotoListState.Loaded -> { - // Build ordered list of (folder, photos-by-day) for the unified scroll val folderSections = buildFolderSections(pl) - val groupedListState = rememberLazyListState() - val denseListState = rememberLazyListState() + val listState = rememberLazyListState() val scope = rememberCoroutineScope() - val listState = if (viewMode == ViewMode.GROUPED) groupedListState else denseListState - - // Grouped mode: folder scroll index (accounts for day headers) - val groupedScrollIndex = remember(folderSections, columnCount) { + // Folder scroll index for jump list + val folderScrollIndex = remember(folderSections, columnCount) { var idx = 0 - folderSections.associate { (folder, byDay) -> + folderSections.associate { section -> val startIdx = idx idx += 1 // folder header - byDay.forEach { (_, dayPhotos) -> - if (dayPhotos.size >= 3) idx += 1 - idx += ((dayPhotos.size + columnCount - 1) / columnCount) - } - folder.name to startIdx + idx += ((section.allPhotos.size + columnCount - 1) / columnCount) + section.folder.name to startIdx } } - // Dense mode: folder scroll index (folder header + photo rows only) - val denseScrollIndex = remember(folderSections, columnCount) { - var idx = 0 - folderSections.associate { folderSection -> - val startIdx = idx - idx += 1 // folder header - idx += ((folderSection.allPhotos.size + columnCount - 1) / columnCount) - folderSection.folder.name to startIdx - } - } - - val folderScrollIndex = if (viewMode == ViewMode.GROUPED) groupedScrollIndex else denseScrollIndex - - // Scroll spy — keyed on viewMode so it recomposes when switching - val activeFolder by key(viewMode) { - remember { - derivedStateOf { - val firstVisible = listState.firstVisibleItemIndex - folderScrollIndex.entries - .filter { it.value <= firstVisible } - .maxByOrNull { it.value }?.key - ?: folderSections.firstOrNull()?.folder?.name - } + val activeFolder by remember(folderScrollIndex) { + derivedStateOf { + val firstVisible = listState.firstVisibleItemIndex + folderScrollIndex.entries + .filter { it.value <= firstVisible } + .maxByOrNull { it.value }?.key + ?: folderSections.firstOrNull()?.folder?.name } } Row(modifier = Modifier.fillMaxSize()) { - // Left: folder jump list — jumps in whichever list is active FolderJumpList( folders = pl.folders, activeFolder = activeFolder, selectedPhotos = selectedPhotos, - onJump = { folderName -> - val idx = folderScrollIndex[folderName] ?: return@FolderJumpList + onJump = { name -> + val idx = folderScrollIndex[name] ?: return@FolderJumpList scope.launch { listState.animateScrollToItem(idx) } }, modifier = Modifier.width(190.dp).fillMaxHeight(), @@ -141,57 +92,22 @@ fun BrowseScreen(state: AppState) { VerticalDivider() - // Right: swap between grouped and dense view - when (viewMode) { - ViewMode.GROUPED -> UnifiedPhotoScroll( - folderSections = folderSections, - selectedPhotos = selectedPhotos, - device = device, - adbBin = config.adbPath ?: "adb", - listState = groupedListState, - columnCount = columnCount, - onTogglePhoto = { state.togglePhotoSelection(it) }, - onSelectAllFolder = { folder -> - folder.forEach { p -> - if (p !in selectedPhotos) state.togglePhotoSelection(p) - } - }, - onDeselectAllFolder = { folder -> - folder.forEach { p -> - if (p in selectedPhotos) state.togglePhotoSelection(p) - } - }, - onSelectDay = { dayPhotos -> - val allSelected = dayPhotos.all { it in selectedPhotos } - dayPhotos.forEach { p -> - val inSet = p in selectedPhotos - if (allSelected && inSet) state.togglePhotoSelection(p) - else if (!allSelected && !inSet) state.togglePhotoSelection(p) - } - }, - modifier = Modifier.weight(1f), - ) - ViewMode.DENSE -> DensePhotoGrid( - folderSections = folderSections, - selectedPhotos = selectedPhotos, - device = device, - adbBin = config.adbPath ?: "adb", - columnCount = columnCount, - listState = denseListState, - onTogglePhoto = { state.togglePhotoSelection(it) }, - onSelectAllFolder = { folder -> - folder.forEach { p -> - if (p !in selectedPhotos) state.togglePhotoSelection(p) - } - }, - onDeselectAllFolder = { folder -> - folder.forEach { p -> - if (p in selectedPhotos) state.togglePhotoSelection(p) - } - }, - modifier = Modifier.weight(1f), - ) - } + DensePhotoGrid( + folderSections = folderSections, + selectedPhotos = selectedPhotos, + device = device, + adbBin = config.adbPath ?: "adb", + columnCount = columnCount, + listState = listState, + onTogglePhoto = { state.togglePhotoSelection(it) }, + onSelectAllFolder = { folder -> + folder.forEach { p -> if (p !in selectedPhotos) state.togglePhotoSelection(p) } + }, + onDeselectAllFolder = { folder -> + folder.forEach { p -> if (p in selectedPhotos) state.togglePhotoSelection(p) } + }, + modifier = Modifier.weight(1f), + ) } } is PhotoListState.Error -> { @@ -208,10 +124,10 @@ fun BrowseScreen(state: AppState) { } } -// ─── Data helpers ────────────────────────────────────────────────────────────── +// ─── Data helpers ───────────────────────────────────────────────────────────── private fun buildFolderSections(pl: PhotoListState.Loaded): List { - val zoneId = java.time.ZoneId.systemDefault() + val zoneId = ZoneId.systemDefault() return pl.folders.map { folder -> val photos = pl.all .filter { it.sourceFolder == folder.name } @@ -225,7 +141,7 @@ private fun buildFolderSections(pl: PhotoListState.Loaded): List } } -// ─── Left jump list ─────────────────────────────────────────────────────────── +// ─── Folder jump list ───────────────────────────────────────────────────────── @Composable private fun FolderJumpList( @@ -244,38 +160,24 @@ private fun FolderJumpList( if (cameraRoll.isNotEmpty()) { item { - Text( - "CAMERA", - style = MaterialTheme.typography.labelSmall, + Text("CAMERA", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 12.dp, top = 8.dp, bottom = 2.dp), - ) + modifier = Modifier.padding(start = 12.dp, top = 8.dp, bottom = 2.dp)) } items(cameraRoll.size) { i -> - JumpRow( - folder = cameraRoll[i], - isActive = cameraRoll[i].name == activeFolder, - selectedCount = selectedPhotos.count { it.sourceFolder == cameraRoll[i].name }, - onClick = { onJump(cameraRoll[i].name) }, - ) + JumpRow(cameraRoll[i], cameraRoll[i].name == activeFolder, + selectedPhotos.count { it.sourceFolder == cameraRoll[i].name }) { onJump(cameraRoll[i].name) } } } if (other.isNotEmpty()) { item { - Text( - "OTHER", - style = MaterialTheme.typography.labelSmall, + Text("OTHER", style = MaterialTheme.typography.labelSmall, color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 2.dp), - ) + modifier = Modifier.padding(start = 12.dp, top = 12.dp, bottom = 2.dp)) } items(other.size) { i -> - JumpRow( - folder = other[i], - isActive = other[i].name == activeFolder, - selectedCount = selectedPhotos.count { it.sourceFolder == other[i].name }, - onClick = { onJump(other[i].name) }, - ) + JumpRow(other[i], other[i].name == activeFolder, + selectedPhotos.count { it.sourceFolder == other[i].name }) { onJump(other[i].name) } } } } @@ -283,37 +185,20 @@ private fun FolderJumpList( } @Composable -private fun JumpRow( - folder: FolderInfo, - isActive: Boolean, - selectedCount: Int, - onClick: () -> Unit, -) { +private fun JumpRow(folder: FolderInfo, isActive: Boolean, selectedCount: Int, onClick: () -> Unit) { Row( - modifier = Modifier - .fillMaxWidth() - .clickable(onClick = onClick) - .background( - if (isActive) MaterialTheme.colorScheme.secondaryContainer - else MaterialTheme.colorScheme.surface, - ) + modifier = Modifier.fillMaxWidth().clickable(onClick = onClick) + .background(if (isActive) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface) .padding(horizontal = 12.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween, ) { Column(modifier = Modifier.weight(1f)) { - Text( - folder.name, - style = MaterialTheme.typography.bodySmall, + Text(folder.name, style = MaterialTheme.typography.bodySmall, fontWeight = if (isActive) FontWeight.SemiBold else FontWeight.Normal, - maxLines = 1, - overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, - ) - Text( - "${folder.photoCount}", - style = MaterialTheme.typography.labelSmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + maxLines = 1, overflow = TextOverflow.Ellipsis) + Text("${folder.photoCount}", style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) } if (selectedCount > 0) { Badge(containerColor = MaterialTheme.colorScheme.primary) { @@ -323,94 +208,7 @@ private fun JumpRow( } } -// ─── Right unified scroll ───────────────────────────────────────────────────── - -@Composable -private fun UnifiedPhotoScroll( - folderSections: List, - selectedPhotos: Set, - device: com.bolenpad.photophetch.model.DeviceInfo?, - adbBin: String, - listState: LazyListState, - columnCount: Int, - onTogglePhoto: (PhonePhoto) -> Unit, - onSelectAllFolder: (List) -> Unit, - onDeselectAllFolder: (List) -> Unit, - onSelectDay: (List) -> Unit, - modifier: Modifier = Modifier, -) { - - // Column count — adjustable via +/- buttons in the top bar - LazyColumn( - state = listState, - modifier = modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = 32.dp), - ) { - folderSections.forEach { section -> - val folderSelected = section.allPhotos.count { it in selectedPhotos } - val allFolderSelected = folderSelected == section.allPhotos.size - - // ── Folder divider header ────────────────────────────────────── - item(key = "folder-${section.folder.name}") { - FolderDividerHeader( - name = section.folder.name, - total = section.allPhotos.size, - selected = folderSelected, - allSelected = allFolderSelected, - onSelectAll = { onSelectAllFolder(section.allPhotos) }, - onDeselectAll = { onDeselectAllFolder(section.allPhotos) }, - ) - } - - // ── Day groups — adaptive headers ────────────────────────────── - // Dense days (≥3 photos): full header with count + Select day button - // Sparse days (<3 photos): small inline date chip on first photo of the day - section.byDay.forEach { (day, dayPhotos) -> - val daySelected = dayPhotos.count { it in selectedPhotos } - val isDense = dayPhotos.size >= 3 - - if (isDense) { - item(key = "day-${section.folder.name}-$day") { - DaySubHeader( - day = day, - total = dayPhotos.size, - selected = daySelected, - onSelectDay = { onSelectDay(dayPhotos) }, - ) - } - } - - val rows = dayPhotos.chunked(columnCount) - items( - count = rows.size, - key = { idx -> "row-${section.folder.name}-$day-$idx" }, - ) { rowIdx -> - val rowPhotos = rows[rowIdx] - val isFirstRow = rowIdx == 0 - Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp), - horizontalArrangement = Arrangement.spacedBy(3.dp), - ) { - rowPhotos.forEachIndexed { colIdx, photo -> - val showDateChip = !isDense && isFirstRow && colIdx == 0 - PhotoThumb( - photo = photo, - isSelected = photo in selectedPhotos, - device = device, - adbBin = adbBin, - onToggle = { onTogglePhoto(photo) }, - dateChip = if (showDateChip) day else null, - modifier = Modifier.weight(1f), - ) - } - repeat(columnCount - rowPhotos.size) { Spacer(Modifier.weight(1f)) } - } - Spacer(Modifier.height(3.dp)) - } - } - } - } -} +// ─── Folder divider header ──────────────────────────────────────────────────── @Composable internal fun FolderDividerHeader( @@ -436,61 +234,25 @@ internal fun FolderDividerHeader( if (selected > 0) "$selected / $total selected" else "$total photos", style = MaterialTheme.typography.labelSmall, color = if (selected > 0) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant, + else MaterialTheme.colorScheme.onSurfaceVariant, ) } Row(horizontalArrangement = Arrangement.spacedBy(4.dp)) { - TextButton( - onClick = onSelectAll, - enabled = !allSelected, - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), - ) { Text("Select all", style = MaterialTheme.typography.labelMedium) } + TextButton(onClick = onSelectAll, enabled = !allSelected, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { + Text("Select all", style = MaterialTheme.typography.labelMedium) + } if (selected > 0) { - TextButton( - onClick = onDeselectAll, - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), - ) { Text("Deselect all", style = MaterialTheme.typography.labelMedium) } + TextButton(onClick = onDeselectAll, + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp)) { + Text("Deselect all", style = MaterialTheme.typography.labelMedium) + } } } } } } -@Composable -private fun DaySubHeader( - day: java.time.LocalDate, - total: Int, - selected: Int, - onSelectDay: () -> Unit, -) { - val fmt = remember { java.time.format.DateTimeFormatter.ofPattern("EEEE, MMMM d, yyyy") } - Row( - modifier = Modifier.fillMaxWidth() - .padding(start = 12.dp, end = 8.dp, top = 10.dp, bottom = 4.dp), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column { - Text(fmt.format(day), style = MaterialTheme.typography.bodyMedium, fontWeight = FontWeight.SemiBold) - Text( - if (selected > 0) "$selected / $total selected" else "$total photos", - style = MaterialTheme.typography.labelSmall, - color = if (selected > 0) MaterialTheme.colorScheme.primary - else MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - TextButton( - onClick = onSelectDay, - contentPadding = PaddingValues(horizontal = 8.dp, vertical = 4.dp), - ) { - Text( - if (selected == total) "Deselect day" else "Select day", - style = MaterialTheme.typography.labelMedium, - ) - } - } -} - // ─── Top bar ────────────────────────────────────────────────────────────────── @OptIn(ExperimentalMaterial3Api::class) @@ -499,12 +261,10 @@ private fun BrowseTopBar( deviceName: String, selectedCount: Int, columnCount: Int, - viewMode: ViewMode, onBack: () -> Unit, onSelectNone: () -> Unit, onImport: () -> Unit, onColumnChange: (Int) -> Unit, - onToggleView: () -> Unit, ) { TopAppBar( title = { Text(deviceName) }, @@ -514,15 +274,6 @@ private fun BrowseTopBar( } }, actions = { - // View mode toggle - FilterChip( - selected = viewMode == ViewMode.DENSE, - onClick = onToggleView, - label = { Text(if (viewMode == ViewMode.DENSE) "Dense" else "Grouped", - style = MaterialTheme.typography.labelMedium) }, - modifier = Modifier.padding(end = 4.dp), - ) - // Grid size controls IconButton(onClick = { onColumnChange(-1) }, enabled = columnCount > 2) { Text("−", style = MaterialTheme.typography.titleMedium) } @@ -551,121 +302,17 @@ private fun HeicWarningBanner(onDismiss: () -> Unit) { colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.tertiaryContainer), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp), ) { - Row( - modifier = Modifier.padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalAlignment = Alignment.CenterVertically, - ) { + Row(modifier = Modifier.padding(12.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically) { Icon(Icons.Default.Warning, contentDescription = null, modifier = Modifier.size(20.dp)) Column(modifier = Modifier.weight(1f)) { Text("Phone is shooting in HEIC", fontWeight = FontWeight.SemiBold, style = MaterialTheme.typography.bodySmall) - Text( - "Files will be converted to JPEG. To avoid: Settings → Camera → Formats → Most Compatible.", + Text("Files will be converted to JPEG. To avoid: Settings → Camera → Formats → Most Compatible.", style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onTertiaryContainer, - ) + color = MaterialTheme.colorScheme.onTertiaryContainer) } TextButton(onClick = onDismiss) { Text("Dismiss") } } } } - -// ─── Photo thumbnail cell ───────────────────────────────────────────────────── - -@Composable -private fun PhotoThumb( - photo: PhonePhoto, - isSelected: Boolean, - device: DeviceInfo?, - adbBin: String, - onToggle: () -> Unit, - dateChip: LocalDate? = null, - modifier: Modifier = Modifier, -) { - var thumbnail by remember(photo.devicePath) { mutableStateOf(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( - modifier = modifier - .aspectRatio(1f) - .clickable(onClick = onToggle) - .then(if (isSelected) Modifier.border(3.dp, MaterialTheme.colorScheme.primary) else Modifier), - ) { - if (thumbnail != null) { - Image( - bitmap = thumbnail!!, - contentDescription = photo.filename, - 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(24.dp), - ) - } - } - - // Badges top-left - Row( - modifier = Modifier.align(Alignment.TopStart).padding(3.dp), - horizontalArrangement = Arrangement.spacedBy(2.dp), - ) { - if (photo.isHeic) { - Badge(containerColor = MaterialTheme.colorScheme.tertiaryContainer) { - Text("HEIC", style = MaterialTheme.typography.labelSmall) - } - } - if (photo.mediaType == MediaType.VIDEO) { - Badge(containerColor = MaterialTheme.colorScheme.tertiary) { - Text("▶", style = MaterialTheme.typography.labelSmall) - } - } - } - - // Sparse-day date chip — bottom left, small pill - if (dateChip != null) { - val fmt = remember { DateTimeFormatter.ofPattern("MMM d") } - Text( - fmt.format(dateChip), - fontSize = 10.sp, - color = Color.White, - modifier = Modifier - .align(Alignment.BottomStart) - .padding(4.dp) - .background(Color.Black.copy(alpha = 0.5f), shape = MaterialTheme.shapes.extraSmall) - .padding(horizontal = 4.dp, vertical = 1.dp), - ) - } - - // Selection indicator top-right - Icon( - if (isSelected) Icons.Default.CheckCircle else Icons.Default.RadioButtonUnchecked, - contentDescription = null, - tint = if (isSelected) MaterialTheme.colorScheme.primary else Color.White.copy(alpha = 0.8f), - modifier = Modifier.align(Alignment.TopEnd).padding(4.dp).size(22.dp), - ) - } -} diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/GridModels.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/GridModels.kt index 913c459..a83a028 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/GridModels.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/GridModels.kt @@ -3,20 +3,12 @@ package com.bolenpad.photophetch.ui import com.bolenpad.photophetch.model.PhonePhoto import java.time.LocalDate -/** A folder's photos grouped by day, used by both Grouped and Dense grid views */ +/** A folder's photos grouped by day */ data class FolderSection( val folder: FolderInfo, val byDay: List>>, val allPhotos: List, ) -/** Item types used in the DensePhotoGrid flat list */ -enum class GridItemType { FOLDER_HEADER, DAY_BOUNDARY, PHOTO_ROW } - -/** A single item in the DensePhotoGrid flat list */ -data class GridItem( - val type: GridItemType, - val folderSection: FolderSection? = null, - val rowPhotos: List? = null, - val dayDate: LocalDate? = null, -) +/** Item types used in DensePhotoGrid flat list */ +enum class GridItemType { FOLDER_HEADER, PHOTO_ROW }