web: a scan right-clicked inside the tree reads that folder and what lies under it, and leaves the rest of the roll alone

A roll is dated folders inside dated folders, and the reader who wants April
re-read is not a reader who asked for the four years around it. Right-clicking a
subfolder row carried the roll's own menu, and RESCAN on it read the whole tree
from the top — every folder of every layer opened again, every frame in them
asked for its size and its time, to reach the one folder the pointer was on. On
a library of a hundred thousand that is a walk of minutes for a folder of
twenty.

The scan now takes the folder it was pointed at, spelled as the walk spells a
path: '' for the picked folder, `2026/04/` for a subfolder. The walk is handed
that path as its queue and reads down from there, so the tree is entered three
levels in rather than at its root, and the menu row passes the path of the row
under the pointer. A right click on the head of the tree still reads the whole
roll, which is what a right click on the picked folder has always meant.

What a reading kept to one folder must not do is speak for the roll. What it
names in the column is a branch of the tree, so the column keeps what it has and
the rows outside the folder being re-read stand where they are. What it counts
is a branch too, and its numbers drawn over the rows would count a roll down to
one folder of itself, so the column falls back on the catalogue until the
reading is through — the frame the reader is waiting for is the frame that was
re-read, and it is in the column the moment it lands.

Nor does it write a position down. `walk/ROLL.json` belongs to the reading that
walks the whole roll: a fraction of the tree written into it hands the next
visit a roll with the rest of itself missing from the walk, and it clears a file
another reading may be in the middle of. One folder is short enough to read
again, and the frames it does not re-read are skipped on their size and their
time anyway. A jump is refused for the same reason — the reading answers to the
folder it was kept to, and a click elsewhere is a different reading's business.
This commit is contained in:
2026-10-01 17:29:47 +07:00
parent 39f80088c7
commit c93f9fdd19
2 changed files with 78 additions and 25 deletions
+36 -15
View File
@@ -195,7 +195,7 @@ export function Library() {
// right click landed on the empty part of the column, which is the way in for
// one more folder; `root` means it landed on a row at the top level — the head
// of a tree — which also carries folding the whole tree.
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; x: number; y: number } | null>(null);
const [menu, setMenu] = useState<{ folder: LibraryFolder | null; root?: boolean; rel?: string; x: number; y: number } | null>(null);
// Where the drag of the column divider started, and how wide the column was
// then — the pointer only ever reports a position, not a distance moved.
const grip = useRef<{ x: number; w: number } | null>(null);
@@ -394,7 +394,10 @@ export function Library() {
const shape = progress.dirs.length ? `${progress.dirs.length}:${progress.dirs[progress.dirs.length - 1].id}` : '';
if (shape !== drew) {
drew = shape;
setDirs(progress.dirs);
// The names of a reading kept to one folder are the folders under that
// one and no others: the column keeps what it has, and the rows it
// already drew stay where they are.
if (!progress.from) setDirs(progress.dirs);
}
// A window beside the reading is handed the progress of another build, which
// may have no such count: the frames it has stored are then read back on a
@@ -497,12 +500,16 @@ export function Library() {
}, [menu]);
const scan = useCallback(
async (folder: LibraryFolder, quiet = false) => {
async (folder: LibraryFolder, quiet = false, from = '') => {
setNote(null);
try {
const result = await startScan(folder);
if (!quiet) setNote(t('lib.scanned', { folder: folder.name, added: result.added, total: result.total }));
setDirs(result.dirs);
const result = await startScan(folder, from);
if (!quiet) setNote(t('lib.scanned', { folder: from ? `${folder.name}/${from.slice(0, -1)}` : folder.name, added: result.added, total: result.total }));
// A reading kept to one folder names the folders under that one, which
// is no reason to fold the column down to them: the roll is still the
// roll, and the rows outside the folder being re-read are rows the
// reader is looking at.
if (!from) setDirs(result.dirs);
// No read back here. The reading handed its frames over as it stored
// them, and the screen watching it reads the catalogue back once the
// reading is done — the same read, half a beat later, and not a second
@@ -652,7 +659,11 @@ export function Library() {
// running the count is the scan's own, so a row says how far the reading has
// come; the frames only reach the catalogue in one batch at the end, and a row
// that stood still until then would say nothing at all.
const read = live?.progress.counts ?? null;
// A reading kept to one folder counts the frames of that folder and of what
// lies under it, with nothing at all to say about the rest of the roll: its
// numbers drawn over the column would count a roll down to a branch of itself.
// The column falls back on the catalogue until the reading is through.
const read = live && !live.progress.from ? live.progress.counts : null;
const nodes = useMemo<Node[]>(() => {
const hits = new Map<string, number>();
const counted = (key: string, hit: number | undefined) => read?.[key] ?? hit ?? 0;
@@ -1074,20 +1085,30 @@ export function Library() {
return next;
});
}}
// A folder's own menu: rename, scan again, forget. All three
// act on the folder that was picked, so a right click on a
// subfolder row brings up its roll's menu. A row at the top
// level is a whole tree's head, so it also carries folding
// what sits under it, where a reader actually right-clicks.
// Shift+F10 raises the same event, which is the keyboard's
// way in.
// A folder's own menu: rename, scan again, forget. Rename
// and forget act on the folder that was picked, so a right
// click on a subfolder row brings up its roll's menu. The
// scan acts on the row under the pointer instead: the
// reader who right-clicks April wants April read, and
// reading the four years around it to reach the one folder
// they are looking at is a roll read for nothing. A row at
// the top level is a whole tree's head, so it also carries
// folding what sits under it, where a reader actually
// right-clicks. Shift+F10 raises the same event, which is
// the keyboard's way in.
onContextMenu={(e) => {
if (!folder) return;
e.preventDefault();
const box = e.currentTarget.getBoundingClientRect();
// The row's own path under the picked folder, '' for
// the picked folder itself — what a scan started from
// here would be kept to. Spelled the way the walk
// spells it, trailing slash and all.
const under = n.key.slice(folder.name.length + 1);
setMenu({
folder,
root: n.depth === 0,
rel: under ? `${under}/` : '',
x: Math.min(e.clientX || box.left, window.innerWidth - 190),
y: Math.min(e.clientY || box.top, window.innerHeight - 96),
});
@@ -1406,7 +1427,7 @@ export function Library() {
disabled={!!live}
onClick={() => {
setMenu(null);
void scan(menu.folder!);
void scan(menu.folder!, false, menu.rel ?? '');
}}
>
<span className="lib-menu-icon" aria-hidden="true">↻</span>