From 426488a7881efe7847e181c369fe20b1782b077c Mon Sep 17 00:00:00 2001 From: 3dtours Date: Tue, 29 Sep 2026 10:55:07 +0700 Subject: [PATCH] studio: say the roll is still being read while the catalogue is off screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reading of a folder belongs to the tab, not to the catalogue screen: it was made to survive the hand-over to the studio in 753eea0, and it does. What was missing was any sign of it up there — a visitor who hands a half-read roll to the studio saw a page that said nothing about the frames still landing, and had no way to tell a reading in flight from one that had quietly died. The one thing that did say so, the toolbar line, lived on the screen they had just left. The header now carries it, immediately left of the way back into the catalogue, where the two belong together: the ring the catalogue already uses for a roll in hand, and the count the toolbar states, "4/12", whose title is the same "Scanning 4/12 — 0 new…" line in the visitor's language. It reads the session through scanSession() and watches it with watchScan() exactly as the catalogue does, so a pass that moves the progress moves the header too — the session object outlives them both, only its progress is replaced each pass. No new state is introduced, no new copy: the markup borrows .lib-spin and the lib.scanning key the catalogue already had. Verified: tsc --noEmit and vite build clean; scripts/scan-nav-check.mjs grew one step at the first hand-over, "the studio header shows the reading the tab is doing", which waits for [data-key="studio-scan-count"] mid-scan and matches it against \d+/\d+ — it passes with 4/12, the same reading the toolbar shows at that moment; the other 18 steps of that check, the 52 steps of library-check.mjs and roll-walk-check.mjs all still pass. ponytail: the header states progress, it does not offer to stop the scan; the catalogue's own STOP stays the one place that ends a reading. Add a control here when someone asks to stop a roll from the studio. Co-authored-by: PenguinHarness --- docker/frontend/scripts/scan-nav-check.mjs | 7 ++++++ docker/frontend/src/styles/app.css | 3 +++ docker/frontend/src/ui/TopBar.tsx | 27 +++++++++++++++++++++- 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/docker/frontend/scripts/scan-nav-check.mjs b/docker/frontend/scripts/scan-nav-check.mjs index 6b1f736..1ce9c45 100644 --- a/docker/frontend/scripts/scan-nav-check.mjs +++ b/docker/frontend/scripts/scan-nav-check.mjs @@ -209,6 +209,13 @@ await page.click('[data-key="lib-studio"]'); await page.waitForSelector('[data-key="nav-library"]', { timeout: 15_000 }); step('the handover stays in the page, it does not reload it', await noReload(), `${await docs()} document load(s)`); step('and it lands in the studio', new URL(page.url()).pathname === '/app', page.url()); +// The reading belongs to the tab, so the header of the screen that did not start +// it says it is still going, beside the way back into the catalogue. +const badge = await page + .waitForSelector('[data-key="studio-scan-count"]', { timeout: 15_000 }) + .then((el) => el.textContent()) + .catch(() => null); +step('the studio header shows the reading the tab is doing', /^\d+\/\d+$/.test(badge ?? ''), String(badge)); // Back to the catalogue mid-scan: the screen that started the scan is gone and // back, and the roll is still being read — the reading belongs to the tab. diff --git a/docker/frontend/src/styles/app.css b/docker/frontend/src/styles/app.css index e5180ae..9fe92de 100644 --- a/docker/frontend/src/styles/app.css +++ b/docker/frontend/src/styles/app.css @@ -1061,6 +1061,9 @@ input[type="range"] { width: 100%; accent-color: var(--accent); } animation: lib-spin 0.7s linear infinite; } @keyframes lib-spin { to { transform: rotate(360deg); } } +/* The same ring, out in the header: the reading belongs to this tab, not to a + screen, so the studio says so too while it runs. */ +.lib-scan { display: inline-flex; align-items: center; gap: 6px; white-space: nowrap; } @media (prefers-reduced-motion: reduce) { .lib-spin { animation-duration: 2.4s; } } diff --git a/docker/frontend/src/ui/TopBar.tsx b/docker/frontend/src/ui/TopBar.tsx index 78b4b16..d63a675 100644 --- a/docker/frontend/src/ui/TopBar.tsx +++ b/docker/frontend/src/ui/TopBar.tsx @@ -1,6 +1,7 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useReducer, useRef, useState } from 'react'; import { useI18n, LANGS } from '../i18n/I18nProvider'; import { ACCENTS, FONT_SETS, useTheme, type FontSetId } from '../theme/ThemeProvider'; +import { scanSession, watchScan } from '../engine/library'; import { Avatar } from './Avatar'; import type { User } from '../api'; @@ -53,6 +54,16 @@ export function TopBar({ const [menu, setMenu] = useState<'theme' | 'lang' | 'account' | null>(null); const wrap = useRef(null); + // The catalogue's own reading runs in this tab whatever screen is up, so the + // header says so while it does. + const live = scanSession(); + const [, bump] = useReducer((n: number) => n + 1, 0); + useEffect(() => { + const unwatch = watchScan(() => bump()); + bump(); // a scan already running when the studio comes up + return unwatch; + }, []); + useEffect(() => { if (!menu) return; const away = (e: MouseEvent) => { @@ -100,6 +111,20 @@ export function TopBar({ {/* The catalogue is a page of its own, open to guests as much as to members — it reads the visitor's own folders and never the server. */} + {live ? ( + + + ) : null} {t('nav.library')}