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>
87 lines
3.1 KiB
TypeScript
87 lines
3.1 KiB
TypeScript
import type { JournalBlock } from '@/lib/journal';
|
|
|
|
/**
|
|
* Block-list operations of the journal editor, as plain functions so they can
|
|
* be tested without rendering the editor (#555).
|
|
*/
|
|
|
|
/**
|
|
* Photo blocks may carry a legacy positional reference ("1", "2") instead of an
|
|
* asset UUID. Those only ever resolved against a subpage's album; on a
|
|
* standalone journal page there is no album, so the photo silently disappears.
|
|
* Flag them in the editor so the author can re-pick before publishing.
|
|
*/
|
|
const ASSET_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
|
export function isLegacyAssetRef(assetId: string): boolean {
|
|
return assetId.length > 0 && !ASSET_UUID.test(assetId);
|
|
}
|
|
|
|
/**
|
|
* Slots after a multi-pick (#602): picked photos fill the empty slots first,
|
|
* in pick order, and the rest are appended. With `capacity` (a pair holds two)
|
|
* the list never grows past it; overflow picks are dropped.
|
|
*/
|
|
export function fillAssetSlots(slots: string[], picked: string[], capacity?: number): string[] {
|
|
const next = [...slots];
|
|
const queue = [...picked];
|
|
for (let i = 0; i < next.length && queue.length > 0; i++) {
|
|
if (!next[i]) next[i] = queue.shift()!;
|
|
}
|
|
next.push(...queue);
|
|
return capacity === undefined ? next : next.slice(0, capacity);
|
|
}
|
|
|
|
/** One photo block per picked asset, for "+ Photos" (#602). */
|
|
export function createPhotoBlocks(assetIds: string[]): JournalBlock[] {
|
|
return assetIds.map((assetId) => ({ type: 'photo', assetId, caption: '', layout: 'contained' }));
|
|
}
|
|
|
|
/** The block the "Add Block" toolbar inserts for each type. */
|
|
export function createBlock(type: JournalBlock['type']): JournalBlock {
|
|
switch (type) {
|
|
case 'heading':
|
|
return { type: 'heading', level: 2, text: 'New Heading' };
|
|
case 'paragraph':
|
|
return { type: 'paragraph', html: 'Enter paragraph text here...' };
|
|
case 'quote':
|
|
return { type: 'quote', text: 'Enter quote text...', author: '' };
|
|
case 'photo':
|
|
return { type: 'photo', assetId: '', caption: '', layout: 'contained' };
|
|
case 'photo-pair':
|
|
return { type: 'photo-pair', assetIds: ['', ''], caption: '' };
|
|
case 'photo-grid':
|
|
return { type: 'photo-grid', assetIds: ['', '', ''], caption: '' };
|
|
case 'facts':
|
|
return {
|
|
type: 'facts',
|
|
items: [
|
|
{ label: '', value: '' },
|
|
{ label: '', value: '' },
|
|
],
|
|
};
|
|
case 'map':
|
|
return { type: 'map', caption: '', line: true, items: [] };
|
|
case 'album':
|
|
return { type: 'album', albumId: '', layout: 'grid' };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The blocks with the one at `index` moved a step up or down, or null when it
|
|
* is already at that end — the arrow buttons are disabled there, but a stale
|
|
* click must not reorder anything.
|
|
*/
|
|
export function moveBlock(
|
|
blocks: JournalBlock[],
|
|
index: number,
|
|
direction: 'up' | 'down',
|
|
): JournalBlock[] | null {
|
|
const target = direction === 'up' ? index - 1 : index + 1;
|
|
if (target < 0 || target >= blocks.length) return null;
|
|
const next = [...blocks];
|
|
const [moved] = next.splice(index, 1);
|
|
next.splice(target, 0, moved);
|
|
return next;
|
|
}
|