The fifth plugin out, and the cheapest so far — offscale's shape rather than
music's or vault's. Nothing stayed behind and nothing had to be redesigned,
for three structural reasons recorded in PLUGIN.md:
• No reverse dependency. Nothing in src/ imported anything of photos'.
• Configuration was already right. The URL and API key live in photos_config,
a per-user registry encrypted at rest and driven from the app. Vault had to
move VAULTWARDEN_URL out of .env and invent a settings panel; here the
sidecar already read the database, and its own comment already said why
nothing reads process.env.
• The frontend was already panels. The screen was a WorkspaceView around
exactly photos-nav and photos-view, so web/panels.ts and web/layout.ts are
the screen's contents with the wrapper removed.
The router's prefix is now derived from mountPrefix rather than the literal
'/api/photos', which was correct only by coincidence — a third-party publisher
mounts at /api/p/<publisher>/photos and the literal would forward the wrong
path to the sidecar.
Palette debt paid down as the runbook requires: 6 classes (it says 47; someone
had already done most of it), all of them state colours wearing palette
numbers, now `success` and `warning` from the closed tone set so they follow
the theme.
Immich itself is still provisioned by the app store rather than by this plugin
— the `provisioned` half of its old catalogue entry has no home until Docker
provisioning for plugins exists. Point it at an instance you already run.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
57 lines
1.8 KiB
TypeScript
57 lines
1.8 KiB
TypeScript
import type { GridAsset } from './shared';
|
|
import type { Selection } from './useSelection';
|
|
import { useMemo } from 'react';
|
|
import { AssetTile } from './AssetTile';
|
|
import { justifyRows } from './shared';
|
|
import { useContainerWidth } from './useContainerWidth';
|
|
|
|
// A justified grid over a flat list of assets — albums, search results, a person's photos.
|
|
//
|
|
// The timeline uses TimelineGrid instead, because it has to lay out months it has not downloaded yet. This one
|
|
// always has every asset in hand, so it can lay the whole thing out in one pass.
|
|
|
|
type PhotoGridProps = {
|
|
assets: GridAsset[];
|
|
token: string | null;
|
|
selection: Selection;
|
|
rowHeight?: number;
|
|
gap?: number;
|
|
empty?: React.ReactNode;
|
|
};
|
|
|
|
export const PhotoGrid = ({ assets, token, selection, rowHeight = 210, gap = 4, empty }: PhotoGridProps) => {
|
|
const [ref, width] = useContainerWidth<HTMLDivElement>();
|
|
|
|
const rows = useMemo(
|
|
() =>
|
|
width > 0
|
|
? justifyRows({ items: assets, ratioOf: (a) => a.ratio, containerWidth: width, targetHeight: rowHeight, gap })
|
|
: [],
|
|
[assets, width, rowHeight, gap],
|
|
);
|
|
|
|
return (
|
|
<div ref={ref} className="w-full">
|
|
{assets.length === 0 && empty}
|
|
<div className="flex flex-col" style={{ gap }}>
|
|
{rows.map((row, index) => (
|
|
<div key={index} className="flex" style={{ gap }}>
|
|
{row.tiles.map(({ item, width: w, height: h }) => (
|
|
<AssetTile
|
|
key={item.id}
|
|
asset={item}
|
|
token={token}
|
|
width={w}
|
|
height={h}
|
|
selected={selection.selected.has(item.id)}
|
|
selecting={selection.isSelecting}
|
|
onToggle={(id, ev) => selection.toggle(id, ev)}
|
|
/>
|
|
))}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|