fix(library): update real-time folder tree photo counts and persist scan state across page reloads
This commit is contained in:
@@ -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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -1 +1 @@
|
||||
d01beb3a629be66f3e70b82a325b26c92dd620e79a41de3ab73a578781f5bf45 recipescam-images.tar.gz
|
||||
3c54c3f27787b7c1a660483aaed2a310eef61b466e8251767b4ef2d80b3d6505 recipescam-images.tar.gz
|
||||
|
||||
Reference in New Issue
Block a user