feat: photography features & image optimizations

- Hero carousel: gallery.yaml hero accepts single UUID or array, crossfade component
- Full-bleed single albums: subpages with 1 album skip grid, render photos directly
- EXIF on hover: camera/lens/focal length overlay on grid items (toggle via exifOnHover)
- Customizable grid: columns/gap/aspectRatio/layout (masonry|uniform) via gallery.yaml
- Per-subpage grid overrides: each subpage can override global grid config
- True masonry: per-image aspect ratios from EXIF dimensions
- next/image migration: homepage hero, album covers, grid thumbnails, about portrait
- ThumbHash blur placeholders and dominant color backgrounds
- Custom next/image loader via image proxy with width/quality params
- Lightbox fade-in transition with dominant color background
- Password-protected subpages, health check, rate limiting, OG images
This commit is contained in:
ralksta
2026-02-14 10:30:27 +01:00
parent 3c69860a64
commit b7217c4a76
31 changed files with 1998 additions and 162 deletions

View File

@@ -4,6 +4,8 @@
*/
import { encodeAssetId } from './tokens';
import { thumbHashToBlurDataUrl, thumbHashToDominantHex } from './thumbhash';
import type { ImmichAsset } from './immich';
/**
* Generate a public image proxy URL for an asset.
@@ -18,3 +20,63 @@ export function imageUrl(assetId: string, size: 'thumbnail' | 'preview' | 'origi
export function exifUrl(assetId: string): string {
return `/api/exif/${encodeAssetId(assetId)}`;
}
/**
* Placeholder data derived from an asset's ThumbHash.
*/
export interface PlaceholderData {
blurDataURL: string;
dominantColor: string;
}
/**
* Generate blur placeholder and dominant color from an asset's ThumbHash.
* Returns null if the asset has no ThumbHash.
*/
export function assetPlaceholder(asset: Pick<ImmichAsset, 'thumbhash'>): PlaceholderData | null {
if (!asset.thumbhash) return null;
try {
return {
blurDataURL: thumbHashToBlurDataUrl(asset.thumbhash),
dominantColor: thumbHashToDominantHex(asset.thumbhash),
};
} catch {
return null;
}
}
/**
* Compact EXIF summary for hover overlays.
*/
export interface ExifSummary {
camera?: string;
lens?: string;
focalLength?: string;
}
/**
* Extract a compact EXIF summary from an asset's exifInfo.
* Returns undefined if no relevant EXIF data is available.
*/
export function assetExifSummary(asset: Pick<ImmichAsset, 'exifInfo'>): ExifSummary | undefined {
const exif = asset.exifInfo;
if (!exif) return undefined;
const camera = exif.model || undefined;
const lens = exif.lensModel || undefined;
const focalLength = exif.focalLength ? `${exif.focalLength}mm` : undefined;
if (!camera && !lens && !focalLength) return undefined;
return { camera, lens, focalLength };
}
/**
* Compute the natural aspect ratio (width / height) from EXIF dimensions.
* Returns undefined if dimensions are not available.
*/
export function assetAspectRatio(asset: Pick<ImmichAsset, 'exifInfo'>): number | undefined {
const w = asset.exifInfo?.exifImageWidth;
const h = asset.exifInfo?.exifImageHeight;
if (w && h && h > 0) return w / h;
return undefined;
}