Fix compile errors from initial build

- Main.kt: move ConfigStore/AppState outside application {} scope (remember
  is not valid outside a composable)
- IosConnector: split runGphoto2/runGphoto2InDir to avoid named+positional
  vararg mixing (Kotlin does not allow this)
- ConnectScreen: fix SmartPhone -> Smartphone icon name
- ConnectScreen/BrowseScreen/ImportScreen/SettingsScreen: update deprecated
  Icons.Default.ArrowBack to Icons.AutoMirrored.Filled.ArrowBack
- AndroidConnector: remove unused variable assignment in remoteChecksum
- ExifFilter: replace return@filter labels with private helper function to
  eliminate ambiguous label warnings
This commit is contained in:
Kyle Bolen
2026-07-28 03:09:28 +00:00
parent 145b7bc955
commit c4608e9c0e
13 changed files with 395 additions and 44 deletions

View File

@@ -9,15 +9,19 @@ import com.bolenpad.photophetch.ui.App
import com.bolenpad.photophetch.ui.AppState
import com.bolenpad.photophetch.util.ConfigStore
fun main() = application {
val configStore = remember { ConfigStore() }
val appState = remember { AppState(configStore) }
fun main() {
// ConfigStore and AppState live outside the composition — they are
// application-lifetime singletons, not composable state.
val configStore = ConfigStore()
val appState = AppState(configStore)
Window(
onCloseRequest = ::exitApplication,
title = "PhotoPhetch",
state = WindowState(size = DpSize(900.dp, 700.dp)),
) {
App(appState)
application {
Window(
onCloseRequest = ::exitApplication,
title = "PhotoPhetch",
state = WindowState(size = DpSize(900.dp, 700.dp)),
) {
App(appState)
}
}
}

View File

@@ -138,7 +138,7 @@ class AndroidConnector(
// Android has md5sum or sha256sum depending on version; try sha256sum first
var output = runAdb(serial, "shell", "sha256sum", photo.devicePath)
if (output.contains("not found") || output.isBlank()) {
output = runAdb(serial, "shell", "md5sum", photo.devicePath)
runAdb(serial, "shell", "md5sum", photo.devicePath)
// md5sum isn't SHA-256 — return null so Copier uses its own verification
return null
}

View File

@@ -105,8 +105,8 @@ class IosConnector(
val destFile = destinationDir.resolve(photo.filename)
// --get-file downloads to current dir; we set working dir to destinationDir
val output = runGphoto2(
workingDir = destinationDir,
val output = runGphoto2InDir(
destinationDir,
"--port", port,
"--get-file", fileIndex,
"--filename", photo.filename,
@@ -225,7 +225,9 @@ class IosConnector(
private fun requirePort(device: DeviceInfo): String =
device.gphotoPort ?: throw DeviceException("No gphoto2 port for device: ${device.displayName}")
private fun runGphoto2(vararg args: String, workingDir: Path? = null): String {
private fun runGphoto2(vararg args: String): String = runGphoto2InDir(null, *args)
private fun runGphoto2InDir(workingDir: Path?, vararg args: String): String {
val cmd = buildList {
add(gphoto2Bin)
addAll(args)

View File

@@ -8,7 +8,7 @@ import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.DateRange
import androidx.compose.material.icons.filled.RadioButtonUnchecked
@@ -133,7 +133,7 @@ private fun BrowseTopBar(
title = { Text(device?.displayName ?: "Browse Photos") },
navigationIcon = {
IconButton(onClick = onBack) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
},
actions = {

View File

@@ -6,7 +6,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PhoneAndroid
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.SmartPhone
import androidx.compose.material.icons.filled.Smartphone
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@@ -153,7 +153,7 @@ private fun DeviceCard(device: DeviceInfo, onClick: () -> Unit) {
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
if (device.type == DeviceType.IOS) Icons.Default.SmartPhone else Icons.Default.PhoneAndroid,
if (device.type == DeviceType.IOS) Icons.Default.Smartphone else Icons.Default.PhoneAndroid,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.primary,

View File

@@ -2,7 +2,7 @@ package com.bolenpad.photophetch.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.FolderOpen
import androidx.compose.material3.*
import androidx.compose.runtime.*
@@ -36,7 +36,7 @@ fun ImportScreen(state: AppState) {
// Header
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { state.navigateTo(Screen.BROWSE) }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
Text("Import Configuration", style = MaterialTheme.typography.headlineSmall)
}

View File

@@ -2,7 +2,7 @@ package com.bolenpad.photophetch.ui
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@@ -24,7 +24,7 @@ fun SettingsScreen(state: AppState) {
) {
Row(verticalAlignment = Alignment.CenterVertically) {
IconButton(onClick = { state.navigateTo(Screen.CONNECT) }) {
Icon(Icons.Default.ArrowBack, contentDescription = "Back")
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back")
}
Text("Settings", style = MaterialTheme.typography.headlineSmall)
}

View File

@@ -58,33 +58,30 @@ object ExifFilter {
photos: List<PhonePhoto>,
stripLivePhotoMotion: Boolean = true,
): List<PhonePhoto> {
return photos.filter { photo ->
// Always exclude Live Photo motion clips if requested
if (stripLivePhotoMotion && photo.isLivePhotoMotion) return@filter false
return photos.filter { photo -> isCameraRollOriginal(photo, stripLivePhotoMotion) }
}
// Exclude if the device path contains a known non-camera directory
val pathLower = photo.devicePath.lowercase()
if (excludedPathFragments.any { fragment -> pathLower.contains(fragment) }) {
return@filter false
}
private fun isCameraRollOriginal(photo: PhonePhoto, stripLivePhotoMotion: Boolean): Boolean {
// Always exclude Live Photo motion clips if requested
if (stripLivePhotoMotion && photo.isLivePhotoMotion) return false
// Exclude if filename starts with a screenshot prefix
val filenameLower = photo.filename.lowercase()
if (screenshotFilePrefixes.any { prefix -> filenameLower.startsWith(prefix) }) {
return@filter false
}
// Exclude if the device path contains a known non-camera directory
val pathLower = photo.devicePath.lowercase()
if (excludedPathFragments.any { fragment -> pathLower.contains(fragment) }) return false
// Exclude files with no EXIF Make/Model AND not in a DCIM path
// (indicates received image or screenshot — not a camera original)
val hasExif = photo.exifMake != null || photo.exifModel != null
val inDcim = pathLower.contains("dcim")
if (!hasExif && !inDcim) return@filter false
// Exclude if filename starts with a screenshot prefix
val filenameLower = photo.filename.lowercase()
if (screenshotFilePrefixes.any { prefix -> filenameLower.startsWith(prefix) }) return false
// Skip files with completely unknown media type
if (photo.mediaType == MediaType.UNKNOWN) return@filter false
// Exclude files with no EXIF Make/Model AND not in a DCIM path
val hasExif = photo.exifMake != null || photo.exifModel != null
val inDcim = pathLower.contains("dcim")
if (!hasExif && !inDcim) return false
true
}
// Skip files with completely unknown media type
if (photo.mediaType == MediaType.UNKNOWN) return false
return true
}
/**