Compare commits
5 Commits
29595d5a7f
...
v1.0.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11a1faa395 | ||
|
|
d041298c5c | ||
|
|
b416e528b0 | ||
|
|
207f55d263 | ||
|
|
4b6a5fccf6 |
@@ -40,10 +40,39 @@ compose.desktop {
|
|||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
packageName = "PhotoPhetch"
|
packageName = "PhotoPhetch"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = "1.0.1"
|
||||||
description = "Import photos from phones into PhotoPhile"
|
description = "Import photos from phones into PhotoPhile"
|
||||||
vendor = "bolenpad"
|
vendor = "bolenpad"
|
||||||
copyright = "© 2026 Kyle Bolen"
|
copyright = "© 2026 Kyle Bolen"
|
||||||
|
|
||||||
|
modules(
|
||||||
|
// Networking and naming — needed by logback JNDI
|
||||||
|
"java.naming",
|
||||||
|
// JMX — needed by logback JMX appender
|
||||||
|
"java.management",
|
||||||
|
// Instrumentation — needed by logback/class loading
|
||||||
|
"java.instrument",
|
||||||
|
// Reflection support for Unsafe — needed by coroutines, serialization
|
||||||
|
"jdk.unsupported",
|
||||||
|
// XML processing — needed by logback XML config parser
|
||||||
|
"java.xml",
|
||||||
|
// Logging — java.util.logging bridge
|
||||||
|
"java.logging",
|
||||||
|
// Security
|
||||||
|
"java.security.jgss",
|
||||||
|
"java.security.sasl",
|
||||||
|
// Desktop/AWT (for Swing integration)
|
||||||
|
"java.desktop",
|
||||||
|
// Compiler API — sometimes needed by Kotlin reflection
|
||||||
|
"java.compiler",
|
||||||
|
// SQL — needed by some Kotlin stdlib internals
|
||||||
|
"java.sql",
|
||||||
|
// Network — needed by HTTP calls (Gitea API, pymobiledevice3)
|
||||||
|
"java.net.http",
|
||||||
|
// Preferences — used by some desktop apps
|
||||||
|
"java.prefs",
|
||||||
|
)
|
||||||
|
|
||||||
macOS {
|
macOS {
|
||||||
bundleID = "com.bolenpad.photophetch"
|
bundleID = "com.bolenpad.photophetch"
|
||||||
// Uncomment and add icon.icns to src/main/resources/ for a custom icon:
|
// Uncomment and add icon.icns to src/main/resources/ for a custom icon:
|
||||||
|
|||||||
11
release.sh
11
release.sh
@@ -39,9 +39,16 @@ fi
|
|||||||
NOTES=$(cat "$NOTES_FILE")
|
NOTES=$(cat "$NOTES_FILE")
|
||||||
echo "→ Release notes: $NOTES_FILE"
|
echo "→ Release notes: $NOTES_FILE"
|
||||||
|
|
||||||
# ── Gitea token ───────────────────────────────────────────────────────────────
|
# ── Gitea token — from macOS Keychain, env var, or prompt ────────────────────
|
||||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
read -rsp "GITEA_TOKEN not set. Enter token: " GITEA_TOKEN
|
# Try macOS Keychain first
|
||||||
|
GITEA_TOKEN=$(security find-generic-password -a kbolen -s gitea-photophetch -w 2>/dev/null || echo "")
|
||||||
|
fi
|
||||||
|
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||||
|
echo "No token found in Keychain or environment."
|
||||||
|
echo "Store it once with:"
|
||||||
|
echo " security add-generic-password -a kbolen -s gitea-photophetch -w YOUR_TOKEN"
|
||||||
|
read -rsp "Or enter token now: " GITEA_TOKEN
|
||||||
echo
|
echo
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
9
releases/v1.0.1.md
Normal file
9
releases/v1.0.1.md
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
## PhotoPhetch v1.0.1 — Packaging Fixes
|
||||||
|
|
||||||
|
### Bug fixes
|
||||||
|
|
||||||
|
- **Fixed crash on launch** when installed as a .app bundle: bundled JVM was missing required modules (java.naming, java.xml, java.desktop, and others needed by logback and Kotlin coroutines)
|
||||||
|
- **Fixed 'exiftool not installed' false warning** when launched as .app: the bundled app does not inherit the shell PATH, so Homebrew tools in /usr/local/bin and /opt/homebrew/bin were not found. ExternalTools now searches common Homebrew paths directly
|
||||||
|
- **Fixed adb not found** when launched as .app: same PATH issue, now resolved lazily using Homebrew path search
|
||||||
|
- **Fixed EXIF verification false warnings** on Android: Samsung and other Android manufacturers were incorrectly flagged as 'Origin unknown'. Any non-blank EXIF Make is now considered a verified camera original; only absent Make triggers the warning
|
||||||
|
- **Preserve folder structure now sticky**: the toggle in the Import Configuration screen now saves your preference and remembers it between imports
|
||||||
@@ -24,6 +24,12 @@ class AndroidConnector(
|
|||||||
|
|
||||||
private val log = LoggerFactory.getLogger(AndroidConnector::class.java)
|
private val log = LoggerFactory.getLogger(AndroidConnector::class.java)
|
||||||
|
|
||||||
|
// Resolve adb path lazily — ExternalTools searches Homebrew paths
|
||||||
|
// so the packaged .app finds adb even without shell PATH
|
||||||
|
private val resolvedAdb: String by lazy {
|
||||||
|
com.bolenpad.photophetch.util.ExternalTools.resolve(adbBin)
|
||||||
|
}
|
||||||
|
|
||||||
// Directories on Android that contain camera-roll originals.
|
// Directories on Android that contain camera-roll originals.
|
||||||
// Used as fallback if dynamic DCIM discovery finds nothing.
|
// Used as fallback if dynamic DCIM discovery finds nothing.
|
||||||
private val fallbackCameraDirs = listOf(
|
private val fallbackCameraDirs = listOf(
|
||||||
@@ -213,7 +219,7 @@ class AndroidConnector(
|
|||||||
|
|
||||||
private fun runAdb(serial: String?, vararg args: String): String {
|
private fun runAdb(serial: String?, vararg args: String): String {
|
||||||
val cmd = buildList {
|
val cmd = buildList {
|
||||||
add(adbBin)
|
add(resolvedAdb)
|
||||||
if (serial != null) {
|
if (serial != null) {
|
||||||
add("-s")
|
add("-s")
|
||||||
add(serial)
|
add(serial)
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ class Copier(
|
|||||||
*/
|
*/
|
||||||
private fun readExifMake(file: Path): String? {
|
private fun readExifMake(file: Path): String? {
|
||||||
return try {
|
return try {
|
||||||
val process = ProcessBuilder("exiftool", "-Make", "-s3", file.absolutePathString())
|
val process = ProcessBuilder(com.bolenpad.photophetch.util.ExternalTools.resolve("exiftool"), "-Make", "-s3", file.absolutePathString())
|
||||||
.redirectErrorStream(false) // keep stderr separate
|
.redirectErrorStream(false) // keep stderr separate
|
||||||
.start()
|
.start()
|
||||||
val stdout = process.inputStream.bufferedReader().readText()
|
val stdout = process.inputStream.bufferedReader().readText()
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import com.bolenpad.photophetch.model.DeviceInfo
|
import com.bolenpad.photophetch.model.DeviceInfo
|
||||||
|
import com.bolenpad.photophetch.util.ExternalTools
|
||||||
import com.bolenpad.photophetch.model.DeviceType
|
import com.bolenpad.photophetch.model.DeviceType
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -49,7 +50,8 @@ fun ConnectScreen(state: AppState) {
|
|||||||
// exiftool warning — checked once per session
|
// exiftool warning — checked once per session
|
||||||
val exiftoolMissing = remember {
|
val exiftoolMissing = remember {
|
||||||
try {
|
try {
|
||||||
val p = ProcessBuilder("exiftool", "-ver").redirectErrorStream(true).start()
|
val p = ProcessBuilder(ExternalTools.resolve("exiftool"), "-ver")
|
||||||
|
.redirectErrorStream(true).start()
|
||||||
p.inputStream.readBytes()
|
p.inputStream.readBytes()
|
||||||
p.waitFor() != 0
|
p.waitFor() != 0
|
||||||
} catch (_: Exception) { true }
|
} catch (_: Exception) { true }
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.bolenpad.photophetch.util
|
||||||
|
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves paths for external command-line tools (exiftool, adb, python3, etc.)
|
||||||
|
*
|
||||||
|
* macOS .app bundles launch with a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin)
|
||||||
|
* that doesn't include Homebrew. This utility searches common Homebrew install
|
||||||
|
* locations so tools work whether launched from the command line or as a .app.
|
||||||
|
*/
|
||||||
|
object ExternalTools {
|
||||||
|
|
||||||
|
/** Common binary locations to search, in priority order */
|
||||||
|
private val searchPaths = listOf(
|
||||||
|
"/usr/local/bin", // Homebrew on Intel Mac
|
||||||
|
"/opt/homebrew/bin", // Homebrew on Apple Silicon
|
||||||
|
"/usr/bin", // System tools
|
||||||
|
"/bin",
|
||||||
|
"${System.getProperty("user.home")}/.local/bin", // pipx and user installs
|
||||||
|
"/usr/local/opt/exiftool/bin", // Homebrew formula-specific
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the full path to a tool binary.
|
||||||
|
* Returns the full path if found in a known location, or just the bare
|
||||||
|
* name (relying on PATH) if not found — which works for command-line launches.
|
||||||
|
*/
|
||||||
|
fun resolve(toolName: String): String {
|
||||||
|
for (dir in searchPaths) {
|
||||||
|
val file = File(dir, toolName)
|
||||||
|
if (file.exists() && file.canExecute()) {
|
||||||
|
return file.absolutePath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return toolName // fall back to PATH lookup
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a tool is available (either in known paths or on PATH).
|
||||||
|
*/
|
||||||
|
fun isAvailable(toolName: String): Boolean {
|
||||||
|
val path = resolve(toolName)
|
||||||
|
if (path != toolName) return true // found at explicit path
|
||||||
|
|
||||||
|
// Try running it to check PATH
|
||||||
|
return try {
|
||||||
|
val p = ProcessBuilder(toolName, "--version")
|
||||||
|
.redirectErrorStream(true).start()
|
||||||
|
p.inputStream.readBytes()
|
||||||
|
p.waitFor() == 0 || true // any exit = it exists
|
||||||
|
} catch (_: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user