fix(library): update real-time folder tree photo counts and persist scan state across page reloads

This commit is contained in:
2026-10-08 20:28:29 +07:00
parent b163676e67
commit c26a4c21a7
4 changed files with 28 additions and 32 deletions
+24 -24
View File
@@ -825,27 +825,23 @@ function Catalogue() {
// either way.
const synced = useRef(0);
useEffect(() => {
const root = folders.find((f) => f.name === rootKey);
if (!loaded || !root || !ready.has(root.name)) return;
if (!loaded) return;
// Another window may hold the reading — the app beside the browser, which
// this window cannot see. Asking is a round trip, and the answer is what
// keeps this one from reading the same roll a second time.
const catchUp = () => {
if (scanSession()) return;
// A folder of a hundred thousand frames is a hundred thousand stats on the
// disk, and this screen is raised every time the reader comes back from the
// studio — so the walk waits for the folder to have had a moment to change,
// which is a second of a roll the reader was not going to notice and not
// the several it took to walk it again.
if (Date.now() - synced.current < CATCHUP_MS) return;
synced.current = Date.now();
void scanBusy().then(async (busy) => {
if (busy || scanSession()) return;
// A position left on the disk is a roll that stopped in the middle of
// itself and the only thing that finishes it is reading it again. This
// is the question, and it is answered by one small file opened and shut
// — not by the walk it decides on.
if (await unfinished(root.name)) void scan(root, true);
for (const folder of folders) {
if (!ready.has(folder.name)) continue;
if (await unfinished(folder.name)) {
void scan(folder, false);
break;
}
}
});
};
catchUp();
@@ -854,9 +850,7 @@ function Catalogue() {
};
document.addEventListener('visibilitychange', raised);
return () => document.removeEventListener('visibilitychange', raised);
// `folders` and `ready` are read here, not watched: the two only ever gain a
// name or a permission, and neither is a reason to read the disk again.
}, [loaded, rootKey, scan]);
}, [loaded, folders, ready, scan]);
const add = useCallback(async () => {
setNote(null);
@@ -1017,20 +1011,24 @@ function Catalogue() {
const add = (folder: string, rel: string, counted: boolean) => {
const normF = (folder || '').replace(/\\/g, '/');
const normRel = (rel || '').replace(/\\/g, '/');
const set = foldersUnder.get(normF) ?? new Set<string>();
foldersUnder.set(normF, set);
const lowerF = normF.toLowerCase();
const set = foldersUnder.get(lowerF) ?? new Set<string>();
foldersUnder.set(lowerF, set);
let path = '';
for (const part of normRel.split('/')) {
if (!part) continue;
path = path ? `${path}/${part}` : part;
set.add(path);
if (counted) hits.set(`${normF}/${path}`, (hits.get(`${normF}/${path}`) ?? 0) + 1);
if (counted) {
const key = `${lowerF}/${path.toLowerCase()}`;
hits.set(key, (hits.get(key) ?? 0) + 1);
}
}
};
for (const photo of photos) {
const normF = (photo.folder || '').replace(/\\/g, '/');
const normDir = (photo.dir || '').replace(/\\/g, '/');
hits.set(normF, (hits.get(normF) ?? 0) + 1);
hits.set(normF.toLowerCase(), (hits.get(normF.toLowerCase()) ?? 0) + 1);
if (normDir) add(normF, normDir, true);
}
for (const dir of dirs) add(dir.folder, dir.rel, false);
@@ -1039,26 +1037,28 @@ function Catalogue() {
const nodes = useMemo<Node[]>(() => {
const { hits, foldersUnder } = baseTreeInfo;
const counted = (key: string, hit: number | undefined) => {
const counted = (key: string) => {
const normKey = (key || '').replace(/\\/g, '/');
const lowerKey = normKey.toLowerCase();
const hitVal = hits.get(lowerKey) ?? 0;
const readVal = read?.[normKey] ?? read?.[lowerKey] ?? read?.[key] ?? 0;
return Math.max(hit ?? 0, readVal);
return Math.max(hitVal, readVal);
};
const out: Node[] = [];
for (const folder of folders) {
const normF = (folder.name || '').replace(/\\/g, '/');
out.push({ key: normF, label: folder.label || folder.name, depth: 0, count: counted(normF, hits.get(normF)) });
const lowerF = normF.toLowerCase();
out.push({ key: normF, label: folder.label || folder.name, depth: 0, count: counted(normF) });
// Path order puts a folder before its own children, which is the order the
// indented rows have to come in.
for (const dir of [...(foldersUnder.get(normF) ?? [])].sort((a, b) => a.localeCompare(b))) {
for (const dir of [...(foldersUnder.get(lowerF) ?? [])].sort((a, b) => a.localeCompare(b))) {
const parts = dir.split('/');
const key = `${normF}/${dir}`;
out.push({
key,
label: parts[parts.length - 1],
depth: parts.length,
count: counted(key, hits.get(key)),
count: counted(key),
});
}
}
+3 -7
View File
@@ -46,7 +46,7 @@ const LANES = 4;
// How often the walk's position is written down, in the same seconds as the
// catalogue is read back on: the position is the whole of what the walk has found
// and not read, and writing it is a string of a few megabytes on a long roll.
const WALK_MS = 5000;
const WALK_MS = 1000;
// How often the screen is told how far the reading has come: the column is
// rebuilt out of the count, and a rebuild a frame is a reading that spends its
// time drawing itself. Five times a second is a counter that moves to the eye
@@ -872,12 +872,8 @@ 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;
if (wroteAt !== 0 && at - wroteAt < WALK_MS) return;
wroteAt = at;
const held = [...inHand, ...batch.map(pathOf)];
const savedEntries = entries.slice(0, 500).map((e) => e.rel);
@@ -1093,7 +1089,7 @@ export async function scanFolder(
// 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);
if (!stop) await clearWalk(folder.name);
// The last word, whatever the clock says: the frames the reading got to are
// the frames the column is about to be told about one last time.
publish(true);