crates/engine/src/camera_preview.rs: LOOK_VERSION keeps both history lines and the higher value.
crates/engine/src/media.rs: RENDER_CACHE_VERSION keeps the higher value.
The availability worker that answers "is this file there?" for the grid and the
Info panel can now also check a long list of files (every library photo's file,
for the sidebar's Missing Photos count), without that list getting in the way:
- Availability::sweep takes a list, checked one file at a time and only when
nothing is asked; files checked less than 15 s ago are skipped, and one sweep
runs at a time. The files found gone are kept (is_gone, gone_len), and
gone_generation, which replaces generation, changes only when a file is found
gone or back: a file found there at its first check changes nothing the
Missing Photos view shows, and wakes nobody.
- Availability::restart_sweeps / sweep_in drop what is left of a sweep when
another library is opened, and refuse a list that was being made for the old
one.
- Availability::sweep_yields_to takes a callback; while it says file work is
going on (an import, a scan, a folder listing), the worker starts no sweep
check. What is asked is still answered at once, and the sweep goes on where it
stopped, so a share is not asked for a stat of every file while a scan reads
it: 32 readers got 101 files/s alone and 65 files/s with one stat loop beside
them.
Nothing in the app starts a sweep yet; the worker tests exercise it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
shift_iso writes back only a time it fully reads (a date alone, or HH:MM
with optional seconds and fraction, an optional zone, nothing after);
iso_seconds stays lenient for sorting and rules. photo.setCaptureTime
leaves such photos as they were and counts them as `unreadable`, and a
set that would move other photos by an unreadable time is refused; alone,
the photo gets the typed time. The dialog's preview says the photo's time
can't be read, OK says what was left, any error keeps the dialog open
(button and ui.dialog.confirm), and Set mode doesn't start from an
unreadable time.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Use chrono for UTC/calendar conversion instead of hand-rolled copies
Replaces the hand-rolled Howard Hinnant civil/days-from-civil copies and the
leap-year/days-in-month table with chrono as a workspace dependency:
- apps/lightcraft/src/logging.rs: timestamp (log line time)
- crates/catalog/src/dates.rs: civil + weekday
- crates/catalog/src/stacks.rs: iso_seconds (date -> seconds)
- crates/engine/src/cmd/face_recognize.rs: capture_secs (native only)
- crates/meta/src/datetime.rs: DateTime::unix_seconds and the valid() day check
Output formats and semantics are unchanged (pre-epoch clamp, day rollover that
normalize_iso relies on, leap-second tolerance, EXIF/XMP parsing). chrono uses
only std, so wasm stays safe. AGENTS.md/CLAUDE.md now say time goes through chrono.
Follow-up to storytold/cadcraft#1785
* Never panic on a clock outside chrono's range; keep 5-digit years unprefixed
chrono's From<SystemTime> unwraps, so a clock past year 262143 panicked
inside the logger. timestamp now uses the fallible DateTime::from_timestamp
and reads such a clock as the epoch, keeping the old pre-epoch clamp.
timestamp and catalog's civil write years past 9999 with {:04} like the old
{y:04} did, because %Y would add a '+'. Tests cover the out-of-range and
5-digit-year cases.
* Don't build an unrepresentable SystemTime in the far-future test
UNIX_EPOCH + Duration::from_secs(u64::MAX / 2) overflows Windows'
SystemTime and panicked before timestamp() ran, failing the Windows
test job. Build it with checked_add and only assert where it exists.
* time: preserve old out-of-range date conversion (no epoch fallback)
`DateTime::from_timestamp(...).unwrap_or_default()` silently reset any instant
past chrono's year range to 1970-01-01, changing output the old hand-rolled
Howard-Hinnant code converted exactly (e.g. 10_000_000_000_000 s was
318857-05-20 17:46:40, not the epoch). Restore the old conversion domain with a
cycle-based helper that routes the calendar through chrono (the calendar
repeats every 400 years = 146_097 days, so month/day/time come from chrono and
only the year is carried in i128, keeping every i64 exact).
- apps/lightcraft/src/logging.rs: `timestamp` via `utc_parts`; keep the
pre-1970 epoch clamp; u64 secs past i64::MAX saturate, not epoch.
- crates/catalog/src/dates.rs: `civil` cycle-based (the callers stay inside
0000-9999, but a far clock no longer resets to the epoch).
Pinned old outputs in tests:
logging u64::MAX/2 s -> 292277026596-12-04T15:30:07.000Z (was epoch),
10_000_000_000_000 s -> 318857-05-20T17:46:40.000Z
civil 10_000_000_000_000 -> 318857-05-20T17:46:40,
-10_000_000_000_000 -> -314918-08-13T06:13:20, i64 extremes
* Don't build a year-318857 SystemTime unchecked in the logging test
Windows' SystemTime only reaches about year 30828, so
UNIX_EPOCH + Duration::from_secs(10_000_000_000_000) panicked before
timestamp() ran and failed the Windows test job. Build it with
checked_add and assert where the platform can hold it. The year-10000
case fits everywhere and stays unconditional.
* Name the 400-year range extension as the one exception to the time rule
The rule said never to hand-roll calendar math, but the range fix
(shifting by whole 400-year Gregorian cycles so dates past chrono's year
range keep their old output) is cycle arithmetic, so the rule and the code
disagreed. The rule now names that exception: the library still computes
month, day and time, and only whole cycles are carried in the year.
* Say the library computes month and day; time-of-day is plain arithmetic
Narrows the range-extension wording: the date library computes the
calendar fields, and splitting seconds into hours, minutes and seconds
is ordinary arithmetic, which is what the code does.
Folder templates (and rename / export names) can now give month folders by
name: {date:%Y}/{date:%B} → 2026/January, {date:%b} → Jan. The names are
English in every interface language so a library's folders don't change
with the language; a month that doesn't parse gives nothing (a folder
level becomes "unknown").
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The cancelled result is never used, but scan_files still checked each listed file's cached probe against the file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The row read "1 folders, 1 files"; it now reads "folders: 1, files: 1" (all 13 catalogs).
`listed` is stored with Release and loaded with Acquire, so a reader that sees the listing over also sees what was stored before it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The scan lists the whole folder tree before it checks a photo. On a large
folder on a network share that takes minutes, and the scan's row showed
"0 of 64085" with an empty bar all that while: it looked frozen. ✕ did
not stop the listing either.
- engine: SyncProgress counts the folders and files the walk listed and
says when the listing is over; a folder note (walk::NoteFolder) now
answers whether to go on, and a cancelled scan answers no.
- ui: while the folder is listed, the row has an indeterminate bar and
reads "Listing folders: N folders, M files · folder" (all 13
catalogs); the dialog says the same; then the photos checked are
counted as before.
Co-Authored-By: Claude <noreply@anthropic.com>
Japanese photo printing and New Year cards cannot pick their paper: the Print
page setup only offers the ISO and US sizes. Add the customary papers at their
exact metric sizes, after the existing entries (whose sizes and order are
unchanged): L判 89×127 mm, 2L判 127×178, KG判 102×152, はがき 100×148,
往復はがき 148×200, 六切 203×254, 四切 254×305 and 名刺 55×91 (portrait).
Names resolve case-, space- and punctuation-insensitively, and take the
Japanese customary names as aliases (L判, Lサイズ, L-size, 2L判, KG判,
はがき/ハガキ/葉書/ポストカード, 往復はがき, 六切/6切, 四切/4切, 名刺). The
Print dialog and `print.set`/`print.get` use the same names, the labels show
the customary name with its metric size, and the PDF page size and MediaBox
follow the paper.
Tests: the layout test pins every new size to 0.1 pt, the aliases, and the
unchanged ISO/US sizes and unknown-name rejection; the Print view test checks
the labels; and an engine test exports a PDF on はがき (283.46×419.53 pt) and
L判 (252.28×359.99 pt) at 254 ppi (ten pixels per millimetre, so the MediaBox
is exact) and reads the actual page size, plus the customary name through
print.set.
Docs: ROADMAP progress log + revision history and header, docs/gaps.md
(Gap 15 revision), docs/parity-checklist.md (LRC-PRINT-LAYOUT row), dated
2026-10-22; existing revision rows keep their dates.
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.
Held for the owner: #995 and #999 overlap, and #995's author offered on 10-12 to
close#995 once his commit was in #999. docs/parity-checklist.md: header keeps the
#1485 note and drops the #995 part.
crates/engine/src/lightroom_catalog.rs: test helpers chained; native_catalog_rows takes #842's last_page argument, native_catalog_with_last_table_page builds on it
docs/parity-checklist.md: LRC-LIB-CATALOG-IMPORT keeps both texts
mask.applyEffect, saveEffect and updateEffect read an `id` that isn't a
mask id as the selected mask (applyEffect {"id": "1"} changed mask 2);
mask.update, mask.component and mask.refineDetail cast `component` with
as-casts and fell back to component 0 or the last selection. They share
the strict opt_mask_id / opt_index helpers now.
photo.label read its label with str_param, which is None both when the
label is missing and when it isn't a string, and None meant "none": so
photo.label {} or {"label": 3} cleared the colour label of every selected
photo. Only "none" or null clear now; anything else that isn't a known
label is an error. Every caller in the app already passes a label.