feat: simplify gallery.yaml and add support for album name overrides
This commit is contained in:
@@ -9,23 +9,20 @@ hero:
|
|||||||
|
|
||||||
# Standalone albums shown directly on the homepage
|
# Standalone albums shown directly on the homepage
|
||||||
albums:
|
albums:
|
||||||
- a89c6085-a105-46c2-baa4-338bc69cab93
|
- a89c6085-a105-46c2-baa4-338bc69cab93: "Wüstenzauber"
|
||||||
- 9aee9367-a239-4651-980d-3368bc70b7b6
|
- 9aee9367-a239-4651-980d-3368bc70b7b6: "Heimatliebe"
|
||||||
|
|
||||||
# Subpages group multiple albums under one URL path
|
# Subpages group multiple albums under one URL path
|
||||||
subpages:
|
subpages:
|
||||||
- name: Deutschland
|
Deutschland:
|
||||||
albums:
|
- 9aee9367-a239-4651-980d-3368bc70b7b6
|
||||||
- 9aee9367-a239-4651-980d-3368bc70b7b6
|
Dubai:
|
||||||
|
- a89c6085-a105-46c2-baa4-338bc69cab93: "Burj Khalifa & Co"
|
||||||
- name: Dubai
|
# Support for subpages with passwords or custom grids in the map format
|
||||||
albums:
|
Polen:
|
||||||
- a89c6085-a105-46c2-baa4-338bc69cab93
|
|
||||||
|
|
||||||
- name: Polen
|
|
||||||
password: clientpass123
|
password: clientpass123
|
||||||
albums:
|
albums:
|
||||||
- 4ad0ed89-55de-4e69-937f-2d9988f63ad1
|
- 4ad0ed89-55de-4e69-937f-2d9988f63ad1: "Krakau & mehr"
|
||||||
|
|
||||||
# Footer — optional minimal footer with social links
|
# Footer — optional minimal footer with social links
|
||||||
footer:
|
footer:
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ hero: 00000000-0000-0000-0000-000000000000
|
|||||||
# - 22222222-2222-2222-2222-222222222222
|
# - 22222222-2222-2222-2222-222222222222
|
||||||
|
|
||||||
# Standalone albums — shown directly on the homepage as cards
|
# Standalone albums — shown directly on the homepage as cards
|
||||||
|
# Use a string for the UUID, or a map for an inline name override.
|
||||||
albums:
|
albums:
|
||||||
- 11111111-1111-1111-1111-111111111111
|
- 11111111-1111-1111-1111-111111111111: "Meine Favoriten" # inline override
|
||||||
- 22222222-2222-2222-2222-222222222222
|
- 22222222-2222-2222-2222-222222222222
|
||||||
|
|
||||||
# Show camera/lens EXIF on photo hover (default: true)
|
# Show camera/lens EXIF on photo hover (default: true)
|
||||||
@@ -30,21 +31,17 @@ albums:
|
|||||||
# aspectRatio: "1" # "1", "3/2", "auto", etc. default: "1"
|
# aspectRatio: "1" # "1", "3/2", "auto", etc. default: "1"
|
||||||
# layout: masonry # "masonry" or "uniform", default: "masonry"
|
# layout: masonry # "masonry" or "uniform", default: "masonry"
|
||||||
|
|
||||||
# Subpages — group albums into collections at /subpage-slug/album-slug
|
# Subpages — group albums into collections at /subpage-slug/album-slug.
|
||||||
|
# You can use a simple map for concise definitions, or a full object for passwords/grids.
|
||||||
subpages:
|
subpages:
|
||||||
- name: Japan
|
Japan: # Simple map format
|
||||||
grid: # override global grid for this subpage
|
- 33333333-3333-3333-3333-333333333333: "Tokyo Nights"
|
||||||
columns: 4
|
- 44444444-4444-4444-4444-444444444444
|
||||||
layout: uniform
|
|
||||||
aspectRatio: "3/2"
|
|
||||||
albums:
|
|
||||||
- 33333333-3333-3333-3333-333333333333 # Tokyo
|
|
||||||
- 44444444-4444-4444-4444-444444444444 # Kyoto
|
|
||||||
|
|
||||||
- name: Wedding – Smith
|
Wedding: # Advanced object format (needed for passwords/grid overrides)
|
||||||
password: clientpass123 # visitors must enter this to view
|
password: clientpass123
|
||||||
albums:
|
albums:
|
||||||
- 55555555-5555-5555-5555-555555555555
|
- 55555555-5555-5555-5555-555555555555: "The Big Day"
|
||||||
|
|
||||||
# Footer — optional minimal footer with social links
|
# Footer — optional minimal footer with social links
|
||||||
# footer:
|
# footer:
|
||||||
|
|||||||
129
lib/config.ts
129
lib/config.ts
@@ -148,9 +148,11 @@ interface GalleryYaml {
|
|||||||
email?: string;
|
email?: string;
|
||||||
website?: string;
|
website?: string;
|
||||||
};
|
};
|
||||||
subpages?: Array<{
|
subpages?:
|
||||||
|
| Record<string, string[] | Array<string | Record<string, string>>>
|
||||||
|
| Array<{
|
||||||
name: string;
|
name: string;
|
||||||
albums: string[];
|
albums: Array<string | Record<string, string>>;
|
||||||
password?: string;
|
password?: string;
|
||||||
grid?: {
|
grid?: {
|
||||||
columns?: number;
|
columns?: number;
|
||||||
@@ -239,6 +241,8 @@ export interface AppConfig {
|
|||||||
footer: FooterConfig | null;
|
footer: FooterConfig | null;
|
||||||
/** Show the /map page. Default: false. */
|
/** Show the /map page. Default: false. */
|
||||||
map: boolean;
|
map: boolean;
|
||||||
|
/** Explicit album name overrides from gallery.yaml. */
|
||||||
|
albumOverrides: Record<string, string>;
|
||||||
cacheTtl: number;
|
cacheTtl: number;
|
||||||
rateLimitRpm: number;
|
rateLimitRpm: number;
|
||||||
}
|
}
|
||||||
@@ -318,45 +322,101 @@ export function getConfig(): AppConfig {
|
|||||||
// Resolve theme: string shorthand → preset, object → merged with preset base
|
// Resolve theme: string shorthand → preset, object → merged with preset base
|
||||||
const theme = resolveTheme(gallery.theme);
|
const theme = resolveTheme(gallery.theme);
|
||||||
|
|
||||||
|
const albumOverrides: Record<string, string> = {};
|
||||||
|
|
||||||
|
function processAlbumEntry(entry: string | Record<string, string>, context: string): string {
|
||||||
|
if (typeof entry === 'string') {
|
||||||
|
return validateUuid(entry, context);
|
||||||
|
}
|
||||||
|
// Map format: { "uuid": "Custom Name" }
|
||||||
|
const [uuid, name] = Object.entries(entry)[0];
|
||||||
|
const validatedUuid = validateUuid(uuid, context);
|
||||||
|
albumOverrides[validatedUuid] = name;
|
||||||
|
return validatedUuid;
|
||||||
|
}
|
||||||
|
|
||||||
// Parse standalone albums
|
// Parse standalone albums
|
||||||
const standaloneAlbumIds = (gallery.albums ?? []).map((id) =>
|
const standaloneAlbumIds = (gallery.albums ?? []).map((entry) =>
|
||||||
validateUuid(id, 'gallery.yaml albums'),
|
processAlbumEntry(entry, 'gallery.yaml albums'),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (standaloneAlbumIds.length === 0 && (!gallery.subpages || gallery.subpages.length === 0)) {
|
if (standaloneAlbumIds.length === 0 && (!gallery.subpages || (Array.isArray(gallery.subpages) ? gallery.subpages.length === 0 : Object.keys(gallery.subpages).length === 0))) {
|
||||||
throw new Error('gallery.yaml must define at least one album or subpage');
|
throw new Error('gallery.yaml must define at least one album or subpage');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse subpages
|
// Parse subpages
|
||||||
const subpages: SubpageConfig[] = (gallery.subpages ?? []).map((sp) => {
|
let subpages: SubpageConfig[] = [];
|
||||||
if (!sp.name || !sp.albums || sp.albums.length === 0) {
|
|
||||||
throw new Error(
|
if (Array.isArray(gallery.subpages)) {
|
||||||
`Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`,
|
// Original list format
|
||||||
);
|
subpages = gallery.subpages.map((sp) => {
|
||||||
}
|
if (!sp.name || !sp.albums || sp.albums.length === 0) {
|
||||||
return {
|
throw new Error(
|
||||||
name: sp.name,
|
`Subpage "${sp.name || '(unnamed)'}" must have a name and at least one album`,
|
||||||
slug: slugify(sp.name),
|
);
|
||||||
albumIds: sp.albums.map((id) => validateUuid(id, `subpage "${sp.name}"`)),
|
}
|
||||||
password: sp.password,
|
return {
|
||||||
...(sp.grid
|
name: sp.name,
|
||||||
? {
|
slug: slugify(sp.name),
|
||||||
grid: {
|
albumIds: sp.albums.map((entry) => processAlbumEntry(entry, `subpage "${sp.name}"`)),
|
||||||
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
password: sp.password,
|
||||||
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
...(sp.grid
|
||||||
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
? {
|
||||||
...(sp.grid.layout != null
|
grid: {
|
||||||
? {
|
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
||||||
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
||||||
? sp.grid.layout
|
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
||||||
: 'masonry') as GridConfig['layout'],
|
...(sp.grid.layout != null
|
||||||
}
|
? {
|
||||||
: {}),
|
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
||||||
},
|
? sp.grid.layout
|
||||||
}
|
: 'masonry') as GridConfig['layout'],
|
||||||
: {}),
|
}
|
||||||
};
|
: {}),
|
||||||
});
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
} else if (gallery.subpages) {
|
||||||
|
// New map format: { "Subpage Name": ["uuid1", { "uuid2": "Name" }] } OR { "Subpage Name": { password: "...", albums: [...] } }
|
||||||
|
subpages = Object.entries(gallery.subpages).map(([name, value]) => {
|
||||||
|
// If it's an array, it's just albums
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
slug: slugify(name),
|
||||||
|
albumIds: value.map((entry) => processAlbumEntry(entry, `subpage "${name}"`)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's an object, it might have password, grid, albums
|
||||||
|
const sp = value as any;
|
||||||
|
const albumEntries = sp.albums || [];
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
slug: slugify(name),
|
||||||
|
albumIds: albumEntries.map((entry: any) => processAlbumEntry(entry, `subpage "${name}"`)),
|
||||||
|
password: sp.password,
|
||||||
|
...(sp.grid
|
||||||
|
? {
|
||||||
|
grid: {
|
||||||
|
...(sp.grid.columns != null ? { columns: sp.grid.columns } : {}),
|
||||||
|
...(sp.grid.gap != null ? { gap: sp.grid.gap } : {}),
|
||||||
|
...(sp.grid.aspectRatio != null ? { aspectRatio: sp.grid.aspectRatio } : {}),
|
||||||
|
...(sp.grid.layout != null
|
||||||
|
? {
|
||||||
|
layout: (VALID_LAYOUTS.includes(sp.grid.layout)
|
||||||
|
? sp.grid.layout
|
||||||
|
: 'masonry') as GridConfig['layout'],
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Collect ALL album IDs (standalone + subpage) — deduplicated
|
// Collect ALL album IDs (standalone + subpage) — deduplicated
|
||||||
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
|
const subpageAlbumIds = new Set(subpages.flatMap((sp) => sp.albumIds));
|
||||||
@@ -396,6 +456,7 @@ export function getConfig(): AppConfig {
|
|||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
map: gallery.map === true,
|
map: gallery.map === true,
|
||||||
|
albumOverrides,
|
||||||
cacheTtl: env.CACHE_TTL * 1000,
|
cacheTtl: env.CACHE_TTL * 1000,
|
||||||
rateLimitRpm: env.RATE_LIMIT_RPM,
|
rateLimitRpm: env.RATE_LIMIT_RPM,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -166,10 +166,14 @@ class ImmichClient {
|
|||||||
const allowedIds = new Set(this.config.albums);
|
const allowedIds = new Set(this.config.albums);
|
||||||
const filtered = all
|
const filtered = all
|
||||||
.filter((album) => allowedIds.has(album.id))
|
.filter((album) => allowedIds.has(album.id))
|
||||||
.map((album) => ({
|
.map((album) => {
|
||||||
...album,
|
const name = this.config.albumOverrides[album.id] ?? album.albumName;
|
||||||
slug: slugify(album.albumName),
|
return {
|
||||||
}));
|
...album,
|
||||||
|
albumName: name,
|
||||||
|
slug: slugify(name),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Log album summary on first load so admins can see what's published
|
// Log album summary on first load so admins can see what's published
|
||||||
if (!this.hasLoggedAlbums) {
|
if (!this.hasLoggedAlbums) {
|
||||||
@@ -277,7 +281,10 @@ class ImmichClient {
|
|||||||
|
|
||||||
// Filter out trashed assets
|
// Filter out trashed assets
|
||||||
album.assets = (album.assets || []).filter((a) => !a.isTrashed);
|
album.assets = (album.assets || []).filter((a) => !a.isTrashed);
|
||||||
album.slug = slugify(album.albumName);
|
|
||||||
|
const name = this.config.albumOverrides[album.id] ?? album.albumName;
|
||||||
|
album.albumName = name;
|
||||||
|
album.slug = slugify(name);
|
||||||
|
|
||||||
cache.set(cacheKey, album, this.config.cacheTtl);
|
cache.set(cacheKey, album, this.config.cacheTtl);
|
||||||
return album;
|
return album;
|
||||||
|
|||||||
Reference in New Issue
Block a user