fix(library): fix photo count live update and optimize scan speed
This commit is contained in:
@@ -943,25 +943,30 @@ function Catalogue() {
|
|||||||
// lies under it, with nothing at all to say about the rest of the roll: its
|
// 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.
|
// 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.
|
// The column falls back on the catalogue until the reading is through.
|
||||||
const read = live && !live.progress.from ? live.progress.counts : null;
|
const read = live ? live.progress.counts : null;
|
||||||
// Memoize photo count map derived from photos and dirs array separately so that
|
// Memoize photo count map derived from photos and dirs array separately so that
|
||||||
// live scan progress ticks (every 200ms) do not re-scan tens of thousands of photos.
|
// live scan progress ticks (every 200ms) do not re-scan tens of thousands of photos.
|
||||||
const baseTreeInfo = useMemo(() => {
|
const baseTreeInfo = useMemo(() => {
|
||||||
const hits = new Map<string, number>();
|
const hits = new Map<string, number>();
|
||||||
const foldersUnder = new Map<string, Set<string>>();
|
const foldersUnder = new Map<string, Set<string>>();
|
||||||
const add = (folder: string, rel: string, counted: boolean) => {
|
const add = (folder: string, rel: string, counted: boolean) => {
|
||||||
const set = foldersUnder.get(folder) ?? new Set<string>();
|
const normF = (folder || '').replace(/\\/g, '/');
|
||||||
foldersUnder.set(folder, set);
|
const normRel = (rel || '').replace(/\\/g, '/');
|
||||||
|
const set = foldersUnder.get(normF) ?? new Set<string>();
|
||||||
|
foldersUnder.set(normF, set);
|
||||||
let path = '';
|
let path = '';
|
||||||
for (const part of rel.split('/')) {
|
for (const part of normRel.split('/')) {
|
||||||
|
if (!part) continue;
|
||||||
path = path ? `${path}/${part}` : part;
|
path = path ? `${path}/${part}` : part;
|
||||||
set.add(path);
|
set.add(path);
|
||||||
if (counted) hits.set(`${folder}/${path}`, (hits.get(`${folder}/${path}`) ?? 0) + 1);
|
if (counted) hits.set(`${normF}/${path}`, (hits.get(`${normF}/${path}`) ?? 0) + 1);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
for (const photo of photos) {
|
for (const photo of photos) {
|
||||||
hits.set(photo.folder, (hits.get(photo.folder) ?? 0) + 1);
|
const normF = (photo.folder || '').replace(/\\/g, '/');
|
||||||
if (photo.dir) add(photo.folder, photo.dir, true);
|
const normDir = (photo.dir || '').replace(/\\/g, '/');
|
||||||
|
hits.set(normF, (hits.get(normF) ?? 0) + 1);
|
||||||
|
if (normDir) add(normF, normDir, true);
|
||||||
}
|
}
|
||||||
for (const dir of dirs) add(dir.folder, dir.rel, false);
|
for (const dir of dirs) add(dir.folder, dir.rel, false);
|
||||||
return { hits, foldersUnder };
|
return { hits, foldersUnder };
|
||||||
@@ -969,19 +974,24 @@ function Catalogue() {
|
|||||||
|
|
||||||
const nodes = useMemo<Node[]>(() => {
|
const nodes = useMemo<Node[]>(() => {
|
||||||
const { hits, foldersUnder } = baseTreeInfo;
|
const { hits, foldersUnder } = baseTreeInfo;
|
||||||
const counted = (key: string, hit: number | undefined) => Math.max(hit ?? 0, read?.[key] ?? 0);
|
const counted = (key: string, hit: number | undefined) => {
|
||||||
|
const normKey = (key || '').replace(/\\/g, '/');
|
||||||
|
return Math.max(hit ?? 0, read?.[normKey] ?? read?.[key] ?? 0);
|
||||||
|
};
|
||||||
const out: Node[] = [];
|
const out: Node[] = [];
|
||||||
for (const folder of folders) {
|
for (const folder of folders) {
|
||||||
out.push({ key: folder.name, label: folder.label || folder.name, depth: 0, count: counted(folder.name, hits.get(folder.name)) });
|
const normF = (folder.name || '').replace(/\\/g, '/');
|
||||||
|
out.push({ key: normF, label: folder.label || folder.name, depth: 0, count: counted(normF, hits.get(normF)) });
|
||||||
// Path order puts a folder before its own children, which is the order the
|
// Path order puts a folder before its own children, which is the order the
|
||||||
// indented rows have to come in.
|
// indented rows have to come in.
|
||||||
for (const dir of [...(foldersUnder.get(folder.name) ?? [])].sort((a, b) => a.localeCompare(b))) {
|
for (const dir of [...(foldersUnder.get(normF) ?? [])].sort((a, b) => a.localeCompare(b))) {
|
||||||
const parts = dir.split('/');
|
const parts = dir.split('/');
|
||||||
|
const key = `${normF}/${dir}`;
|
||||||
out.push({
|
out.push({
|
||||||
key: `${folder.name}/${dir}`,
|
key,
|
||||||
label: parts[parts.length - 1],
|
label: parts[parts.length - 1],
|
||||||
depth: parts.length,
|
depth: parts.length,
|
||||||
count: counted(`${folder.name}/${dir}`, hits.get(`${folder.name}/${dir}`)),
|
count: counted(key, hits.get(key)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -790,13 +790,15 @@ export async function scanFolder(
|
|||||||
// Count the frame the moment the scan gets to it, before it knows whether the
|
// Count the frame the moment the scan gets to it, before it knows whether the
|
||||||
// frame is new: the rows say how far the reading has come, not what it kept.
|
// frame is new: the rows say how far the reading has come, not what it kept.
|
||||||
const count = (rel: string) => {
|
const count = (rel: string) => {
|
||||||
const cut = rel.lastIndexOf('/');
|
const relNorm = rel.replace(/\\/g, '/');
|
||||||
counts[folder.name] = (counts[folder.name] ?? 0) + 1;
|
const cut = relNorm.lastIndexOf('/');
|
||||||
|
const normRoot = normPath(folder.name);
|
||||||
|
counts[normRoot] = (counts[normRoot] ?? 0) + 1;
|
||||||
let path = '';
|
let path = '';
|
||||||
for (const part of (cut < 0 ? '' : rel.slice(0, cut)).split('/')) {
|
for (const part of (cut < 0 ? '' : relNorm.slice(0, cut)).split('/')) {
|
||||||
if (!part) continue;
|
if (!part) continue;
|
||||||
path = path ? `${path}/${part}` : part;
|
path = path ? `${path}/${part}` : part;
|
||||||
const key = `${folder.name}/${path}`;
|
const key = `${normRoot}/${path}`;
|
||||||
counts[key] = (counts[key] ?? 0) + 1;
|
counts[key] = (counts[key] ?? 0) + 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -928,25 +930,31 @@ export async function scanFolder(
|
|||||||
|
|
||||||
let preview: Uint8Array | null = null;
|
let preview: Uint8Array | null = null;
|
||||||
if (heic) {
|
if (heic) {
|
||||||
preview = await heicThumbnail(bytes);
|
preview = await oneRawAtATime(async () => {
|
||||||
if (!preview) {
|
let p = await heicThumbnail(bytes);
|
||||||
try {
|
if (!p) {
|
||||||
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
try {
|
||||||
preview = await heicToJpeg(fullBytes, 320);
|
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
} catch {
|
p = await heicToJpeg(fullBytes, 320);
|
||||||
preview = null;
|
} catch {
|
||||||
|
p = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return p;
|
||||||
|
});
|
||||||
} else if (raw || isTiff) {
|
} else if (raw || isTiff) {
|
||||||
preview = await rawThumbnail(bytes, file.name);
|
preview = await oneRawAtATime(async () => {
|
||||||
if (!preview && isTiff) {
|
let p = await rawThumbnail(bytes, file.name);
|
||||||
try {
|
if (!p && isTiff) {
|
||||||
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
try {
|
||||||
preview = tiffThumbnail(fullBytes);
|
const fullBytes = new Uint8Array(await file.arrayBuffer());
|
||||||
} catch {
|
p = tiffThumbnail(fullBytes);
|
||||||
preview = null;
|
} catch {
|
||||||
|
p = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return p;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const src = preview ? new Blob([preview as BlobPart], { type: 'image/jpeg' }) : (raw || heic || isTiff) ? null : file;
|
const src = preview ? new Blob([preview as BlobPart], { type: 'image/jpeg' }) : (raw || heic || isTiff) ? null : file;
|
||||||
@@ -998,16 +1006,9 @@ export async function scanFolder(
|
|||||||
// folder is being read while it is being read.
|
// folder is being read while it is being read.
|
||||||
const at = rel.lastIndexOf('/');
|
const at = rel.lastIndexOf('/');
|
||||||
progress.at = at < 0 ? '' : rel.slice(0, at);
|
progress.at = at < 0 ? '' : rel.slice(0, at);
|
||||||
// A frame a reload handed back that was counted already is read without
|
progress.done++;
|
||||||
// being counted twice; the ones behind it are frames that reading found and
|
count(rel);
|
||||||
// never reached, and they are counted here as they are reached.
|
const read = readOne(handle, rel);
|
||||||
if (restored) restored--;
|
|
||||||
else {
|
|
||||||
progress.done++;
|
|
||||||
count(rel);
|
|
||||||
}
|
|
||||||
// Queue image decoding one at a time so memory usage stays low and UI stays smooth
|
|
||||||
const read = oneRawAtATime(() => readOne(handle, rel));
|
|
||||||
const lane = read
|
const lane = read
|
||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
.finally(() => lanes.delete(lane));
|
.finally(() => lanes.delete(lane));
|
||||||
@@ -1015,14 +1016,15 @@ export async function scanFolder(
|
|||||||
if (lanes.size >= LANES) await Promise.race(lanes);
|
if (lanes.size >= LANES) await Promise.race(lanes);
|
||||||
publish();
|
publish();
|
||||||
|
|
||||||
// Micro-yield to the browser main thread event loop so scanning never blocks UI rendering or user interactions
|
// Micro-yield to main thread every 64 files to keep scan fast while keeping UI responsive
|
||||||
processed++;
|
processed++;
|
||||||
if (processed % 3 === 0) {
|
if (processed % 64 === 0) {
|
||||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
await Promise.all(lanes);
|
await Promise.all(lanes);
|
||||||
await flush();
|
await flush();
|
||||||
|
publish(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
// How many of the frames in hand were counted by the reading that handed them
|
// How many of the frames in hand were counted by the reading that handed them
|
||||||
|
|||||||
Reference in New Issue
Block a user