Commit Graph

323 Commits

Author SHA1 Message Date
3dtours db983e54d4 fix(library): one session for the whole tab, and a shelf that does not take the machine
The library is the accounts' shelf, but the tab kept two copies of that fact: the
screen read `/me` once as it mounted and then stopped listening, so a visitor who
signed in at the studio came back to the panel asking for the login they had just
given. One record, read by the studio, the landing page and the shelf alike, is
now the only copy — a sign-in anywhere is a sign-in everywhere.

- auth.ts (new): the tab's one session record. `useAuth()` reads it through
  `useSyncExternalStore`, a `/me` already in flight is joined rather than
  repeated, and a call that fails leaves the record *unknown* rather than signed
  out, so a cold API never reads as a guest.
- TopBar: LIBRARY is a link for a member and a dead button for everyone else. An
  anchor wearing `aria-disabled` still navigates on a click, and the screen
  behind it would only send the guest home again — so a guest gets the shape of
  the button and nothing to press.
- Library: the gate is that shared record, not a copy taken at mount. A guest who
  opens `/library` straight is turned home with `replace`, so a Back press does
  not walk them into the same wall. The screen is mounted the whole time the tab
  is open, so the turn is for the address the visitor actually opened: a guest on
  the studio stays on the studio.
- engine/library.ts: the thumbs hand the thread back. Two lanes instead of four,
  a frame's worth of idle between them, and one job per frame — a second caller
  joins the decode already running instead of starting another.

Verified in Chrome (stubbed API, harness run against both the dev server and the
built bundle): a guest gets a dead LIBRARY button and stays on /app; signing in at
the studio turns it into the link and opens the shelf, with no second login and no
"log in to browse" note; the same after a sign-in on the landing page; /library
opened cold as a guest lands on / with Back going to /app; opened cold signed in,
the shelf is up. On a folder of 22 frames the open runs 2 decodes at a time (was
5) and repeats no frame (44 decodes, was 56 — 12 frames were read twice); the
tiles still come out 22 on the open, 24 by hand, 22 again after a reload.
2026-10-09 20:50:17 +07:00
3dtours e1707bd9b9 feat(library): give a folder its missing thumbnails, and the wall its stars
Four asks, one screen: a folder can be given the tiles it is short of, by hand
or on the way in; the rescan row says RESCAN...; and the wall of thumbnails
scores a frame without the trip to the stage.

- Library.tsx + i18n: "RESCAN FROM THE TOP" is "RESCAN..." in both languages.
  The reading is unchanged — a row's roll is still read from the top, its
  subfolders included.
- engine/library.ts + Library.tsx: the tile of one frame is one function now
  (`makeThumb`: the store, the backup behind it, then the file's own preview
  for a RAW or a HEIC and a decode for anything else). The wall's lazy path
  calls it instead of keeping its own copy of the same branches.
- engine/library.ts: `missingThumbs` asks which of a folder's rows no tile was
  ever drawn for, in one transaction, and `makeThumbs` makes those — a few
  frames at a time, and giving up when the caller has moved on.
- Library.tsx: the folder menu carries MAKE THUMBS, which reads the row's
  folder and what lies under it; and opening a folder asks for its own frames'
  tiles by itself, silently, in the background — the branch too when the
  screen is drawing the branch. A frame that lands after the ask is asked for
  again, which is why the frame count is a dependency of the effect.
- Library.tsx: a wall card carries the same five stars the stage does, under
  the picture, and rates the frame on the click. The click stops at the star,
  so scoring a frame is not picking it.

Verified in Chrome at 1440x900 against OPFS standing in for the disk, three
frames with one of them in a subfolder: opening the folder leaves two tiles
(the node's own frames) and MAKE THUMBS takes the roll to three, the frame in
the subfolder included; clearing the store and reloading brings the two back
on the open; the menu reads RESCAN... and MAKE THUMBS; and the wall's fourth
star lights four, writes star 4 to the catalogue row, and takes it back on the
second click. The same checks pass on the built bundle at :8090.
2026-10-09 20:04:24 +07:00
3dtours d231d298a5 fix(studio): keep the tone curve's channels, and drop the strip that repeats them
Two halves of one report: the R/G/B channel picks belong on the graph, and
the chips down the sidebar were reading as a second copy of the sliders.

- ToneCurvePanel.tsx + app.css: the channel row (RGB/R/G/B and RESET) and
  the .curve-tabs rules come back, so the card picks a channel again and
  bends that one. Undoes the row's removal; the graph itself is unchanged.
- app.css: the strip a LIGHT panel opens is hidden on a desktop, with the
  row of panel chips it is opened from. Both name the seven rows the panel
  already stands in, so a desktop was drawing every knob twice — the strip
  beside the sliders, and the sliders. The phone keeps it: there the panels
  are the strip, not the column.

Verified in Chrome at 1440x900 and 820x700 with a photo loaded: on the
desktop the strip is gone and the card carries RGB/R/G/B/RESET, the R tab
bends the red curve while the composite stays drawn beneath it; on the
phone the strip still opens TONE -> EV..BLACK, TONE CURVE, and the card is
the same.
2026-10-09 18:44:00 +07:00
3dtours 05c2380a8b fix(studio): leave the tone curve with the graph alone
The floating tone-curve card opened with its own row of channel tabs
(RGB / R / G / B) and a RESET button. The panel is meant to be a graph,
so that row is gone: the card is now the head and the curve, nothing
else. The curve edits the composite rgb channel, which is what the row
selected by default anyway.

- ToneCurvePanel.tsx: drop CURVE_CHANNELS, the channel state and the
  curve-tabs block; every read and write goes through 'rgb'.
- app.css: drop .curve-tabs / .curve-tab / .curve-dot / .curve-reset
  and the .curve-line.ghost path that only drew the other channels.

skipped: selecting a channel other than the composite one, and RESET,
add when a channel chooser comes back on purpose.
2026-10-09 18:20:07 +07:00
3dtours 2d2e662e1a fix(studio): open a new photo with the GPS stamp off
A photo carrying EXIF GPS arrived with GPS WATERMARK already ON: adoptPhoto
set useGeotag to the presence of the position, so the file's own EXIF decided
to print a location. The stamp is the visitor's to ask for.

A new photo now opens with it OFF whatever the file carries; the position is
still read off the photo and is there the moment the chip is switched on by
hand (and the device-locate try for an EXIF-less frame is unchanged).

Verified in a real Chrome against the built bundle: a GPS photo opens OFF
with no hint, switching the chip ON reads back 'GPS read from the photo.'; a
no-GPS photo opens OFF and offers the manual entry.
2026-10-09 18:12:12 +07:00
3dtours fa2e9e133e fix(library): stop the wall's window loop that blanked the screen
Selecting thumbs and flipping SINGLE <-> ALL THUMBS crashed the view with
React #185 ("Maximum update depth exceeded") and left the app white.

useWallWindow measures after every render through a layout effect with no
dependency array, and it ended by setting state with a value it had just
found. A set made from a layout effect renders again whether or not the value
moved, so an unchanged measurement re-rendered, which remeasured, which set
again: the loop. It is now guarded on the last window actually written (a
ref), and nothing is set that the wall already stands at.

Reproduced with the throwaway storm probe against a 3000-file roll (the tree
vanished at round 2, ~50 identical renders). After the fix: 12 rounds of hard
SINGLE/ALL THUMBS toggling on the production bundle, no error, no white
screen.
2026-10-09 17:10:44 +07:00
3dtours 1647362c68 fix(library): stop calling a .tif a RAW, and stop the wall's render loop
Three things, all from one look at a folder of TIFF scans.

A `.tif` is no longer a RAW. Nothing here develops one, and `tif`/`tiff` sat in
`RAW_EXT` for no decoder's sake: opening a TIFF asked for PRO and its tile wore
a RAW badge, for a file the engine has nothing to hand back. Both extensions
are dropped, and a TIFF keeps its own route instead — the tile reads the file's
own pixels (`useThumbUrls`, via `tiffDevelop`) and so does the studio — so a
`.tif` still opens and still draws, it just is not a RAW any more.

The wall's render loop. `painted` was a fresh `.slice()` on every render, so
`useThumbUrls` saw a new list each time round and its effect ran again, and
again: about twenty thousand renders a second while the page sat idle, and the
file reads frozen at the first screenful, so a tile scrolled to never came up
(7 of 24 cards drawn). `painted` is memoised on `listed, wall.from, wall.to`
now — no date formats in eight idle seconds, 28 of 28 cards after a scroll.

Scrolling between the two modes. SINGLE → ALL THUMBS lands the wall on the
frame the strip was showing, centred in the window; ALL THUMBS → SINGLE swipes
the strip to the frame nearest the wall's middle, centred in the row. Each way
the frame the other mode was standing on is the one you come back to.
2026-10-09 16:12:18 +07:00
3dtours 259e5f3378 fix(library): draw a .tif tile on the wall's own route
Two things kept a scanned `.tif` off the wall, and both had to be fixed for
its tile to come out as a picture.

CanvasKit was never up on the catalogue's route: the studio boots it when the
workspace mounts, but a visitor who only ever opens the library never does, so
the JPEG inside the file had no decoder and the tile was drawn as nothing. The
`.tif` branch now brings the engine up itself.

And the pixels were being read out of that JPEG through an image `readPixels`
handed a buffer of ours, which CanvasKit answers by throwing rather than
filling — the frame came back black to a check on the return value. It now
takes the buffer CanvasKit hands back, the form the canvas calls in this repo
have always used. Measured in the browser on a 5472x3648 JPEG-compressed scan:
the tile went from 2378 bytes of solid black to 68670 bytes at mean 189.2 sd
74.6, against the 189.3/74.6 the same picture's uncompressed copy yields.

`scripts/tiff-decode-check.mjs` gains the fixture that was broken — a
JPEG-compressed strip — judged by how far a channel strays from the libvips
reference rather than by equality, since that strip is lossy. Its bundle now
re-exports the shim alongside the reader so the two are one module instance and
the fixture's JPEG has a decoder behind it.
2026-10-09 15:21:38 +07:00
3dtours c3d46b0d15 fix(library): read a .tif's own pixels for its tile
A `.tif` in an album drew nothing: the tile took the file's embedded JPEG —
which a TIFF has not got, so the search found the frame's own pixel data and
kept three hundred kilobytes of it as the frame's tile — and nothing else in
the app draws a TIFF either, `createImageBitmap` answering InvalidStateError
and CanvasKit's `MakeImageFromEncoded` answering null to one.
`src/engine/tiffDecode.ts` reads the file itself, strips or tiles, and hands
back a shrunk frame, which is what the tile is made of now. The tiles that
were kept are dropped once (v4) so a frame already looked at is drawn right.
2026-10-09 15:04:06 +07:00
3dtours 29759721cb fix(library): read a frame's date with one set of options
exifr keeps a page-long cache of the options object it is handed, keyed by
that object itself, so a fresh literal per frame pinned a frame's worth of
tag sets (about thirty Sets) for as long as the tab lived: 3601 frames left
~16MB of heap behind, 9601 left ~50MB, and a roll of a hundred thousand
frames is what a tab cannot hold.

One options object per call site now, which is what that cache is for.
Measured on the deployed bundle: 3601 frames rest at 8MB, not 23MB.

scripts/ram-check.mjs reads a roll of many folders, queues folders behind
the reading in front of them and reports the heap left after a forced
collection, which is how those numbers were taken.
2026-10-09 14:52:28 +07:00
3dtours 9fc7b89e74 test(library): read the shelf roll at a walking pace so the landing step can see it
A roll read at 150ms a frame finishes before the flush clock can be told apart from a batch-only flush. At 900ms the step that watches the frames land has 14 counts to show for the reading instead of 3, and a build that only hands the rows over when a batch is full fails it.
2026-10-09 12:28:21 +07:00
3dtours 45fc852d04 fix(library): land a reading's rows as they are read, and keep the copy the reader reads offline
- scan: rows go down on a clock as well as on a full batch. The reader's folder
  of 26 RAWs is under one batch of 50, so nothing of it reached the screen until
  the reading was over — a wall that stood empty for the whole scan and then
  filled in one go. Frames now reach the screen every 400ms while the roll is
  still being read, so the count climbs and the first tiles are drawn as the
  frames land.
- tiles: a HEIC or a RAW has its tile written the first time it is drawn, so
  every later visit reads the tile back instead of the file.
- resume: a reading a reload cut off is offered by the next visit with a chip
  instead of being run behind the reader's back.
- offline listing: the desk's listing is kept whole in IndexedDB under
  `recipescam-offline`/`reads`, past the 200 000 characters localStorage held,
  so an offline visit opens on the listing it kept instead of on nothing.
- offline writes: a vote or a view cast with no server is held and goes out when
  one answers again, with a bar that says how many are waiting.
- sw: the shell precaches its own bundle, stylesheet and icon, read out of the
  document — a worker that precached only the document answered an offline
  launch with a page whose JavaScript the machine did not have.
- checks in docker/frontend/scripts: tree-scan-check (counts while reading,
  resume after a reload, tiles of a long roll), offline-read-check,
  offline-queue-check.
2026-10-09 12:02:05 +07:00
3dtours f834d10762 fix(library): sync React photos state after scan and support case-insensitive existing photo lookup 2026-10-08 20:50:18 +07:00
3dtours c26a4c21a7 fix(library): update real-time folder tree photo counts and persist scan state across page reloads 2026-10-08 20:28:29 +07:00
3dtours b163676e67 fix: realtime folder photo counts during scan discovery and decode 2026-10-08 20:08:19 +07:00
3dtours 779ae14a21 fix: realtime subfolder discovery, count updates, and deep level scan indicator 2026-10-08 19:09:27 +07:00
3dtours bfda4b4b9e feat: show scanning icon for parent and subfolder nodes along scan path 2026-10-08 19:02:06 +07:00
3dtours 582599049c fix(library): guarantee trailing slash in rollWalk relative paths so active scan spinner highlights all ancestor folders 2026-10-08 18:14:48 +07:00
3dtours 0533ee7080 fix(library): propagate active scanning spinner icon along full folder path hierarchy to root 2026-10-08 18:00:49 +07:00
3dtours 3d12635a62 fix(library): strictly enforce single folder photo filtering unless subfolder option is enabled 2026-10-08 17:37:06 +07:00
3dtours 2a3dd6fdbe fix(library): the folder picked is the folder that is shown
A node drew every frame of the branch under it unless the reader shut the
subfolders out, so picking A drew A and everything below it. The pick is
now the scope — the node's own frames — and the branch is what the
SUB FOLDERS chip asks for.

The year filter was already read off the frames the open node drew, so
it follows the same pick. On a tree of 13 frames (r1 r2 | A/a1..a3 |
A/deep/d1 | C/c1..c6 | E/e1, one year per folder) the chip off offers
2024 on the root, 2023 on A, 2022 on A/deep and 2021 on C; the chip on
offers all five years on the root and 2023+2022 on A.

The remembered answer moves to a new key: the chip wrote its first answer
down on the very first visit, so the old key holds 'true' for every
reader who never touched it, and reading it would keep them on a default
they never chose.
2026-10-08 17:29:34 +07:00
3dtours 848800a77d fix(boot): bring the session and the catalogue back without waiting on /me
A reload drew nothing until the account round trip landed, which on a
faraway API is the whole of the wait: 8.8s to the photo on the studio's
stage and 8.2s to the library's first row, against 0.9s and 0.3s now
(measured with every /api answer held back 8s).

The studio's restore effect returned early on `!authReady`. The gate is
only needed by the two files that ask a tier question — a RAW, which is
a PRO develop, and a catalogue frame, which walks `loadFile` — so it
keeps those and lets the session's own JPEG up on the first pass.
`restoredRef` stops the second pass, when /me lands, from decoding the
frame the first one already showed.

The library mounted nothing until /me answered. What the first paint
reads is now the answer the last visit got, the way the studio reads its
own session back: the frames are already on this disk and have no reason
to sit behind a round trip. An answer the API contradicts puts the note
back on screen the moment it arrives, and an API that never answers is
not written down as this account's standing.
2026-10-08 17:09:53 +07:00
3dtours c8fe5c919d fix(studio): give the temperature and tint tracks their colour back
Every range rule now names `input[type="range"]` itself, so the class-only WB
rules tied with the plain hairline that follows them and lost — both sliders
were drawing the same grey line as every other knob, and their node sat a pixel
off the thicker track. Measured on the panel: the gradients are back in amber
and blue, and the ring is centred on the line.
2026-10-08 14:38:25 +07:00
3dtours 8f286506b2 feat(studio): hand an export straight to a social network
EXPORT only ever wrote a file, and posting it meant leaving the studio, finding
the file and uploading it again. The dialog now offers FACEBOOK, INSTAGRAM,
REDDIT and X beside the sizes, and the render goes to the network instead of the
download folder.

What the browser can do decides how, since a page may not touch another origin's
composer: a phone's share sheet opens the target app with the picture already
attached, a desktop gets the picture on the clipboard and the site in a new tab
for one paste, and an origin with no clipboard API downloads the file beside the
open site. The line under the row says so before the click.
2026-10-08 14:38:22 +07:00
3dtours 5f78875549 feat(studio): draw every slider as a line with a hollow node, and move the shapes into TOOLS
The default ring was a solid disc on a coloured bar, which read as a crude
block next to the rest of the rail; every range input is now a hairline
track with an outline node in the accent colour, and the temperature and
tint strips keep their gradients on the track itself.

CROP, ROTATE and WATERMARK reshape the frame rather than grade it, so they
belong with the tools that work on the whole image.
2026-10-08 11:28:14 +07:00
3dtours af23f3a7c9 feat(library): read the walk without tiling, and scope the filters to the open folder
The scan made a JPEG tile for every frame as it walked, so a wall of 12MP
files spent its whole budget in the walk; the tile now waits until the card
is actually drawn (makeTile + putPhotoThumb), and a rawish frame whose date
is unreadable retries at 4MB instead of stopping at the head.

The year menu was built from the whole catalogue, so it offered values the
open folder could not produce; it now follows the rows on screen and clears
the choice when a move leaves it stale.
2026-10-08 11:28:14 +07:00
3dtours efe07e20ba feat(library): let the scan mark follow the reading down the column
The mark was pinned to the folder the reading was asked of, so a walk through
a whole roll said C2 while the frames under it were what was being read. It
now hangs on the folder the reading stands in — `progress.at` — and on every
row above it up to and including the roll's own row: reading D:/A/B1/C2 marks
C2, B1, A and D:, and as the walk moves the chain moves with it. A reading
that has not reached a frame yet has nowhere to stand, and stands on the
folder it was asked of, so the ring is on the column from the click.
2026-10-08 10:35:02 +07:00
3dtours 8d3b7bf134 fix(library): match the scan mark to the row by the walk's own key
The ring said which folder a reading was asked of by taking the row's own
name apart and comparing the pieces, which is the same path spelled twice and
only nearly the same. When the two spellings drifted the asked row never
matched, and the fallback that used to carry the mark on the roll's row had
gone, so a reading ran with nothing on the column at all.

The mark is now built with the key the walk gives the row — `folder/sub`,
through `normPath`, the same way a frame id is spelled — and a reading whose
own row is not on the column (a branch folded away, a folder gone from the
disk) falls back to the roll's own row, so a reading is never invisible.
2026-10-08 10:17:45 +07:00
3dtours 04ed177e5c fix(library): mark only the folder a reading was asked of and the rows above it 2026-10-08 09:58:35 +07:00
3dtours a87ee4d528 fix(library): ring the folder a reading was asked of, from the click 2026-10-08 09:41:16 +07:00
3dtours cda08c3908 fix(library): keep the shell the deploy replaced out of a failed navigation, and say when another tab holds the library open 2026-10-08 09:23:23 +07:00
3dtours 3242b3aa48 fix(library): mark the roll's own row while it is read, and count rows from the catalogue up 2026-10-08 08:05:18 +07:00
3dtours c80c8729a8 fix(library): mark only the folder a reading stands in, and keep a one-folder reading out of the roll's count
The column's ring was drawn from the open node's roll: a row of another roll
had no path under it, so the empty prefix matched every row of the roll being
read and the whole column lit up. The roll's own row was marked too, at every
moment of a reading of it, and a reading kept to one folder counted its frames
onto the roll's row as well — a roll counted down to one branch of itself.
A queued request was drawn from the reading's roll instead of its own, so a
request on a second roll showed nothing at all.
2026-10-07 23:10:21 +07:00
3dtours 8bc5e08926 fix(library): fix scanning spinner icon on active folder nodes and live counts update 2026-10-07 21:53:22 +07:00
3dtours c42e336a12 fix(library): unconditionally prevent browser context menu on folder tree nodes and fix absolute path matching 2026-10-07 21:40:14 +07:00
3dtours a372cc94f3 fix(library): fix folder context menu lookup and ready check 2026-10-07 21:29:19 +07:00
3dtours aa981e4e65 fix(library): fix photo count live update and optimize scan speed 2026-10-07 21:16:10 +07:00
3dtours 801877d763 fix(library): fix open location folder tree scroll and folder update path matching 2026-10-07 20:12:12 +07:00
3dtours 9bfb39be91 fix(library): resolve root folder rename, open in explorer, open at location centering, UPDATE rescan bug, and optimize scan RAM memory usage 2026-10-07 19:16:03 +07:00
3dtours b835415c20 fix(library): support thumbnail generation and display for HEIC and TIF files 2026-10-07 18:43:11 +07:00
3dtours 57005ae1e2 fix(library): contain zoom in stage viewport and add drag/pan support for single view 2026-10-07 18:34:45 +07:00
3dtours 5dbcdd36e7 fix(ui): format LIGHT tab main chips to bold and accent, unbold TONE CURVE and DYNAMIC RANGE sub-chips 2026-10-07 18:22:07 +07:00
3dtours 347b66099a style(studio): place Color Chrome sub-buttons on 1 single row and format chip labels bold with theme accent colors 2026-10-07 17:59:50 +07:00
3dtours da7a23bb4b fix(studio): ensure Color Chrome sub-chips are hidden when switching tabs 2026-10-07 17:41:00 +07:00
3dtours 4704e936ba feat(studio): add right-click context menu (Solo mode, Collapse all, Expand all) for DevelopPanels, pin Color Chrome sub-chips, and fix library single view sorting & time range select 2026-10-07 17:35:20 +07:00
3dtours eb47afc63e feat(studio & library): sync Light & Color tab parameters (vibrance, highlight, shadow, white, black) to Gradient Mask & add multi-field sorting options 2026-10-07 12:36:55 +07:00
3dtours 1c7be6dfd4 perf(library): persistent SPA shell, in-memory folder tree caching, and zero-flicker LRU thumbnail caching 2026-10-07 12:05:58 +07:00
3dtours 3b4c49c41d feat(library & studio): add EXIF info bar, portrait frame panning, and multi-photo selection with Panorama/HDR context menu 2026-10-07 10:44:59 +07:00
3dtours 44601897b0 chore: bump service worker cache version to recipescam-v11 2026-10-07 07:31:47 +07:00
3dtours 5338f66134 fix(raw): prioritize camera embedded preview for 100% exact color matching across SONY, OLYMPUS, FUJIFILM, NIKON, CANON, RICOH, LUMIX, LEICA, IPHONE, DNG 2026-10-07 07:29:42 +07:00