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

@@ -0,0 +1,87 @@
/**
* The gallery.yaml ↔ page-builder translation for album entries.
*
* Extracted from PageBuilder.tsx so it can be tested on its own: this is the
* code that decides what survives a save, and a field it does not know about
* is discarded silently rather than failing (#614).
*/
import { DEFAULT_ALBUM_SORT, isAlbumSortMode } from '@/lib/albumSort';
import { type AlbumEntryObject } from '@/lib/config/schema';
import type { AlbumEntry } from './types';
export type RawAlbumEntry = string | Record<string, string | AlbumEntryObject>;
/**
* Whether an entry carries anything beyond its ID.
*
* Both collapse rules below depend on this, and they are the reason every new
* per-album option has to be listed here: an entry that looks "empty" is
* serialized back to a bare UUID string, so a field missing from this check is
* silently dropped on the next save.
*/
export function hasAlbumOptions(entry: AlbumEntry): boolean {
return Boolean(
entry.description ||
entry.password ||
entry.heroImage ||
(entry.sort && entry.sort !== DEFAULT_ALBUM_SORT) ||
entry.assetOrder?.length ||
entry.grid ||
entry.coverPosition ||
entry.location ||
entry.download,
);
}
export function parseAlbumEntries(raw: RawAlbumEntry[] | undefined): AlbumEntry[] {
if (!raw) return [];
return raw.map((entry) => {
if (typeof entry === 'string') return { id: entry };
const [id, value] = Object.entries(entry)[0];
if (typeof value === 'string') return { id, title: value };
return {
id,
title: value.title,
description: value.description,
password: value.password,
heroImage: value.heroImage,
sort: isAlbumSortMode(value.sort) ? value.sort : undefined,
assetOrder: value.assetOrder,
grid: value.grid,
coverPosition: value.coverPosition,
location: value.location,
download: value.download,
};
});
}
export function serializeAlbumEntries(entries: AlbumEntry[]): RawAlbumEntry[] {
return entries.map((entry) => {
const extras = hasAlbumOptions(entry);
if (!entry.title && !extras) return entry.id;
if (entry.title && !extras) return { [entry.id]: entry.title };
const val: AlbumEntryObject = {};
// Only when set: a sort-only entry would otherwise be written with an empty
// title, which deriveGallery ignores but which still lands in the YAML.
if (entry.title) val.title = entry.title;
if (entry.description) val.description = entry.description;
if (entry.password) val.password = entry.password;
if (entry.heroImage) val.heroImage = entry.heroImage;
if (entry.sort && entry.sort !== DEFAULT_ALBUM_SORT) val.sort = entry.sort;
// Persisted regardless of the mode, so manual → newest → manual does not
// throw away a hand-curated order.
if (entry.assetOrder?.length) val.assetOrder = entry.assetOrder;
if (entry.grid) val.grid = entry.grid;
if (entry.coverPosition) val.coverPosition = entry.coverPosition;
// No UI sets this yet; it is preserved so that saving an unrelated change
// cannot quietly republish exact coordinates for an album deliberately
// placed at city level or hidden from the map (#469).
if (entry.location) val.location = entry.location;
// Same story as location: set by hand in YAML since #475, with no UI, so
// the builder was turning original downloads back off on the next save.
if (entry.download) val.download = entry.download;
return { [entry.id]: val };
});
}

View File

@@ -13,6 +13,17 @@ export interface AlbumEntry {
grid?: { columns?: number; gap?: number; aspectRatio?: string; layout?: string };
/** EXPERIMENTAL: focal point for the cover crop, e.g. "50% 25%" or "top" */
coverPosition?: string;
/**
* Map precision for this album: `exact`, `city`, `country` or `hidden` (#469).
*
* Carried verbatim rather than narrowed. The builder has no UI for it yet, so
* its only job is to hand back exactly what it was given — including a value
* this version does not recognise, which deriveGallery() is the right place
* to reject.
*/
location?: string;
/** Whether visitors may download the originals from this album (#475). */
download?: boolean;
}
export interface Section {
@@ -37,6 +48,8 @@ export interface Subpage {
grid?: { columns?: number; gap?: number; aspectRatio?: string; layout?: string };
/** The album-cover tiles on this page only (#523). */
coverGrid?: { columns?: number; gap?: number; aspectRatio?: string; layout?: string };
/** Map precision for every album here that does not set its own (#469). */
location?: string;
}
/**