85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
591 lines
25 KiB
TypeScript
591 lines
25 KiB
TypeScript
import { useState, useCallback, useRef, useMemo, useEffect, useLayoutEffect } from "react";
|
|
import type { LeftSidebarHandle, SidebarTab } from "./components/sidebar/LeftSidebar";
|
|
import { useRenderQueue } from "./components/renders/useRenderQueue";
|
|
import { usePlayerStore } from "./player";
|
|
import { StudioOverlays } from "./components/StudioOverlays";
|
|
import { SaveQueuePausedBanner } from "./components/SaveQueuePausedBanner";
|
|
import { useCaptionStore } from "./captions/store";
|
|
import { useCaptionSync } from "./captions/hooks/useCaptionSync";
|
|
import { usePersistentEditHistory } from "./hooks/usePersistentEditHistory";
|
|
import { usePanelLayout } from "./hooks/usePanelLayout";
|
|
import { useFileManager } from "./hooks/useFileManager";
|
|
import { usePreviewPersistence } from "./hooks/usePreviewPersistence";
|
|
import { useTimelineEditing } from "./hooks/useTimelineEditing";
|
|
import type { TimelineZIndexReorderCommit } from "./hooks/useTimelineEditingTypes";
|
|
import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab";
|
|
import { useDomEditSession } from "./hooks/useDomEditSession";
|
|
import { useSdkSelectionSync } from "./hooks/useSdkSelectionSync";
|
|
import { useStudioSdkSessions } from "./hooks/useStudioSdkSessions";
|
|
import { usePreviewDocumentVersion } from "./hooks/usePreviewDocumentVersion";
|
|
import { useBlockHandlers } from "./hooks/useBlockHandlers";
|
|
import { useAppHotkeys } from "./hooks/useAppHotkeys";
|
|
import { useClipboard } from "./hooks/useClipboard";
|
|
import { readStudioUiPreferences, writeStudioUiPreferences } from "./utils/studioUiPreferences";
|
|
import { deleteSelectedKeyframes } from "./hooks/timelineEditingHelpers";
|
|
import { useCaptionDetection } from "./hooks/useCaptionDetection";
|
|
import { useRenderClipContent } from "./hooks/useRenderClipContent";
|
|
import { useConsoleErrorCapture } from "./hooks/useConsoleErrorCapture";
|
|
import { useFrameCapture } from "./hooks/useFrameCapture";
|
|
import { useLintModal } from "./hooks/useLintModal";
|
|
import { useCompositionDimensions } from "./hooks/useCompositionDimensions";
|
|
import { useToast } from "./hooks/useToast";
|
|
import { useCompositionContentLoader } from "./hooks/useCompositionContentLoader";
|
|
import { useStudioUrlState } from "./hooks/useStudioUrlState";
|
|
import {
|
|
buildStudioContextValue,
|
|
useDragOverlay,
|
|
useInspectorState,
|
|
} from "./hooks/useStudioContextValue";
|
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
import { StudioHeader } from "./components/StudioHeader";
|
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
import { STUDIO_KEYFRAMES_ENABLED } from "./components/editor/manualEditingAvailability";
|
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
import { StudioPreviewArea } from "./components/StudioPreviewArea";
|
|
import { StudioRightPanel } from "./components/StudioRightPanel";
|
|
import { TimelineToolbar } from "./components/TimelineToolbar";
|
|
import { StudioPlaybackProvider, StudioShellProvider } from "./contexts/StudioContext";
|
|
import { PanelLayoutProvider } from "./contexts/PanelLayoutContext";
|
|
import { ViewModeProvider, useViewModeState } from "./contexts/ViewModeContext";
|
|
import { StoryboardView } from "./components/storyboard/StoryboardView";
|
|
import { FileManagerProvider } from "./contexts/FileManagerContext";
|
|
import { DomEditProvider } from "./contexts/DomEditContext";
|
|
import { StudioSplash } from "./components/StudioSplash";
|
|
import { useServerConnection } from "./hooks/useServerConnection";
|
|
import {
|
|
normalizeStudioCompositionPath,
|
|
readStudioUrlStateFromWindow,
|
|
resolveMasterCompositionPath,
|
|
} from "./utils/studioUrlState";
|
|
import { trackStudioSessionStart } from "./telemetry/events";
|
|
import { hasFiredSessionStart, markSessionStartFired } from "./telemetry/config";
|
|
|
|
type CanvasRect = { left: number; top: number; width: number; height: number };
|
|
// fallow-ignore-next-line complexity
|
|
export function StudioApp() {
|
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
const viewModeValue = useViewModeState();
|
|
|
|
useEffect(() => {
|
|
if (resolving || waitingForServer) return;
|
|
if (hasFiredSessionStart()) return;
|
|
markSessionStartFired();
|
|
trackStudioSessionStart({ has_project: projectId != null });
|
|
}, [projectId, resolving, waitingForServer]);
|
|
|
|
const [activeCompPath, setActiveCompPath] = useState<string | null>(null);
|
|
const [activeCompPathHydrated, setActiveCompPathHydrated] = useState(
|
|
() => initialUrlStateRef.current.activeCompPath == null,
|
|
);
|
|
const [compIdToSrc, setCompIdToSrc] = useState<Map<string, string>>(new Map());
|
|
const [previewIframe, setPreviewIframe] = useState<HTMLIFrameElement | null>(null);
|
|
const [compositionLoading, setCompositionLoading] = useState(true);
|
|
const [refreshKey, setRefreshKey] = useState(0);
|
|
const [previewDocumentVersion, refreshPreviewDocumentVersion] = usePreviewDocumentVersion();
|
|
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
|
|
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
|
|
const activeCompPathRef = useRef(activeCompPath);
|
|
activeCompPathRef.current = activeCompPath;
|
|
const leftSidebarRef = useRef<LeftSidebarHandle>(null);
|
|
const renderQueue = useRenderQueue(projectId);
|
|
const captionEditMode = useCaptionStore((s) => s.isEditMode);
|
|
const captionHasSelection = useCaptionStore((s) => s.selectedSegmentIds.size > 0);
|
|
const captionSync = useCaptionSync(projectId);
|
|
const timelineElements = usePlayerStore((s) => s.elements);
|
|
const setSelectedTimelineElementId = usePlayerStore((s) => s.setSelectedElementId);
|
|
const timelineDuration = usePlayerStore((s) => s.duration);
|
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
const isMasterView = !activeCompPath || activeCompPath === "index.html";
|
|
const activePreviewUrl = activeCompPath
|
|
? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
|
|
: null;
|
|
const effectiveTimelineDuration = useMemo(() => {
|
|
const maxEnd =
|
|
timelineElements.length > 0
|
|
? Math.max(...timelineElements.map((el) => el.start + el.duration))
|
|
: 0;
|
|
return Math.max(timelineDuration, maxEnd);
|
|
}, [timelineDuration, timelineElements]);
|
|
const [timelineVisible, setTimelineVisible] = useState(
|
|
() =>
|
|
initialUrlStateRef.current.timelineVisible ??
|
|
readStudioUiPreferences().timelineVisible ??
|
|
true,
|
|
);
|
|
const toggleTimelineVisibility = useCallback(() => {
|
|
setTimelineVisible((v) => {
|
|
writeStudioUiPreferences({ timelineVisible: !v });
|
|
return !v;
|
|
});
|
|
}, []);
|
|
const { toasts, showToast, dismissToast } = useToast();
|
|
const panelLayout = usePanelLayout({
|
|
rightCollapsed: initialUrlStateRef.current.rightCollapsed,
|
|
rightPanelTab: initialUrlStateRef.current.rightPanelTab,
|
|
});
|
|
const editHistory = usePersistentEditHistory({ projectId });
|
|
const domEditSaveTimestampRef = useRef(0);
|
|
const handleDomZIndexReorderCommitRef = useRef<TimelineZIndexReorderCommit | null>(null);
|
|
const pendingTimelineEditPathRef = useRef(new Set<string>());
|
|
const isGestureRecordingRef = useRef(false);
|
|
const reloadPreview = useCallback(() => setRefreshKey((k) => k + 1), []);
|
|
const fileManager = useFileManager({
|
|
projectId,
|
|
showToast,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
setRefreshKey,
|
|
});
|
|
const masterCompPath = useMemo(
|
|
() => resolveMasterCompositionPath(fileManager.fileTree),
|
|
[fileManager.fileTree],
|
|
);
|
|
const { sdkHandle, editFlowSdkSession } = useStudioSdkSessions(
|
|
projectId,
|
|
activeCompPath,
|
|
domEditSaveTimestampRef,
|
|
masterCompPath,
|
|
);
|
|
useEffect(() => {
|
|
if (activeCompPathHydrated) return;
|
|
if (!fileManager.fileTreeLoaded) return;
|
|
const nextCompPath = normalizeStudioCompositionPath(
|
|
initialUrlStateRef.current.activeCompPath,
|
|
fileManager.fileTree,
|
|
);
|
|
setActiveCompPath((current) => (current === nextCompPath ? current : nextCompPath));
|
|
setActiveCompPathHydrated(true);
|
|
}, [activeCompPathHydrated, fileManager.fileTree, fileManager.fileTreeLoaded]);
|
|
const previewPersistence = usePreviewPersistence({
|
|
projectId,
|
|
showToast,
|
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
previewIframeRef,
|
|
activeCompPathRef,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview: () => setRefreshKey((k) => k + 1),
|
|
pendingTimelineEditPathRef,
|
|
});
|
|
const timelineEditing = useTimelineEditing({
|
|
projectId,
|
|
activeCompPath,
|
|
timelineElements,
|
|
showToast,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
previewIframeRef,
|
|
pendingTimelineEditPathRef,
|
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
|
isRecordingRef: isGestureRecordingRef,
|
|
sdkSession: editFlowSdkSession,
|
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
handleDomZIndexReorderCommitRef,
|
|
});
|
|
const {
|
|
activeBlockParams,
|
|
setActiveBlockParams,
|
|
handleAddBlock,
|
|
handleTimelineBlockDrop,
|
|
handlePreviewBlockDrop,
|
|
} = useBlockHandlers({
|
|
projectId,
|
|
blockCtxDeps: {
|
|
activeCompPath,
|
|
timelineElements,
|
|
readProjectFile: fileManager.readProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
refreshFileTree: fileManager.refreshFileTree,
|
|
reloadPreview,
|
|
showToast,
|
|
},
|
|
previewIframeRef,
|
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
});
|
|
const clearDomSelectionRef = useRef<() => void>(() => {});
|
|
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
|
|
const handleDomEditElementDeleteRef = useRef<(s: DomEditSelection) => Promise<void>>(
|
|
async () => {},
|
|
);
|
|
const domEditDeleteBridge = (s: DomEditSelection) => handleDomEditElementDeleteRef.current(s);
|
|
const resetKeyframesRef = useRef<() => boolean>(() => false);
|
|
const deleteSelectedKeyframesRef = useRef<() => void>(() => {});
|
|
const invalidateGsapCacheRef = useRef<() => void>(() => {});
|
|
const { handleCopy, handlePaste, handleCut } = useClipboard({
|
|
projectId,
|
|
activeCompPath,
|
|
domEditSelectionRef: domEditSelectionBridgeRef,
|
|
showToast,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
|
|
handleDomEditElementDelete: domEditDeleteBridge,
|
|
previewIframeRef,
|
|
});
|
|
const appHotkeys = useAppHotkeys({
|
|
toggleTimelineVisibility,
|
|
handleTimelineElementDelete: timelineEditing.handleTimelineElementDelete,
|
|
handleTimelineElementSplit: timelineEditing.handleTimelineElementSplit,
|
|
handleDomEditElementDelete: domEditDeleteBridge,
|
|
domEditSelectionRef: domEditSelectionBridgeRef,
|
|
clearDomSelectionRef,
|
|
editHistory,
|
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
readProjectFile: fileManager.readProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
domEditSaveTimestampRef,
|
|
showToast,
|
|
syncHistoryPreviewAfterApply: previewPersistence.syncHistoryPreviewAfterApply,
|
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
leftSidebarRef,
|
|
handleCopy,
|
|
handlePaste,
|
|
handleCut,
|
|
onResetKeyframes: () => resetKeyframesRef.current(),
|
|
onDeleteSelectedKeyframes: () => deleteSelectedKeyframesRef.current(),
|
|
onAfterUndoRedo: () => invalidateGsapCacheRef.current(),
|
|
onGroupSelection: () => domEditSessionRef.current.handleGroupSelection(),
|
|
onUngroupSelection: () => domEditSessionRef.current.handleUngroupSelection(),
|
|
activeCompPath,
|
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
onToggleRecording: STUDIO_KEYFRAMES_ENABLED
|
|
? () => handleToggleRecordingRef.current()
|
|
: undefined,
|
|
});
|
|
const sidebarTabRef = useRef({
|
|
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
|
|
get: () => leftSidebarRef.current?.getTab() ?? "compositions",
|
|
});
|
|
const domEditSession = useDomEditSession({
|
|
projectId,
|
|
activeCompPath,
|
|
isMasterView,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
compositionLoading,
|
|
previewIframeRef,
|
|
timelineElements,
|
|
setSelectedTimelineElementId,
|
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
showToast,
|
|
refreshPreviewDocumentVersion,
|
|
queueDomEditSave: previewPersistence.queueDomEditSave,
|
|
readProjectFile: fileManager.readProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
updateEditingFileContent: fileManager.updateEditingFileContent,
|
|
domEditSaveTimestampRef,
|
|
editHistory: { recordEdit: editHistory.recordEdit },
|
|
fileTree: fileManager.fileTree,
|
|
importedFontAssetsRef: fileManager.importedFontAssetsRef,
|
|
projectDir: fileManager.projectDir,
|
|
projectIdRef: fileManager.projectIdRef,
|
|
previewIframe,
|
|
refreshKey,
|
|
previewDocumentVersion,
|
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
applyStudioManualEditsToPreviewRef: previewPersistence.applyStudioManualEditsToPreviewRef,
|
|
syncPreviewHistoryHotkey: appHotkeys.syncPreviewHistoryHotkey,
|
|
reloadPreview,
|
|
setRefreshKey,
|
|
openSourceForSelection: fileManager.openSourceForSelection,
|
|
selectSidebarTab: sidebarTabRef.current.select,
|
|
getSidebarTab: sidebarTabRef.current.get,
|
|
sdkSession: editFlowSdkSession,
|
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
});
|
|
domEditSelectionBridgeRef.current = domEditSession.domEditSelection;
|
|
handleDomZIndexReorderCommitRef.current = domEditSession.handleDomZIndexReorderCommit;
|
|
clearDomSelectionRef.current = domEditSession.clearDomSelection;
|
|
handleDomEditElementDeleteRef.current = domEditSession.handleDomEditElementDelete;
|
|
resetKeyframesRef.current = domEditSession.handleResetSelectedElementKeyframes;
|
|
invalidateGsapCacheRef.current = domEditSession.invalidateGsapCache;
|
|
deleteSelectedKeyframesRef.current = () => deleteSelectedKeyframes(domEditSession);
|
|
useSdkSelectionSync(
|
|
editFlowSdkSession,
|
|
domEditSession.domEditSelection,
|
|
domEditSession.domEditGroupSelections,
|
|
);
|
|
|
|
useCaptionDetection({
|
|
projectId,
|
|
activeCompPath,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
captionHasSelection,
|
|
previewIframeRef,
|
|
captionSync,
|
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
});
|
|
const renderClipContent = useRenderClipContent({
|
|
projectIdRef: fileManager.projectIdRef,
|
|
compIdToSrc,
|
|
activePreviewUrl,
|
|
effectiveTimelineDuration,
|
|
});
|
|
const compositionDimensions = useCompositionDimensions();
|
|
const { lintModal, linting, handleLint, closeLintModal, findingsByFile } = useLintModal(
|
|
projectId,
|
|
refreshKey,
|
|
);
|
|
const frameCapture = useFrameCapture({
|
|
projectId,
|
|
activeCompPath,
|
|
showToast,
|
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
});
|
|
const {
|
|
consoleErrors,
|
|
setConsoleErrors,
|
|
resetErrors: resetConsoleErrors,
|
|
} = useConsoleErrorCapture(previewIframe);
|
|
const dragOverlay = useDragOverlay(fileManager.handleImportFiles);
|
|
const handleToggleRecordingRef = useRef<() => void>(() => {});
|
|
const domEditSessionRef = useRef(domEditSession);
|
|
domEditSessionRef.current = domEditSession;
|
|
const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
|
|
domEditSessionRef,
|
|
previewIframeRef,
|
|
showToast,
|
|
isGestureRecordingRef,
|
|
});
|
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
const recordingToggle = STUDIO_KEYFRAMES_ENABLED ? handleToggleRecording : undefined;
|
|
const canvasRectRef = useRef<CanvasRect | null>(null);
|
|
useLayoutEffect(() => {
|
|
if (gestureState !== "recording" || !previewIframe) {
|
|
canvasRectRef.current = null;
|
|
return;
|
|
}
|
|
const r = previewIframe.getBoundingClientRect();
|
|
canvasRectRef.current = { left: r.left, top: r.top, width: r.width, height: r.height };
|
|
}, [gestureState, previewIframe]);
|
|
|
|
const handlePreviewIframeRef = useCallback(
|
|
(iframe: HTMLIFrameElement | null) => {
|
|
previewIframeRef.current = iframe;
|
|
setPreviewIframe(iframe);
|
|
appHotkeys.syncPreviewTimelineHotkey(iframe);
|
|
appHotkeys.syncPreviewHistoryHotkey(iframe);
|
|
resetConsoleErrors();
|
|
refreshPreviewDocumentVersion();
|
|
},
|
|
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
);
|
|
const { setEditingFile } = fileManager;
|
|
const handleSelectComposition = useCompositionContentLoader({
|
|
projectId,
|
|
setEditingFile,
|
|
setActiveCompPath,
|
|
showToast,
|
|
});
|
|
const {
|
|
designPanelActive,
|
|
inspectorPanelActive,
|
|
inspectorButtonActive,
|
|
shouldShowSelectedDomBounds,
|
|
} = useInspectorState(
|
|
panelLayout.rightPanelTab,
|
|
panelLayout.rightInspectorPanes,
|
|
panelLayout.rightCollapsed,
|
|
isPlaying,
|
|
gestureState === "recording",
|
|
);
|
|
useStudioUrlState({
|
|
projectId,
|
|
activeCompPath,
|
|
duration: effectiveTimelineDuration,
|
|
isPlaying,
|
|
compositionLoading,
|
|
refreshKey,
|
|
previewIframeRef,
|
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
rightCollapsed: panelLayout.rightCollapsed,
|
|
timelineVisible,
|
|
activeCompPathHydrated,
|
|
domEditSelection: domEditSession.domEditSelection,
|
|
buildDomSelectionFromTarget: domEditSession.buildDomSelectionFromTarget,
|
|
applyDomSelection: domEditSession.applyDomSelection,
|
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
initialState: initialUrlStateRef.current,
|
|
});
|
|
const studioCtxValue = buildStudioContextValue({
|
|
projectId: projectId!,
|
|
activeCompPath,
|
|
setActiveCompPath,
|
|
showToast,
|
|
previewIframeRef,
|
|
captionEditMode,
|
|
compositionLoading,
|
|
refreshKey,
|
|
setRefreshKey,
|
|
timelineElements,
|
|
isPlaying,
|
|
editHistory,
|
|
handleUndo: appHotkeys.handleUndo,
|
|
handleRedo: appHotkeys.handleRedo,
|
|
renderQueue,
|
|
compositionDimensions,
|
|
waitForPendingDomEditSaves: previewPersistence.waitForPendingDomEditSaves,
|
|
handlePreviewIframeRef,
|
|
refreshPreviewDocumentVersion,
|
|
timelineVisible,
|
|
toggleTimelineVisibility,
|
|
});
|
|
const timelineToolbar = useMemo(
|
|
() => (
|
|
<TimelineToolbar
|
|
toggleTimelineVisibility={toggleTimelineVisibility}
|
|
domEditSession={domEditSession}
|
|
onSplitElement={timelineEditing.handleTimelineElementSplit}
|
|
/>
|
|
),
|
|
[toggleTimelineVisibility, domEditSession, timelineEditing.handleTimelineElementSplit],
|
|
);
|
|
if (resolving || waitingForServer || !projectId)
|
|
return <StudioSplash waiting={waitingForServer} />;
|
|
return (
|
|
<StudioShellProvider value={studioCtxValue}>
|
|
<StudioPlaybackProvider value={studioCtxValue}>
|
|
<ViewModeProvider value={viewModeValue}>
|
|
<PanelLayoutProvider value={panelLayout}>
|
|
<FileManagerProvider value={fileManager}>
|
|
<DomEditProvider value={domEditSession}>
|
|
<div
|
|
className="flex flex-col h-full w-full bg-neutral-950 relative"
|
|
onDragOver={dragOverlay.onDragOver}
|
|
onDragEnter={dragOverlay.onDragEnter}
|
|
onDragLeave={dragOverlay.onDragLeave}
|
|
onDrop={dragOverlay.onDrop}
|
|
>
|
|
<StudioHeader
|
|
captureFrameHref={frameCapture.captureFrameHref}
|
|
captureFrameFilename={frameCapture.captureFrameFilename}
|
|
handleCaptureFrameClick={frameCapture.handleCaptureFrameClick}
|
|
refreshCaptureFrameTime={frameCapture.refreshCaptureFrameTime}
|
|
capturing={frameCapture.capturing}
|
|
inspectorButtonActive={inspectorButtonActive}
|
|
inspectorPanelActive={inspectorPanelActive}
|
|
onExport={() => {
|
|
void (async () => {
|
|
await previewPersistence.waitForPendingDomEditSaves();
|
|
await renderQueue.startRender(undefined);
|
|
})();
|
|
}}
|
|
/>
|
|
{previewPersistence.domEditSaveQueuePaused && (
|
|
<SaveQueuePausedBanner
|
|
message={previewPersistence.domEditSaveQueuePaused}
|
|
onRetry={previewPersistence.resetDomEditSaveQueueBreaker}
|
|
/>
|
|
)}
|
|
{viewModeValue.viewMode === "storyboard" && (
|
|
<StoryboardView
|
|
projectId={projectId}
|
|
onSelectComposition={handleSelectComposition}
|
|
/>
|
|
)}
|
|
<div
|
|
className={`flex flex-1 min-h-0${
|
|
viewModeValue.viewMode === "storyboard" ? " hidden" : ""
|
|
}`}
|
|
>
|
|
<StudioLeftSidebar
|
|
leftSidebarRef={leftSidebarRef}
|
|
onSelectComposition={handleSelectComposition}
|
|
onAddBlock={handleAddBlock}
|
|
onPreviewBlock={setBlockPreview}
|
|
onLint={handleLint}
|
|
linting={linting}
|
|
lintFindingCount={lintModal?.length ?? findingsByFile.size}
|
|
lintFindingsByFile={findingsByFile}
|
|
/>
|
|
<StudioPreviewArea
|
|
timelineToolbar={timelineToolbar}
|
|
renderClipContent={renderClipContent}
|
|
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
|
|
handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
|
|
handleTimelineBlockDrop={handleTimelineBlockDrop}
|
|
handlePreviewBlockDrop={handlePreviewBlockDrop}
|
|
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
|
|
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
|
|
handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
|
|
handleTimelineGroupMove={timelineEditing.handleTimelineGroupMove}
|
|
handleTimelineGroupResize={timelineEditing.handleTimelineGroupResize}
|
|
handleToggleTrackHidden={timelineEditing.handleToggleTrackHidden}
|
|
handleToggleElementHidden={timelineEditing.handleToggleElementHidden}
|
|
handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
|
|
handleTimelineElementSplit={timelineEditing.handleTimelineElementSplit}
|
|
handleRazorSplit={timelineEditing.handleRazorSplit}
|
|
handleRazorSplitAll={timelineEditing.handleRazorSplitAll}
|
|
setCompIdToSrc={setCompIdToSrc}
|
|
setCompositionLoading={setCompositionLoading}
|
|
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
|
|
isGestureRecording={gestureState === "recording"}
|
|
recordingState={gestureState}
|
|
onToggleRecording={recordingToggle}
|
|
blockPreview={blockPreview}
|
|
gestureOverlay={
|
|
gestureState === "recording" && previewIframe ? (
|
|
<GestureTrailOverlay
|
|
samples={gestureRecording.samplesRef.current}
|
|
sampleCount={gestureRecording.samplesRef.current.length}
|
|
trail={gestureRecording.trailRef.current}
|
|
canvasRect={canvasRectRef.current!}
|
|
compositionSize={compositionDimensions ?? undefined}
|
|
mode="recording"
|
|
/>
|
|
) : undefined
|
|
}
|
|
/>
|
|
{!panelLayout.rightCollapsed && (
|
|
<StudioRightPanel
|
|
designPanelActive={designPanelActive}
|
|
activeBlockParams={activeBlockParams}
|
|
onCloseBlockParams={() => {
|
|
setActiveBlockParams(null);
|
|
panelLayout.setRightPanelTab("design");
|
|
}}
|
|
recordingState={gestureState}
|
|
recordingDuration={gestureRecording.recordingDuration}
|
|
onToggleRecording={recordingToggle}
|
|
sdkSession={sdkHandle.session}
|
|
reloadPreview={reloadPreview}
|
|
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
recordEdit={editHistory.recordEdit}
|
|
onToggleElementHidden={timelineEditing.handleToggleElementHidden}
|
|
/>
|
|
)}
|
|
</div>
|
|
<StudioOverlays
|
|
projectId={projectId}
|
|
projectDir={fileManager.projectDir}
|
|
lintModal={lintModal}
|
|
closeLintModal={closeLintModal}
|
|
consoleErrors={consoleErrors}
|
|
clearConsoleErrors={() => setConsoleErrors(null)}
|
|
domEditSession={domEditSession}
|
|
activeCompPath={activeCompPath}
|
|
dragOverlayActive={dragOverlay.active}
|
|
toasts={toasts}
|
|
dismissToast={dismissToast}
|
|
/>
|
|
</div>
|
|
</DomEditProvider>
|
|
</FileManagerProvider>
|
|
</PanelLayoutProvider>
|
|
</ViewModeProvider>
|
|
</StudioPlaybackProvider>
|
|
</StudioShellProvider>
|
|
);
|
|
}
|