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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user