Commit Graph
305 Commits
Author SHA1 Message Date
Claude cd8d2b96a2 Merge PR #259 2026-10-08 17:50:50 +00:00
Claude 90fe6d1616 Merge PR #325
# Conflicts:
#	crates/ui-egui/src/panels/detail.rs
#	crates/ui-egui/src/tests_panels.rs
2026-10-08 17:34:31 +00:00
Claude fa630a233a Merge PR #307
# Conflicts:
#	docs/parity.md
2026-10-08 17:34:04 +00:00
Claude 259de3d9df Merge PR #312 2026-10-08 17:33:04 +00:00
Claude 0b101be155 Issue #181: saved presets and Export with Previous drop keys this version doesn't know
#287 made app.export refuse unknown params. Stored settings (a user export
preset, prefs.json lastExport) written by another version are not the
caller's typo, and refusing them would break that preset or Export with
Previous for good after an update: their unknown keys (also inside
watermark) are dropped before the strict check. What the caller passes is
still checked strictly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01912WaK6Rmew3wxZuPyX7E8
2026-10-08 17:29:00 +00:00
Claude 2565f893c4 Merge PR #257
# Conflicts:
#	docs/parity.md
2026-10-08 17:21:02 +00:00
Claude cb173c22dc Merge PR #311
# Conflicts:
#	docs/parity.md
2026-10-08 17:20:41 +00:00
Claude 7e21d185b8 Merge PR #258 2026-10-08 17:18:58 +00:00
Claude 74b9129a05 Merge PR #287 2026-10-08 17:18:58 +00:00
Nathaniel LedfordandClaude Sonnet 5.5 0a474cc06e Permanent delete of several photos is one consistent op list; restored photos join album/selection
Review of the onDeleted work found that purging photos one at a time built each
op list against the unchanged catalog, so photos sharing an album or a stack made
Empty Recently Deleted (and Fresh imports) fail with NoStack or leave dangling
album entries. Catalog::delete_photos_permanently_ops builds one list for all of
them; Empty, Delete Permanently and the Fresh purge use it. Photos an import
restores now take part in album/albumName/selection, and undo labels say Restore.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 11:29:05 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 2928880e81 Import: onDeleted policy (skip|restore|fresh) for files in Recently Deleted; library.emptyRecentlyDeleted
A file that is in Recently Deleted was always skipped as a duplicate with no way
forward (#298). The report now flags such duplicates (existingDeleted); onDeleted
restores the trashed photo with its edits or replaces it with a fresh import; and
Empty Recently Deleted removes every trashed photo in one undoable step.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 11:16:42 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 7f6e4e48f7 Selection::state_of: one answer to active / selected / not selected for every view
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 11:01:50 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 8d8178dc0a Engine: crop.drag command; validate custom crop ratios (#295)
crop.drag {handle, from, to, start?} runs drag_crop through the command
layer so the UI, CLI, control channel and MCP agree; a locked crop keeps the
shape of its frame. crop.aspect now rejects non-finite, non-positive and
beyond-20:1 ratios and non-numeric arrays instead of storing a degenerate
lock that produced an empty crop.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 10:35:46 -04:00
midasdfandCodex 69640df8a7 LR-EXP-WATERMARK: shape Japanese vertical watermark glyphs
Co-Authored-By: Codex <noreply@openai.com>
2026-10-08 23:34:43 +09:00
Nathaniel LedfordandClaude Sonnet 5.5 198286e589 Folders: round-4 review fixes (counts at the edge, edge-space names, smart albums)
- ui: counts stay at the visible edge when the sidebar scrolls sideways
  (with the row's colour behind them where a long name runs under); the
  smart-album dialog's live count includes the album's folder
- catalog: a folder path is used as given (a name with a space at its edge is
  not its neighbour: Remove acted on the wrong folder); a plain POSIX path
  is checked against the folder without building a key (about half the cost
  on a big library, with a test that it agrees with the careful check)
- engine: editing a smart album's rules keeps the folder the editor cannot
  show; a shown folder that loses its last photo gives way to All Photos;
  library.state and view.json name the folder only while it is shown; a blank
  folder filter is no filter

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 09:06:18 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 5d13d59520 Folders: review fixes (smart albums, disk guards, deep chains, long titles)
- engine: a smart album made from a folder view keeps the folder; only a
  folder that shows exactly its own photos can be the source (not the
  startup disk, not a folder above disks, not one with no photos); a
  folder source and a folder filter never coexist; a folder source with no
  folder shows nothing; a saved view whose folder is gone opens on All
  Photos; removing a folder above a disk needs `disk: true` even with one
  disk mounted; library.folders documents volume/selectable
- ui: deep chains stop indenting after five levels, the grid title gives
  way to the photo count (full name on hover), disclosure triangles have
  spoken names, the remove dialog says what is included and has a disk
  title; test temp folders are cleaned up on failure

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 09:06:18 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 0732edc64f Folders: every folder its own node; choosing one loads it like an album
- catalog: a row is exactly one folder (no folding of single-child levels);
  the UI starts a folder that holds nothing and leads to one folder open
- engine: a library folder is a source (LibrarySource::LibraryFolder with
  Session::library_folder, like Local's browse): library.source
  {kind: libraryFolder, path}, kept with the library's view, shown by
  library.state, followed through folder.rename / folder.move and undo,
  dropped back to All Photos when its folder is removed
- ui: clicking a folder replaces whatever the grid showed (Picks, Recently
  Deleted, a Local folder…) instead of narrowing it with a filter chip; the
  grid is titled after the folder

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 09:06:18 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 82e827b9d9 Sidebar: Folders section for the folders the library's photos were imported from
Another way to explore the library next to By Date and Keywords: a tree of
the folders that hold imported photos, with counts (subfolders included),
under the disk or network share each is on. Local stays the way to browse
any folder on disk.

- catalog: Catalog::folder_tree and Filter::library_folder. Each disk is a
  volume row (drive letter, Windows share, /Volumes/<share>, /media, /mnt,
  gvfs, /net…), found from the stored paths alone, so an offline share costs
  nothing. Levels that hold nothing and lead to one folder fold into one row;
  depth is bounded. The tree and the filter read paths through one splitter
  (separators, `.`, `..`, verbatim UNC prefix, repeated separators), and a
  row that would cover other disks' photos is not choosable
- engine: library.folders, library.removeFolder (to Recently Deleted, one
  undo step, no file touched; a whole disk or a folder spanning several
  needs `disk: true`, the startup disk never), libraryFolder in
  library.filter, chip; a chosen folder follows folder.rename / folder.move
  and their undo
- ui: collapsible Folders section; the chosen folder's row is always opened
  into view; right-click (row or triangle) Rename, Move, Show in Finder,
  Remove from Library…; disk rows Remove Disk from Library…; both ask first

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-08 09:06:18 -04:00
wstockandClaude Fable 5.1 88d9b7196d Issues #181, #183: app.export refuses unknown params and bad values; watermark keys documented and range-checked
`ExportOptions::from_json` reads what it recognises and defaults the rest, so
`longEdgee=400` exported at 3000 px, `longEdge=banana` at full size, and a
`watermark` of `5` or `{"text":"x","bogus":1}` passed with `ok:true`, while
`size: 3` was silently clamped to 0.5 of the short edge (#181, #183).

`from_json` stays lenient for stored prefs and dialog prefill. New
`ExportOptions::validate(cmd, params)` / `from_params(params)` check every key
against the option and target lists (unknown ones name the nearest known key),
every value's kind and range, and a `watermark` object's keys, kinds and ranges
(`size` 0.005..0.5 and `inset` 0..0.4 of the short edge, `imageWidth` 0.01..1 of
the width, `opacity` 0..1, `color` [r,g,b]), returning
`invalid parameters for \`app.export\`: …` like the other commands. The three
places that run an export use it — the desktop `app.export`, the headless
backend behind `lightcraft-cli run` and the MCP `export` tool, and
`lightcraft-cli render --opt` — and `export.savePreset` validates a preset's
params when it is saved. Built-in presets, the Export dialog's `to_json`,
Export with Previous and `render --opt` all pass unchanged.

The `app.export` param string (`lightcraft-cli commands`, `engine.commands`),
the MCP `export` tool schema and docs/parity.md LR-EXP-WATERMARK now list the
watermark keys, their units and ranges.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 11:06:59 +00:00
wstockandClaude Fable 5.1 f7c0b9e49f Issue #182: library.select and id queries reject a photo id that is not in the library
`select_photos {"ids": [9999]}` answered `selected: 1` and left the session
with no usable active photo; `develop.get {"id": 9999}` answered too.
`library.select` now checks every id (and `active`) against the catalog
before touching the selection and returns `no such photo N`, so the
previous selection and active photo stay in place; `photo_arg` (develop.get,
photo.inspect, photo.allMetadata, history.list) does the same for an
explicit `id`. Engine and MCP tests cover every select mode.

Fixes #182

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-08 07:45:41 +00:00
Daniel VoshartandClaude Sonnet 5.5 4b57d5dc5a Fetch: a bearer token is sent only when the caller names its variable
Options::token_env (default none) replaces the hard-coded LIGHTCRAFT_SAM3_TOKEN, so a download of one model can
never send another model's token to its host. SAM 3 names its variable through lightcraft_segment::fetch::options().
The decision (non-empty, https, the host the download began at) is a small function with its own test.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 22:59:01 -04:00
Hanmo d614dc72bd M3: isolate global Tint direction correction for compatibility review 2026-10-08 10:26:18 +08:00
Hanmo 024e983a7e M3: honour Nikon CropArea and embedded preview colour space 2026-10-08 10:16:44 +08:00
Brandon Thomas 88f38c7e2a Merge pull request #248 from andrewyao (web_time in raw preview decode timing) 2026-10-07 21:06:13 -04:00
Brandon Thomas df574a537a Merge PR #213 from Orthic-Labs/orthic/arw-lossless-ycbcr
# Conflicts:
#	crates/ui-egui/src/headless.rs
2026-10-07 20:53:51 -04:00
Brandon ThomasandClaude Opus 5.5 71c8382b10 Merge main into #213 (keep main's Windows test fixes from #207; parity row combines RW2 and the downsized ARW)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:53:25 -04:00
Andrew YaoandClaude Opus 5.5 0a952b3536 Web: time raw preview decodes with web_time so wasm doesn't panic
load_bytes_now timed the binned demosaic with std::time::Instant, which
panics on wasm32-unknown-unknown ("time not implemented on this
platform"). Opening a raw in the web build aborted the decode. Use
web_time::Instant, as library.rs already does.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-07 17:53:19 -07:00
Brandon ThomasandClaude Opus 5.5 2e48f25997 Merge main into #243 (combine with #207's Windows test fixes)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:33:23 -04:00
Brandon ThomasandClaude Opus 5.5 d716d74af5 Merge main into #243 (combine with #207's Windows test fixes: hide_home_above plus local.addRoot, FakeFs path normalising plus case folding)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:32:47 -04:00
radialmonsterandClaude Opus 5.5 62325bd45e Thumbnails: retire the replaced preview cache so its old jobs can't write
Reopening a library (or opening another one, or set_disk_cache_bytes
falling back to attaching) replaced MediaCache::rendered with a new
PreviewCache, but render jobs still in flight kept the old object.
Each PreviewCache checked only its own generation, so after a clear on
the new cache the old one's generation was still current: a finishing
thumbnail job wrote its pixels through it into the directory the clear
had just emptied. The UI rejected the result, but the JPEG stayed.

attach_disk_cache now retires the cache it replaces before swapping it:
PreviewCache::retire takes the generation write lock (so it waits for a
write in progress, like clear) and marks the object retired; every
get/put/put_deferred through it is then refused and its clear() is a
no-op, as its directory may now belong to the replacement. Files on
disk are left alone, so a plain reopen still finds its thumbnails, and
resizing the budget in place keeps the same cache object as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:23:17 -04:00
Brandon Thomas b90c06b347 Merge pull request #210 from voshart/pr/fetch-crate
Fetch: the model downloader becomes its own crate (no behaviour change)
2026-10-07 20:15:46 -04:00
Brandon Thomas 52f64ce0b9 Merge pull request #215 from XRTce/rw2-panasonic-lumix
LR-IMP-FORMATS: Panasonic RW2 / Leica RWL / Panasonic RAW in every raw format, with the RW2 camera look
2026-10-07 20:15:41 -04:00
Brandon ThomasandClaude Opus 5.5 9b41e33a24 Merge main into #213 (keep main's waits in the two flaky UI tests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:14:41 -04:00
Brandon ThomasandClaude Opus 5.5 9da8163ae2 Merge main into #243 (keep main's settle and step_until in the two flaky UI tests)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:12:10 -04:00
Brandon ThomasandClaude Opus 5.5 46f5d212e7 Merge main into #207 (keep main's step_until wait for the folder row)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:11:00 -04:00
Brandon Thomas b63ea8f5c2 Merge pull request #222 from Ndsanes/fix/craft-fonts-relative-path
Build: resolve a relative CRAFT_FONTS_DIR against the workspace root
2026-10-07 20:09:28 -04:00
Brandon Thomas 97e34434d0 Merge pull request #217 from nledford/feat/random-sort
Library: Random sort with a stable seed and Reshuffle
2026-10-07 20:09:14 -04:00
Brandon ThomasandClaude Opus 5.5 034bb6e324 Rename: drop a redundant from != to (clippy nonminimal_bool)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:08:52 -04:00
Brandon ThomasandClaude Opus 5.5 e574839724 Merge main into #213 (camera_preview: main's proxy edge/size with the sensor_proxy fallback)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 20:01:23 -04:00
radialmonsterandClaude Opus 5.5 73cf0d50bb Thumbnails: keep textures when the disk cache is resized
Changing the disk cache size (library.preferences cacheMb) re-attached a
new PreviewCache object, and the renderer treats a new cache object as a
library reset: every texture, the loupe included, was dropped and
re-rendered. Thumbnails are keyed by content, so a new budget leaves them
valid. The budget now changes in place on the attached cache (same object,
same generation); the next write trims to it, as before. A preview clear,
a generation bump and a new library still invalidate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:04:03 -04:00
f2f7a1cb26 T0: optimize thumbnail requests and reject obsolete preview work
Skip unchanged thumbnail jobs and reuse bounded settings hashes (both maps
capped at 1024 entries). Reject stale render results and cache writes after
edits, library changes and preview clears: render requests carry a request
id and cache generation, PreviewCache writes and reads hold the generation
lock that clear() takes, and library.clearPreviews also drops cached
sources. The web worker index follows the same generation rule.

The decoder guards, Windows path fixes and test fixture changes that were
bundled in a327b98 are separate commits; the benchmark follows this one.

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 19:04:03 -04:00
928c2c974f Windows: treat separator-only path differences as the same file
Catalog paths may mix `/` and `\` on Windows, so raw string comparison saw
`N:/photos/IMG.jpg` and `N:/photos\IMG.jpg` as different files:

- Rename: planning, the move list, move_file_with and file_moves now compare
  as paths. A separator-only "rename" is a no-op: no file move, no catalog
  change and no undo entry, alone or batched with a genuine rename.
  Previously it attempted a plain rename onto the existing file, failed, and
  rolled back the rest of the batch. Case-only renames still work (#95).
- Import: the already-in-library index is keyed by PathBuf.
- Tests build expected paths with Path::join, and the CLI test escapes the
  device path with serde_json instead of string formatting.

Split from a327b98 (T0 thumbnail work), with the follow-up rename fix that
the committed version needed.

Co-authored-by: Codex <noreply@openai.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-07 18:50:00 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 520d7b8616 Library: switching libraries no longer shows the old library's sidebar
A catalog loaded from disk counts revisions from 0, like a new empty one, and the sidebar
counts, keyword tree, date groups and grid list are cached by revision alone. Opening an empty
library after one that was never edited this session therefore kept the old library's numbers.
Each catalog loaded in a process now gets its own revision range.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 15:40:18 -04:00
NDsans b040f9bb8f Build: resolve a relative CRAFT_FONTS_DIR against the workspace root
A build script's cwd is its package root, so CRAFT_FONTS_DIR=../craft-fonts —
the spelling the README and craft-fonts' integration docs both use, written
relative to the workspace root — resolved to crates/craft-fonts and failed.

The failure is silent from the user's side: the app runs, the language menu
lists every language, and selecting Chinese or Japanese paints boxes. Warn
when no font ends up embedded, not only when the variable is set but wrong.
2026-10-08 03:08:28 +08:00
Nathaniel LedfordandClaude Sonnet 5.5 6de619ca9b Random sort: choosing Random starts a fresh shuffle
Switching to Random from another key (without an explicit seed) derives a new seed
instead of reusing the one left behind, which was the fixed seed-0 order on a new library.
Re-choosing Random or changing only the direction keeps the shuffle on screen.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:08:11 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 89446df6a8 Random sort: review fixes (null seed, 2^53 seed bound, real tests), README
- library.shuffle treats a null seed as none given
- seeds are limited to 0..2^53-1 so JSON-double clients read them back exactly
- album test now fails if manual order overrides Random; persistence test goes through
  close/reopen of the library instead of a serde round trip
- README lists random among the sort options

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 14:03:52 -04:00
Nathaniel LedfordandClaude Sonnet 5.5 a72118ec6e Library: Random sort with a stable seed and Reshuffle
SortKey::Random orders photos by a splitmix64 rank of (Sort.seed, id): same seed gives
the same order, and added/edited photos never reshuffle the others. library.sort takes
an optional seed; library.shuffle picks a new one (below 2^53 for JSON clients). Menus
gain Random and Reshuffle; Ascending/Descending are disabled for Random. No effect in
Recently Added (always newest import first).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 13:33:38 -04:00
Orthic LabsandCodex 94780ed299 LR-IMP-FORMATS: decode downsized Sony lossless ARWs
Co-authored-by: Codex <noreply@openai.com>
2026-10-07 22:36:00 +05:30
Claude cabe5b7399 LR-PROF-CAMERACOLOR: Panasonic RW2 file-local camera look
RW2 / RWL / RAW join ARW and NEF: the starting look (matrix, hue/saturation table, tone and chroma curves) is
fitted to the file's own embedded JPEG behind the same gates, with white balance relative to the as-shot look.
Panasonic previews always show the whole active area while the default crop is the in-camera aspect ratio, so when
the preview matches the active area instead of the crop, the reference is cropped to the default crop before
fitting (corpus_rw2_with_an_in_camera_crop_gets_a_camera_look).

On 174 CC0 samples with a preview 140 fits are accepted (93 of the 114 bodies with a preview), median ΔE against
the camera JPEG 5.0 with matching lightness; most of the others are compacts and kit zooms whose camera JPEG is
distortion-corrected (tag 0x0119 isn't used yet). `lightcraft-cli calibrate` keeps collecting ARW and NEF only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QtQmZ6peZXGn3wiqeowV2v
2026-10-07 16:36:06 +00:00
Daniel VoshartandClaude Sonnet 5.5 7fa7506db4 Fetch: the model downloader is its own crate, lightcraft-fetch
The downloader in lightcraft-segment (mirrors, resume, pinned SHA-256, pure-Rust
HTTPS) was already generic apart from four SAM 3 constants. Move it to
crates/fetch (L0, native only) so other model features can use it without
depending on the SAM 3 inference code. lightcraft-segment keeps SAM3_FILES,
DEFAULT_MIRRORS, MIRRORS_ENV and wraps mirrors(); no behaviour change. The
downloader's 13 tests moved with it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-07 12:07:24 -04:00