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:
60
web/src/lib/stores/bulkAction.svelte.ts
Normal file
60
web/src/lib/stores/bulkAction.svelte.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user