import { Plural, Trans, useLingui } from "@lingui/react/macro"; import type { MediaAsset, MediaCollection, MediaSource } from "@rakazo/contracts"; import { Button } from "@rakazo/ui-web"; import { Plus } from "lucide-react"; import { useCallback, useEffect, useRef, useState } from "react"; import { LoadingState } from "../../components/ai/primitives"; import { AssetGrid, useAssetSelection } from "../../components/media/library/AssetGrid"; import { ConnectSourceDialog } from "../../components/media/library/ConnectSourceDialog"; import { CreateProjectDialog } from "../../components/media/library/CreateProjectDialog"; import { CollectionCard, FilterChips, IndexingProgress, LibraryModeToggle, LibrarySearch, } from "../../components/media/library/LibraryChrome"; import { usePhotosSync } from "../../components/media/library/usePhotosSync"; import { VirtualGrid } from "../../components/media/library/VirtualGrid"; import type { LibraryFilter } from "../../lib/media-library"; import { assetQueryFor, hasAssetQuery, INGEST_PENDING_STATES } from "../../lib/media-library"; import { usePolling } from "../../lib/media-poll"; import { rpcErrorMessage, useMediaStudio } from "../../lib/media-studio"; import { rpc } from "../../lib/rpc"; import type { MediaViewProps } from "./nav"; const PAGE = 200; type Paged = { items: T[]; cursor: string | null; done: boolean; loading: boolean }; const emptyPage = (): Paged => ({ items: [], cursor: null, done: false, loading: false }); /** * Cursor-paged list that appends on demand. A new `key` starts over and drops * pages still in flight for the previous key. */ function usePagedList( key: string, load: (cursor: string | null) => Promise<{ items: T[]; nextCursor: string | null }>, enabled: boolean, ) { const [state, setState] = useState>(emptyPage); const [error, setError] = useState(null); const generation = useRef(0); const busy = useRef(false); const loadRef = useRef(load); loadRef.current = load; const stateRef = useRef(state); stateRef.current = state; const more = useCallback(async () => { const current = stateRef.current; if (busy.current || current.done) return; busy.current = true; const mine = generation.current; setState((s) => ({ ...s, loading: true })); try { const page = await loadRef.current(current.cursor); if (mine !== generation.current) return; setError(null); setState((s) => ({ items: [...s.items, ...page.items], cursor: page.nextCursor, done: page.nextCursor === null, loading: false, })); } catch (cause) { if (mine !== generation.current) return; setError(cause); setState((s) => ({ ...s, loading: false, done: true })); } finally { if (mine === generation.current) busy.current = false; } }, []); const reset = useCallback(() => { generation.current += 1; busy.current = false; stateRef.current = emptyPage(); setState(emptyPage()); setError(null); }, []); useEffect(() => { void key; reset(); if (enabled) void more(); }, [key, enabled, reset, more]); const restart = useCallback(() => { reset(); void more(); }, [reset, more]); return { ...state, error, more, reset: restart }; } export function LibraryView({ navigate, mode = "sessions", }: MediaViewProps & { mode?: "sessions" | "themes" }) { const { t } = useLingui(); const { canLinkFolder } = useMediaStudio(); const [filters, setFilters] = useState>(() => new Set()); const [text, setText] = useState(""); const [debouncedText, setDebouncedText] = useState(""); const [sources, setSources] = useState(null); const [pending, setPending] = useState(false); const [connectOpen, setConnectOpen] = useState(false); const [createOpen, setCreateOpen] = useState(false); const selection = useAssetSelection(); useEffect(() => { const timer = setTimeout(() => setDebouncedText(text), 250); return () => clearTimeout(timer); }, [text]); const searching = hasAssetQuery(filters, debouncedText); const query = assetQueryFor(filters, debouncedText); const queryKey = JSON.stringify(query); const kind = mode === "themes" ? "theme" : "session"; const collections = usePagedList( kind, (cursor) => rpc.media.collections.list({ kind, cursor, limit: PAGE }), !searching, ); const assets = usePagedList( queryKey, (cursor) => rpc.media.assets.search({ ...query, cursor, limit: PAGE }), searching, ); const refreshStatus = useCallback(async () => { const [list, work] = await Promise.all([ rpc.media.sources.list({}), rpc.media.assets.search({ ingestStates: [...INGEST_PENDING_STATES], cursor: null, limit: 1 }), ]); setSources(list); setPending(work.items.length > 0); }, []); const photos = usePhotosSync( useCallback(() => { void refreshStatus(); collections.reset(); }, [refreshStatus, collections.reset]), ); useEffect(() => { void refreshStatus().catch(() => setSources([])); }, [refreshStatus]); const indexing = photos.syncing || pending || (sources ?? []).some((source) => source.status === "syncing"); usePolling(refreshStatus, indexing); // Indexing just finished: the grouping changed underneath us. const wasIndexing = useRef(indexing); useEffect(() => { if (wasIndexing.current && !indexing) { if (searching) assets.reset(); else collections.reset(); } wasIndexing.current = indexing; }, [indexing, searching, assets.reset, collections.reset]); const noSources = sources !== null && sources.length === 0 && !photos.syncing; const selectedIds = [...selection.selected]; return (
navigate(next === "people" ? { name: "people" } : { name: "library", mode: next }) } />
{selectedIds.length > 0 ? ( <> ) : null}
{indexing ? ( ) : null} {sources === null ? (
) : noSources ? (

Connect your photos and videos.

) : searching ? ( assets.items.length === 0 && !assets.loading ? ( Nothing matches. ) : ( void assets.more()} /> ) ) : collections.items.length === 0 && !collections.loading ? ( indexing ? null : ( {mode === "themes" ? No themes yet. : No sessions yet.} ) ) : ( collections.items[index]?.id ?? index} minTileWidth={220} gap={16} tileAspect={10 / 16} extraHeight={44} onNearEnd={() => void collections.more()} renderItem={(index) => { const collection = collections.items[index]; if (!collection) return null; return ( navigate({ name: "collection", collectionId: collection.id })} /> ); }} /> )} void refreshStatus()} /> navigate({ name: "project", projectId })} />
); } function EmptyMessage({ error, children }: { error: unknown; children: React.ReactNode }) { const { t } = useLingui(); return (
{error ? rpcErrorMessage(error, t`Could not load the library.`) : children}
); }