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
|
||||
|
||||
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.automirrored.filled.ArrowBack
|
||||
import androidx.compose.material3.*
|
||||
@@ -23,86 +25,130 @@ fun ImportScreen(state: AppState) {
|
||||
StagingPath.validateStagingRoot(Paths.get(config.stagingRoot))
|
||||
} else "Staging directory not configured"
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize().padding(32.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
// Header
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
val canImport = stagingError == null && importConfig.folderName.isNotBlank() && selected.isNotEmpty()
|
||||
|
||||
// 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),
|
||||
) {
|
||||
IconButton(onClick = { state.navigateTo(Screen.BROWSE) }) {
|
||||
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
|
||||
}
|
||||
Text("Import Configuration", style = MaterialTheme.typography.headlineSmall)
|
||||
}
|
||||
|
||||
// Summary
|
||||
Card {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text("${selected.size} files selected", fontWeight = FontWeight.SemiBold)
|
||||
device?.let { Text("From: ${it.displayName}", style = MaterialTheme.typography.bodySmall) }
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Destination
|
||||
Text("Destination Folder", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
"${config.stagingRoot}/",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = importConfig.folderName,
|
||||
onValueChange = { state.updateImportConfig { copy(folderName = it) } },
|
||||
label = { Text("Folder name") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
if (stagingError != null) {
|
||||
Text(stagingError, color = MaterialTheme.colorScheme.error, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Options
|
||||
Text("Options", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
|
||||
ToggleRow(
|
||||
label = "Convert HEIC → JPEG",
|
||||
description = "Recommended for PhotoPhile on Linux. Uses macOS sips.",
|
||||
checked = importConfig.convertHeicToJpeg,
|
||||
onCheckedChange = { state.updateImportConfig { copy(convertHeicToJpeg = it) } },
|
||||
)
|
||||
ToggleRow(
|
||||
label = "Strip Live Photo motion clips",
|
||||
description = "Excludes .MOV companions from Live Photos.",
|
||||
checked = importConfig.stripLivePhotoMotion,
|
||||
onCheckedChange = { state.updateImportConfig { copy(stripLivePhotoMotion = it) } },
|
||||
)
|
||||
ToggleRow(
|
||||
label = "Preserve folder structure",
|
||||
description = "Files go into subfolders matching their source (e.g. Camera/, Screenshots/).",
|
||||
checked = importConfig.preserveFolderStructure,
|
||||
onCheckedChange = { state.updateImportConfig { copy(preserveFolderStructure = it) } },
|
||||
)
|
||||
ToggleRow(
|
||||
label = "Delete from phone after verification",
|
||||
description = "Only deletes files that passed SHA-256 check. You'll confirm on the next screen.",
|
||||
checked = importConfig.deleteAfterVerify,
|
||||
onCheckedChange = { state.updateImportConfig { copy(deleteAfterVerify = it) } },
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f))
|
||||
|
||||
Button(
|
||||
onClick = { state.startImport() },
|
||||
enabled = stagingError == null && importConfig.folderName.isNotBlank() && selected.isNotEmpty(),
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
// ── Scrollable body ──────────────────────────────────────────────────
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(horizontal = 32.dp, vertical = 20.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp),
|
||||
) {
|
||||
Text("Start Import (${selected.size} files)", style = MaterialTheme.typography.titleMedium)
|
||||
// Summary card
|
||||
Card {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text("${selected.size} files selected", fontWeight = FontWeight.SemiBold)
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Destination
|
||||
Text("Destination Folder", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Text(
|
||||
"${config.stagingRoot}/",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
OutlinedTextField(
|
||||
value = importConfig.folderName,
|
||||
onValueChange = { state.updateImportConfig { copy(folderName = it) } },
|
||||
label = { Text("Folder name") },
|
||||
modifier = Modifier.weight(1f),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
if (stagingError != null) {
|
||||
Text(
|
||||
stagingError,
|
||||
color = MaterialTheme.colorScheme.error,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider()
|
||||
|
||||
// Options
|
||||
Text("Options", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold)
|
||||
|
||||
ToggleRow(
|
||||
label = "Convert HEIC → JPEG",
|
||||
description = "Recommended for PhotoPhile on Linux. Uses macOS sips (built-in).",
|
||||
checked = importConfig.convertHeicToJpeg,
|
||||
onCheckedChange = { state.updateImportConfig { copy(convertHeicToJpeg = it) } },
|
||||
)
|
||||
ToggleRow(
|
||||
label = "Strip Live Photo motion clips",
|
||||
description = "Excludes .MOV companions from Live Photos.",
|
||||
checked = importConfig.stripLivePhotoMotion,
|
||||
onCheckedChange = { state.updateImportConfig { copy(stripLivePhotoMotion = it) } },
|
||||
)
|
||||
ToggleRow(
|
||||
label = "Preserve folder structure",
|
||||
description = "Files go into subfolders matching their source (Camera/, Screenshots/, etc.).",
|
||||
checked = importConfig.preserveFolderStructure,
|
||||
onCheckedChange = { state.updateImportConfig { copy(preserveFolderStructure = it) } },
|
||||
)
|
||||
ToggleRow(
|
||||
label = "Delete from phone after verification",
|
||||
description = "Only deletes files that passed SHA-256 check. You'll confirm on the next screen.",
|
||||
checked = importConfig.deleteAfterVerify,
|
||||
onCheckedChange = { state.updateImportConfig { copy(deleteAfterVerify = it) } },
|
||||
)
|
||||
|
||||
// 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(
|
||||
onClick = { state.startImport() },
|
||||
enabled = canImport,
|
||||
modifier = Modifier.fillMaxWidth().height(52.dp),
|
||||
) {
|
||||
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)) {
|
||||
Text(label)
|
||||
Text(description, style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant)
|
||||
Text(
|
||||
description,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
Switch(checked = checked, onCheckedChange = onCheckedChange)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user