release: v0.19.0

Release v0.19.0. See the `## [0.19.0] — 2026-09-29` section of
[CHANGELOG.md](https://github.com/ralksta/immich-folio/blob/dev/CHANGELOG.md)
for the full list.

Highlights: client proofing links (#683), custom content pages (#738),
asset picker search (#740), concurrent-edit detection (#742), env-locked
settings (#739), an accessibility baseline (#741), three security fixes
with advisories (#737, #755, #759), and a pre-release quality pass
(#780–#797).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: ralksta <ralksta@users.noreply.github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Ralf <rohlenbo79@gmail.com>
Co-authored-by: Paul Thies <110185070+ImScheinox@users.noreply.github.com>
Co-authored-by: lancetm714 <108419425+lancetm714@users.noreply.github.com>
Co-authored-by: Lance Martin <lancetaylor95@gmail.com>
This commit is contained in:
Ralksta
2026-09-29 22:50:30 +02:00
committed by GitHub
parent 5f251923f0
commit ad8b09700b
297 changed files with 19398 additions and 1800 deletions

56
lib/proofExport.ts Normal file
View File

@@ -0,0 +1,56 @@
/**
* Turning a submitted selection into something the photographer can act on.
*
* The client picked from web previews; the photographer edits RAW files. So
* the most useful export is the list of file names *without* their extension:
* pasted into Lightroom's or Capture One's filename filter, it matches
* IMG_0412.CR3 as readily as IMG_0412.JPG.
*/
export interface ExportAsset {
/** 1-based position in the album, as the client saw it. */
position: number;
fileName: string;
takenAt?: string;
}
export type ExportFormat = 'lightroom' | 'txt' | 'csv';
/** `IMG_0412.JPG` → `IMG_0412`. A name without a dot stays as it is. */
export function baseName(fileName: string): string {
const dot = fileName.lastIndexOf('.');
return dot > 0 ? fileName.slice(0, dot) : fileName;
}
/** Quote a CSV field when it needs it (RFC 4180), and defuse spreadsheet formulas. */
function csvField(value: string): string {
// A leading =, +, - or @ makes Excel and LibreOffice evaluate the cell. File
// names come from the client's camera, not from us — never let one run.
// Tab and CR start a formula in some spreadsheet apps as well (OWASP).
const safe = /^[=+\-@\t\r]/.test(value) ? `'${value}` : value;
return /[",\r\n]/.test(safe) ? `"${safe.replace(/"/g, '""')}"` : safe;
}
export function formatExport(assets: ExportAsset[], format: ExportFormat): string {
switch (format) {
case 'lightroom':
// De-duplicated: a RAW+JPEG pair uploaded to Immich shares one base name.
return Array.from(new Set(assets.map((a) => baseName(a.fileName)))).join(', ');
case 'txt':
return assets.map((a) => a.fileName).join('\n') + (assets.length ? '\n' : '');
case 'csv': {
const rows = [['position', 'file_name', 'taken_at']].concat(
assets.map((a) => [String(a.position), a.fileName, a.takenAt ?? '']),
);
return rows.map((row) => row.map(csvField).join(',')).join('\r\n') + '\r\n';
}
}
}
export function exportContentType(format: ExportFormat): string {
return format === 'csv' ? 'text/csv; charset=utf-8' : 'text/plain; charset=utf-8';
}
export function exportExtension(format: ExportFormat): string {
return format === 'csv' ? 'csv' : 'txt';
}