ImportScreen: scrollable content with pinned bottom button

- Body scrolls via verticalScroll
- Header and Start Import button stay fixed outside the scroll
- Summary card now shows source folder breakdown for multi-folder imports
- HorizontalDivider separates pinned button from scroll area
This commit is contained in:
Kyle Bolen
2026-07-28 05:29:17 +00:00
parent fd5753382f
commit edaf4cb0d7

View File

@@ -1,6 +1,8 @@
package com.bolenpad.photophetch.ui package com.bolenpad.photophetch.ui
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.* import androidx.compose.material3.*
@@ -23,23 +25,51 @@ fun ImportScreen(state: AppState) {
StagingPath.validateStagingRoot(Paths.get(config.stagingRoot)) StagingPath.validateStagingRoot(Paths.get(config.stagingRoot))
} else "Staging directory not configured" } else "Staging directory not configured"
Column( val canImport = stagingError == null && importConfig.folderName.isNotBlank() && selected.isNotEmpty()
modifier = Modifier.fillMaxSize().padding(32.dp),
verticalArrangement = Arrangement.spacedBy(20.dp), // Outer column: header + scrollable body + pinned button
Column(modifier = Modifier.fillMaxSize()) {
// ── Header (always visible) ──────────────────────────────────────────
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(start = 4.dp, top = 8.dp, end = 16.dp, bottom = 4.dp),
) { ) {
// Header
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { state.navigateTo(Screen.BROWSE) }) { IconButton(onClick = { state.navigateTo(Screen.BROWSE) }) {
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
} }
Text("Import Configuration", style = MaterialTheme.typography.headlineSmall) Text("Import Configuration", style = MaterialTheme.typography.headlineSmall)
} }
// Summary HorizontalDivider()
// ── Scrollable body ──────────────────────────────────────────────────
Column(
modifier = Modifier
.weight(1f)
.verticalScroll(rememberScrollState())
.padding(horizontal = 32.dp, vertical = 20.dp),
verticalArrangement = Arrangement.spacedBy(20.dp),
) {
// Summary card
Card { Card {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) { Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
) {
Text("${selected.size} files selected", fontWeight = FontWeight.SemiBold) Text("${selected.size} files selected", fontWeight = FontWeight.SemiBold)
device?.let { Text("From: ${it.displayName}", style = MaterialTheme.typography.bodySmall) } device?.let {
Text("From: ${it.displayName}", style = MaterialTheme.typography.bodySmall)
}
// Show source folders breakdown if multi-folder selection
val folders = selected.map { it.sourceFolder }.distinct().sorted()
if (folders.size > 1) {
Text(
"Folders: ${folders.joinToString(", ")}",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
} }
} }
@@ -47,7 +77,10 @@ fun ImportScreen(state: AppState) {
// Destination // Destination
Text("Destination Folder", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) Text("Destination Folder", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
Text( Text(
"${config.stagingRoot}/", "${config.stagingRoot}/",
style = MaterialTheme.typography.bodySmall, style = MaterialTheme.typography.bodySmall,
@@ -62,7 +95,11 @@ fun ImportScreen(state: AppState) {
) )
} }
if (stagingError != null) { if (stagingError != null) {
Text(stagingError, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall) Text(
stagingError,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodySmall,
)
} }
HorizontalDivider() HorizontalDivider()
@@ -72,7 +109,7 @@ fun ImportScreen(state: AppState) {
ToggleRow( ToggleRow(
label = "Convert HEIC → JPEG", label = "Convert HEIC → JPEG",
description = "Recommended for PhotoPhile on Linux. Uses macOS sips.", description = "Recommended for PhotoPhile on Linux. Uses macOS sips (built-in).",
checked = importConfig.convertHeicToJpeg, checked = importConfig.convertHeicToJpeg,
onCheckedChange = { state.updateImportConfig { copy(convertHeicToJpeg = it) } }, onCheckedChange = { state.updateImportConfig { copy(convertHeicToJpeg = it) } },
) )
@@ -84,7 +121,7 @@ fun ImportScreen(state: AppState) {
) )
ToggleRow( ToggleRow(
label = "Preserve folder structure", label = "Preserve folder structure",
description = "Files go into subfolders matching their source (e.g. Camera/, Screenshots/).", description = "Files go into subfolders matching their source (Camera/, Screenshots/, etc.).",
checked = importConfig.preserveFolderStructure, checked = importConfig.preserveFolderStructure,
onCheckedChange = { state.updateImportConfig { copy(preserveFolderStructure = it) } }, onCheckedChange = { state.updateImportConfig { copy(preserveFolderStructure = it) } },
) )
@@ -95,14 +132,23 @@ fun ImportScreen(state: AppState) {
onCheckedChange = { state.updateImportConfig { copy(deleteAfterVerify = it) } }, onCheckedChange = { state.updateImportConfig { copy(deleteAfterVerify = it) } },
) )
Spacer(Modifier.weight(1f)) // Extra bottom padding so last item isn't flush against the button
Spacer(Modifier.height(8.dp))
}
// ── Pinned bottom button ─────────────────────────────────────────────
HorizontalDivider()
Box(modifier = Modifier.padding(horizontal = 32.dp, vertical = 16.dp)) {
Button( Button(
onClick = { state.startImport() }, onClick = { state.startImport() },
enabled = stagingError == null && importConfig.folderName.isNotBlank() && selected.isNotEmpty(), enabled = canImport,
modifier = Modifier.fillMaxWidth().height(52.dp), modifier = Modifier.fillMaxWidth().height(52.dp),
) { ) {
Text("Start Import (${selected.size} files)", style = MaterialTheme.typography.titleMedium) Text(
"Start Import (${selected.size} files)",
style = MaterialTheme.typography.titleMedium,
)
}
} }
} }
} }
@@ -121,8 +167,11 @@ private fun ToggleRow(
) { ) {
Column(modifier = Modifier.weight(1f).padding(end = 8.dp)) { Column(modifier = Modifier.weight(1f).padding(end = 8.dp)) {
Text(label) Text(label)
Text(description, style = MaterialTheme.typography.bodySmall, Text(
color = MaterialTheme.colorScheme.onSurfaceVariant) description,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
} }
Switch(checked = checked, onCheckedChange = onCheckedChange) Switch(checked = checked, onCheckedChange = onCheckedChange)
} }