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:
@@ -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>
|
||||
|
||||
@@ -321,6 +321,11 @@ async function tile(blob: Blob, size?: { width: number; height: number } | null)
|
||||
|
||||
export interface ScanProgress {
|
||||
folder: string;
|
||||
// The subfolder this reading was kept to, spelled the way the walk spells a
|
||||
// path (`''` for the whole roll). A reading kept to one folder is a reading of
|
||||
// a fraction of one roll, and everything downstream — the column's counts, the
|
||||
// position on the disk — belongs to the reading that walks the whole of it.
|
||||
from: string;
|
||||
total: number;
|
||||
done: number;
|
||||
added: number;
|
||||
@@ -494,20 +499,29 @@ export async function scanFolder(
|
||||
folder: LibraryFolder,
|
||||
onProgress?: (p: ScanProgress) => void,
|
||||
shouldStop?: () => boolean,
|
||||
jump?: () => string | null
|
||||
jump?: () => string | null,
|
||||
from = ''
|
||||
): Promise<ScanProgress> {
|
||||
const known = new Map((await listPhotos(folder.name)).map((p) => [p.id, p]));
|
||||
const saved = await loadWalk(folder.name);
|
||||
// A reading kept to one folder keeps no position of its own. The position file
|
||||
// belongs to the reading that walks the whole roll, and a fraction of the tree
|
||||
// written into it would hand the next visit a roll with the rest of itself
|
||||
// missing from the walk. One folder is short enough to read again, and the
|
||||
// frames it does not re-read are skipped on their size and their time.
|
||||
const bounded = from !== '';
|
||||
const saved = bounded ? null : await loadWalk(folder.name);
|
||||
const tree: Walk = { root: folder.handle, pending: [], walked: new Set(saved?.walked ?? []) };
|
||||
// A queue comes back as paths, so the folders are asked for again; a reading
|
||||
// with nothing written down starts at the picked folder.
|
||||
// with nothing written down starts at the folder it was pointed at — the
|
||||
// picked one, or the one the reader right-clicked inside it.
|
||||
if (saved) {
|
||||
for (const rel of saved.pending) {
|
||||
const dir = await openAt(folder.handle, rel);
|
||||
if (dir) tree.pending.push({ dir, rel });
|
||||
}
|
||||
} else {
|
||||
tree.pending.push({ dir: folder.handle, rel: '' });
|
||||
const dir = from ? await openAt(folder.handle, from) : folder.handle;
|
||||
if (dir) tree.pending.push({ dir, rel: from });
|
||||
}
|
||||
// The frames this reading has found and not read: the walk fills it, the lanes
|
||||
// empty it, and a frame taken off the front is one a reload will not read
|
||||
@@ -519,6 +533,7 @@ export async function scanFolder(
|
||||
const names: string[] = [];
|
||||
const progress: ScanProgress = saved?.progress ?? {
|
||||
folder: folder.name,
|
||||
from,
|
||||
total: 0,
|
||||
done: 0,
|
||||
added: 0,
|
||||
@@ -527,6 +542,11 @@ export async function scanFolder(
|
||||
counts: {},
|
||||
};
|
||||
progress.folder = folder.name;
|
||||
// Which folder the reading was pointed at, for the same reason the name is:
|
||||
// the screen up when the reading ends is not the one that started it, and a
|
||||
// reading kept to one folder has to say so to a column that would otherwise
|
||||
// draw its counts over the whole roll.
|
||||
progress.from = from;
|
||||
// A position written down by an older visit has no count to carry on from.
|
||||
progress.written ??= 0;
|
||||
const found = new Set(progress.dirs.map((d) => d.id));
|
||||
@@ -570,6 +590,10 @@ export async function scanFolder(
|
||||
// again is skipped on its size and its time.
|
||||
let wroteAt = 0;
|
||||
const write = async () => {
|
||||
// A reading kept to one folder has no position to write: the file it would
|
||||
// go in is the whole roll's, and a fraction of the tree written there is a
|
||||
// roll that comes back with the rest of itself missing.
|
||||
if (bounded) return;
|
||||
const at = Date.now();
|
||||
if (at - wroteAt < WALK_MS) return;
|
||||
wroteAt = at;
|
||||
@@ -735,7 +759,12 @@ export async function scanFolder(
|
||||
// is asked for nothing until it is through: the frames already found are
|
||||
// what that reading was in the middle of.
|
||||
if (!entries.length) {
|
||||
walked = await walkPass(tree, entries, names, isSupportedPhoto, jump ?? (() => null));
|
||||
// A reading kept to one folder is asked for nothing: the frames of the
|
||||
// folder it was pointed at, and of what lies below it, and then it is
|
||||
// through. A jump would take it out of the subtree it was kept to, and
|
||||
// the reading it left behind is the reading that folder wanted.
|
||||
const asked = bounded ? () => null : (jump ?? (() => null));
|
||||
walked = await walkPass(tree, entries, names, isSupportedPhoto, asked);
|
||||
// The names come in before the frames they hold: the column grows one pass
|
||||
// ahead of the strip, which is the whole point of reading layer by layer.
|
||||
for (const rel of names) {
|
||||
@@ -763,8 +792,10 @@ export async function scanFolder(
|
||||
} while (!walked && !stop);
|
||||
await flush();
|
||||
// A reading that came to its end has no position worth keeping: the next one
|
||||
// walks the roll from the top and skips what has not moved.
|
||||
if (!stop) await clearWalk(folder.name);
|
||||
// walks the roll from the top and skips what has not moved. A reading kept to
|
||||
// one folder never wrote one down, and the file it would clear is the whole
|
||||
// roll's — a position another reading is in the middle of.
|
||||
if (!stop && !bounded) await clearWalk(folder.name);
|
||||
return progress;
|
||||
}
|
||||
|
||||
@@ -957,11 +988,11 @@ export function jumpScan(rel: string | null): void {
|
||||
// Start reading a roll. Only one at a time: the walk reads one frame at a time on
|
||||
// this thread, so a second scan would only slow the first one down. The promise
|
||||
// settles when the scan does — the caller that started it may be long gone.
|
||||
export function startScan(folder: LibraryFolder): Promise<ScanProgress> {
|
||||
export function startScan(folder: LibraryFolder, from = ''): Promise<ScanProgress> {
|
||||
if (live) return Promise.reject(new Error('a scan is already running'));
|
||||
const session: ScanSession = {
|
||||
folder: folder.name,
|
||||
progress: { folder: folder.name, total: 0, done: 0, added: 0, written: 0, dirs: [], counts: {} },
|
||||
progress: { folder: folder.name, from, total: 0, done: 0, added: 0, written: 0, dirs: [], counts: {} },
|
||||
stop: false,
|
||||
jump: null,
|
||||
};
|
||||
@@ -980,7 +1011,8 @@ export function startScan(folder: LibraryFolder): Promise<ScanProgress> {
|
||||
announce();
|
||||
},
|
||||
() => session.stop,
|
||||
() => session.jump
|
||||
() => session.jump,
|
||||
from
|
||||
).finally(() => {
|
||||
if (heart) {
|
||||
clearInterval(heart);
|
||||
|
||||
Reference in New Issue
Block a user