95 Commits

Author SHA1 Message Date
Kyle Bolen
11a1faa395 Release v1.0.1 v1.0.1 2026-08-02 18:32:00 +00:00
Kyle Bolen
d041298c5c Fix adb path resolution: lazy eval instead of default param
ExternalTools.resolve() called as default constructor param was
evaluated at class load time, before the filesystem was checked.
Use lazy delegation instead so adb path is resolved on first use.
2026-08-02 18:29:35 +00:00
Kyle Bolen
b416e528b0 Fix tool resolution for packaged .app bundle
.app bundles launch with minimal PATH (/usr/bin:/bin only), missing
Homebrew paths. ExternalTools.resolve() searches:
- /usr/local/bin (Homebrew Intel)
- /opt/homebrew/bin (Homebrew Apple Silicon)
- ~/.local/bin (pipx installs)

Updated: exiftool (ConnectScreen + Copier), adb (AndroidConnector).
This fixes 'exiftool not installed' warning when launching as .app.
2026-08-02 18:12:53 +00:00
Kyle Bolen
207f55d263 Add comprehensive JVM module list for packaged distribution
Include all modules commonly needed by logback, coroutines, Swing,
Kotlin serialization, and networking to prevent NoClassDefFoundError
when running as a packaged .app bundle.
2026-08-02 18:04:27 +00:00
Kyle Bolen
4b6a5fccf6 Add missing JVM modules to packaged distribution
javax.naming.NamingException (used by logback's JNDI lookup) was not
included in the bundled JVM image. Explicitly include:
- java.naming (javax.naming)
- java.management (JMX, used by logback)
- java.instrument (used by logback)
- jdk.unsupported (reflection support)
- java.security.jgss (networking)
2026-08-02 18:02:49 +00:00
Kyle Bolen
29595d5a7f Release v1.0.0 v1.0.0 2026-08-02 03:53:04 +00:00
Kyle Bolen
ec1d2f1660 Restructure release workflow: dev prepares, Mac builds+uploads
Two scripts:
- prepare-release.sh (run by me/Kiro on dev machine):
  Writes releases/vX.Y.Z.md, bumps packageVersion, commits, tags, pushes

- release.sh (run by you on Mac):
  Reads version from current git tag, reads notes from releases/<tag>.md,
  builds DMG, creates Gitea release, uploads DMG.
  No user input needed — just run ./release.sh

Workflow:
  Dev:  ./prepare-release.sh 1.0.0 'Release notes'
  Mac:  git fetch --tags && git checkout v1.0.0 && ./release.sh
2026-08-02 03:51:42 +00:00
Kyle Bolen
b437cbf6e1 release.sh: auto-generate notes from git log, prompt to accept/edit/quit 2026-08-02 03:49:14 +00:00
Kyle Bolen
0042f3b6ba Add release.sh: one-command build + tag + Gitea release upload
Usage: ./release.sh 1.0.0 'Release notes here'
Or:    ./release.sh 1.0.0   (prompts for notes)

Requires GITEA_TOKEN env var (or prompts).
Does: version bump → commit → build DMG → tag → push → create
Gitea release → upload DMG → print download URL.
2026-08-02 03:46:26 +00:00
Kyle Bolen
694dca8817 gradle.properties: skip JDK vendor check for packaging
Compose Desktop warns about Homebrew JDK but it works fine for
macOS packaging. compose.desktop.packaging.checkJdkVendor=false
suppresses the error without switching JDKs.
2026-08-02 03:19:56 +00:00
Kyle Bolen
c34e1201ab Packaging setup: DMG distribution + proper log location
logback.xml: write to ~/Library/Logs/PhotoPhetch/ (standard macOS
app log location), rolling 7-day retention, 50MB cap. Console output
retained for development runs.

build.gradle.kts: fix packaging config — remove missing icon.icns
reference, add copyright, add signing block (disabled by default).

To build:
  ./gradlew packageDmg
  # Output: build/compose/binaries/main/dmg/PhotoPhetch-1.0.0.dmg

Logs viewable in Console.app or:
  tail -f ~/Library/Logs/PhotoPhetch/photophetch.log
2026-08-02 03:12:44 +00:00
Kyle Bolen
dc32a3a3d7 Fix EXIF verification for non-Apple devices + sticky preserve folder
EXIF verification: any non-blank Make is now considered verified.
Only absent/empty Make triggers 'Origin unknown' warning. Samsung,
Google, etc. are all legitimate camera makes.
Also cache exifMake to avoid calling exiftool twice per file.

Preserve folder structure: add defaultPreserveFolderStructure to
AppConfig. Toggling in ImportScreen saves it as the new default.
prepareImport() reads from config instead of always resetting to true.
2026-07-30 12:48:15 +00:00
Kyle Bolen
a8906fa265 Fix Color import in preview dialog LIVE badge 2026-07-30 06:36:04 +00:00
Kyle Bolen
6912deac14 Remove video preview - JavaFX MOV playback not viable in Compose Desktop
Removed LivePhotoPlayer.kt, JavaFX plugin/deps from build.gradle.kts
and libs.versions.toml. PhotoPreviewDialog shows still HEIC only.
LIVE badge in dialog uses the same green as in the grid.
2026-07-30 06:35:34 +00:00
Kyle Bolen
bbb3bb1aae Use JavaFX Gradle plugin for proper module path setup
Replace manual classifier-based jar deps with the openjfx javafx-plugin
which correctly sets up the JavaFX module path, native lib paths, and
JVM args needed for media codecs (AVFoundation on macOS).

This fixes 'media type not supported' which was caused by JavaFX being
on classpath instead of module path — native .dylib codecs only load
when JavaFX runs as a named module.
2026-07-30 06:25:38 +00:00
Kyle Bolen
3c2ec07bdb Fix JavaFX media: extract native libs from jar to temp dir
The 'media type not supported' error occurs because the JavaFX native
codec .dylib files aren't extracted when loading from classpath.
extractJavafxNativeLibs() finds the javafx-media jar, extracts the
.dylib files to a temp dir, and adds it to java.library.path.

Also remove --add-modules JVM args (caused boot layer init failure).
2026-07-30 06:20:03 +00:00
Kyle Bolen
d128835c0d Add JavaFX module args and auto-detect mac/mac-aarch64 platform
--add-modules javafx.media,javafx.swing,javafx.graphics needed for
native codec loading on macOS (AVFoundation for MOV playback).
Platform auto-detects mac-aarch64 vs mac based on os.arch.
2026-07-30 06:18:10 +00:00
Kyle Bolen
f21e8dbd3a Fix LivePhotoPlayer: proper init order, error logging, setOnReady
Use SwingUtilities.invokeLater + Platform.runLater for correct init
order. Player only starts in setOnReady callback. Error logging added
so we can see what's failing in the terminal.
2026-07-30 06:10:35 +00:00
Kyle Bolen
7b28c87f2c Fix preview dialog: scrollable + cap video height at 200dp
Dialog content is now scrollable so buttons are always reachable.
Video player capped at 200dp height — no longer pushes buttons
off screen when the JavaFX panel expands.
2026-07-30 06:09:15 +00:00
Kyle Bolen
42039bade1 Live Photo inline video player using JavaFX MediaPlayer
Add OpenJFX dependencies (javafx-media, javafx-swing, javafx-graphics,
javafx-base 21.0.3 mac platform) to build.gradle.kts.

LivePhotoPlayer.kt: embeds JavaFX MediaPlayer in a SwingPanel via
JFXPanel Swing interop. Auto-plays, loops infinitely, muted.

PhotoPreviewDialog: pulls MOV companion to temp file, shows still HEIC
above and looping video below in a 16:9 panel. Falls back gracefully
if JavaFX is not available (isJavaFxMediaAvailable() check).
2026-07-30 06:03:43 +00:00
Kyle Bolen
4171ff04d2 Fix HEIC preview + clean up EXIF unverified messages
PhotoPreviewDialog: always use sips-converted JPEG from cache for
display. On cache miss: pull file, run sips --setProperty format jpeg
to convert, cache the JPEG. HEIC now displays inline without QuickTime.

Verify/VerifyRow: replace 'Make: absent — not a confirmed camera original'
with 'Origin unknown' (row) and 'Origin unknown — may not be your photo'
(preview dialog). Less developer-jargon, more understandable.
2026-07-30 05:58:51 +00:00
Kyle Bolen
bc3a89e858 Fix selection indicator: high-contrast white circle on any background
Replace low-contrast RadioButtonUnchecked (50% white) and primary-tint
CheckCircle with a custom indicator:
- Unselected: white circle (90% opacity) with dark border — visible on
  both light and dark thumbnails
- Selected: filled primary-color circle with white checkmark inside

The circle is also directly clickable (same as the thumbnail tap).
2026-07-30 05:51:12 +00:00
Kyle Bolen
de240be014 Fix mangled showCheckbox expression from sed substitution 2026-07-30 05:40:01 +00:00
Kyle Bolen
8681bddb60 Verify screen: Done button is the single commit action
Done button now handles everything in one tap:
- 'Confirm: delete N from phone + remove N from staging' — red button
- 'Confirm: delete N files from phone' — red button
- 'Confirm: remove N from staging' — red button
- 'Done — Back to Start' — normal blue (nothing to commit)

Deletes phone files (if delete-after-verify + checkboxes), removes
soft-removed files from staging, then navigates home.

Removed separate 'Delete N files' button — one action is clearer.
2026-07-30 05:39:38 +00:00
Kyle Bolen
8cf8a7a197 Fix: rename removedFromImport to softRemoved (missed in previous commit) 2026-07-30 05:38:06 +00:00
Kyle Bolen
80eee705a7 Verify screen: soft-remove with undo, reactive summary counts
Soft-remove (X button):
- Row dims to 40% opacity with 'Removed — tap ✕ to undo' overlay
- X button turns blue to indicate it's now an undo action
- File stays on screen, counts update immediately
- 'N removed' chip appears in summary header
- 'N removed — tap to undo all' chip in filter bar with X to undo all

On Done: soft-removed files are deleted from staging at that point.
Until Done is clicked, all removals are fully reversible.
2026-07-30 05:37:29 +00:00
Kyle Bolen
0139d47d5f Fix LIVE badge visibility + checkbox contrast
LIVE badge: explicit Color(0xFF00C853) green with white bold text,
no longer uses secondary color scheme which was dark-on-dark.

Checkbox: show faint white RadioButtonUnchecked when not selected so
user can see the selection zone on dark/thumbnail backgrounds.
2026-07-30 05:33:55 +00:00
Kyle Bolen
6863bad7a0 Browse + Verify screen improvements
DenseThumb:
- LIVE badge moved to bottom-left (no overlap with checkbox)
- Video size badge at bottom-left
- Checkbox stays top-right only
- + button at bottom-right opens PhotoPreviewDialog

PhotoPreviewDialog (BrowseScreen):
- Shows cached thumbnail in a dialog
- For Live Photos: also pulls and opens MOV via macOS 'open'

VerifyScreen redesign:
- Filter bar: All / OK / Unverified / Failed
- Thumbnail chip (44dp) on left of each row — click to preview
- Errors/warnings get tinted row background (red/yellow)
- X button on each row to remove from import (deletes from staging)
- Status chips in summary header
- Checkbox-based delete still works when deleteAfterVerify=on
- Delete confirmation as AlertDialog (cleaner than inline card)
2026-07-30 05:30:43 +00:00
Kyle Bolen
f90baa92d4 Fix exiftool warning pollution + verify screen messaging
Copier: exiftool output parsed with stderr separate; Warning:/Error:
lines stripped from stdout so IPTC/XMP warnings don't appear as
the Make value.

VerifyScreen:
- 'kept on phone' suffix on unverified only shown when deleteAfterVerify=on
- Summary header adapts: no delete messaging when delete is off,
  '(pre-unchecked)' label only when checkboxes are visible
2026-07-30 05:23:48 +00:00
Kyle Bolen
35984b0b85 Verify screen: per-file checkboxes for delete + preview dialog
VerifyScreen redesigned as review-before-delete:
- All imported files shown with status icons (green/yellow/red)
- Checkboxes appear when deleteAfterVerify is on
- Pre-checked: EXIF-verified files (safe to delete)
- Pre-unchecked: EXIF-unverified files (kept on phone by default)
- Failed copies: no checkbox (excluded from deletion entirely)
- Uncheck any file to protect it from deletion
- 'Delete N files' button → confirmation card
- Confirmation shows count of excluded unverified files
- Click any row → PreviewDialog with full image + EXIF status

AppState: add deleteSpecificFromDevice(devicePaths) to delete exactly
the set of paths the user confirmed on the verify screen.
2026-07-30 05:16:09 +00:00
Kyle Bolen
1d239fb60a EXIF verification at import time + click-to-preview on verify screen
Copier: run exiftool -Make -s3 on each pulled file after copy.
- exifVerified=true: Make contains expected camera make
- exifVerified=false: Make absent or unexpected (not deleted from phone)
- exifVerified=null: exiftool not installed

CopyResult: add exifVerified + exifMake fields.

deleteVerifiedFromDevice: skip files where exifVerified=false.

VerifyScreen:
- Green row: EXIF verified camera original
- Yellow row: EXIF unverified — shows Make value, warns not deleted
- Red row: copy failed
- Click any row: PreviewDialog shows full image + EXIF status + path

ConnectScreen: show warning banner if exiftool not installed, with
install command (brew install exiftool).
2026-07-30 05:14:08 +00:00
Kyle Bolen
347f182c30 Hide Live Photo folder entirely from UI
ExifFilter.isHiddenFolder(): returns true for 'Live Photo' folder.
buildFolderList: filters hidden folders before building FolderInfo list.
Live Photo MOVs are now completely invisible — the LIVE badge on the
HEIC is the only signal needed. No placeholder thumbnails in the panel.
2026-07-30 04:55:18 +00:00
Kyle Bolen
595d583f6e Live Photo handling: hide MOV companions, LIVE badge, atomic import/delete
PhonePhoto: add companionDevicePath (MOV path stored on the HEIC).

iOS helper: detect Live Photo pairs by IMG_XXXX filename matching.
MOVs with a paired HEIC get isLivePhotoMotion=true, sourceFolder='Live Photo'.
HEICs with a paired MOV get isLivePhotoStill=true, companionDevicePath set.

Grid: Live Photo MOVs appear only in 'Live Photo' folder (Other section).
HEIC stills show a LIVE badge. Camera folder stays clean.

Import: 'Import Live Photo motion clips' toggle (default off).
When on, Copier pulls the companion MOV alongside the HEIC atomically.

Delete: deleteVerifiedFromDevice also deletes the companion MOV when
deleting a Live Photo still.
2026-07-30 04:43:41 +00:00
Kyle Bolen
6876709051 Detect Live Photo companions and show video file size
iOS helper: classify_iphone_file now detects Live Photo MOV companions
by checking if IMG_XXXX.MOV has a matching IMG_XXXX.HEIC/JPG in the
same DCIM folder. Falls back to size heuristic (<5MB = Live Photo).

Live Photo companions appear in 'Live Photo' group (Other section) —
excluded from camera-roll default selection.

DenseThumb: show file size on video badge (e.g. '▶ 2MB' or '▶ 42MB')
so Live Photo clips (small) are visually distinct from real videos (large).
2026-07-30 04:24:22 +00:00
Kyle Bolen
e6d27d3ab3 iPhone file classification by filename pattern
iOS helper: classify_iphone_file() groups photos by name pattern:
- Camera: IMG_XXXX.{HEIC,JPG} — camera originals
- Edited: IMG_EXXXX.{HEIC,JPG} — Photos.app edits
- Videos: IMG_XXXX.MOV and other video files
- Screenshots: any .PNG (iPhone camera never produces PNG)
- Screen Recordings: RPReplay_Final*.mp4
- Other: everything else (web saves, app exports, etc.)

ExifFilter.isCameraRollFolder: Camera and Edited are camera-roll,
Screenshots/Screen Recordings/Other are not.

sourceFolder now reflects these groups instead of raw DCIM folder names
(145APPLE etc.) so the left panel shows meaningful categories.
2026-07-30 04:19:28 +00:00
Kyle Bolen
716124bff5 Fix scroll-back placeholders: in-memory bitmap cache in DensePhotoGrid
bitmapCache (MutableMap keyed on devicePath) lives at DensePhotoGrid
level and survives Compose cell recycling. DenseThumb checks memory
cache first — scroll-back is now instant for already-loaded cells.
Disk cache (ThumbnailCache) is only hit when bitmap not in memory.
2026-07-30 04:02:54 +00:00
Kyle Bolen
558a7add89 HEIC thumbnails: pull full file (partial reads unreliable for sips conversion)
1MB partial HEIC reads are not sufficient for sips to convert to JPEG.
Revert to full file pull for HEIC thumbnails. Files are cached permanently
so each HEIC is only transferred once — subsequent launches skip already-cached files.
2026-07-30 03:57:44 +00:00
Kyle Bolen
30b600e2a4 HEIC thumbnails: extract embedded thumbnail instead of full file pull
For HEIC photos: read first 1MB (thumbnail track is near file start),
then use sips -getProperty thumbnail to extract the embedded JPEG
thumbnail without decoding the full image. Falls back to full resample
if thumbnail extraction fails.

For JPEG/PNG: unchanged, 512KB partial read + resample.

This avoids transferring 3-5MB per HEIC photo just for a thumbnail.
2026-07-30 03:54:40 +00:00
Kyle Bolen
6ed4362856 Fix HEIC thumbnails: pull full file + convert to JPEG via sips
HEIC is not front-loaded — partial 512KB reads produce broken output.
batch-thumbs now pulls full file for HEIC/HEIF, partial for JPEG/PNG.

sips: add --setProperty format jpeg to all thumbnail conversions so
HEIC files are always converted to JPEG. SkiaImage on JVM desktop
cannot decode HEIC natively — JPEG is required.
2026-07-30 03:53:24 +00:00
Kyle Bolen
fbd228b768 Add decode failure logging to DenseThumb to diagnose sparse thumbnails 2026-07-30 03:30:47 +00:00
Kyle Bolen
309031aed7 Fix iOS thumbnails: thumbnailVersion triggers DenseThumb retry
When the background batch pre-fetch writes new thumbnails to cache,
thumbnailVersion increments. DenseThumb uses it as a LaunchedEffect key
so cells retry loading from cache after each batch completes.

Also removed thumbFailed permanent gate — thumbnails retry on each
thumbnailVersion change instead of giving up after one failure.
2026-07-30 03:28:31 +00:00
Kyle Bolen
eef4f61050 iOS thumbnails: progress logging + newest-first loading order
Sort photos by dateTaken descending before pre-fetching so most
recent photos get thumbnails first — the ones most likely to import.
Log progress after each batch: 'iOS thumbnails: 30/600 loaded (batch 1/20)'.
Log skip count for already-cached photos on subsequent launches.
2026-07-28 08:48:11 +00:00
Kyle Bolen
6e3f6aa20e iOS thumbnails: batch pre-fetch after photo listing
After loadPhotos completes for an iPhone, spawn a background coroutine
that runs batch-thumbs in chunks of 30 photos per Python process call.
One Python process handles 30 thumbnails instead of one per photo —
reduces startup overhead by 30x.

sips resizes each pulled file to 240px and writes to the cache.
DenseThumb then reads from cache instantly. Files already cached are
skipped.
2026-07-28 08:45:54 +00:00
Kyle Bolen
b12cb4dd1e iOS thumbnails: add batch-thumbs command for future optimization
batch-thumbs pulls multiple files in one Python process session,
amortising the pymobiledevice3 startup cost. Not yet wired into the
app — thumbnails load lazily per-cell and cache permanently.

Known limitation: first load of iPhone thumbnails is slow (~2-3s each)
because each pull spawns a new Python process. After first view they
are instant from the on-disk cache.
2026-07-28 08:44:54 +00:00
Kyle Bolen
66faf4c7e5 iOS thumbnails: use partial 512KB read for faster thumbnail generation
Add pull-thumb command to helper script that uses afc.fread() to read
only the first 512KB of each photo. EXIF thumbnails are in the first
few KB so sips can generate a resized thumbnail from the partial file.
Falls back to full afc.pull() if fread is unavailable.

IosConnector.pullToFile() uses pull-thumb for thumbnail requests.
Full afc.pull() still used for actual photo import.
2026-07-28 08:37:57 +00:00
Kyle Bolen
f739114f7d Fix iOS date: st_mtime is datetime.datetime object, use isoformat() directly 2026-07-28 08:33:36 +00:00
Kyle Bolen
fa2e95bb45 Simplify iOS date parsing: use datetime tuple, confirmed working format 2026-07-28 08:32:33 +00:00
Kyle Bolen
884dc7ec30 Fix iOS date: use 'datetime' tuple key from AFC stat
pymobiledevice3 AfcService.stat() returns 'datetime' as a tuple
(year, month, day, hour, minute, second, microsecond) not 'st_mtime'.
Parse it directly into a datetime object and emit as ISO string.
2026-07-28 08:31:56 +00:00
Kyle Bolen
6f7dad3296 Add debug logging to diagnose iOS stat date issue
iOS helper: log raw stat keys+values for first file to stderr.
IosConnector: already logs stderr at WARN level.
logback.xml: print DEBUG+ for photophetch package to console so
stat output is visible when running ./gradlew run.
2026-07-28 08:29:37 +00:00
Kyle Bolen
fecdef3acf Fix date parsing: handle both nanosecond and second AFC timestamps
Threshold: >1e15 = nanoseconds, >1e9 = seconds. Previous threshold of
1e18 was too high for nanosecond timestamps in the year 2025 range
(which are ~1.75e18, so actually fine). Also revert pull to use
afc.pull() since it exists in this pymobiledevice3 version.
2026-07-28 08:26:30 +00:00