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:
@@ -90,7 +90,7 @@ const context = await browser.newContext();
|
||||
// The folder the picker would have returned, plus the revival of its handles on
|
||||
// the way back out of IndexedDB.
|
||||
await context.addInitScript(
|
||||
({ origin, names, folder }) => {
|
||||
({ origin, names, subs, folder }) => {
|
||||
const bytes = new Map();
|
||||
const load = async (name) => {
|
||||
if (!bytes.has(name)) bytes.set(name, fetch(`${origin}/${name}`).then((r) => r.arrayBuffer()));
|
||||
@@ -103,20 +103,29 @@ await context.addInitScript(
|
||||
});
|
||||
return handle;
|
||||
};
|
||||
const dirHandle = () => {
|
||||
const handle = { kind: 'directory', name: folder };
|
||||
// The picked folder holds `names` at its top level and the subfolders in
|
||||
// `subs` — a roll really is a folder of dated directories, which is what the
|
||||
// tree column walks.
|
||||
const dirHandle = (label, files, dirs) => {
|
||||
const handle = { kind: 'directory', name: label };
|
||||
Object.defineProperties(handle, {
|
||||
values: { value: () => names.map(fileHandle)[Symbol.iterator]() },
|
||||
values: {
|
||||
value: () =>
|
||||
[...files.map(fileHandle), ...Object.entries(dirs).map(([name, kids]) => dirHandle(name, kids, {}))][
|
||||
Symbol.iterator
|
||||
](),
|
||||
},
|
||||
queryPermission: { value: async () => 'granted' },
|
||||
requestPermission: { value: async () => 'granted' },
|
||||
});
|
||||
return handle;
|
||||
};
|
||||
window.showDirectoryPicker = async () => dirHandle();
|
||||
const root = () => dirHandle(folder, names, subs);
|
||||
window.showDirectoryPicker = async () => root();
|
||||
|
||||
const revive = (row) => {
|
||||
if (row?.handle?.__fake && !Object.getOwnPropertyDescriptor(row.handle, 'getFile')) {
|
||||
row.handle = row.handle.kind === 'directory' ? dirHandle() : fileHandle(row.handle.name);
|
||||
row.handle = row.handle.kind === 'directory' ? root() : fileHandle(row.handle.name);
|
||||
}
|
||||
return row;
|
||||
};
|
||||
@@ -136,7 +145,7 @@ await context.addInitScript(
|
||||
};
|
||||
}
|
||||
},
|
||||
{ origin: `http://127.0.0.1:${SAMPLE_PORT}`, names: [JPG, RAW], folder: 'CheckRoll' }
|
||||
{ origin: `http://127.0.0.1:${SAMPLE_PORT}`, names: [JPG], subs: { 2026: [RAW] }, folder: 'CheckRoll' }
|
||||
);
|
||||
|
||||
const page = await context.newPage();
|
||||
@@ -169,10 +178,10 @@ const catalogue = () =>
|
||||
// --- the run ----------------------------------------------------------------
|
||||
await page.goto(`${base}/library`);
|
||||
await page.click('[data-key="lib-add"]');
|
||||
const tiles = '[data-key^="lib-photo-"]';
|
||||
await page.waitForSelector(tiles, { timeout: 120_000 });
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, tiles, { timeout: 120_000 });
|
||||
step('both frames indexed as tiles', true, `2 tiles from ${JPG} + ${RAW}`);
|
||||
const thumbs = '[data-key^="lib-thumb-"]';
|
||||
await page.waitForSelector(thumbs, { timeout: 120_000 });
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 2, thumbs, { timeout: 120_000 });
|
||||
step('both frames indexed as thumbnails', true, `2 tiles from ${JPG} + ${RAW}`);
|
||||
|
||||
const indexed = await catalogue();
|
||||
for (const name of [JPG, RAW]) {
|
||||
@@ -180,11 +189,37 @@ for (const name of [JPG, RAW]) {
|
||||
step(`thumbnail for ${name}`, !!row && row.bytes > 1000 && row.jpeg, `${row?.bytes ?? 0} bytes, jpeg=${row?.jpeg}`);
|
||||
}
|
||||
|
||||
// One tap on a tile hands the frame to the studio. The RAW is the harder half —
|
||||
// it has to be read back off the (stand-in) handle and developed — so it is the
|
||||
// one that gets opened.
|
||||
// The tree: the picked folder, with the subfolder a frame sits in under it, each
|
||||
// carrying the frames at or below it. The RAW is the nested one.
|
||||
const nodes = await page.$$eval('[data-key^="lib-node-"]', (els) =>
|
||||
els.map((e) => [e.dataset.key, e.querySelector('.adm-album-count')?.textContent])
|
||||
);
|
||||
step(
|
||||
'tree lists the folder and its subfolder',
|
||||
nodes.length === 2 && nodes[0][0] === 'lib-node-CheckRoll' && nodes[1][0] === 'lib-node-CheckRoll/2026',
|
||||
JSON.stringify(nodes)
|
||||
);
|
||||
|
||||
// Opening the subfolder redraws the strip and the frame above it.
|
||||
await page.click('[data-key="lib-node-CheckRoll/2026"]');
|
||||
await page.waitForFunction((sel) => document.querySelectorAll(sel).length === 1, thumbs, { timeout: 30_000 });
|
||||
const staged = await page.locator('[data-key="lib-stage-meta"]').innerText();
|
||||
step('the open subfolder draws its own strip and frame', /P1010256\.RW2/.test(staged), staged.replace(/\n/g, ' · '));
|
||||
|
||||
// The switch: the middle becomes every thumbnail of the open node at once.
|
||||
await page.click('[data-key="lib-view-grid"]');
|
||||
await page.waitForSelector('[data-key^="lib-photo-"]', { timeout: 30_000 });
|
||||
const cards = await page.$$eval('[data-key^="lib-photo-"]', (els) => els.map((e) => e.dataset.key));
|
||||
step('the grid view draws a card per frame', cards.length === 1, JSON.stringify(cards));
|
||||
await page.click('[data-key="lib-view-preview"]');
|
||||
await page.waitForSelector(thumbs, { timeout: 30_000 });
|
||||
|
||||
// One tap on a thumbnail raises the frame, and the button under it hands it to
|
||||
// the studio. The RAW is the harder half — it has to be read back off the
|
||||
// (stand-in) handle and developed — so it is the one that gets opened.
|
||||
const rawId = indexed.thumbs.find((t) => t.id.endsWith(`/${RAW}`)).id;
|
||||
await page.click(`[data-key="lib-open-${rawId}"]`);
|
||||
await page.click(`[data-key="lib-thumb-${rawId}"]`);
|
||||
await page.click('[data-key="lib-open"]');
|
||||
await page.waitForURL((url) => url.pathname.startsWith('/app'), { timeout: 120_000 });
|
||||
await page.waitForFunction(() => !document.querySelector('.dropzone'), null, { timeout: 180_000 });
|
||||
step('studio developed the frame from its handle', true, 'stage no longer on the drop zone');
|
||||
@@ -215,9 +250,10 @@ for (let i = 0; i < 40 && !filed.length; i++) {
|
||||
step('the look was filed back against the frame', filed.length === 1, JSON.stringify(filed));
|
||||
|
||||
await page.goto(`${base}/library`);
|
||||
await page.waitForSelector(`[data-key="lib-photo-${rawId}"]`);
|
||||
const meta = await page.locator(`[data-key="lib-photo-${rawId}"] .adm-meta`).innerText();
|
||||
step('the tile says the frame is edited', /đã chỉnh|edited/.test(meta), meta.replace(/\n/g, ' · '));
|
||||
await page.waitForSelector(`[data-key="lib-thumb-${rawId}"]`);
|
||||
await page.click(`[data-key="lib-thumb-${rawId}"]`);
|
||||
const meta = await page.locator('[data-key="lib-stage-meta"]').innerText();
|
||||
step('the frame says it was edited', /đã chỉnh|edited/.test(meta), meta.replace(/\n/g, ' · '));
|
||||
|
||||
await browser.close();
|
||||
sampleServer.close();
|
||||
|
||||
Reference in New Issue
Block a user