Files
atelier/web/Collections/useCollectionsManager.ts
T
pastilhas 859fb5767c web: three panels — Generate, Gallery, Collections — and a layout
ported from the standalone app's hash-tab UI. The panels do not coordinate through props (the panel
contract only ever hands a component its panelId) or usePanelChannel-as-selection:

- Gallery's lightbox is ?selected=<id>, Collections' detail is ?collection=<id> — different names
  since both panels render on the same route at once
- Generate bumps useAtelierRefresh (added to officerdev/src/channels.ts) when a job finishes;
  Gallery subscribes to it — a genuine signal, not a selection, so it stays a channel

images render through GET /files/:filename with a ?token= query param appended client-side
(lib/api.ts's fileUrl), the same escape hatch FileViewer and the music plugin use for exactly this:
a browser cannot set an Authorization header on an <img src>.

Providers is not here — see PLUGIN.md for why it became a platform settings screen instead.

assets/icon.png and assets/splash.png are [placeholder] flat swatches, not real artwork — see
assets/PLACEHOLDER.md.
2026-08-20 04:01:10 +00:00

77 lines
2.4 KiB
TypeScript

import { useCallback, useEffect, useState } from 'react';
import { useAtelierClient } from '../lib/api';
import type { CollectionDetail, CollectionSummary } from '../lib/api';
export const useCollectionsManager = (openId?: string) => {
const client = useAtelierClient();
const [collections, setCollections] = useState<CollectionSummary[]>([]);
const [detail, setDetail] = useState<CollectionDetail | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [draftName, setDraftName] = useState('');
const refresh = useCallback(async () => {
try {
setCollections(await client.listCollections());
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
} finally {
setLoading(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
void refresh();
}, [refresh]);
// The detail view is driven by the URL, so opening one is a navigation and this only follows it.
useEffect(() => {
if (!openId) {
setDetail(null);
return;
}
let cancelled = false;
client
.getCollection(openId)
.then((data) => !cancelled && setDetail(data))
.catch((err: Error) => !cancelled && setError(err.message));
return () => {
cancelled = true;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [openId]);
const create = async () => {
const name = draftName.trim();
if (!name) return;
try {
await client.createCollection(name);
setDraftName('');
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
}
};
const remove = async (id: string) => {
try {
await client.deleteCollection(id);
await refresh();
} catch (err) {
setError(err instanceof Error ? err.message : String(err));
}
};
const removeImage = async (collectionId: string, generationId: string) => {
const { images } = await client.removeFromCollection(collectionId, generationId);
setDetail((current) => (current ? { ...current, images } : current));
void refresh();
};
return { collections, detail, loading, error, draftName, setDraftName, create, remove, removeImage, refresh };
};
export type CollectionsManager = ReturnType<typeof useCollectionsManager>;