/** * Bulk-action status, written by BulkActionBar and read by the header * StatusPill and individual PhotoTile overlays. * * State lifecycle: * startBulk → pill spins, all target tiles go "pending" * setDetail → pill shows the filename currently being processed (fan-out ops) * doneBulk → pill shows completion label, tiles flash green, auto-clears after 3 s * removedBulk→ destructive completion (archive / delete): tiles flash a red cross, * then the caller hides them via markRemoved; map auto-clears after 3 s * failBulk → tiles flash red, auto-clears after 2 s */ import { SvelteMap, SvelteSet } from 'svelte/reactivity'; interface BulkActionState { active: boolean; label: string; detail?: string; } export const bulkAction = $state({ active: false, label: '' }); // SvelteMap (not `$state(new Map())`) so a `.get(uid)` read in a PhotoTile // reliably re-runs when the entry flips — the plain-Map proxy form wasn't // re-rendering the timeline tiles' overlay. export const bulkPhotoStates = new SvelteMap(); /** * UIDs hidden from the timeline grid the instant a removing action (archive / * delete / restore) succeeds, so tiles vanish without waiting on the ~1s * server-reconcile refetch. The caller clears each id once the refetch lands. * This is a pure UI overlay — it never touches the query cache, so it can't * corrupt the facet/drill caches the way a direct cache eviction did. */ export const removedIds = $state(new SvelteSet()); export function markRemoved(ids: string[]): void { for (const id of ids) removedIds.add(id); } export function clearRemoved(ids: string[]): void { for (const id of ids) removedIds.delete(id); } let doneTimer: ReturnType | null = null; export function startBulk(label: string, ids: string[]): void { if (doneTimer !== null) { clearTimeout(doneTimer); doneTimer = null; } bulkPhotoStates.clear(); for (const id of ids) bulkPhotoStates.set(id, 'pending'); bulkAction.active = true; bulkAction.label = label; bulkAction.detail = undefined; } export function setDetail(path: string): void { bulkAction.detail = path; } export function doneBulk(label: string, ids: string[]): void { for (const id of ids) bulkPhotoStates.set(id, 'done'); bulkAction.active = false; bulkAction.label = label; bulkAction.detail = undefined; if (doneTimer !== null) clearTimeout(doneTimer); doneTimer = setTimeout(() => { bulkAction.label = ''; bulkPhotoStates.clear(); doneTimer = null; }, 3000); } /** * Destructive completion (archive / permanent delete): flash a red cross on the * target tiles instead of the green check. The caller hides the tiles via * markRemoved shortly after the flash; this timer only cleans up the state map. */ export function removedBulk(label: string, ids: string[]): void { for (const id of ids) bulkPhotoStates.set(id, 'removed'); bulkAction.active = false; bulkAction.label = label; bulkAction.detail = undefined; if (doneTimer !== null) clearTimeout(doneTimer); doneTimer = setTimeout(() => { bulkAction.label = ''; bulkPhotoStates.clear(); doneTimer = null; }, 3000); } export function failBulk(ids: string[]): void { for (const id of ids) bulkPhotoStates.set(id, 'error'); bulkAction.active = false; bulkAction.label = ''; bulkAction.detail = undefined; setTimeout(() => { for (const id of ids) bulkPhotoStates.delete(id); }, 2000); }