release: v0.17.0 (#677)
See CHANGELOG.md, section [0.17.0] — 2026-09-23.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
assetCaption,
|
||||
assetExifSummary,
|
||||
downloadUrl,
|
||||
archiveUrl,
|
||||
assetAspectRatio,
|
||||
} from '@/lib/urls';
|
||||
import { encodeAssetId, decodeAssetId } from '@/lib/tokens';
|
||||
@@ -44,7 +45,11 @@ import { albumStructuredData } from '@/lib/structuredData';
|
||||
import { absoluteUrl } from '@/lib/siteUrl';
|
||||
import { SubpageGridView } from './SubpageGridView';
|
||||
import { EssayView } from './EssayView';
|
||||
import { parseEssayMarkdown } from '@/lib/essay';
|
||||
import { parseEssayMarkdown, type EssayBlock } from '@/lib/essay';
|
||||
import { pinsForEntry } from '@/lib/journalMap';
|
||||
import { expandAlbumBlocks } from '@/lib/journalAlbum';
|
||||
import { mapBlockAssetIds } from '@/lib/journal';
|
||||
import { strictestPrecision, type LocationPrecision } from '@/lib/mapPrecision';
|
||||
import { loadEssayFromFile } from '@/lib/admin/journal-service';
|
||||
import { getServerDictionary } from '@/lib/i18n/server';
|
||||
|
||||
@@ -374,12 +379,13 @@ export default async function PathPage({ params, searchParams }: PathPageProps)
|
||||
layout={resolveLayout(mergeAlbumGrid(album.id, spGrid))}
|
||||
gridStyle={buildGridStyle(mergeAlbumGrid(album.id, spGrid))}
|
||||
backLinkHref={`/${subpageSlug}`}
|
||||
backLinkLabel={`Back to ${subpageName}`}
|
||||
backLinkLabel={getServerDictionary().common.backTo(subpageName)}
|
||||
watermark={config.watermark}
|
||||
showExifPanel={hasExifPanelContent(config.exif)}
|
||||
showGear={config.exif.camera}
|
||||
proofing={proofingFor(subpageData?.subpage)}
|
||||
allowMailto={config.proofing.allowMailto}
|
||||
downloadArchiveUrl={config.albumDownloads[album.id] ? archiveUrl(album.id) : undefined}
|
||||
{...heroData}
|
||||
/>
|
||||
);
|
||||
@@ -433,6 +439,43 @@ export default async function PathPage({ params, searchParams }: PathPageProps)
|
||||
openAlbums.map((a) => immich.getAlbumBySlug(a.slug, slug, forceFresh)),
|
||||
);
|
||||
const allAssets = allAlbums.flatMap((a) => (a ? a.assets : []));
|
||||
|
||||
// Album blocks in an essay file: expand them the way the journal page
|
||||
// does, from a raw fetch, and add those photos to the page's assets.
|
||||
// Their ids are encoded here because this path hands EssayView tokens.
|
||||
if (essayParsed?.blocks.some((b) => b.type === 'album')) {
|
||||
const byAlbum = new Map<string, typeof allAssets>();
|
||||
for (const b of essayParsed.blocks) {
|
||||
if (b.type !== 'album' || !b.albumId || byAlbum.has(b.albumId)) continue;
|
||||
try {
|
||||
byAlbum.set(b.albumId, await immich.getAlbumAssetsRaw(b.albumId));
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[essay] ${slug}: album ${b.albumId} could not be loaded:`, error);
|
||||
}
|
||||
}
|
||||
const expanded = expandAlbumBlocks(
|
||||
essayParsed.blocks,
|
||||
(id) => byAlbum.get(id),
|
||||
config.albumManualOrders,
|
||||
);
|
||||
const albumIds = new Set(expanded.albumAssetIds);
|
||||
const known = new Set(allAssets.map((a) => a.id));
|
||||
for (const list of byAlbum.values()) {
|
||||
for (const asset of list) {
|
||||
if (!albumIds.has(asset.id) || known.has(asset.id)) continue;
|
||||
known.add(asset.id);
|
||||
allAssets.push(asset);
|
||||
}
|
||||
}
|
||||
essayParsed = {
|
||||
...essayParsed,
|
||||
blocks: expanded.blocks.map((b) =>
|
||||
mapBlockAssetIds(b, (id) => (albumIds.has(id) ? encodeAssetId(id) : id)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
const images = toPhotoItems(
|
||||
allAssets,
|
||||
config.exif.onHover && config.exif.camera,
|
||||
@@ -459,6 +502,32 @@ export default async function PathPage({ params, searchParams }: PathPageProps)
|
||||
};
|
||||
}
|
||||
|
||||
// Map blocks: pins from the subpage's own albums, each album's
|
||||
// `location:` precision applied (an asset in two albums takes the
|
||||
// stricter). Dropped entirely when the site has no map.
|
||||
if (essayParsed.blocks.some((b) => b.type === 'map')) {
|
||||
const precisionByAsset = new Map<string, LocationPrecision>();
|
||||
for (const album of allAlbums) {
|
||||
if (!album) continue;
|
||||
const level = config.albumLocationPrecision[album.id] ?? 'exact';
|
||||
for (const asset of album.assets) {
|
||||
const prev = precisionByAsset.get(asset.id);
|
||||
precisionByAsset.set(asset.id, prev ? strictestPrecision([prev, level]) : level);
|
||||
}
|
||||
}
|
||||
const precisionOf = (id: string) => precisionByAsset.get(id) ?? 'exact';
|
||||
const allIds = allAssets.map((a) => a.id);
|
||||
essayParsed = {
|
||||
...essayParsed,
|
||||
blocks: essayParsed.blocks.flatMap((b): EssayBlock[] => {
|
||||
if (b.type !== 'map') return [b];
|
||||
if (!config.map) return [];
|
||||
const pins = pinsForEntry(b.items, allAssets, allIds, precisionOf);
|
||||
return [{ type: 'map', caption: b.caption, line: b.line, items: [], pins }];
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
// A published story is not an album handover, so an essay only gets the
|
||||
// proofing controls when its subpage asks for them explicitly — the
|
||||
// global default does not reach in here.
|
||||
@@ -521,6 +590,7 @@ export default async function PathPage({ params, searchParams }: PathPageProps)
|
||||
showGear={config.exif.camera}
|
||||
proofing={proofingFor(result.subpage)}
|
||||
allowMailto={config.proofing.allowMailto}
|
||||
downloadArchiveUrl={config.albumDownloads[album.id] ? archiveUrl(album.id) : undefined}
|
||||
{...heroData}
|
||||
/>
|
||||
);
|
||||
@@ -622,6 +692,7 @@ export default async function PathPage({ params, searchParams }: PathPageProps)
|
||||
showGear={config.exif.camera}
|
||||
proofing={proofingFor()}
|
||||
allowMailto={config.proofing.allowMailto}
|
||||
downloadArchiveUrl={config.albumDownloads[album.id] ? archiveUrl(album.id) : undefined}
|
||||
{...heroData}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user