release: v0.17.0 (#677)
See CHANGELOG.md, section [0.17.0] — 2026-09-23.
This commit is contained in:
@@ -31,6 +31,12 @@ interface AlbumDetailViewProps {
|
||||
proofing?: boolean;
|
||||
/** Offer the "send by email" button in the proofing modal. */
|
||||
allowMailto?: boolean;
|
||||
/**
|
||||
* The ZIP endpoint for this album, when it offers downloads (#475). Renders a
|
||||
* "download album" link in the header and lets the proofing modal offer a
|
||||
* "download selected" action.
|
||||
*/
|
||||
downloadArchiveUrl?: string;
|
||||
/** Neighbouring albums in the surrounding list, for the foot navigation. */
|
||||
nav?: AlbumNavPair;
|
||||
/** JSON-LD for this album, or null when no site URL is configured (#472). */
|
||||
@@ -52,6 +58,7 @@ export function AlbumDetailView({
|
||||
showGear = true,
|
||||
proofing,
|
||||
allowMailto,
|
||||
downloadArchiveUrl,
|
||||
nav,
|
||||
structuredData,
|
||||
}: AlbumDetailViewProps) {
|
||||
@@ -87,6 +94,24 @@ export function AlbumDetailView({
|
||||
</p>
|
||||
)}
|
||||
{album.description && <p className="album-header__description">{album.description}</p>}
|
||||
{downloadArchiveUrl && (
|
||||
<a className="album-header__download" href={downloadArchiveUrl}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4" />
|
||||
<polyline points="7 10 12 15 17 10" />
|
||||
<line x1="12" y1="15" x2="12" y2="3" />
|
||||
</svg>
|
||||
{t.common.downloadAlbum}
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<p className="album-header__meta">
|
||||
<span className="album-header__meta-count">{t.common.photos(images.length)}</span>
|
||||
@@ -102,6 +127,8 @@ export function AlbumDetailView({
|
||||
showExifPanel={showExifPanel}
|
||||
proofing={proofing}
|
||||
allowMailto={allowMailto}
|
||||
downloadArchiveUrl={downloadArchiveUrl}
|
||||
albumName={album.albumName}
|
||||
/>
|
||||
{nav && <AlbumNav {...nav} />}
|
||||
</>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Lightbox, type LightboxWatermark } from '@/components/Lightbox';
|
||||
import { ProofingProvider, useProofing } from '@/components/ProofingContext';
|
||||
import { ProofingModal } from '@/components/ProofingModal';
|
||||
import { FadeIn } from '@/components/FadeIn';
|
||||
import { LeafletMap, escapeHtml } from '@/components/LeafletMap';
|
||||
import type { ParsedEssay, EssayBlock } from '@/lib/essay';
|
||||
import { renderInlineMarkdown } from '@/lib/essay';
|
||||
import type { PhotoItem } from './PhotoGrid';
|
||||
@@ -59,6 +60,77 @@ function EssayViewContent({
|
||||
? assetMap.get(essay.frontmatter.coverAssetId)
|
||||
: undefined;
|
||||
|
||||
/**
|
||||
* One photo tile of a pair or a grid: sized by its real ratio (see
|
||||
* .essay-pair-grid), opens the lightbox, carries the proofing heart. The pair
|
||||
* used to spell this out a second time next to the photo case; a third copy
|
||||
* for the grid is where that stops.
|
||||
*/
|
||||
const renderTile = ({ item, index }: { item: PhotoItem; index: number }, key: number) => {
|
||||
const isFav = proofing ? proofing.isFavorite(item.id) : false;
|
||||
return (
|
||||
<div
|
||||
key={key}
|
||||
className="essay-image-wrapper photo-grid__item"
|
||||
onClick={() => setLightboxIndex(index)}
|
||||
style={{
|
||||
...(item.dominantColor ? { backgroundColor: item.dominantColor } : {}),
|
||||
flexGrow: item.aspectRatio ?? 1.5,
|
||||
aspectRatio: `${item.aspectRatio ?? 1.5}`,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.previewUrl || item.thumbUrl}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, 550px"
|
||||
loading="lazy"
|
||||
{...(item.blurDataURL
|
||||
? { placeholder: 'blur' as const, blurDataURL: item.blurDataURL }
|
||||
: {})}
|
||||
/>
|
||||
{proofing && (
|
||||
<button
|
||||
type="button"
|
||||
className="photo-grid__fav-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
proofing.toggleFavorite(item.id);
|
||||
}}
|
||||
aria-label={isFav ? t.proofing.removeFavorite : t.proofing.addFavorite}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
right: '12px',
|
||||
zIndex: 5,
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: isFav ? '#ff4d4f' : 'rgba(255,255,255,0.85)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.6))',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill={isFav ? 'currentColor' : 'none'}
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const renderBlock = (block: EssayBlock, idx: number) => {
|
||||
switch (block.type) {
|
||||
case 'heading': {
|
||||
@@ -183,72 +255,42 @@ function EssayViewContent({
|
||||
return (
|
||||
<FadeIn key={idx}>
|
||||
<div className="essay-figure essay-figure--wide">
|
||||
<div className="essay-pair-grid">
|
||||
{[res1, res2].map(({ item, index }, pIdx) => {
|
||||
const isFav = proofing ? proofing.isFavorite(item.id) : false;
|
||||
return (
|
||||
<div
|
||||
key={pIdx}
|
||||
className="essay-image-wrapper photo-grid__item"
|
||||
onClick={() => setLightboxIndex(index)}
|
||||
style={{
|
||||
...(item.dominantColor ? { backgroundColor: item.dominantColor } : {}),
|
||||
// Width proportional to the ratio — see .essay-pair-grid.
|
||||
flexGrow: item.aspectRatio ?? 1.5,
|
||||
aspectRatio: `${item.aspectRatio ?? 1.5}`,
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
src={item.previewUrl || item.thumbUrl}
|
||||
alt=""
|
||||
fill
|
||||
sizes="(max-width: 640px) 100vw, 550px"
|
||||
loading="lazy"
|
||||
{...(item.blurDataURL
|
||||
? { placeholder: 'blur' as const, blurDataURL: item.blurDataURL }
|
||||
: {})}
|
||||
/>
|
||||
{proofing && (
|
||||
<button
|
||||
type="button"
|
||||
className="photo-grid__fav-btn"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
proofing.toggleFavorite(item.id);
|
||||
}}
|
||||
aria-label={isFav ? t.proofing.removeFavorite : t.proofing.addFavorite}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '12px',
|
||||
right: '12px',
|
||||
zIndex: 5,
|
||||
width: '44px',
|
||||
height: '44px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
color: isFav ? '#ff4d4f' : 'rgba(255,255,255,0.85)',
|
||||
filter: 'drop-shadow(0 2px 4px rgba(0,0,0,0.6))',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill={isFav ? 'currentColor' : 'none'}
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="essay-pair-grid">{[res1, res2].map(renderTile)}</div>
|
||||
{block.caption && (
|
||||
<figcaption
|
||||
className="essay-figcaption"
|
||||
dangerouslySetInnerHTML={{ __html: block.caption }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
|
||||
case 'photo-grid': {
|
||||
// Unresolved ids drop out individually; a grid with fewer than two
|
||||
// photos left is not a grid any more and renders as nothing, like a
|
||||
// pair with a missing half.
|
||||
const resolved = block.assetIds.flatMap((id) => {
|
||||
const r = assetMap.get(id);
|
||||
return r ? [r] : [];
|
||||
});
|
||||
if (resolved.length < 2) return null;
|
||||
|
||||
// Rows of three, each justified like a pair: one shared height, widths
|
||||
// from the real ratios, nothing cropped.
|
||||
const rows: (typeof resolved)[] = [];
|
||||
for (let i = 0; i < resolved.length; i += 3) rows.push(resolved.slice(i, i + 3));
|
||||
|
||||
return (
|
||||
<FadeIn key={idx}>
|
||||
<div className="essay-figure essay-figure--wide">
|
||||
<div className="essay-grid">
|
||||
{rows.map((row, rIdx) => (
|
||||
<div key={rIdx} className="essay-grid-row">
|
||||
{row.map(renderTile)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{block.caption && (
|
||||
<figcaption
|
||||
@@ -261,6 +303,70 @@ function EssayViewContent({
|
||||
);
|
||||
}
|
||||
|
||||
case 'facts':
|
||||
if (block.items.length === 0) return null;
|
||||
return (
|
||||
<FadeIn key={idx}>
|
||||
<dl className="essay-facts">
|
||||
{block.items.map((item, i) => (
|
||||
<div key={i} className="essay-facts__item">
|
||||
<dt className="essay-facts__label">{item.label}</dt>
|
||||
<dd
|
||||
className="essay-facts__value"
|
||||
dangerouslySetInnerHTML={{ __html: item.value }}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
</FadeIn>
|
||||
);
|
||||
|
||||
case 'map': {
|
||||
// The server page attaches `pins` (typed points as typed, photo pins
|
||||
// quantised). Without them — the studio preview — the typed points
|
||||
// still show, so an author sees the map take shape while writing;
|
||||
// photo pins appear on the live page. No pins at all: no map.
|
||||
const pins =
|
||||
block.pins ??
|
||||
block.items.flatMap((item) =>
|
||||
item.kind === 'point'
|
||||
? [
|
||||
item.label
|
||||
? { lat: item.lat, lng: item.lng, label: item.label }
|
||||
: { lat: item.lat, lng: item.lng },
|
||||
]
|
||||
: [],
|
||||
);
|
||||
if (pins.length === 0) return null;
|
||||
return (
|
||||
<FadeIn key={idx}>
|
||||
<figure className="essay-figure essay-figure--wide">
|
||||
<LeafletMap
|
||||
className="essay-map"
|
||||
fitMaxZoom={13}
|
||||
line={block.line}
|
||||
markers={pins.map((pin, i) => ({
|
||||
lat: pin.lat,
|
||||
lng: pin.lng,
|
||||
html: `<div class="map-marker">${i + 1}</div>`,
|
||||
...(pin.label
|
||||
? {
|
||||
popupHtml: `<div class="map-popup"><div class="map-popup__body"><h3 class="map-popup__city">${escapeHtml(pin.label)}</h3></div></div>`,
|
||||
}
|
||||
: {}),
|
||||
}))}
|
||||
/>
|
||||
{block.caption && (
|
||||
<figcaption
|
||||
className="essay-figcaption"
|
||||
dangerouslySetInnerHTML={{ __html: block.caption }}
|
||||
/>
|
||||
)}
|
||||
</figure>
|
||||
</FadeIn>
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
@@ -399,7 +505,13 @@ export function EssayView(props: EssayViewProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ProofingProvider albumTokens={albumTokens} allowMailto={props.allowMailto ?? true}>
|
||||
<ProofingProvider
|
||||
albumTokens={albumTokens}
|
||||
// The essay's own title, so its favourites do not share the provider's
|
||||
// default key with every album (and story) on the site.
|
||||
albumName={props.title || props.essay.frontmatter.title}
|
||||
allowMailto={props.allowMailto ?? true}
|
||||
>
|
||||
<EssayViewContent {...props} />
|
||||
</ProofingProvider>
|
||||
);
|
||||
|
||||
@@ -60,6 +60,17 @@ interface PhotoGridProps {
|
||||
proofing?: boolean;
|
||||
/** Offer the "send by email" button in the proofing modal. */
|
||||
allowMailto?: boolean;
|
||||
/**
|
||||
* The ZIP endpoint for this album, when it offers downloads. Lets the
|
||||
* proofing modal offer a "download selected" action (#475).
|
||||
*/
|
||||
downloadArchiveUrl?: string;
|
||||
/**
|
||||
* The album's display name. Scopes the proofing favourites to this album —
|
||||
* without it every album shares the provider's default storage key, so a
|
||||
* selection made in one album leaks into the next.
|
||||
*/
|
||||
albumName?: string;
|
||||
}
|
||||
|
||||
function PhotoGridInner({
|
||||
@@ -193,7 +204,7 @@ function PhotoGridInner({
|
||||
}}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
aria-label={`View photo ${index + 1}`}
|
||||
aria-label={t.lightbox.openPhoto(index + 1)}
|
||||
aria-haspopup="dialog"
|
||||
style={{
|
||||
...(asset.dominantColor ? { backgroundColor: asset.dominantColor } : {}),
|
||||
@@ -366,7 +377,12 @@ export function PhotoGrid(props: PhotoGridProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<ProofingProvider albumTokens={albumTokens} allowMailto={props.allowMailto ?? true}>
|
||||
<ProofingProvider
|
||||
albumTokens={albumTokens}
|
||||
albumName={props.albumName}
|
||||
allowMailto={props.allowMailto ?? true}
|
||||
downloadArchiveUrl={props.downloadArchiveUrl}
|
||||
>
|
||||
<PhotoGridInner {...props} />
|
||||
</ProofingProvider>
|
||||
);
|
||||
|
||||
@@ -200,3 +200,87 @@
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* Map: the entry's geotagged photos as pins, a story-sized Leaflet map. */
|
||||
.essay-map {
|
||||
width: 100%;
|
||||
height: 420px;
|
||||
border-radius: var(--radius-sm, 0);
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border-subtle, rgba(128, 128, 128, 0.15));
|
||||
background: var(--bg-secondary, #222);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.essay-map {
|
||||
height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Facts: a short definition list in the text column — distance, venue, date. */
|
||||
.essay-facts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 1rem 2rem;
|
||||
max-width: var(--essay-contained, 680px);
|
||||
margin: 2.5rem auto;
|
||||
padding: 1.25rem 0;
|
||||
border-top: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.12));
|
||||
border-bottom: 1px solid var(--border-subtle, rgba(255, 255, 255, 0.12));
|
||||
}
|
||||
|
||||
.essay-facts__item {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.essay-facts__label {
|
||||
font-family: var(--font-caption);
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.15em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.essay-facts__value {
|
||||
margin: 0;
|
||||
font-family: var(--font-sans);
|
||||
font-size: 1rem;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.essay-facts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid: rows of up to three, each row justified exactly like a pair. */
|
||||
.essay-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
width: 100%;
|
||||
max-width: var(--essay-wide);
|
||||
margin: 3.5rem auto;
|
||||
}
|
||||
|
||||
.essay-grid-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
|
||||
.essay-grid-row > .essay-image-wrapper {
|
||||
flex-basis: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.essay-grid-row {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.essay-grid-row > .essay-image-wrapper {
|
||||
flex: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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