From f6aede99f18f3aa6eabac6bcd733fb1310c2b173 Mon Sep 17 00:00:00 2001 From: Kyle Bolen Date: Tue, 28 Jul 2026 05:20:02 +0000 Subject: [PATCH] Fix device name, ImportScreen layout, and preserve folder default - AndroidConnector: use model: field for display name instead of product: (which is a cryptic codename like 'o1quew' on Samsung) - ImportScreen: clean up layout with ToggleRow helper, remove duplicate toggles, all 4 options in one Options section - AppState: preserveFolderStructure defaults to true --- .../photophetch/device/AndroidConnector.kt | 11 +- .../com/bolenpad/photophetch/ui/AppState.kt | 2 +- .../bolenpad/photophetch/ui/ImportScreen.kt | 144 ++++++------------ 3 files changed, 59 insertions(+), 98 deletions(-) diff --git a/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt b/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt index cfb8453..4724001 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt @@ -49,12 +49,17 @@ class AndroidConnector( if (parts.size < 2 || parts[1] != "device") return@mapNotNull null val serial = parts[0] val model = parts.firstOrNull { it.startsWith("model:") } - ?.removePrefix("model:") ?: "Android Device" + ?.removePrefix("model:") + ?.replace("_", " ") + ?: "Android Device" val product = parts.firstOrNull { it.startsWith("product:") } ?.removePrefix("product:") + ?.replace("_", " ") + // Prefer model name — it's human-readable (e.g. "SM G991B") + // product is often a cryptic codename (e.g. "o1quew") + val displayName = model.ifBlank { product ?: "Android Device" } DeviceInfo( - displayName = product?.replace("_", " ")?.replaceFirstChar { it.uppercase() } - ?: model.replace("_", " "), + displayName = displayName.trim(), type = DeviceType.ANDROID, adbSerial = serial, ) diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt index 0247930..2d0907e 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/AppState.kt @@ -230,7 +230,7 @@ class AppState(private val configStore: ConfigStore) { convertHeicToJpeg = true, stripLivePhotoMotion = true, deleteAfterVerify = false, - preserveFolderStructure = false, + preserveFolderStructure = true, folderName = "", ) ) diff --git a/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt b/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt index b12933d..5cecad0 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/ui/ImportScreen.kt @@ -3,7 +3,6 @@ package com.bolenpad.photophetch.ui import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack -import androidx.compose.material.icons.filled.FolderOpen import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment @@ -13,10 +12,6 @@ import androidx.compose.ui.unit.dp import com.bolenpad.photophetch.util.StagingPath import java.nio.file.Paths -/** - * Import configuration screen — shown after photo selection, before starting the copy. - * User confirms destination folder name, conversion settings, and delete-after-verify preference. - */ @Composable fun ImportScreen(state: AppState) { val importConfig by state.importConfig.collectAsState() @@ -24,7 +19,6 @@ fun ImportScreen(state: AppState) { val selected by state.selectedPhotos.collectAsState() val device by state.selectedDevice.collectAsState() - // Validate staging root val stagingError = if (config.stagingRoot.isNotBlank()) { StagingPath.validateStagingRoot(Paths.get(config.stagingRoot)) } else "Staging directory not configured" @@ -41,19 +35,17 @@ fun ImportScreen(state: AppState) { Text("Import Configuration", style = MaterialTheme.typography.headlineSmall) } - // Summary card + // 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) - } + device?.let { Text("From: ${it.displayName}", style = MaterialTheme.typography.bodySmall) } } } HorizontalDivider() - // Destination folder name + // Destination Text("Destination Folder", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Text( @@ -75,93 +67,36 @@ fun ImportScreen(state: AppState) { HorizontalDivider() - // Conversion options - Text("Conversion", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) + // Options + Text("Options", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - Text("Convert HEIC → JPEG") - Text( - "Recommended for PhotoPhile on Linux. Uses macOS sips (built-in).", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = importConfig.convertHeicToJpeg, - onCheckedChange = { state.updateImportConfig { copy(convertHeicToJpeg = it) } }, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - Text("Strip Live Photo motion clips") - Text( - "Excludes .MOV companions from Live Photos. You want JPEGs, not motion clips.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = importConfig.stripLivePhotoMotion, - onCheckedChange = { state.updateImportConfig { copy(stripLivePhotoMotion = it) } }, - ) - } - - HorizontalDivider() - - // Delete after verify - Text("After Import", style = MaterialTheme.typography.titleSmall, fontWeight = FontWeight.SemiBold) - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - Text("Preserve folder structure") - Text( - "Copy files into subfolders matching their device path (e.g. Camera/, Screenshots/). Enabled automatically in Show All mode.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = importConfig.preserveFolderStructure, - onCheckedChange = { state.updateImportConfig { copy(preserveFolderStructure = it) } }, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.SpaceBetween, - ) { - Column(modifier = Modifier.weight(1f)) { - Text("Delete from phone after verification") - Text( - "Only deletes files that passed SHA-256 verification. You'll confirm on the next screen.", - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } - Switch( - checked = importConfig.deleteAfterVerify, - onCheckedChange = { state.updateImportConfig { copy(deleteAfterVerify = it) } }, - ) - } + 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)) - // Start import button Button( onClick = { state.startImport() }, enabled = stagingError == null && importConfig.folderName.isNotBlank() && selected.isNotEmpty(), @@ -171,3 +106,24 @@ fun ImportScreen(state: AppState) { } } } + +@Composable +private fun ToggleRow( + label: String, + description: String, + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, +) { + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Column(modifier = Modifier.weight(1f).padding(end = 8.dp)) { + Text(label) + Text(description, style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant) + } + Switch(checked = checked, onCheckedChange = onCheckedChange) + } +}