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')}