A file only browsed in Local was counted as already in the library and
silently dropped; it is now new, and synchronizing brings it into the
library, as an import does.
A photo whose file was renamed or moved within the folder showed as
missing while its file counted as a skipped duplicate, so "Remove
missing" sent the photo and its edits to Recently Deleted and imported
nothing. The scan now matches a missing photo's content hash among the
folder's files and lists it as moved; synchronizing relinks it (on by
default, "Relink photos whose file was renamed or moved"), in the same
undo step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-applies fix/issue-104-ui-fs on top of the data-safety fixes (integrate/safety):
- availability cache: originals and smart previews are checked on a worker thread; the UI
answers from the cache (grid, Info panel, photo menu, Missing Photos view)
- import split into ImportJob::prepare (worker) + commit_prepared, with Cancel; Copy keeps
#96's verified copies
- Find Missing Photos: find_candidates (snapshot, no I/O) + plan_find_missing (all file I/O,
on a worker, never the session) with #105's matching rules unchanged (name+size+hash;
renamed by size+hash; single name+size; ambiguous look-alikes skipped and reported; files
already in use excluded). The plan is applied with library.findMissing {found, missing,
ambiguous}, re-checked against the catalog then: photos relinked meanwhile are left alone,
files now in use are not taken. library.findMissing {folder} still runs synchronously.
- Build / Discard Smart Previews with background: true runs on a worker with progress and
Stop, keeping #106's atomic writes, is_valid checks and `repaired` count (also in
library.previewProgress); the render worker's smart-preview fallback uses is_valid
- Rename preview planned on a worker (plan_rename_photos) on the same planning core as
Session::plan_rename_with (MoveFs, case-only respelling by file identity, #95)
- Local folder listing via fs_cached; tests_offline (slow injected probe, frames < 300 ms)
Tests: worker-planned Find Missing equals the synchronous command for a renamed + ambiguous
library; a plan is re-checked when applied; background smart previews repair a damaged proxy.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rollbacks in move_file, apply_rename and undo/redo no longer ignore
errors: every move that can't be undone is reported (old -> new), and the
catalog follows the files that kept their new names (an undoable partial
rename; a logged change for undo/redo), so nothing shows as missing.
A file that can't go back keeps the sidecars that moved with it.
library.findMissing: same name + size confirmed by the stored content
hash first, then renamed files by size + content hash, then a unique
name + size match; several look-alikes are skipped and reported in
'ambiguous' instead of guessed. Files already used by a photo are skipped.
Stacked on fix/issue-95-case-only-rename (same functions).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- merge output_path: a bounded search for a free name instead of
`for k in 1..` + unreachable!().
- library open: Option::insert returns the new library, no expect.
- relink: a one-op batch unwraps through <[Op; 1]>::try_from, no expect.
- lightcraft-mcp (already clean) now #![deny] unwrap/expect/panic/
unimplemented/todo/unreachable; clippy.toml as in the steering PR.
crates/engine/src/export.rs keeps one expect (open PR #51 edits that
file), so lightcraft-engine doesn't get the deny attribute yet.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Missing Photos (sidebar count, the view, library.missing, Find Missing)
now shares one scope rule, cmd::missing::checked_path: non-deleted
library photos backed by a file. Local browse records, which can
outnumber the library by orders of magnitude, are never stat'ed. The
Missing view checks only the photos its query kept instead of every
catalog record.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Raw variants the decoder can't read yet (e.g. Nikon Huffman-compressed NEF
from a D3200/D5100) silently fell back to the camera's embedded JPEG, so
users edited a JPEG with the camera's picture style (e.g. Monochrome) baked
in without being told.
- catalog: Photo.preview_only (serde default; old catalogs load), set /
cleared by Op::SetContent with an exact inverse; Photo::develops_raw().
- engine: ProbeInfo.preview_only carries the decoder's Unsupported reason
into import, the import review (previewOnly) and catalog.query
(previewOnly); photo.inspect includes it. photo.reload and photo.relink
re-probe and clear/set it when the file decodes (or not) now.
- source_info: a preview-only raw is a rendered source (relative WB scale,
display tone curve) instead of raw:true/5500 K + filmic tone map on a
display-referred JPEG; import uses the JPEG default preset for it; the
Edit panel shows the rendered-file WB sliders.
- UI: "Preview" badge on grid thumbnails, a pill on the loupe, a notice in
Edit and Info (amber caution token, Info icon drawn in code).
- Tests: catalog serde/undo, engine import -> reopen -> reload/relink,
headless UI badge/notice widgets.
Refs #10
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Auto versions: leaving an edited photo keeps its settings as an Auto version (when they
differ from its latest version; 20 kept); saved with the library, not an undo step.
- View ▸ Slideshow (⌥⌘↩, `interval`): the photos in view full screen, wrapping; Space
pauses, ←/→ step, Esc ends.
- Photo ▸ Convert to DNG (photo.convertToDng): lossless DNG next to the raw with the settings
embedded; the photo and its virtual copies are relinked (Relink gains an optional format),
originals kept, undoable. Import: Copy as DNG (`dng`) for copy imports.
- A fine grid over the photo while a geometry slider is dragged.
- View ▸ Show Photo Counts; Help ▸ System Info… (app.systemInfo, JSON for agents).
- Native menus: modified Enter accelerators.
- Parity rows refreshed (target / quick collection, auto sync, location rules, bring to front).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- library.missing lists photos whose file isn't where the library expects it; photo.relink
points one at its new location; library.findMissing {folder} relinks every missing photo
whose file (same name and size) is somewhere in a folder, in one undoable step. New
Op::Relink: like a rename in the catalog, but undo/redo never move files.
- UI: File → Find Missing Photos…, photo menu → Locate Missing File… (when the file is gone).
- Renderer: a remembered failure is retried once the catalog changes (a relinked file).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>