import { toast } from 'svelte-sonner'; import { batchEdit } from './batch'; import { getPhoto, updatePhoto, type UpdatePhotoBody } from './photoprism'; import { queryClient } from '$lib/queryClient'; import { push as pushUndo } from '$lib/stores/undo.svelte'; import type { PpPhoto } from '$lib/types/photoprism'; /** * Shared helpers for bulk metadata mutations across the timeline. Three call * sites: the keyboard culling layer (`gridKeyNav`), the `BulkActionBar` row, * and the bulk metadata sidebar shown on multi-select. Each needs the same * "fetch full photo body → deep-merge patch → PUT → invalidate" round-trip * (PhotoPrism's PUT only persists nested fields when the body is whole), so * the wiring lives here to keep the call sites declarative. */ /** Fetch the freshest photo body, seeding the per-photo cache. PhotoPrism's * PUT needs the full body to persist `Rating` / `Color` / `Details.*`; the * cache lookup means the subsequent patch pass reuses this fetch. */ export async function freshPhoto(uid: string): Promise { const cached = queryClient.getQueryData(['photo', uid]); if (cached) return cached; const p = await getPhoto(uid); queryClient.setQueryData(['photo', uid], p); return p; } export function invalidatePhotos(uids: string[]): void { void queryClient.invalidateQueries({ queryKey: ['photos'] }); for (const id of uids) { void queryClient.invalidateQueries({ queryKey: ['photo', id] }); } } /** * Refresh the sidebar facet sections after a metadata mutation. The Colors / * Ratings panels read `['marks']` + `['photos','marks-pool']`; Notes reads * `['photos','with-notes']`; keywords / labels / people read their own keys. * Optimistic cache writes keep the active tile in sync, but the facet panels * sit on separate queries that otherwise stay stale until their staleTime * expires — so call this on the success path of any marks/keyword/note apply. */ export function invalidateFacets(): void { void queryClient.invalidateQueries({ queryKey: ['marks'] }); void queryClient.invalidateQueries({ queryKey: ['photos', 'marks-pool'] }); void queryClient.invalidateQueries({ queryKey: ['photos', 'with-notes'] }); void queryClient.invalidateQueries({ queryKey: ['photos', 'keywords'] }); void queryClient.invalidateQueries({ queryKey: ['labels'] }); void queryClient.invalidateQueries({ queryKey: ['subjects'] }); } export function invalidateAllPhotoCaches(): void { void queryClient.invalidateQueries({ queryKey: ['photos'] }); void queryClient.invalidateQueries({ queryKey: ['marks'] }); void queryClient.invalidateQueries({ queryKey: ['labels'] }); void queryClient.invalidateQueries({ queryKey: ['review-groups'] }); void queryClient.invalidateQueries({ queryKey: ['heaps'] }); } /** * Apply a patch to every uid. The patch can be a static body or a per-photo * function (used by keyword merges which need to read each photo's current * Details.Keywords before extending it). When `inverseBuilder` is provided, * an undo entry is registered that restores each photo's pre-patch state. */ export async function patchTargets( ids: string[], patch: UpdatePhotoBody | ((p: PpPhoto) => UpdatePhotoBody), label: string, inverseBuilder?: (photo: PpPhoto) => UpdatePhotoBody ): Promise { if (ids.length === 0) return; const inverses = inverseBuilder ? new Map( await Promise.all( ids.map(async (id) => { const p = await freshPhoto(id); return [id, inverseBuilder(p)] as const; }) ) ) : null; const tid = toast.loading(`${label} · ${ids.length}…`); const { updated, errors } = await batchEdit(ids, async (id) => { const p = await freshPhoto(id); const body = typeof patch === 'function' ? patch(p) : patch; if (Object.keys(body).length === 0) return p; return updatePhoto(p, body); }); invalidatePhotos(ids); invalidateFacets(); if (errors.length) { toast.error(`${label} · ${updated.length} ok, ${errors.length} failed`, { id: tid }); } else { toast.success(`${label} · ${ids.length}`, { id: tid }); } if (inverses) { pushUndo(`${label} (${ids.length})`, async () => { await batchEdit(ids, async (id) => { const p = await freshPhoto(id); const inv = inverses.get(id) ?? {}; if (Object.keys(inv).length === 0) return p; return updatePhoto(p, inv); }); invalidatePhotos(ids); }); } }