2026-08-23 23:18:14 +02:00
|
|
|
/**
|
|
|
|
|
* Client-safe EXIF formatting. No `fs`, no crypto — `lib/urls.ts` cannot be
|
|
|
|
|
* imported from a client component, and the lightbox needs this.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The camera as one readable name.
|
|
|
|
|
*
|
|
|
|
|
* EXIF carries maker and model separately, and most makers repeat themselves in
|
|
|
|
|
* the model: Immich reports `make: "LEICA CAMERA AG"` with `model: "LEICA Q3"`,
|
|
|
|
|
* Nikon `"NIKON CORPORATION"` with `"NIKON Z 6"`. Printing both gave
|
|
|
|
|
* "LEICA CAMERA AG LEICA Q3", which is wrong twice over — it says the brand
|
|
|
|
|
* twice, and it was long enough to collide with its own label in the info
|
|
|
|
|
* panel (#514).
|
|
|
|
|
*
|
|
|
|
|
* Only the maker's first word is compared, and only against whole words of the
|
|
|
|
|
* model, so "OM Digital Solutions" is dropped from "OM-1" without a stray "om"
|
|
|
|
|
* inside another word passing for a brand. Makers the model does not name —
|
|
|
|
|
* Sony's "ILCE-7M4", say — keep their prefix.
|
|
|
|
|
*/
|
|
|
|
|
export function formatCamera(make?: string | null, model?: string | null): string {
|
|
|
|
|
const cleanModel = model?.trim() ?? '';
|
|
|
|
|
const cleanMake = make?.trim() ?? '';
|
|
|
|
|
|
|
|
|
|
if (!cleanModel) return cleanMake;
|
|
|
|
|
if (!cleanMake) return cleanModel;
|
|
|
|
|
|
|
|
|
|
const brand = cleanMake.split(/\s+/)[0].toLowerCase();
|
|
|
|
|
const lowerModel = cleanModel.toLowerCase();
|
|
|
|
|
const namesBrand = lowerModel.split(/[\s\-_]+/).includes(brand) || lowerModel.startsWith(brand);
|
|
|
|
|
|
|
|
|
|
return namesBrand ? cleanModel : `${cleanMake} ${cleanModel}`;
|
|
|
|
|
}
|
2026-10-02 18:14:23 +02:00
|
|
|
|
|
|
|
|
function escapeRegExp(value: string): string {
|
|
|
|
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The lens, without the camera it is built into.
|
|
|
|
|
*
|
|
|
|
|
* Phones report the whole device as the lens: an iPhone gives
|
|
|
|
|
* `model: "iPhone 14 Pro"` and `lensModel: "iPhone 14 Pro back triple camera
|
|
|
|
|
* 6.86mm f/1.78"`, so the tile and the lightbox strip read the model twice
|
|
|
|
|
* ("IPHONE 14 PRO · IPHONE 14 PRO BACK TRIPLE CAMERA …"). When the camera is
|
|
|
|
|
* shown next to it, the model is removed from the lens as a whole phrase
|
|
|
|
|
* (case-insensitive, never inside another word). A lens that is nothing but
|
|
|
|
|
* the model says nothing the camera does not, and becomes empty.
|
|
|
|
|
*/
|
|
|
|
|
export function formatLens(lens?: string | null, model?: string | null): string {
|
|
|
|
|
const cleanLens = lens?.trim() ?? '';
|
|
|
|
|
const cleanModel = model?.trim() ?? '';
|
|
|
|
|
if (!cleanLens || !cleanModel) return cleanLens;
|
|
|
|
|
|
|
|
|
|
const phrase = new RegExp(`(^|\\s)${escapeRegExp(cleanModel)}(?=\\s|$)`, 'i');
|
|
|
|
|
if (!phrase.test(cleanLens)) return cleanLens;
|
|
|
|
|
return cleanLens.replace(phrase, ' ').replace(/\s+/g, ' ').trim();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** The aperture sign. `text-transform: uppercase` turns it into "Ƒ". */
|
|
|
|
|
export const APERTURE_SIGN = 'ƒ';
|