web: a catalogue of the folders on the visitor's own disk

A RAW studio that cannot see a folder is one photo at a time. /library
now takes a folder through Chromium's directory picker, keeps the handle
in IndexedDB so the folder is there on the next visit, and walks it into
a grid: one thumbnail per frame, the frame's own date, and the recipe it
was last graded with. Nothing is uploaded and nothing is read twice —
the RAW itself is opened only when a tile is clicked, at which point the
studio develops it and the recipe comes back on top. The studio files
every change back against the frame, debounced, so reopening a RAW is
not doing the grade again.

Thumbnails come off LibRaw's unpack_thumb for a RAW, which is a seek and
a copy where a develop is a full decode of every pixel, and off
createImageBitmap for anything else. A RAW with no preview inside it
gets a placeholder tile rather than a minute of decoding per file.

  node scripts/library-check.mjs
  ok  both frames indexed as tiles — 2 tiles from P1010256.JPG + P1010256.RW2
  ok  thumbnail for P1010256.JPG — 40400 bytes, jpeg=true
  ok  thumbnail for P1010256.RW2 — 39895 bytes, jpeg=true      (LibRaw preview)
  ok  studio developed the frame from its handle
  ok  the address was handed back — url=/app (no ?lib= left behind)
  ok  the look was filed back against the frame — baseFilter=none, 19 knobs
  ok  the tile says the frame is edited
This commit is contained in:
2026-09-28 17:46:12 +07:00
parent 0f2e109aa2
commit 81637b8502
8 changed files with 966 additions and 14 deletions
+15
View File
@@ -973,6 +973,21 @@ input[type="range"] { width: 100%; accent-color: var(--accent); }
padding: 10px;
}
.adm-card img { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: var(--radius-sm); background: var(--bg-sunken); }
/* A frame whose RAW carries no preview of its own: the tile stands in for the
picture and opens the file just the same. */
.lib-noimg {
display: flex;
align-items: center;
justify-content: center;
aspect-ratio: 4 / 3;
border-radius: var(--radius-sm);
background: var(--bg-sunken);
color: var(--text-dim);
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.14em;
cursor: pointer;
}
.adm-meta { display: flex; flex-direction: column; gap: 2px; font-size: 11.5px; color: var(--text-dim); min-width: 0; }
.adm-meta b { color: var(--text); font-family: var(--mono); }
.adm-meta span { overflow-wrap: anywhere; }