Adds Georgian (ქართული, code ka, system tags ka / ka-GE / ka_GE) with
complete plain and formats catalogs, the Edit > Language command and the
Settings choice. Sentence-cased messages go through i18n::upper_first,
which leaves Georgian letters alone (upper-casing Mkhedruli would turn it
into Mtavruli). The Settings language choices wrap now that eleven
endonyms no longer fit on one line, and the face-model Remove button goes
through tr().
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YMTAbGNRxnMEzRvYnQGphF
A library with no photos opened on the Detail view, whose empty state said "Choose a photo in
the grid or filmstrip" while the grid was empty and hidden: the only screen that mentioned
import was off screen, and Import itself was nowhere but the File menu and its shortcut.
- An empty library (no photos, or only deleted or browsed ones) opens on the Photo Grid with the
sidebar (Square Grid stays as it is), in the desktop app and in `lightcraft-cli snapshot`. The
desktop app switches before it notes what its settings file holds, so the switch alone isn't
saved.
- The grid and the Detail view show "Your library is empty" with Import Photos… (primary) and
Import Lightroom Catalog… (not on the web) as buttons, and a line saying photos and folders
can be dropped into the window, with the key Import Photos… is bound to now (`⌘⇧I` on macOS,
`Ctrl+Shift+I` elsewhere, or a remapped one). Not while a folder is browsed under Local or
Recently Deleted is shown: their photos aren't in the library but are on screen.
- The right-hand panel says "No photos yet / Import photos to get started" there instead of
"Select a photo to edit".
- The top bar has an Import button beside Export in every library. The centred search field's
reserve grows by both sides' share (620, and a 190 minimum), so the filter icon stays clear of
the icons on the right at every window width, also with macOS's title bar in the window. The
top bar's icons are named for screen readers, and a refused click says why in a toast.
- `empty_state` is `empty_message` with buttons that do the next step.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Lightroom Classic's Loupe Overlay ▸ Layout Image: a PNG with transparency (a
cover's masthead and cover lines, a banner's safe areas) drawn over the photo
in the loupe, fitted and centred, following zoom and pan. Opacity and matte
(the photo darkened outside the graphic's box) in steps, ⌥⌘O shows / hides it,
the choice is saved with the UI state. The file is read off the UI thread; an
unreadable one is reported on the canvas. engine::export::read_graphic is the
watermark logo's reader, now shared.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Mj2w3LwYjFhMHALZuFp2rU
Retain the complete reviewed change, including prior integration resolutions,
on current upstream.
#736: deliver Thai UI fonts in desktop and web releases
Use the merged craft-fonts input containing Noto Sans Thai across release,
FreeBSD and Nix builds. Retain the small Thai face in the wasm subset and
reject required release inputs that lack it. Exercise the reported Thai
folder name through all UI families and include original font licence texts
in Windows installers as well as portable packages.
Refs #736; completes delivery of the fallback added in #750.
Validation: UI tests, UI-only strict Clippy, 28 wasm checks, HEIF checks,
assets, layers, parity and formatting pass. All 21 unrelated workspace
failures reproduce with upstream Rust sources and the old font input;
existing Rust 1.95 metadata/engine Clippy findings remain unchanged.
A pass drawn again after a discard still has the widgets of the pass
thrown away, so a control request answered there found a row where it no
longer is; it also added a 240 fps sample to perf.fps. Control requests
and the fps sample now run on a frame's first pass only. The Keyword
List's hold is kept for the pass right after it was asked for, so it
never moves a panel that was not drawn in between.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Scrolled down a long Keyword List, ticking or unticking a keyword grew or
shrank the Keywords box above it (a row of chips, suggestions) and slid
the tree under the pointer. The panel now moves its scroll offset by the
same amount and draws the pass again, so the list stays put. It holds
only while the box's top is scrolled out of sight and the change does not
come from the box itself (the pointer on it, or typing in its field).
The offset is set on the scroll area as the redrawn pass starts, from the
offset the pass before started at: a scroll asked for during a pass lands
a frame later, and egui pulls a shrunken panel's offset back by the end
of a pass. Ticks clicked one after another (a script, a quick hand) then
land on the rows clicked.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in batch 35 / v0.8.0. Code merged cleanly; conflicts only in docs: the LR-MERGE-HDR row keeps main's
lens-correction text plus this branch's white-balance item, and this branch's What's New entry moves to the
2026-10-14 section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
Brings in #1423 (HDR merges keep the lens corrections), which landed in batch 35 / v0.8.0. hdr.rs merged cleanly:
the result's lens corrections come from the reference frame after the brightness ordering. Conflicts only in docs:
the LR-MERGE-HDR row keeps main's lens-correction text plus this branch's two items, and this branch's What's New
entry moves to the 2026-10-14 section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015gRtdWF1E9358FfzhzYqCS
photo.allMetadata listed no lens for a RW2: Panasonic writes no Exif
LensModel, and the maker note that names the lens is in the Exif of
the embedded JpgFromRaw. List that maker note tag (LensType, 0x0051)
as a row of a Panasonic group, the same name photo.inspect shows, for
a RW2/RWL and for a JPEG with the note in its own Exif.
Checked on the CC0 sample DMC-G80 P1000403.RW2: Panasonic / Lens Type
= SIGMA 60mm F2.8 DN.
import_reads_files_off_the_ui_thread_and_can_be_cancelled: the probe was
a 150 ms sleep judged by a 300 ms wall-clock frame budget, which the
loaded FreeBSD QEMU VM can miss. It now waits on the sleeping-NAS gate of
tests_offline, which can also let a counted number of reads through:
frames must go on while every read is held (a watchdog records a frame
that waited), one batch is let through and joins the catalog, then
Cancel and wake: the files being read finish and nothing new starts.
compare_at_one_to_one_gets_a_sharp_window_per_pane ("both originals stay
decoded"): the media cache takes its budget from the process-wide memory
budget, which settings_dialog_by_keyboard_and_clicks sets to 1 GB for a
moment (and left there when it failed on FreeBSD). That leaves a 512 MiB
share, too small for two 24 MP originals (288 MB each), so one pane's
was evicted. compare_at now gives its session the default 768 MiB share;
LIGHTCRAFT_MEMORY_MB=1024 reproduces the failure without this.
dropping_the_lock_frees_the_library_while_another_thread_spawns_processes:
"the spawning thread never got a child off the ground" when a spawn took
longer than the 2 s of lock cycles. The cycles now go on until at least
5 children were spawned during them (at most a minute).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deflate-compressed DNG tiles are written at zlib level 1 instead of 6. Measured on
Sony A7R VI files (67 MP), 4 threads, best of 3:
- Photo Merge DNG (half floats, 3 channels): 3.88 → 0.55 s, 265.7 → 274.8 MB (+3.4 %)
- raw export with Deflate (16-bit CFA): 0.89 → 0.18 s, 96.7 → 103.0 MB (+6.5 %)
- rendered DNG: 4.60 → 0.69 s, 239.0 → 267.1 MB (+11.8 %)
Lossless either way; both written files decode back bit-exactly. Lossless-JPEG
DNGs (the default for convert/import) are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep the HDR histogram and SDR/HDR legend, virtualized sidebar rows and current preset grouping while retaining shared chevrons and inset interactive clipping buttons. Apply shared submenu arrows to the new album import menus. Drop the earlier inpainting compatibility change now present upstream.
Validation: formatting, strict native Clippy and HEIF checks passed; 811 compiled UI tests passed; native SDR and HDR screenshots checked. Full workspace CI is blocked by all_masks_overlay_shows_every_visible_mask_at_once, reproduced unchanged on clean upstream b3d392b4. Publish this prepared rebase at the contributor’s explicit request with that baseline failure documented.
Co-Authored-By: GPT-6.1 Sol <noreply@openai.com>
The connect-mode MCP client accepted any newline-less or wrong-id reply as
the answer to its request. Require a complete newline-terminated reply of
at most 64 MiB, the matching integer id, boolean ok and a result or string
error; otherwise close the connection and report an unknown outcome without
resending. Idless pre-dispatch refusals from the control server still work.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
settings_dialog_by_keyboard_and_clicks: the Performance tab's GPU status
hint reads process-wide GPU state, which other tests flip through
app.gpu. Without a GPU driver (the FreeBSD VM) the reason is a long wgpu
message that wraps to two lines, while "disabled by the GPU rendering
preference" is one, so the controls below moved by 20 px between the
frame that located "button:settingsMemory-2" and the frame that clicked
it. The status is now one truncated line with the full text on hover;
gpu_status_never_moves_the_performance_controls checks the buttons stay
put (it fails without the fix when no Vulkan driver loads).
offline_originals_never_block_a_frame: the slow NAS probe was a 400 ms
sleep judged by a 300 ms wall-clock frame budget, which a loaded QEMU VM
missed (1.06 s). The probe now waits on a gate the test opens; frames
must finish while it is closed (a watchdog records any frame that had to
wait), so no timing budget is needed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds an option under Settings > Import > Cameras and Cards that opens Import from Device when a camera or memory card shows up. It's off by default, and a card that was already in at startup doesn't ask.
The thumbnail cache wrote .jpg files into the library's thumbs folder, which is usually inside
Pictures: photo galleries listed every thumbnail as a photo (issues #610, #1066), and every read
brought the file's date forward, so each launch made them all look new.
- Cache files are named <32 hex>.lcthumb (JPEG data as before). A .jpg of before is taken over
under the new name when it is asked for; the rest when the cache is tidied, which the engine does
once per cache folder, on a thread of its own with a row in the activity stack that shows its
progress and can stop it. Nothing is rendered again.
- Reading writes nothing: what was read is remembered for the session (a full cache lets it go
last), and only a file that went unused for a week has its date brought forward.
- A folder that holds nothing but the cache gets a CACHEDIR.TAG, when the cache makes it and when
it is tidied; one that also holds somebody's files is not tagged (issue #98).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The desktop app opened at 1600 x 1000 every time: a resize, or a zoom by a
double-click on the top bar, was forgotten at quit.
The size the window rests at and whether it is zoomed (macOS) / maximized are
kept in ui.json (`window: {size, zoomed}`) and the window is built from them.
A zoomed window keeps the size it goes back to. A size counts once the window
has rested at it for half a second, so the sizes a resize or the zoom
animation passes through do not; one given just before quitting is taken at
exit. Full screen and the window's place on the desktop are not remembered.
Windows and Linux say whether a window is maximized. On macOS egui-winit only
refreshes that at launch and when the app zooms the window itself, so there
the window's size is gone by as well: zoomed when the system said so or the
window was seen to come to fill its screen, and for as long as it keeps that
size. Whatever is in doubt is a window of the size it has, which opens as
large as it was left.
A `window` in the settings that is not one reads as none and costs no other
setting.
Run on macOS with one display; the other platforms follow from reading winit
and egui-winit, so the tracker row is partial.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An offer taken asked the box to start a new edit on the next frame; a
box not drawn then (its panel switched away at once) took the cursor
whenever it was next drawn. The request holds for one frame.
- Return on the frame the box scrolled out of sight still took the
highlighted offer, which was no longer drawn: it gives nothing.
- In a window with room for the offers on neither side of the box they
covered it: they go to the side with more room and show as many lines
as fit there, the highlighted one among them.
- Ukrainian says "suggestions" as the panel does; four clearer wordings
(uk, ru, de, ja).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>