release: v0.16.0

See CHANGELOG.md for the full v0.16.0 entry.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-09-22 14:12:13 +02:00
committed by GitHub
parent c3f92ca4ec
commit a3a6319c29
120 changed files with 6818 additions and 975 deletions

View File

@@ -10,8 +10,13 @@ import { ALBUM_SORT_MODES, isAlbumSortMode, type AlbumSortMode } from '../albumS
import { LOCATION_PRECISIONS, isLocationPrecision, type LocationPrecision } from '../mapPrecision';
import {
slugify,
findAlbumSlugCollisions,
resolveExifDisplay,
resolveColorMode,
clamp,
PHOTO_GRID_COLUMNS_MIN,
PHOTO_GRID_COLUMNS_MAX,
PHOTO_GRID_GAP_MAX,
AppConfig,
AlbumEntryObject,
SubpageConfig,
@@ -65,11 +70,22 @@ interface RawGridOverrides {
layout?: string;
}
/** Converts raw YAML grid overrides into a typed partial GridConfig. */
/**
* Converts raw YAML grid overrides into a typed partial GridConfig.
*
* `columns` and `gap` are clamped here, not left for the renderer: a subpage
* or album override reaches `--grid-columns` unclamped (app/[...path]/page.tsx),
* so `columns: -1` produced an invalid `repeat(-1, 1fr)` and collapsed the
* whole grid to one column with no error anywhere (#633). The cover grid had
* this already (buildCoverGridVars); this is the same treatment for the photo
* grid it was missing from.
*/
function parseGridOverrides(raw: RawGridOverrides): Partial<GridConfig> {
return {
...(raw.columns != null ? { columns: raw.columns } : {}),
...(raw.gap != null ? { gap: raw.gap } : {}),
...(raw.columns != null
? { columns: clamp(raw.columns, PHOTO_GRID_COLUMNS_MIN, PHOTO_GRID_COLUMNS_MAX) }
: {}),
...(raw.gap != null ? { gap: clamp(raw.gap, 0, PHOTO_GRID_GAP_MAX) } : {}),
...(raw.aspectRatio != null ? { aspectRatio: raw.aspectRatio } : {}),
...(raw.layout != null
? {
@@ -418,10 +434,53 @@ export function deriveGallery(gallery: GalleryYaml): GalleryDerivation {
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
const allAlbumIds = [...new Set([...standaloneAlbumIds, ...subpageAlbumIds])];
const standaloneAlbums = standaloneAlbumIds.filter((id) => !subpageAlbumIds.has(id));
// Every consumer that resolves a slug takes the first match — getAlbumBySlug,
// the sitemap, findPassword — so two subpages that slugify alike leave the
// second unreachable and its password / albums resolved from the first
// (#632). slugify() folds diacritics and punctuation, so distinct names
// collide easily: "Portfolio 2024" and "Portfolio-2024" both become
// "portfolio-2024".
const subpagesBySlug = new Map<string, string[]>();
for (const sp of subpages) {
const names = subpagesBySlug.get(sp.slug) ?? [];
names.push(sp.name);
subpagesBySlug.set(sp.slug, names);
}
for (const [slug, names] of subpagesBySlug) {
if (names.length > 1) {
throw new Error(
`Subpages ${names.map((n) => `"${n}"`).join(' and ')} all produce the slug "${slug}". ` +
`Rename one so their URLs do not collide.`,
);
}
}
// Same rule applies to albums that share a route (standalone albums among
// themselves, or one subpage's albums including its sections). Only a title
// override is known here without asking Immich — an album that keeps its
// Immich name can still collide, which is what the doctor's equivalent
// check, run against the live album list, is for.
const checkAlbumOverrideCollisions = (context: string, ids: string[]) => {
const named = ids
.map((id) => ({ id, name: albumOverrides[id] }))
.filter((a): a is { id: string; name: string } => !!a.name);
for (const { slug, albums } of findAlbumSlugCollisions(named)) {
throw new Error(
`${context}: albums titled ${albums.map((a) => `"${a.name}"`).join(' and ')} all produce ` +
`the slug "${slug}". Give one a different title override.`,
);
}
};
checkAlbumOverrideCollisions('gallery.yaml albums', standaloneAlbums);
for (const sp of subpages) {
checkAlbumOverrideCollisions(`subpage "${sp.name}"`, sp.albumIds);
}
return {
albums: allAlbumIds,
standaloneAlbums: standaloneAlbumIds.filter((id) => !subpageAlbumIds.has(id)),
standaloneAlbums,
subpages,
albumOverrides,
albumDescriptions,
@@ -587,8 +646,11 @@ export function getConfig(): AppConfig {
exifOnHover: settings.exifOnHover !== false,
exif: resolveExifDisplay(settings.exif, settings.exifOnHover),
grid: {
columns: settings.grid?.columns ?? 3,
gap: settings.grid?.gap ?? 12,
columns:
settings.grid?.columns != null
? clamp(settings.grid.columns, PHOTO_GRID_COLUMNS_MIN, PHOTO_GRID_COLUMNS_MAX)
: 3,
gap: settings.grid?.gap != null ? clamp(settings.grid.gap, 0, PHOTO_GRID_GAP_MAX) : 12,
aspectRatio: settings.grid?.aspectRatio ?? '1',
layout: (VALID_LAYOUTS.includes(settings.grid?.layout ?? '')
? settings.grid!.layout