diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt index 5cecad0..2d6a455 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt @@ -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) }