feat: bulk action progress — header pill + per-thumbnail states

- New bulkAction store: tracks active/label/detail state for the pill
  and a Map<uid, pending|done|error> for per-tile overlays
- Extract StatusPill.svelte from IndexerStatusPill (generic active/label/detail
  props); IndexerStatusPill becomes a one-line wrapper
- +layout.svelte: render a second StatusPill driven by bulkAction store,
  alongside the indexer pill in the AnimatedMule header
- BulkActionBar: extend withBusy with optional BulkConfig (ids/label/doneLabel);
  pending tiles dim + spinner on start, green checkmark flashes for 400ms before
  cache invalidation removes them; red overlay on error, auto-clears after 2s
- onApprove/batchEdit: wire onProgress callback to setDetail so the pill shows
  the filename currently being processed during fan-out keep operations
- batch.ts: add completedId as third arg to onProgress (backwards-compatible)
- PhotoTile: derive bulkState from store; pending/done/error overlays sit above
  the selection tint; hover-video guarded against pending tiles

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dtoro
2026-06-07 09:43:12 +02:00
parent 86e38e152d
commit da63ad769a
7 changed files with 177 additions and 56 deletions

View File

@@ -0,0 +1,60 @@
/**
* 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
* failBulk → tiles flash red, auto-clears after 2 s
*/
interface BulkActionState {
active: boolean;
label: string;
detail?: string;
}
export const bulkAction = $state<BulkActionState>({ active: false, label: '' });
export const bulkPhotoStates = $state(new Map<string, 'pending' | 'done' | 'error'>());
let doneTimer: ReturnType<typeof setTimeout> | 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);
}
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);
}