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:
56
lib/proofExport.ts
Normal file
56
lib/proofExport.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user