Keep the stored destination consistent with the picker's None fallback after
catalog changes. Extend the real-file import regression to delete the chosen
album before confirmation, then verify import membership and undo.
Document the helper contracts and destination transitions for review coverage.
Co-Authored-By: Codex <noreply@openai.com>
The page's automation call parsed its params with a fallback to {}, so a
typo such as lightcraft.command("photo.delete", "{ids: [3]}") ran
photo.delete with no params, which acts on the selection. Params must
now be a JSON object (blank text or null is {}); anything else rejects
the promise and runs nothing, as the control channel and MCP already do.
Lightroom Classic's F8 hides the right panel; F6 and F7 (filmstrip, left
panel) were bound but F8 did nothing. View > Right Panel (view.rightPanel,
F8 as a secondary key like F6 / F7) hides the right panel and shows the
same one again next time, or Edit when none was hidden. Hiding a tool
ends it, as closing its panel does.
Gap 28 listed rating [ ], flag cycling and F1 help as missing; they are
bound, so the entry now lists only filter-bar keys, Tab / Shift+Tab and F5.
The panel marked the last applied preset only while the selection was
the one it was applied to, and forgot it on the first frame with another
photo: back on the photo, nothing showed which preset it had. It now
remembers the preset per photo together with the photo's settings right
after, and marks it while the photo still has them; another photo shows
its own (or none), and a further edit drops the mark. The Amount slider
is no longer forgotten on a switch either: it comes back with the photo
while no later edit, undo or redo owns the undo step.
A mask slider hides the overlay so the adjustment can be judged, and
nothing showed it again: creating another mask, picking one in the list,
duplicating it or adding a part left it hidden while the Overlay menu
still named Color Overlay. Those now show the overlay, as Lightroom does;
so does a new AI selection from a click or a description.
eframe's web runner doesn't handle ViewportCommand::Title, so the web
build never named what was open, and a host embedding the page could
only guess whether a dropped file had opened. The page title is now
"<file name> — LightCraft" for the active photo, with " •" after the
name while changes are not yet written to browser storage, and the
window title when no photo is active. It is set only when it changes.
In Compare and Survey the selection holds both photos on screen, so the
Info panel's colour-label swatches and stars (and the Set Color Label
context menu) applied to the whole selection, while the bottom bar and the
rating/label keys act on the active photo only. They now go through
compare::target_active like the bottom bar does.
The culling panes' captions showed stars and flags but not the colour
label, so labelling there looked like it did nothing; each caption now
shows the label as a dot.
Reuse the shared album picker in Import Photos, retaining None/New album,
editable names, album-context preselection and existing import semantics.
Keep long paths within the viewport and cover hierarchy, localization,
keyboard choices, real imports, undo and layout regressions.
Co-Authored-By: Codex <noreply@openai.com>
Adds Finnish catalogs (fi.json, fi-formats.json), the language table entry
and menu command, Finnish tests, docs and an attribution row. What's New text
stays English, as in most catalogs.
AI-assisted translation (Claude Code), reviewed by a native Finnish speaker.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Corrections from my review of #1198: Exposure is Esposizione (not Tempo di esposizione, the shutter time), Sharpening is Nitidezza (not Contrasto), Preset stays Preset in every preset label, and blurry photos are sfocate, not mosse.
Adds the 339 interface messages Italian lacked (print, slideshow, mask effects, faces, HDR, external editors, keywords, focus points and their dialogs) and translates the 60 format strings still in English in it-formats.json. Only the What's New lines stay in English, as before: 2,628 of 3,278 messages.
The German catalog mixed du and Sie. It now uses du everywhere: the 11
messages that used Sie (imperatives such as "Wählen Sie", and "Ihr Name oder
Studio") changed; "Sie" and "Ihr" that mean they and their stay.
docs/localization-parity.md records the choice for the next batch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Help → What's New now reads in German down to the oldest entry: every
wrapped line of docs/whats-new.md without a German entry has one. Dates
read DD.MM.YYYY; issue numbers, code spans, file names and link targets
are unchanged, and link texts stay English because the pages they open
are English. Each wrapped line is its own catalog key, so the German
lines break near the English line breaks. "Raw(s)" is "RAW-Datei(en)",
menu paths use →, and the text keeps the catalog's "du" form.
Held back: the 59 lines whose English text contains a ▸ menu path.
Their translations are ready (with →) and go in once #2016 (for #1950)
renames those keys; adding them now would only conflict with it.
localization-parity.md and ROADMAP.md: German re-measured with the
documented count over the catalogs, 2,572 → 3,644 of 3,754 messages
(80% → 97%). The denominator grows because German now translates
What's New lines no other catalog has; other rows keep their counts.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only a drag timed at full size (median of its full-size drafts, after the
first, at most 20 ms) forgets a slow-drag verdict; a drag that drafts small
from its start, or times fewer than two full-size drafts, leaves it unchanged,
as `DragScale::finish` does.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The released 0.9.1 x64 MSI was tested on Windows 11 Pro 24H2 (build
26100.9711) with verbose msiexec logs. A fresh install and an upgrade
from 0.8.0 into a custom folder both end on the completion page, so
#790 is fixed (by #1569). The upgrade keeps the folder, Launch starts
the app as the signed-in user, and both removals leave nothing behind.
Uninstalling from Settings -> Apps still ends without any confirmation
(#892, #931), so that stays in the gap. Hours and percentages are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A max-pool, a channel-copy 2x2 transposed convolution (mode 1) and the
channel-copy 1x1 layer give an exact answer, so a store fault in mode 1
can't hide in tolerance (CodeRabbit review of #2380).
On the GTX 1070 this test passes even without mode 1's stepped stores:
FXC merges them the same way, but the driver runs that store right, so
only mode 0 was shown wrong. Mode 1 keeps the stepped stores as a
precaution; the shader comment and docs/gpu-pipeline.md now say so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On DX12, FXC merged the conv kernel's four output stores (modes 0 and 1)
into one 16-byte store_raw. The DXBC is valid and aligned, but NVIDIA's
driver ran it wrong on a GTX 1070: every second pixel's channels came
out shifted, so AI Denoise on the GPU was silently wrong on Windows.
DXC and Vulkan were right on the same card.
The stores now step by `1 + p.zero` (the always-0 uniform, renamed from
`pad`), which FXC cannot fold, so it keeps them apart. A two-layer
channel-copy network checks the stores exactly; it fails on that card
without the fix.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
GPU handles(): only output-device proofs stay on the CPU; Show All Masks now renders zoom windows (upstream); proof-boundary tolerance in the GPU equivalence test; device-profile draft test.
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Review on #1468: main's #2099 split out text_matches() and lets unknown field
prefixes fall back to plain text. On top of it, text_matches() now also matches
Photo::people(); the free-text token is unquoted before text_matches(); an
unknown field's quoted value is unquoted in the fallback (`10:"30 am"` is the
text `10:30 am`); and the field split keeps the !t.starts_with('"') guard.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With a filter showing one-star photos, taking the star off a photo took it
out of the view but left it the active photo: the grid highlighted nothing,
Detail kept showing it, and the next step (→, Shift+0–5 or Auto Advance)
started from the view's first photo, because `library.next` finds no place
in the view for an active photo the view doesn't show.
A rating, flag or label applied to the selection now does what a delete
already does when it takes the active photo out of the view: the photo after
it becomes active (the one before it at the end). That photo is the next one,
so `advance`, Shift+key and Auto Advance (keys and the Photo menu) don't step
past it. Commands that name their photos with `ids` leave the selection
alone, so Compare and Survey still show the photo they rate; a held view
keeps the photo, and the selection with it.
photo.pick, photo.reject, photo.unflag and photo.label now take `advance`
like photo.rate and photo.flag.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Zoomed in, the photo's corner is off the canvas, and the "Before" label
(\ key or the Before view) left the screen with it. The label's pill now
sits at the canvas' corner when the photo reaches past it, on whichever
axes it does; at Fit nothing changes. Same rule for the hovered preset's
name. Stacked on #2274 (the dark pill behind the label, same lines).
Tests: the pill geometry with the photo inside and past the pane, and a
headless check that at 100 % the registered label rect stays inside the
canvas.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
"Show N hidden locations" put every location removed from Local back at
once. With several hidden, the row now lists them (name, path on hover):
each comes back on its own, or all with Restore All. With one hidden, the
row restores it directly, as before. The command side already had
local.restoreHidden {path}; only the panel called it without a path.
The headless test hides a second place, opens the list from the row,
restores one, checks the other stays hidden, then restores the rest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Queued embedded grid-preview requests now change priority without restarting their work. Undo to an embedded preview retires obsolete thumbnail requests so a late edited render cannot replace the restored preview.
Partially addresses #348.
Co-authored-by: Codex <codex@openai.com>
- .github/workflows/ci.yml: the full `cargo xtask ci` on every pull request and push to main
(Linux x64, craft-fonts, rust-cache for dependencies), so it becomes the merge gate.
- `cargo xtask ci --quick [--base REF]`: fmt, clippy, the changed packages' tests, layers and
assets, plus heif / parity / wasm when the changes touch them, measured against the merge base
with origin/main (xtask/src/ci_quick.rs). Workspace-wide files test everything.
- A machine-wide lock (xtask/src/ci_lock.rs): a second `ci`, from another worktree, waits and
names the run it waits for. LIGHTCRAFT_CI_SLOTS=N, LIGHTCRAFT_CI_LOCK=off|<dir>.
- README, CLAUDE.md quality gate, gaps.md gap 9, target-app-parity.md stability note.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every level of Albums, Local, By Date, Folders and Keywords still steps one
level right of its parent, but the step is chosen each frame from the panel's
width and the deepest open row: 16 pt while that row keeps about 80 pt for its
name and room for its count, else 14, 12, 10 or 8 pt. Past 8 pt the sidebar
scrolls sideways as before. Callers keep passing indents in 16 pt steps;
`indent_x` maps them to the drawn step for the icon, name, chevron, album drop
line and the width a row asks for.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review: gaps.md's header and history record the 2026-10-19 gap 21
change, and the Indonesian parity row counts the 2,555 catalog messages,
with the 69 command labels noted separately.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Address review: the twelve-key-languages row for Indonesian still read 0%
while the shipped table listed it, and ROADMAP repeated it under Also
shipped. Indonesian is a key language, so it moves to its row (100% but
What's New) and out of the other-languages table (back to 4). Localization
mean of the 12 key languages ~55% -> ~64%, 8 of 12 present; the new-script
estimate drops Indonesian (135-239 h); the full-target weighted sum moves
61.2% -> 61.5% (still ~61%).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds crates/ui-egui/locales/id.json and id-formats.json: every plain
(2,258) and formats (297) message any catalog has, plus 69 command
labels no catalog translated yet. What's New stays in English.
Indonesian keeps a noun unchanged after a count, so English plural
suffixes are consumed with {:.0}; dates read "Minggu, 20/09/2026".
Menu triangles use → and ⌄ like the other catalogs. 356 labels reuse
the sibling apps' Indonesian wording (PhotoCraft, VectorCraft,
FilmCraft, FigCraft) so the suite reads the same; credited in
ATTRIBUTION.md.
Registers Locale::Id ("Bahasa Indonesia", Latn), the
app.language.indonesian command, the keyword-list catalog check and
an Indonesian switch/persist/format/date test. Docs: localization.md,
localization-id.md, localization-parity.md, ROADMAP.md.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A click on a grid stack badge expands or collapses the stack. A double-click,
as in Lightroom, toggled it twice: the second click undid the first and the
stack stayed as it was. The badge now ignores the second click of a
double-click.
photo.analyze takes burstGap (seconds, 0..86400, default 10) and
similarity (0..1, default 0.93); values out of range are refused before
anything is measured. The Assisted Culling dialog gets both sliders and
remembers them for the session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
set_aspect always turned the ratio to the image's orientation, so a Custom
ratio taller than wide came out landscape on a landscape photo, and a
ratio picked after X (crop.rotateAspect) turned the crop back. A named
ratio now takes the crop's present orientation (landscape, or portrait
after X; a square crop follows the image), a custom [w, h] is used as
given, and crop.aspect takes orientation: "crop" | "given" to choose.
The window reapplies the remembered ratio to an untouched photo with
orientation "crop", as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
GeoNames cities15000 holds towns of 15 000 people or more, so a photo in a
smaller town got the nearest larger one in City (Portofino → Rapallo,
5.3 km), another municipality. metadata.reverseGeocode takes maxDistance
(metres): past it, City and Sublocation stay empty, State and Country
still fill, and the reply names the town as nearestCity. Hostile values
are refused. The command help, the Fill Location tooltip and the parity
row say what the list holds. Default unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK