web: browse the catalogue as a tree of folders
The library page was a flat wall of thumbnails. It now reads like the admin pictures tab: a folder tree on the left, the picked node's frame on the stage, that node's frames in the strip below, and a chip pair to swap the stage for a wall of every thumbnail in the node. Folders are walked recursively (dot/@-prefixed names skipped, an unreadable subfolder is dropped rather than failing the scan), so a roll with a dated subfolder keeps both levels. Node counts include the subfolders underneath.
This commit is contained in:
@@ -35,10 +35,13 @@ export interface LibraryFolder {
|
||||
}
|
||||
|
||||
export interface LibraryPhoto {
|
||||
// `${folder}/${name}` — one frame appears once in the catalogue, under the
|
||||
// folder it was found in.
|
||||
// `${folder}/${dir}/${name}` — one frame appears once in the catalogue, under
|
||||
// the folder it was found in.
|
||||
id: string;
|
||||
folder: string;
|
||||
// The subfolder it sits in, relative to the picked folder — '' at the root.
|
||||
// This is what the tree column reads.
|
||||
dir: string;
|
||||
name: string;
|
||||
handle: FileSystemFileHandle;
|
||||
thumb: Blob | null;
|
||||
@@ -221,6 +224,29 @@ export interface ScanProgress {
|
||||
added: number;
|
||||
}
|
||||
|
||||
// The walk goes all the way down: a picked folder is usually a roll of
|
||||
// date-named subfolders, and a catalogue that only saw its top level would be
|
||||
// empty. Names beginning with `.` or `@` are skipped — that is `.thumbnails`,
|
||||
// `.git`, and the `@eaDir` a Synology writes beside every frame.
|
||||
async function collectFiles(
|
||||
dir: FileSystemDirectoryHandle,
|
||||
prefix: string,
|
||||
out: { handle: FileSystemFileHandle; rel: string }[]
|
||||
): Promise<void> {
|
||||
for await (const entry of dir.values()) {
|
||||
if (/^[.@]/.test(entry.name)) continue;
|
||||
if (entry.kind === 'directory') {
|
||||
try {
|
||||
await collectFiles(entry as FileSystemDirectoryHandle, `${prefix}${entry.name}/`, out);
|
||||
} catch {
|
||||
// A subfolder that will not open is skipped; the rest of the roll stands.
|
||||
}
|
||||
} else if (isSupportedPhoto(entry.name)) {
|
||||
out.push({ handle: entry as FileSystemFileHandle, rel: `${prefix}${entry.name}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Walk the folder, keep what is new or changed, and leave the rest alone: a
|
||||
// second scan of a 2000-file folder only reads the frames that moved.
|
||||
//
|
||||
@@ -234,13 +260,8 @@ export async function scanFolder(
|
||||
shouldStop?: () => boolean
|
||||
): Promise<ScanProgress> {
|
||||
const known = new Map((await listPhotos(folder.name)).map((p) => [p.id, p]));
|
||||
const entries: FileSystemFileHandle[] = [];
|
||||
for await (const entry of folder.handle.values()) {
|
||||
// `values()` is typed as the base handle, and only a file handle has the
|
||||
// getFile this walk needs.
|
||||
const file = entry as FileSystemFileHandle;
|
||||
if (entry.kind === 'file' && isSupportedPhoto(file.name)) entries.push(file);
|
||||
}
|
||||
const entries: { handle: FileSystemFileHandle; rel: string }[] = [];
|
||||
await collectFiles(folder.handle, '', entries);
|
||||
const progress: ScanProgress = { folder: folder.name, total: entries.length, done: 0, added: 0 };
|
||||
let batch: LibraryPhoto[] = [];
|
||||
// One transaction per batch, a put per row: a store with `keyPath: 'id'` takes
|
||||
@@ -258,12 +279,12 @@ export async function scanFolder(
|
||||
tx.onerror = () => reject(tx.error);
|
||||
});
|
||||
};
|
||||
for (const handle of entries) {
|
||||
for (const { handle, rel } of entries) {
|
||||
if (shouldStop?.()) break;
|
||||
progress.done++;
|
||||
try {
|
||||
const file = await handle.getFile();
|
||||
const id = photoId(folder.name, handle.name);
|
||||
const id = photoId(folder.name, rel);
|
||||
const seen = known.get(id);
|
||||
if (seen && seen.size === file.size && seen.taken && seen.taken === file.lastModified && seen.thumb) {
|
||||
onProgress?.(progress);
|
||||
@@ -271,9 +292,11 @@ export async function scanFolder(
|
||||
}
|
||||
const bytes = new Uint8Array(await file.arrayBuffer());
|
||||
const [thumb, taken] = [await makeThumbnail(file, bytes), (await readCapturedAt(bytes)) ?? file.lastModified];
|
||||
const cut = rel.lastIndexOf('/');
|
||||
batch.push({
|
||||
id,
|
||||
folder: folder.name,
|
||||
dir: cut < 0 ? '' : rel.slice(0, cut),
|
||||
name: handle.name,
|
||||
handle,
|
||||
thumb,
|
||||
|
||||
Reference in New Issue
Block a user