The Print dialog's templates could be saved, applied and deleted, but
not moved to another library or another computer. Each saved template
now has an Export… button that writes it to a .lcprint file, and
Import… beside Save adds templates from such files.
print.template.export {name, path} writes {format, version, name,
settings}. print.template.import {path} reads at most 256 KB, checks
the format, version, name and every setting as a saved template's
settings are checked, drops cell photos and guides, and adds the
template under its name, or under "name (2)" and so on when the name is
in use. The buttons appear where the host provides open and save
dialogs: the desktop app does, and the web build does not.
Review fix: a drop that adds some photos and fails others showed only the first outcome.
`i18n::dropped_photos_outcome` joins the translated added, restored and not-readable parts with a
plain " · " and returns nothing for a drop that only found duplicates.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-up: after counting what the import did, a drop where every file failed showed no
notice at all. It now says how many photos were not readable (existing translated key).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes:
- pl "Finish Lightroom catalog import before adding photos" uses "Lightrooma", as the other pl Lightroom strings do
- the web drop's toast counts imported photos, says "Restored n photos from Recently Deleted" (already translated) when it only restored some, and says nothing when it only found duplicates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review fixes:
- the web build's "Added n photos" toast counts the photos `library.import` reports, not the paths submitted
- `ui.dropFiles` rejects a `paths` list with a non-string entry instead of dropping part of it
- the hover card says a drop will be refused while a Lightroom catalog import is pending, and that refusal text is now translated in all 13 catalogs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A finished scan waits for an open dialog before opening its review; drops are refused while an import runs (and, on the web, while a dialog is open); orphaned doc comment restored to builtin_label.
Co-Authored-By: Claude <noreply@anthropic.com>
While files are dragged over the window a veil and a card say what dropping
does (review N items, import N presets, open the face model; close the open
dialog first when one is open). Dropped photos and folders open the import
review like File > Import; Settings > Import can import them at once. A drop
while a dialog is open is refused with a toast. The web build now reports
unreadable and failed drops. Adds the control request ui.dropFiles.
Co-Authored-By: Claude <noreply@anthropic.com>
crates/ui-egui/locales/{cs,it,pl}-formats.json: kept the translations and added the track's new keys.
docs/parity-checklist.md: newer header line; revision rows from both sides.
docs/whats-new.md: both tracks' 2026-10-15 entries.
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.
The web import picker hard-coded a 13-extension accept list, so raws the engine imports (CR3, NRW, RAF, ORF, RW2, RWL, RAW, PEF, SRW, …) were silently filtered out of the dialog on Windows/Edge, which matches accept case-sensitively. Generate the list from EXTENSIONS, each extension in both cases. Fixes#2212.
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.
default_language() — what the Auto setting resolves to — only read LIGHTCRAFT_LANGUAGE,
which does not exist in a browser, so the web build always started in English. ui-egui
gains an installable default (set_default_language + a pure locale_from_tags that picks
the first shippable language from BCP-47 tags via Locale::parse_tag), and the web app
installs it at startup from navigator.languages (falling back to navigator.language),
read fallibly through js_sys::Reflect so it also works from a worker. Desktop never
installs the override and is unchanged; a language stored in ui.json still wins, which is
exactly Auto semantics.
Tests: locale_from_tags takes the first shippable language (it-IT -> Italiano,
unsupported-first skips ahead, zh-TW -> zh-hant, ja_JP.UTF-8 -> Japanese, pt-BR region
variant), empty/unsupported -> En fallback.
OPFS lookups turned any exception into "absent", and start-up only logged
other read errors, so a library with an unreadable file opened incomplete
and the next save could overwrite the stored copy. Only NotFoundError now
means absent; when a stored library file or the active-library pointer
can't be read, the library isn't opened, nothing is saved and the window
names the file. An unreadable thumbnail index no longer deletes every
stored thumbnail as unindexed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
The browser backup copies the stored library files, so while a save to
browser storage is failing it reported success but left out the queued
changes. It now refuses with the number of unsaved changes and why
(Session::unsaved), and is allowed again once they are written.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AbegtYaPuwqZ81pGfELsFK
Hands each selected photo's original file itself (a raw stays a raw: no
render, no copy, nothing added to the library) to the editor set in
Settings ▸ External Editing ▸ Other editor, e.g. DxO PhotoLab. Also on a
photo's right-click menu (Edit In).
- `photo.openOriginalInExternal {ids?, app?}` (UI command, menus.rs): up to
50 photos; demo photos and missing files are skipped with the reason; when
none has a file it fails saying why (toast + error, no panic). Enabled when
the active photo has a file and the host can start programs (not the web).
- Several photos go to one start of the editor (`Services::open_all_with`,
editors.rs `open_all_with`): a single-window editor such as DxO PhotoLab
drops the files that arrive while it is still starting when started once
per file. The system default ("" editor) still opens them one by one.
- Settings hint, translations of the two new toasts, parity rows
LR-EXP-PSD / MENU-FILE-PHOTOSHOP.
Tested by hand on Windows with DxO PhotoLab 8 (one and several raws).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Photo > Edit In > Edit in PhotoCraft (Cmd+Shift+E, also on the photo
context menu) renders a copy with the edits as a flat 16-bit PSD, which
PhotoCraft saves back to in place, stacks it on the original and opens
it in PhotoCraft, found where its installers put it. Edit in Other
Editor (Cmd+Alt+E) keeps the configured program. Per-editor format,
colour space and bit depth in Settings > External Editing. Export gains
a flat PSD format.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4kqvR9KVCPia3SSXyB8WG
A slideshow template is the Playback (not "prepare previews"), Layout /
Options / Backdrop, Overlays and Titles settings as one versioned JSON
document (format lightcraft.slideshowTemplate, version 1).
- Engine: slide_template (SlideLook, Template, built-ins, user template
files in <settings>/slideshow-templates read through each section's own
merged() validation; size and count caps; newer versions refused; bad
files reported and skipped; file names sanitised so ids never leave the
folder). Applying replaces the four settings in one step and keeps the
replaced ones as "previous" for the session (prefs have no undo).
- Five built-ins of our own: Plain Black, Gallery Wall, Edge to Edge,
Cinema Wide, Shooting Data (no Lightroom names or designs, no images).
- Commands slideshow.templates.list/apply/save/update/rename/delete/
import/export; slideshow.preview {template} previews without applying.
- UI: Template Browser at the top of Settings > Slideshow (built-in, user,
previous settings; click applies, hover previews on the selected photo,
right-click update / rename / export / delete; save and import), file
dialogs on the desktop; strings in the nine locales.
- Docs: parity row (partial), whats-new, control protocol and MCP.
Previews looked more saturated than the exported files on Mac screens. The
pixels are the same: preview and export match to within one level on the CPU
and GPU paths, and exports embed a correct sRGB profile. But wgpu leaves the
window's Metal layer without a colour space for sRGB surfaces, and Core
Animation shows such a layer without colour matching ("If nil, no
colormatching occurs", CAMetalLayer.h). On a P3 screen the sRGB previews
were therefore shown too saturated, while Preview and Photos convert the
exports properly.
While no monitor profile is chosen in Settings > Display, the desktop app now
shows previews through the profile of the screen the window is on
(NSWindow.screen.colorSpace.ICCProfileData, safe objc2 calls), through the
existing display-profile path. It is read again about once a second, so
moving the window to another screen is followed. A chosen profile still wins;
"Use Screen Profile" goes back to it. app.displayProfile reports it as
path: null, screen: true. Other platforms and headless sessions are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The File menu (beside the presets) imports a keyword list file
(Lightroom Classic's, Capture One's, Photo Supreme's .utf8) and exports
LightCraft's, with native file dialogs off the UI thread like the
other file commands; the desktop app gives the synchronous ones too.
Import says how many keywords it added and how many gained synonyms;
export says how many it wrote and names the keywords Capture One won't
import (; , < or > in a name).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1.Added Simplified Chinese font support for web and desktop builds.
2.Kept Inter as the default font, with Chinese used only as a fallback.
3.Verified Chinese glyph tests and a macOS universal build.
Signed-off-by: supper_yijie <syj794844854@163.com>
One config-dir helper (engine::config::config_dir; camera_profiles re-exports it) with both
the face-models and denoise-models defaults; hosts set up both model folders; Settings has
Faces and AI Denoise tabs; both command sets registered; both model dialogs; locale entries
from both; one [features] table per app manifest; Cargo.lock regenerated by cargo; the
DecodedSource cache cost counts both segmentation mattes and the denoised twin.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An off-by-default "Recognise faces" toggle and the face model list in Settings,
with a one-drop install: pick a .onnx (Add a model file…) or drop one on the
window. The engine inspects it (recognised by hash, or described from its
shape), a dialog shows its licence, whether commercial use is allowed, what it
was trained on and what LightCraft had to assume, and Install stays disabled
until "I accept" is ticked. "Get…" opens a model's own page in the browser:
LightCraft never fetches a model itself. A file it cannot use says why.
The models folder is shared by the app, the CLI and the MCP server
(`engine::config`, `$LIGHTCRAFT_FACE_MODELS` overrides `<config>/models`), so a
model installed from one is there for the others. A session has none until a host
asks (`with_default_face_models`), which keeps tests hermetic.
Nothing runs a model yet, and the tab says so. Headless tests cover the whole
flow (the dialog, refusing until accepted, installing, a file it cannot use).
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The desktop app keeps logs/lightcraft.log in its settings folder (#286)
and a failed start names it (#370), but a user still had to find the
folder by hand. The host now hands the UI its log file (Services
.log_file) and Help ▸ Open Log Folder (app.openLogFolder) reveals it
through the existing reveal service: Finder and Explorer select the
file, Linux opens the folder. Off where no log is kept (the web,
--memory, LIGHTCRAFT_NO_PREFS). Translated in de, es, ja, pt-br, ru,
zh-hans and zh-hant; parity row MENU-HELP-LOGFOLDER; What's New and
README note it.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The desktop showed rfd's synchronous dialogs on the UI thread, so the
window stopped answering the compositor while one was up: GNOME marked
LightCraft "not responding", the File menu stayed drawn and the control
channel went quiet. Services.picker lets the host start a dialog
elsewhere and answer through a channel; a command that needs a path
asks for one and returns, and runs again with the answer when the
dialog closes (pick.rs). Import Photos, Import from Folder, Open
Library, Import Lightroom Catalog, Find Missing Photos, the preset and
curve-preset imports and exports, Auto-Tag from Tracklog and Locate
Missing File use it; hosts without a picker (the web, tests) keep the
synchronous dialogs. ui.inspect lists the waiting commands as
fileDialogs. Same cause as storytold/photocraft#574.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The browser accepted and persisted library.preferences cacheMb, but stored thumbnails stayed over that limit: the worker index pruned with the native default (DISK_CACHE_BYTES), so the setting only reached the desktop cache. The worker group now carries the session cache_bytes(), the app refreshes it every frame, and the next stored thumbnail prunes to it.
The main thread's Workers tracked no rendered-thumbnail cache until the
first render request carried one. If Clear Preview Cache ran before that
(e.g. starting on an empty filtered view), the per-frame observation in
finished() had nothing to compare with, and the first request only
recorded the already-cleared generation as its baseline. The persisted
ThumbIndex was never invalidated, so once the filter was removed the
request touched the old entry and the worker read the pre-clear JPEG
back from browser storage. Replacing the cache (another library) before
the first request had the same gap.
Workers::start now takes the session's cache and records it, with its
current generation, as the baseline the persisted index belongs to. A
normal start with no clear observes the same cache and generation and
keeps the index. The comparison moves into wire::CacheWatch so it is
tested natively (cargo test --workspace) next to ThumbIndex.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When open_library failed, the desktop app (and the web build) quietly ran
an in-memory session seeded with the demo photos: it looked like a reset
library and everything done in it was lost at quit.
- The session now starts empty and in memory (no demo photos) and
LightcraftApp::library_problem shows a blocking window with the path and
the error: Try Again, Choose Another Library… (app.openLibrary), Continue
Without Saving, Quit. Command-line files are imported once the user has
chosen.
- A temporary session shows a banner under the top bar (with Open
Library…) for as long as it lasts; it has no library, so nothing is
written anywhere. Opening a library ends it.
- ui.inspect → libraryProblem; widget ids for automation; documented in
control-protocol.md.
- Web build: same window (Try Again disabled: reloading the page retries).
- Headless UI test: locked library → window → Continue Without Saving →
banner, nothing written to the library → lock released → Try Again opens
it. Checked in the real app over the control channel too.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- File > Back Up Library... (web): a zip of the catalog files and every
stored original, built entry by entry; Restore Library from Backup...
reads it into a new library folder and switches only when complete
(the previous library stays in storage).
- Failed flushes to browser storage make the catalog Store refuse
writes, so changes show as unsaved (top-bar warning) and are retried
every 2 s instead of being lost silently.
- A picked/dropped photo whose bytes can't be stored is not added.
- One tab per library (Web Locks); a second tab shows a message.
- Notices: storage not persistent, library failed to open (temporary
session), experimental build; ?reset asks first; a panic shows a
message instead of a frozen page; long toasts wrap.
- Hosting: _headers/.htaccess no longer mark the unhashed .js/.wasm
immutable; HOSTING.md rewritten for the actual build.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>