mirror of
https://github.com/storytold/lightcraft.git
synced 2026-11-09 09:59:52 -06:00
- brand.toml is the only place the product name lives; every crate and app
reads it through dac-brand (env vars with legacy fallback, settings folder
with copy-only migration, default library, preset extensions, i18n {app})
- cargo xtask brand check|test, run|install|package, docs, rename-crates,
deny and immich; packaging files are templates rendered from brand.toml
- icons regenerated from brand/icon.svg; upstream branding removed
- MIT licence (segment and fetch stay Apache-2.0); cargo deny licence policy
- clean-room rules in AGENTS.md/CLAUDE.md; docs/upstream-merge.md
- plan/ rebuilt and committed; parity tracker reopens Classic rows, adds
PLAN.md section 2 rows and the IMM Immich section; plan/immich.md and a
pinned Immich test server
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
252 lines
14 KiB
Markdown
252 lines
14 KiB
Markdown
# Phase 1: Classic shell, scalable catalog and Immich link
|
||
|
||
Part of [PLAN.md](PLAN.md). **Estimate:** 50–85 agent-hours. **Depends on:** Phase 0.
|
||
**Unblocks:** Phases 2, 3 and 4. Those can run in parallel after this phase.
|
||
|
||
**Goal:** the app *is* Lightroom Classic in structure:
|
||
- modules, Classic panels and keys;
|
||
- a catalog that holds 500k+ photos;
|
||
- true 1:1 zoom;
|
||
- network groundwork, so the catalog can be linked to an Immich server.
|
||
|
||
The product name is never written in code; use `brand::` constants (Phase 0.2).
|
||
|
||
---
|
||
|
||
## 1.1 Split the engine crate first (≈ 6–10 h)
|
||
|
||
`crates/engine` is 45k lines and mixes import, export, catalog import, a SQLite reader, faces, denoise, segmentation,
|
||
presets and devices. Split it before new features land in it:
|
||
|
||
| New crate | Takes |
|
||
|---|---|
|
||
| `engine-core` | command registry (`CommandSpec`), session, jobs, journal, undo |
|
||
| `engine-library` | import, devices, folders, collections, keywords, metadata, XMP, `.lrcat`/Luminar import |
|
||
| `engine-develop` | develop commands, presets, profiles, versions, sync, merge |
|
||
| `engine-export` | export, watermarks, naming templates, DNG writing |
|
||
| `engine` | thin facade that re-exports the above, so the UI, CLI, MCP and upstream merges keep compiling |
|
||
|
||
- Update `xtask/src/layers.rs` with the new crates.
|
||
- **Done when:** behaviour is identical (all tests pass), and no file moves change command ids.
|
||
|
||
---
|
||
|
||
## 1.2 Module system and panel framework (≈ 12–20 h)
|
||
|
||
Today `ui-egui` has a `ViewMode` and a `RightPanel` enum (the cloud Lightroom layout).
|
||
|
||
- **A `Module` trait:**
|
||
```rust
|
||
trait Module {
|
||
fn id(&self) -> ModuleId; // Library, Develop, Map, Book, Slideshow, Print, Web
|
||
fn left_panels(&self) -> &[PanelId];
|
||
fn right_panels(&self) -> &[PanelId];
|
||
fn toolbar(&mut self, ui: &mut Ui, cx: &mut Cx);
|
||
fn center(&mut self, ui: &mut Ui, cx: &mut Cx);
|
||
fn keymap(&self) -> &Keymap; // module-local keys layered over global keys
|
||
}
|
||
```
|
||
- **Top bar:**
|
||
- identity plate on the left (text or image from `brand.logo_svg` by default, user-customisable);
|
||
- module picker on the right, with hide/show modules from its context menu;
|
||
- an activity/progress area.
|
||
- **Panel framework:**
|
||
- left, right, top and bottom (filmstrip) panel groups;
|
||
- collapsible panel headers;
|
||
- **solo mode** (opening one panel closes the others);
|
||
- auto hide and show per edge, with a manual override;
|
||
- Tab (side panels), ⇧Tab (all), F5–F8 (each edge), T (toolbar);
|
||
- panel order and visibility saved per module.
|
||
- **Screen modes:** normal, full screen with menu bar, full screen; **lights out** (dim/off, L).
|
||
- **Secondary window:**
|
||
- Grid, Loupe (Normal / Live / Locked), Compare, Survey, Slideshow;
|
||
- its own filmstrip and filter;
|
||
- the KEYC-SECONDWINDOW keys.
|
||
- **Re-home existing views:**
|
||
- Library: grid, loupe, compare, survey, people.
|
||
- Develop: detail view, all editing panels, reference view.
|
||
- Map, Book, Slideshow, Print and Web get placeholder modules ("coming in Phase 3"), so the picker is complete now.
|
||
- Every module, panel and screen mode is a command (`module.switch`, `panel.toggle`, …), reachable from MCP, the
|
||
control channel and the menus.
|
||
|
||
**Done when:** switching modules keeps the selection and filmstrip; the panel state persists across restarts;
|
||
headless UI snapshots exist for each module.
|
||
|
||
---
|
||
|
||
## 1.3 Classic keymap layer (≈ 3–5 h)
|
||
|
||
- Add a keymap set called **"Classic"**, the default. LightCraft's current keys become a selectable alternative set.
|
||
- Module keys:
|
||
- G grid, E loupe, C compare, N survey, D develop;
|
||
- Ctrl+Alt+1…7 switch module;
|
||
- ⌘⌥↑ returns to the previous module.
|
||
- Rating and flag keys:
|
||
- `[` and `]` rating down and up;
|
||
- P/X/U flags and the backtick flag toggle;
|
||
- ⇧+key to set and advance (or Caps Lock auto-advance).
|
||
- Library and Develop keys:
|
||
- B target collection;
|
||
- Ctrl+Alt+0–9 keyword sets;
|
||
- ⇧⌘C and ⇧⌘V copy/paste metadata;
|
||
- R crop, Q remove, ⇧T masking, K brush, M linear, ⇧M radial.
|
||
- Help: F1 opens help; ⌘/ shows the shortcut overlay for the current module.
|
||
- Shortcut editor in Preferences (it reads the command registry, detects conflicts, and imports/exports a keymap
|
||
file).
|
||
- Tracker rows: all KEYC-* rows to ✅.
|
||
|
||
---
|
||
|
||
## 1.4 Library module: Classic panels (≈ 10–16 h)
|
||
|
||
| Panel / feature | Work |
|
||
|---|---|
|
||
| Navigator | Fit/Fill/1:1/custom zoom presets, click-to-pan, synced with the loupe |
|
||
| Catalog panel | All Photographs, **Quick Collection** (B, ⌘B show, ⌥⌘B clear, ⌘⇧B save as collection), Previous Import, Missing Photographs, Added by Previous Export |
|
||
| Folders | Volume headers with free space; add/move/rename/remove folders on disk (journaled, undoable); synchronise folder (new, missing, metadata changes); update location; show parent; show in file manager |
|
||
| Collections | Collection sets (nesting), **target collection**, smart collection rule editor parity (nested any/all groups, every metadata field), collection export/import |
|
||
| Quick Develop | Relative ± buttons for WB, exposure, contrast, highlights, shadows, whites, blacks, clarity, vibrance; presets; crop ratio; treatment |
|
||
| Keywording | Keyword tags as text, suggestions (co-occurrence), **keyword sets** (built-in and user), keyword shortcuts |
|
||
| Keyword List | Hierarchy, synonyms, include/exclude on export, person keywords, import/export of keyword text files, counts and filters |
|
||
| Metadata | Panel presets (Default, EXIF, IPTC, IPTC Extension, Location, Large Caption, Minimal); **metadata presets**; edit many; copy/paste metadata; edit capture time (shift, set, use file date) |
|
||
| Painter tool | Spray keywords, labels, flags, ratings, metadata preset, develop settings, rotation, target collection |
|
||
| Grid cells | Compact/expanded cell styles, badges (keywords, collections, edited, virtual copy, metadata conflict, **Immich linked**), index numbers |
|
||
|
||
---
|
||
|
||
## 1.5 Catalog v4: scale, backup, multiple catalogs (≈ 12–20 h)
|
||
|
||
Today the catalog is an op log plus a full JSON snapshot, held entirely in RAM (BTreeMaps of `Arc<Photo>`). It works up
|
||
to about 85k photos.
|
||
|
||
1. **Benchmark first.** `cargo xtask bench-catalog --photos 250000,1000000` measures open time, peak RSS, filter
|
||
latency, snapshot time and import throughput on the current design. Record the numbers in `docs/catalog.md`.
|
||
2. **New storage behind the same `catalog` API:**
|
||
- a pure-Rust embedded store (candidate `redb`, ACID with MVCC; decide from the benchmark);
|
||
- tables: photos, folders, collections, keywords, develop settings/history, previews index, **remote identities**
|
||
(1.7);
|
||
- secondary indexes for the filter bar's columns;
|
||
- photo records loaded lazily; the grid pages through ids;
|
||
- keep the op log as the undo journal and change feed (useful for sync in Phase 4).
|
||
3. **Migration:** v3 (LightCraft JSON) → v4 runs automatically, keeps a backup, and is tested on the 85k fixture.
|
||
4. **Catalog file:** one catalog = one folder with a `*.{brand.catalog_ext}` entry point.
|
||
- File → New/Open/Open Recent Catalog;
|
||
- a catalog chooser at startup (Alt held, or a preference).
|
||
5. **Backup:** schedule (on exit: daily/weekly/monthly/never), backup folder, integrity test, optimise (compaction plus
|
||
index rebuild).
|
||
6. **Export as Catalog / Import from Another Catalog:** subset with or without originals and previews; conflict rules
|
||
(new, changed: replace settings / metadata / keep), with a preview of changes.
|
||
7. **Metadata vs XMP:** a "changed on disk" / "changed in catalog" badge; Read Metadata from File / Save to File;
|
||
conflict dialog.
|
||
|
||
**Done when:** 500k photos open in under 2 s, filter in under 100 ms, use under 2 GB RSS; 1M photos still usable;
|
||
the crash and fuzz tests are carried over.
|
||
|
||
---
|
||
|
||
## 1.6 True 1:1 zoom and previews (≈ 6–10 h)
|
||
|
||
- Generalise `ui-egui/src/region.rs` (it renders the on-screen window at zoom scale, but only for the loupe and
|
||
Before/After) to a **tiled region renderer**:
|
||
- used by loupe, compare, survey, reference view, soft proof and overlays;
|
||
- zoom levels Fit, Fill, 1:4, 1:3, 1:2, 1:1, 2:1, 3:1, 4:1, 8:1, 11:1;
|
||
- tiles cached per settings hash;
|
||
- the GPU path where available.
|
||
- Preview store: standard previews at a size set per catalog, 1:1 previews built on demand or at import, auto-discard
|
||
of 1:1 previews (day/week/month/never), and Library → Previews → Build/Discard commands.
|
||
|
||
**Done when:** 1:1 pixels equal the full-resolution export pixels (test on corpus raws); panning at 1:1 on 45 MP
|
||
stays above 30 fps.
|
||
|
||
---
|
||
|
||
## 1.7 Network groundwork (≈ 4–6 h)
|
||
|
||
Immich in 1.8 and Phase 4, publish services, maps and model downloads all need this.
|
||
|
||
- New L0 **`net` crate**, written fresh under **MIT**. `crates/fetch` is Apache-2.0 only (PLAN.md §4.1), so it is not
|
||
extended. It uses the same approach as `fetch`: rustls with the RustCrypto provider, pure Rust. It provides:
|
||
- methods GET/POST/PUT/PATCH/DELETE;
|
||
- JSON bodies (serde);
|
||
- **streaming multipart upload** with progress;
|
||
- streaming download (keeping the existing resume and SHA-256 logic);
|
||
- connect and stall timeouts, cancellation, redirects, gzip;
|
||
- optional HTTP proxy;
|
||
- `User-Agent` built from `brand::`;
|
||
- custom CA / self-signed certificates per connection (common on home Immich servers), with trust-on-first-use plus
|
||
fingerprint confirmation.
|
||
- `fetch` stays as it is for model downloads, or later becomes a thin Apache-2.0 wrapper over `net`.
|
||
- **`credentials` module:** secrets stored in the OS keychain where reachable in pure Rust:
|
||
- Linux Secret Service over D-Bus via `zbus`;
|
||
- macOS and Windows behind an isolated platform crate.
|
||
- Fallback: a file encrypted with a key derived from a user passphrase.
|
||
- Never write secrets into the catalog, settings, logs, MCP output or crash reports; add a test that greps the logs.
|
||
- **Connections settings pane:** Preferences → Connections lists accounts for Immich now, and publish services, map
|
||
tiles and geocoding later.
|
||
- **Catalog `remote_identity` table:** (photo_id, service, account_id, remote_id, remote_checksum, remote_updated_at,
|
||
last_synced_at, sync_state). Indexed both ways. This is where Immich links live.
|
||
- **Checksums at import:**
|
||
- compute **SHA-1** of each original, which is what Immich stores for its assets;
|
||
- compute it with the existing content hash in the same read pass;
|
||
- a background job back-fills SHA-1 for existing catalogs.
|
||
|
||
---
|
||
|
||
## 1.8 Immich: connect, link, import (≈ 8–12 h)
|
||
|
||
New crate **`immich`** (L3, depends on `net`, `catalog`): a typed client for the endpoints listed in `plan/immich.md`,
|
||
written by us from the public API docs. A version check refuses servers older than the minimum version, with a clear
|
||
message.
|
||
|
||
1. **IMM-CONNECT:**
|
||
- Preferences → Connections → Add Immich server: URL, API key (with a link to the server's API-key page), Test
|
||
button;
|
||
- shows server version, user and the key's permissions;
|
||
- more than one server/account allowed;
|
||
- commands `immich.connect`, `immich.disconnect`, `immich.status`.
|
||
2. **IMM-LINK:**
|
||
- a background job lists the server's assets (paged, incremental by `updatedAt`);
|
||
- matches them to catalog photos by **SHA-1 checksum**; the fallback is original filename + capture time + file
|
||
size, marked "probable" so the user can confirm;
|
||
- writes `remote_identity` rows;
|
||
- results:
|
||
- grid badge "in Immich";
|
||
- Library filter "Immich: linked / not linked / probable";
|
||
- Metadata panel field with an "Open in Immich" link to the asset's web URL;
|
||
- a smart-collection criterion.
|
||
3. **IMM-IMPORT:**
|
||
- the Import dialog gets an **Immich** source: browse the timeline, albums, people and favourites as thumbnails
|
||
loaded from the server;
|
||
- choose **Copy** (download originals into a dated destination folder, then import normally) or **Link only**
|
||
(catalog entry with remote preview; the original is downloaded on first Develop);
|
||
- Immich rating, favourite, description, tags, albums and GPS are taken into catalog metadata on import (mapping as
|
||
in Phase 4 IMM-SYNC; one-way at this stage);
|
||
- duplicates are skipped by checksum.
|
||
4. **IMM-EXTLIB** (shared originals, the recommended setup for self-hosters):
|
||
- when Immich indexes the same folders as an *external library*, the link uses the checksum, never an upload;
|
||
- a setup helper shows which catalog folders are covered by which Immich external library (path mapping table for
|
||
container paths such as `/mnt/photos` ↔ `/home/me/Photos`);
|
||
- writes XMP sidecars so Immich picks up ratings, descriptions and keywords on its next scan. Phase 4 adds direct
|
||
API sync.
|
||
5. **Tests:**
|
||
- unit tests against recorded HTTP fixtures;
|
||
- integration tests against `cargo xtask immich up`, run as a nightly CI job rather than on every PR, because it
|
||
needs Docker;
|
||
- error paths: offline server, bad key, TLS mismatch and HTTP 5xx. None may crash; each must give a clear error and
|
||
a retry option.
|
||
|
||
---
|
||
|
||
## Exit gate
|
||
|
||
- [ ] Engine split; layering check green; command ids unchanged.
|
||
- [ ] All seven modules in the picker (Library and Develop complete, others placeholders); panel framework, screen
|
||
modes and secondary window done; Classic keymap default; shortcut editor.
|
||
- [ ] Library Classic panels ✅ in tracker (Navigator, Catalog, Folders, Collections, Quick Develop, Keywording,
|
||
Keyword List, Metadata, Painter).
|
||
- [ ] Catalog v4: 500k under 2 s open, under 100 ms filter; backup and optimise; multiple catalogs; export/import
|
||
catalog.
|
||
- [ ] 1:1 zoom pixel-equal to export.
|
||
- [ ] `net` and `credentials` in place; IMM-CONNECT, IMM-LINK, IMM-IMPORT and IMM-EXTLIB ✅ against the pinned Immich
|
||
test server.
|