Files
fofos-admin/web/thumbnail.ts
T
pastilhasandClaude Opus 5 84336b8af5 a framing slot renders the original, not a rung
The preview asked `derived/` for a 640-wide rung and drew it about 1900 pixels wide — stretched three
times over, which reads as a poor photograph rather than as the wrong file. Raising the rung only moves
the problem: the box is most of a panel wide and can be zoomed on top of that, so any rung chosen in
advance is chosen wrong, and the widest one available is whatever the ladder happens to hold.

The original has every pixel there is. It is also, often, a HEIC that no browser can decode — which is
what av-club is for, and the platform already renders those for its own file browser through exactly
this path.

So there is a `/original/*` route: it resolves a path inside `originals/`, forwards to av-club's
`/v1/render` with a width, and streams back something a browser can draw. It decodes nothing itself.
That is the standing rule and the reason there is no image library in this plugin.

Not cached on disk, like the platform's own /render and for the same reason: a full-size render is
large, looked at once, and keyed on a width the panel chose. Caching it would fill a disk to make the
second look at one photograph faster.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 00:02:24 +01:00

51 lines
2.5 KiB
TypeScript

import { getHeaders } from 'hooks/useClient';
/**
* A thumbnail URL for something under `originals/`, authenticated by `?token=`.
*
* ── Why the token is in the query ──
*
* An `<img>` cannot send an Authorization header. `user-middleware` and the permission gate both accept
* the token in the query string for exactly this, and the platform's own file browser does the same.
* Built without it first, and every picture 401s while the layout looks perfectly correct.
*
* ── Why this is a module ──
*
* It was copied into GalleriesPanel and again into RecordsPanel, and a third copy was about to go into
* MediaField. Three copies of a URL shape is three places to be wrong about the rung when the ladder
* changes — which it did, four times in a day.
*/
export const thumbUrl = (path: string, width: 256 | 320 | 480 | 640 | 960 | 1280 = 320): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/derived/${encoded}/${width}.webp?token=${encodeURIComponent(token)}`;
};
/**
* A video's poster, for the same reason.
*
* A video has no width rungs — `poster-448.webp` and friends are cut from its still — so a slot showing
* one asks for the poster rather than a width that does not exist.
*/
export const posterUrl = (path: string): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/derived/${encoded}/poster-512.webp?token=${encodeURIComponent(token)}`;
};
/**
* An ORIGINAL, rendered at a width, for something drawn at size.
*
* The derived ladder is right for a row thumbnail and wrong for a framing slot: that is most of a panel
* wide and can be zoomed on top of it, so a rung chosen in advance is a rung that is wrong. This asks
* for the width the slot actually draws, from the file that has every pixel.
*
* `width` is a hint the server clamps to 256..4096. Ask for what will be drawn — twice it, on a screen
* that is worth it — rather than a round number.
*/
export const originalUrl = (path: string, width: number): string => {
const token = getHeaders()['Authorization']?.replace('Bearer ', '') ?? '';
const encoded = path.split('/').map(encodeURIComponent).join('/');
return `/api/fofos-admin/original/${encoded}?width=${Math.round(width)}&token=${encodeURIComponent(token)}`;
};