Forty-one commits since v0.11.0. See CHANGELOG.md for the full list and the upgrade notes.
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
/**
|
||
* Derivations for the album header — what a whole album can honestly say about
|
||
* itself. Server-side only by convention, but there is nothing here that needs
|
||
* `fs`, so the unit tests import it directly.
|
||
*/
|
||
|
||
import type { ImmichAlbum } from './immich';
|
||
|
||
/**
|
||
* Extra detail for the album header — shooting date and camera/lens. Both are
|
||
* rendered in their own spans and hidden by default; only presets that ask for
|
||
* them show them, so the header keeps reading "86 photos" everywhere else.
|
||
*
|
||
* Both used to be read off the first asset that happened to carry EXIF, so an
|
||
* album shot on three bodies advertised whichever one sorted first — and
|
||
* changing the album's sort mode changed the camera it claimed (#509). The
|
||
* header now only states what the whole album agrees on, and spans a date range
|
||
* when the photos do.
|
||
*/
|
||
export function albumMetaDetail(
|
||
album: ImmichAlbum,
|
||
showGear: boolean,
|
||
): { date?: string; gear?: string } {
|
||
const dates = album.assets
|
||
.map((a) => a.exifInfo?.dateTimeOriginal || a.fileCreatedAt)
|
||
.filter((d): d is string => !!d)
|
||
.map((d) => new Date(d).getTime())
|
||
.filter((t) => !Number.isNaN(t))
|
||
.sort((a, b) => a - b);
|
||
|
||
const first = dates[0];
|
||
const last = dates[dates.length - 1];
|
||
const from = first !== undefined ? formatMonthYear(new Date(first).toISOString()) : undefined;
|
||
const to = last !== undefined ? formatMonthYear(new Date(last).toISOString()) : undefined;
|
||
const date = from && to ? (from === to ? from : `${from} – ${to}`) : from || to;
|
||
|
||
return {
|
||
...(date ? { date } : {}),
|
||
...(showGear ? withGear(album) : {}),
|
||
};
|
||
}
|
||
|
||
/**
|
||
* The camera and lens, but only when every photo that names one names the same
|
||
* one. A mixed album says nothing rather than picking a winner.
|
||
*/
|
||
function withGear(album: ImmichAlbum): { gear?: string } {
|
||
const unique = (values: (string | null | undefined)[]): string | undefined => {
|
||
const set = new Set(values.filter((v): v is string => !!v));
|
||
return set.size === 1 ? [...set][0] : undefined;
|
||
};
|
||
|
||
const model = unique(album.assets.map((a) => a.exifInfo?.model));
|
||
const lens = unique(album.assets.map((a) => a.exifInfo?.lensModel));
|
||
const gear = [model, lens].filter(Boolean).join(' · ');
|
||
|
||
return gear ? { gear } : {};
|
||
}
|
||
|
||
function formatMonthYear(iso: string): string | undefined {
|
||
const d = new Date(iso);
|
||
if (Number.isNaN(d.getTime())) return undefined;
|
||
return `${String(d.getMonth() + 1).padStart(2, '0')}/${d.getFullYear()}`;
|
||
}
|