Remove grouped view mode — dense grid is the only view
Deleted: ViewMode enum, UnifiedPhotoScroll, DaySubHeader, PhotoThumb (grouped-mode cell), view toggle button, groupedListState, groupedScrollIndex. BrowseScreen is now ~100 lines simpler. DensePhotoGrid is the single grid implementation. FolderJumpList and FolderDividerHeader remain.
This commit is contained in:
@@ -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,44 +53,22 @@ 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 {
|
||||
val activeFolder by remember(folderScrollIndex) {
|
||||
derivedStateOf {
|
||||
val firstVisible = listState.firstVisibleItemIndex
|
||||
folderScrollIndex.entries
|
||||
@@ -124,16 +77,14 @@ fun BrowseScreen(state: AppState) {
|
||||
?: 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,59 +92,24 @@ 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(
|
||||
DensePhotoGrid(
|
||||
folderSections = folderSections,
|
||||
selectedPhotos = selectedPhotos,
|
||||
device = device,
|
||||
adbBin = config.adbPath ?: "adb",
|
||||
columnCount = columnCount,
|
||||
listState = denseListState,
|
||||
listState = listState,
|
||||
onTogglePhoto = { state.togglePhotoSelection(it) },
|
||||
onSelectAllFolder = { folder ->
|
||||
folder.forEach { p ->
|
||||
if (p !in selectedPhotos) state.togglePhotoSelection(p)
|
||||
}
|
||||
folder.forEach { p -> if (p !in selectedPhotos) state.togglePhotoSelection(p) }
|
||||
},
|
||||
onDeselectAllFolder = { folder ->
|
||||
folder.forEach { p ->
|
||||
if (p in selectedPhotos) state.togglePhotoSelection(p)
|
||||
}
|
||||
folder.forEach { p -> if (p in selectedPhotos) state.togglePhotoSelection(p) }
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
is PhotoListState.Error -> {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@@ -208,10 +124,10 @@ fun BrowseScreen(state: AppState) {
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Data helpers ──────────────────────────────────────────────────────────────
|
||||
// ─── Data helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private fun buildFolderSections(pl: PhotoListState.Loaded): List<FolderSection> {
|
||||
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<FolderSection>
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 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<FolderSection>,
|
||||
selectedPhotos: Set<PhonePhoto>,
|
||||
device: com.bolenpad.photophetch.model.DeviceInfo?,
|
||||
adbBin: String,
|
||||
listState: LazyListState,
|
||||
columnCount: Int,
|
||||
onTogglePhoto: (PhonePhoto) -> Unit,
|
||||
onSelectAllFolder: (List<PhonePhoto>) -> Unit,
|
||||
onDeselectAllFolder: (List<PhonePhoto>) -> Unit,
|
||||
onSelectDay: (List<PhonePhoto>) -> 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(
|
||||
@@ -440,55 +238,19 @@ internal fun FolderDividerHeader(
|
||||
)
|
||||
}
|
||||
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 ──────────────────────────────────────────────────────────────────
|
||||
@@ -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<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(
|
||||
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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Pair<LocalDate, List<PhonePhoto>>>,
|
||||
val allPhotos: List<PhonePhoto>,
|
||||
)
|
||||
|
||||
/** 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<PhonePhoto>? = null,
|
||||
val dayDate: LocalDate? = null,
|
||||
)
|
||||
/** Item types used in DensePhotoGrid flat list */
|
||||
enum class GridItemType { FOLDER_HEADER, PHOTO_ROW }
|
||||
|
||||
Reference in New Issue
Block a user