fix(library): a server's frames stand at the shape the server stated, not at a guess
This commit is contained in:
@@ -228,8 +228,11 @@ await context.route('**/api/**', (route) => {
|
|||||||
id: a.id,
|
id: a.id,
|
||||||
name: a.name,
|
name: a.name,
|
||||||
takenAt: '2026-04-01T10:00:00.000Z',
|
takenAt: '2026-04-01T10:00:00.000Z',
|
||||||
width: 6000,
|
// Portrait, and deliberately unlike the square thumbnail above it: a wall
|
||||||
height: 4000,
|
// laid out off the server's own numbers and a wall laid out off the pictures
|
||||||
|
// it is handed read as two different shapes here.
|
||||||
|
width: 2000,
|
||||||
|
height: 3000,
|
||||||
size: 12345678,
|
size: 12345678,
|
||||||
}));
|
}));
|
||||||
return send(200, { ok: true, page, size, total: rows.length, hasMore: items.length === size, items });
|
return send(200, { ok: true, page, size, total: rows.length, hasMore: items.length === size, items });
|
||||||
@@ -382,6 +385,27 @@ step(
|
|||||||
`${firstLook.size} frames minted, ${await stored()} of ${await tiles()} tiles kept`
|
`${firstLook.size} frames minted, ${await stored()} of ${await tiles()} tiles kept`
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// The shape a row is laid at. A row is justified to the wall's own width, which is
|
||||||
|
// a sum of the shapes of the frames in it, and the wall takes those shapes off the
|
||||||
|
// row Immich listed — `width` and `height` come with every frame — rather than
|
||||||
|
// standing a whole node at one guessed rectangle and laying the shelf out again
|
||||||
|
// picture by picture as the pictures arrive, which is every row below the one that
|
||||||
|
// landed moving and the ruler's needle moving with them. The fixture says 2000x3000
|
||||||
|
// where the thumbnail it hands out is square, so a wall measured off the pictures
|
||||||
|
// reads as 1.00 here and a wall standing on the guess reads as 1.50.
|
||||||
|
const statedAr = 2000 / 3000;
|
||||||
|
const rowArs = await page.$$eval('.lib-tile', (els) =>
|
||||||
|
els.map((e) => {
|
||||||
|
const r = e.getBoundingClientRect();
|
||||||
|
return r.height ? +(r.width / r.height).toFixed(2) : 0;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
step(
|
||||||
|
'and a frame stands at the shape the server stated, not at the guess nor at the picture',
|
||||||
|
rowArs.length > 0 && rowArs.every((ar) => Math.abs(ar - statedAr) < 0.1),
|
||||||
|
`${rowArs.slice(0, 5).join(', ')} — the server states ${statedAr.toFixed(2)}`
|
||||||
|
);
|
||||||
|
|
||||||
// The visit after — the page again, on the same wall. Every tile the visit before
|
// The visit after — the page again, on the same wall. Every tile the visit before
|
||||||
// drew is in this machine's store by now, so every tile is drawn out of it and the
|
// drew is in this machine's store by now, so every tile is drawn out of it and the
|
||||||
// backend is not asked for one of them a second time.
|
// backend is not asked for one of them a second time.
|
||||||
@@ -447,7 +471,7 @@ step(
|
|||||||
// albums. It is probed before it is stored, so a key that does not answer is a
|
// albums. It is probed before it is stored, so a key that does not answer is a
|
||||||
// dialog still open on a node unchanged.
|
// dialog still open on a node unchanged.
|
||||||
await editDialog('Immich');
|
await editDialog('Immich');
|
||||||
await page.fill('[data-key="immich-newkey"]', 'not-the-key');
|
await page.fill('[data-key="immich-key"]', 'not-the-key');
|
||||||
await page.click('[data-key="immich-save"]');
|
await page.click('[data-key="immich-save"]');
|
||||||
await page.waitForSelector('[data-key="immich-error"]', { timeout: 5_000 }).catch(() => {});
|
await page.waitForSelector('[data-key="immich-error"]', { timeout: 5_000 }).catch(() => {});
|
||||||
step(
|
step(
|
||||||
@@ -456,7 +480,7 @@ step(
|
|||||||
(await page.$('[data-key="immich-modal"]')) !== null,
|
(await page.$('[data-key="immich-modal"]')) !== null,
|
||||||
await page.$eval('[data-key="immich-error"]', (e) => e.textContent).catch(() => 'no word said')
|
await page.$eval('[data-key="immich-error"]', (e) => e.textContent).catch(() => 'no word said')
|
||||||
);
|
);
|
||||||
await page.fill('[data-key="immich-newkey"]', 'good-key');
|
await page.fill('[data-key="immich-key"]', 'good-key');
|
||||||
await page.click('[data-key="immich-save"]');
|
await page.click('[data-key="immich-save"]');
|
||||||
await page.waitForSelector('[data-key="immich-modal"]', { state: 'detached', timeout: 10_000 }).catch(() => {});
|
await page.waitForSelector('[data-key="immich-modal"]', { state: 'detached', timeout: 10_000 }).catch(() => {});
|
||||||
await settle('Immich', 5);
|
await settle('Immich', 5);
|
||||||
|
|||||||
@@ -2101,7 +2101,10 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
|||||||
let sum = 0;
|
let sum = 0;
|
||||||
for (const ar of wallAr.values()) sum += ar;
|
for (const ar of wallAr.values()) sum += ar;
|
||||||
const fallback = wallAr.size ? sum / wallAr.size : WALL_AR;
|
const fallback = wallAr.size ? sum / wallAr.size : WALL_AR;
|
||||||
return listed.map((photo) => wallAr.get(photo.id) ?? fallback);
|
// A frame Immich holds already said its own shape, so it is measured off
|
||||||
|
// nothing and stays where it was laid: `wallAr` is for the frames on the disk,
|
||||||
|
// which nobody has opened yet.
|
||||||
|
return listed.map((photo) => photo.ar ?? wallAr.get(photo.id) ?? fallback);
|
||||||
// `arsAt` is the shape map having grown: the numbers are read off it and not
|
// `arsAt` is the shape map having grown: the numbers are read off it and not
|
||||||
// off the list, so nothing else here would make the shelf look again.
|
// off the list, so nothing else here would make the shelf look again.
|
||||||
}, [listed, arsAt]);
|
}, [listed, arsAt]);
|
||||||
@@ -3320,7 +3323,12 @@ function Catalogue({ admin, user }: { admin: boolean; user: User | null }) {
|
|||||||
}}
|
}}
|
||||||
onContextMenu={(e) => handlePhotoContextMenu(photo, e)}
|
onContextMenu={(e) => handlePhotoContextMenu(photo, e)}
|
||||||
>
|
>
|
||||||
{tile(photo, undefined, (ar) => onAr(photo.id, ar))}
|
{tile(photo, undefined, (ar) => {
|
||||||
|
// Only a frame whose shape is not known
|
||||||
|
// already is worth laying the shelf out
|
||||||
|
// again for.
|
||||||
|
if (photo.ar === undefined) onAr(photo.id, ar);
|
||||||
|
})}
|
||||||
{/* The score over the picture, the same five
|
{/* The score over the picture, the same five
|
||||||
stars the stage gives: the wall is where a
|
stars the stage gives: the wall is where a
|
||||||
roll is gone through, and going back to the
|
roll is gone through, and going back to the
|
||||||
|
|||||||
@@ -312,6 +312,15 @@ function frameName(name: string, id: string, taken: Set<string>): string {
|
|||||||
return `${name} ~${id.slice(0, 8)}`;
|
return `${name} ~${id.slice(0, 8)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The shape a frame stands up in, off the numbers Immich holds of it rather than
|
||||||
|
// off its own pixels: the wall can be laid out from these the moment the rows
|
||||||
|
// land, where a frame whose shape is not known has to wait for its picture and
|
||||||
|
// move every row below it when it does. Immich states neither on every frame, and
|
||||||
|
// a frame of either number missing is one the wall measures itself.
|
||||||
|
function frameAr(asset: ImmichAsset): number | undefined {
|
||||||
|
return asset.width && asset.height ? asset.width / asset.height : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
export interface ImmichSyncResult {
|
export interface ImmichSyncResult {
|
||||||
total: number;
|
total: number;
|
||||||
albums: number;
|
albums: number;
|
||||||
@@ -433,6 +442,7 @@ export async function syncImmich(
|
|||||||
name,
|
name,
|
||||||
server,
|
server,
|
||||||
remoteId: item.id,
|
remoteId: item.id,
|
||||||
|
ar: frameAr(item),
|
||||||
taken: item.takenAt ? Date.parse(item.takenAt) || Date.now() : Date.now(),
|
taken: item.takenAt ? Date.parse(item.takenAt) || Date.now() : Date.now(),
|
||||||
size: item.size ?? 0,
|
size: item.size ?? 0,
|
||||||
addedAt: Date.now(),
|
addedAt: Date.now(),
|
||||||
@@ -497,6 +507,7 @@ export async function syncImmich(
|
|||||||
name,
|
name,
|
||||||
server,
|
server,
|
||||||
remoteId: item.id,
|
remoteId: item.id,
|
||||||
|
ar: frameAr(item),
|
||||||
taken: item.takenAt ? Date.parse(item.takenAt) || Date.now() : Date.now(),
|
taken: item.takenAt ? Date.parse(item.takenAt) || Date.now() : Date.now(),
|
||||||
size: item.size ?? 0,
|
size: item.size ?? 0,
|
||||||
addedAt: Date.now(),
|
addedAt: Date.now(),
|
||||||
@@ -576,6 +587,7 @@ export async function syncImmich(
|
|||||||
name,
|
name,
|
||||||
server,
|
server,
|
||||||
remoteId: item.id,
|
remoteId: item.id,
|
||||||
|
ar: frameAr(item),
|
||||||
taken: item.takenAt ? Date.parse(item.takenAt) || Date.now() : Date.now(),
|
taken: item.takenAt ? Date.parse(item.takenAt) || Date.now() : Date.now(),
|
||||||
size: item.size ?? 0,
|
size: item.size ?? 0,
|
||||||
addedAt: Date.now(),
|
addedAt: Date.now(),
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ export interface LibraryPhoto {
|
|||||||
// route above reads the frame by, and the server says which key fetches it.
|
// route above reads the frame by, and the server says which key fetches it.
|
||||||
remoteId?: string;
|
remoteId?: string;
|
||||||
server?: string;
|
server?: string;
|
||||||
|
// The frame's shape — width over height — as the server stated it, on a frame
|
||||||
|
// Immich holds. A frame on the disk has none: a scan reads a date and a size off
|
||||||
|
// it and never opens it. Where it is here the wall is laid out right the first
|
||||||
|
// time, instead of standing every frame of a node at one guessed rectangle and
|
||||||
|
// moving every row below it as the pictures arrive.
|
||||||
|
ar?: number;
|
||||||
// The tile the catalogue kept of this frame, where one has been made: it is
|
// The tile the catalogue kept of this frame, where one has been made: it is
|
||||||
// made when the frame is first drawn and not when it is found, so a frame just
|
// made when the frame is first drawn and not when it is found, so a frame just
|
||||||
// read has none. Null is the same as absent — a tile that could not be made.
|
// read has none. Null is the same as absent — a tile that could not be made.
|
||||||
|
|||||||
Reference in New Issue
Block a user