fix(studio): the roll stands while the next frame opens

A click on the next frame of the roll emptied the stage before the bytes
were read — `clearPreview` for the picture, and `loadFile` dropping the
catalogue's frame id for the strip — and both the roll and the frame's own
info line lived inside the branch that drew the picture. The whole strip
went with it and was drawn again on the way back: every tile torn down and
read a second time, three loads and three fresh images, for a click that
changed one frame.

The info line and the roll stand outside that branch, so the roll survives
the click, and a caller opening a frame of the catalogue's own roll says so
and keeps the id `loadFile` otherwise clears. The info line now sits above
the roll instead of under it.

probe-strip-click.mjs, desktop and phone: 0 tiles torn down, 0 loads,
the frame clicked is the one lit, the info line above the roll.
This commit is contained in:
2026-10-15 20:41:36 +07:00
parent d7a1a0a489
commit d17ae406e0
2 changed files with 26 additions and 16 deletions
+9 -5
View File
@@ -1374,15 +1374,19 @@ export function Workspace() {
);
const loadFile = useCallback(
async (file: File) => {
async (file: File, keepFrame = false) => {
setError(null);
setBusy(t('auth.busy'));
// A file from the disk is nobody's saved frame: SAVE PHOTO must add one,
// not overwrite whichever of the folder's own was open before. Opening one
// of those sets the id back, after this call. Same for a catalogue frame —
// the caller that hands one over names it again on the way back.
// of those sets the id back, after this call.
setOpenPhotoId(null);
setLibPhotoId(null);
// A frame of the catalogue's own roll is the one exception: the strip under
// the photo reads that id, so dropping it for the length of the load is
// what tore the whole roll down and drew it again — every tile of it read
// a second time — for a click that changed one frame. The caller opening a
// frame of the roll says so, and the roll stands still until the next one.
if (!keepFrame) setLibPhotoId(null);
// A RAW is PRO: the file is parked whole and developed by the engine,
// which only that tier carries. A basic account is told, not silently
// served a JPEG it never asked for.
@@ -2537,7 +2541,7 @@ export function Workspace() {
try {
const row = await getPhoto(id);
if (!row) throw new Error('not in the catalogue');
await loadFile(await readPhotoFile(row));
await loadFile(await readPhotoFile(row), true);
const saved = await loadEdit(id);
if (saved) restoreState(saved.state);
// The catalogue's own standing for this frame, which the reader set on
+17 -11
View File
@@ -2581,19 +2581,25 @@ export function ImageStage({
</div>
)}
{/* The frame's own name and specs, and the roll beside it, stand outside
the branch below. Opening the next frame of the roll empties the stage
while its bytes are read, and a strip inside that branch went with it:
every tile of the roll torn down and drawn again, on the way out and on
the way back, for a click that changed one frame. The roll is under the
row that names the frame it is walking. */}
{previewUrl && (shotSpecs || place || dims || photoName) ? (
<div className="photo-meta-bar" data-key="photo-meta-bar">
{specsLine(shotSpecs ?? null, place, dims)}
{photoName ? (
<span className="photo-meta-name" data-key="photo-name">
{photoName}
</span>
) : null}
</div>
) : null}
{filmstrip}
{previewUrl ? (
<>
{filmstrip}
{shotSpecs || place || dims || photoName ? (
<div className="photo-meta-bar" data-key="photo-meta-bar">
{specsLine(shotSpecs ?? null, place, dims)}
{photoName ? (
<span className="photo-meta-name" data-key="photo-name">
{photoName}
</span>
) : null}
</div>
) : null}
<div className="toolbar">
{onClear ? (
<button type="button" className="btn" data-key="clear-photo" onClick={onClear}>