// Composition drill-down stack management for NLELayout import { useState, useCallback, useRef, useEffect } from "react"; import { usePlayerStore } from "../../player"; import type { CompositionLevel } from "./CompositionBreadcrumb"; interface UseCompositionStackOptions { projectId: string; activeCompositionPath?: string | null; onCompositionChange?: (compositionPath: string | null) => void; } interface UseCompositionStackResult { compositionStack: CompositionLevel[]; updateCompositionStack: React.Dispatch>; handleNavigateComposition: (index: number) => void; handleDrillDown: (element: { id: string; compositionSrc?: string }) => void; masterSeekRef: React.MutableRefObject; compIdToSrc: Map; setCompIdToSrc: React.Dispatch>>; } export function useCompositionStack({ projectId, activeCompositionPath, onCompositionChange, }: UseCompositionStackOptions): UseCompositionStackResult { const [compositionStack, setCompositionStack] = useState([ { id: "master", label: "Master", previewUrl: `/api/projects/${projectId}/preview` }, ]); const onCompositionChangeRef = useRef(onCompositionChange); onCompositionChangeRef.current = onCompositionChange; const updateCompositionStack: typeof setCompositionStack = useCallback((action) => { setCompositionStack((prev) => { const next = typeof action === "function" ? action(prev) : action; const id = next[next.length - 1]?.id; queueMicrotask(() => onCompositionChangeRef.current?.(id === "master" ? null : id)); return next; }); }, []); const masterSeekRef = useRef(0); const [compIdToSrc, setCompIdToSrc] = useState>(new Map()); const compIdToSrcRef = useRef(compIdToSrc); compIdToSrcRef.current = compIdToSrc; const handleNavigateComposition = useCallback( (index: number) => { if (index === 0 && masterSeekRef.current > 0) { usePlayerStore.getState().setCurrentTime(masterSeekRef.current); } usePlayerStore.getState().setElements([]); updateCompositionStack((prev) => prev.slice(0, index + 1)); }, // eslint-disable-next-line react-hooks/exhaustive-deps [], ); const handleDrillDown = useCallback( (element: { id: string; compositionSrc?: string }) => { if (!element.compositionSrc) return; masterSeekRef.current = usePlayerStore.getState().currentTime; const compId = element.id; let resolvedPath = compIdToSrcRef.current.get(compId); if (!resolvedPath) { const src = element.compositionSrc; const compMatch = src.match(/compositions\/.*\.html/); resolvedPath = compMatch ? compMatch[0] : src; } usePlayerStore.getState().setElements([]); updateCompositionStack((prev) => { const currentId = prev[prev.length - 1].id; if (currentId === resolvedPath && prev.length > 1) { return prev.slice(0, -1); } const label = resolvedPath .split("/") .pop() ?.replace(/\.html$/, "") || resolvedPath; const previewUrl = `/api/projects/${projectId}/preview/comp/${resolvedPath}`; return [...prev, { id: resolvedPath, label, previewUrl }]; }); }, // eslint-disable-next-line react-hooks/exhaustive-deps [projectId], ); // Navigate to a composition when activeCompositionPath changes. // eslint-disable-next-line no-restricted-syntax useEffect(() => { if (activeCompositionPath === "index.html") { usePlayerStore.getState().setElements([]); updateCompositionStack((prev) => (prev.length > 1 ? [prev[0]] : prev)); } else if (activeCompositionPath && activeCompositionPath.startsWith("compositions/")) { const label = activeCompositionPath.replace(/^compositions\//, "").replace(/\.html$/, ""); const previewUrl = `/api/projects/${projectId}/preview/comp/${activeCompositionPath}`; usePlayerStore.getState().setElements([]); updateCompositionStack((prev) => { if (prev[prev.length - 1]?.id === activeCompositionPath) return prev; return [ { id: "master", label: "Master", previewUrl: `/api/projects/${projectId}/preview` }, { id: activeCompositionPath, label, previewUrl }, ]; }); } else if (!activeCompositionPath) { usePlayerStore.getState().setElements([]); } }, [activeCompositionPath, projectId, updateCompositionStack]); return { compositionStack, updateCompositionStack, handleNavigateComposition, handleDrillDown, masterSeekRef, compIdToSrc, setCompIdToSrc, }; }