mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 10:14:30 +00:00
C1: the FX button in the track/group header, and its popover — the
"reach FX from the timeline" entry point, last on purpose because it
targets a group or a single clip, never "a track" (N clips = N chains
is the ill-defined thing the design doc refuses to build).
The button (TimelineFxButton.tsx): renders on group rows and on track
rows holding exactly one audio clip, reading "FX" (or "FX n" once the
target's data-fx-chain has n enabled nodes). A multi-clip ungrouped
audio track gets a pointer instead ("Group these clips to add effects
to all of them" + a Group action) rather than silently hiding the
entry point — reuses B6's exact auto-grouping write
(useAudioGroupCarveAssignment, exposed as onGroupClips) with a minted
group id (mintGroupId, exported from useFxCarveGrouping.ts).
The popover (TimelineFxPopover.tsx, components/editor/): a thin
positioner around FxPresetMenu exactly as the property panel renders
it — same audition contract (useFxAudition), same preset-apply
computation (extracted into useApplyAudioFxPreset.ts's
applyPresetToChain, now shared with propertyPanelFxSection.tsx's own
applyPreset rather than duplicated). Escape closes without
deselecting whatever is behind it; an outside pointerdown dismisses.
Footer's "+ effect"/"Open rack ›" both select the target and hand off
to the property panel (a simplification from the step doc's two
distinct behaviors — remotely toggling the rack's own internal
"adding" state isn't plumbed anywhere, and building that plumbing
would be new UI-state wiring beyond what "reuse existing selection
dispatch" asks for).
Writes, one path per target kind, neither a new persistence mechanism:
- Group: B7/B5's existing onSetAudioGroupAttributeLive/Quiet
(data-fx-chain, same as data-volume/data-hidden already do).
- Clip: a NEW onSetElementAttributeLive/Quiet pair
(timelineElementFxAttribute.ts), addressed by the TimelineElement
itself rather than the current selection. This is the one real
architectural gap the step doc's assumption didn't survive: the
property panel's onSetAttributeQuiet closes over domEditSelection,
so writing a clip that isn't already selected has no synchronous
path through it. Extracted the shared live-patch-then-persist core
(persistElementAttribute, timelineEditingHelpers.ts) out of both
this new path and the existing setAudioGroupAttribute, which the
fallow duplication gate flagged as a 66-line clone on first pass —
now a single ~50-line core parameterized by patchLive/readLive, with
each caller a ~15-line wrapper resolving its own patch target
(buildPatchTarget({domId}) for a group, buildPatchTarget(element)
for an arbitrary clip) and live-DOM lookup.
Data plumbing: HfAudioGroup.fxChain (already on the B1 model) mirrored
onto TimelineElement.audioGroupFxChain (timelineDOM.ts's groupInfoFor
cache) and TimelineTrackGroupInfo.fxChain (useTimelineTrackDerivations.ts),
alongside the existing volume/hidden mirrors.
Deferred: the property panel's own rack doesn't (yet) expose a way to
remotely force its add-menu open, so "+ effect" and "Open rack ›"
converge on the same navigation rather than the step doc's two
distinct ones. A grouped multi-clip track (some clips already carry
data-audio-group) gets neither the chain button nor the pointer —
its members' own per-clip FX buttons still work individually, and the
group's own FX button on TimelineGroupHeader covers the group level.
Gates: bun run build clean; packages/studio full suite 4286/4304 (18
pre-existing todo, up from 4276/4294 — 10 new tests, 0 regressions);
new TimelineFxPopover.test.tsx (6) + TimelineFxButton.test.tsx (4)
cover exactly-one-write-per-apply, hover-audition-reverts-on-leave,
Escape-without-deselecting, outside/inside pointerdown dismissal, and
the group-pointer's Group action; oxfmt/oxlint clean on all 22 touched
files; fallow clean (0 new dead-code/unused-export/duplication
findings — the pointer test caught during the first commit attempt).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
598 lines
27 KiB
TypeScript
598 lines
27 KiB
TypeScript
import { useState, useCallback, useRef, useMemo, 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 { ExternalFileConflictBanner } from "./components/ExternalFileConflictBanner";
|
|
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 { usePreviewDocumentVersion } from "./hooks/usePreviewDocumentVersion";
|
|
import { useTimelineEditing } from "./hooks/useTimelineEditing";
|
|
import {
|
|
persistTimelineMoveEditsAtomically,
|
|
type TimelineMoveEditsHandler,
|
|
type TimelineMoveOperation,
|
|
} from "./hooks/timelineMoveAdapter";
|
|
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 { useStudioExternalFileChanges } from "./hooks/useStudioExternalFileChanges";
|
|
import { useBlockHandlers } from "./hooks/useBlockHandlers";
|
|
import { useAppHotkeys } from "./hooks/useAppHotkeys";
|
|
import { useClipboard } from "./hooks/useClipboard";
|
|
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 { useEffectiveTimelineDuration } from "./hooks/useEffectiveTimelineDuration";
|
|
import { useAudioSoloBridge } from "./hooks/useAudioSoloBridge";
|
|
import {
|
|
buildStudioContextValue,
|
|
useGlobalFileDrop,
|
|
useInspectorState,
|
|
} from "./hooks/useStudioContextValue";
|
|
import type { DomEditSelection } from "./components/editor/domEditing";
|
|
import { StudioHeader } from "./components/StudioHeader";
|
|
import { useGestureCommit } from "./hooks/useGestureCommit";
|
|
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
|
|
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
|
|
import { EditorShell } from "./components/EditorShell";
|
|
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 { useStudioSessionStart } from "./hooks/useStudioSessionStart";
|
|
import { useTimelineAddAtPlayhead } from "./hooks/useTimelineAddAtPlayhead";
|
|
import { readStudioUrlStateFromWindow, resolveMasterCompositionPath } from "./utils/studioUrlState";
|
|
import { useHydrateActiveCompPathFromUrl } from "./hooks/useHydrateActiveCompPathFromUrl";
|
|
const getTimelineSelectionSet = () => usePlayerStore.getState().selectedElementIds;
|
|
// fallow-ignore-next-line complexity
|
|
export function StudioApp() {
|
|
const { projectId, resolving, waitingForServer } = useServerConnection();
|
|
const initialUrlStateRef = useRef(readStudioUrlStateFromWindow());
|
|
const viewModeValue = useViewModeState();
|
|
useStudioSessionStart(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);
|
|
useAudioSoloBridge(previewIframeRef);
|
|
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 setTimelineSelectionSet = usePlayerStore((s) => s.setSelectedElementIds);
|
|
const timelineDuration = usePlayerStore((s) => s.duration);
|
|
const isPlaying = usePlayerStore((s) => s.isPlaying);
|
|
const effectiveTimelineDuration = useEffectiveTimelineDuration(
|
|
timelineDuration,
|
|
timelineElements,
|
|
);
|
|
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,
|
|
masterCompPath,
|
|
);
|
|
useHydrateActiveCompPathFromUrl({
|
|
hydrated: activeCompPathHydrated,
|
|
fileTreeLoaded: fileManager.fileTreeLoaded,
|
|
fileTree: fileManager.fileTree,
|
|
initialUrlStateRef,
|
|
setActiveCompPath,
|
|
setHydrated: setActiveCompPathHydrated,
|
|
});
|
|
const previewPersistence = usePreviewPersistence({
|
|
showToast,
|
|
readOptionalProjectFile: fileManager.readOptionalProjectFile,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
recordEdit: editHistory.recordEdit,
|
|
previewIframeRef,
|
|
activeCompPathRef,
|
|
reloadPreview: () => setRefreshKey((k) => k + 1),
|
|
});
|
|
const externalFileChanges = useStudioExternalFileChanges({
|
|
projectId,
|
|
activeCompPath,
|
|
masterCompPath,
|
|
fileManager,
|
|
previewPersistence,
|
|
pendingTimelineEditPathRef,
|
|
reloadPreview,
|
|
});
|
|
const invalidateGsapCacheRef = useRef<() => void>(() => {});
|
|
const invalidateGsapCache = useCallback(() => invalidateGsapCacheRef.current(), []);
|
|
const timelineEditing = useTimelineEditing({
|
|
projectId,
|
|
activeCompPath,
|
|
timelineElements,
|
|
showToast,
|
|
writeProjectFile: fileManager.writeProjectFile,
|
|
observeProjectFileVersion: fileManager.observeProjectFileVersion,
|
|
recordEdit: editHistory.recordEdit,
|
|
domEditSaveTimestampRef,
|
|
reloadPreview,
|
|
previewIframeRef,
|
|
pendingTimelineEditPathRef,
|
|
uploadProjectFiles: fileManager.uploadProjectFiles,
|
|
isRecordingRef: isGestureRecordingRef,
|
|
sdkSession: editFlowSdkSession,
|
|
publishSdkSession: sdkHandle.publish,
|
|
forceReloadSdkSession: sdkHandle.forceReload,
|
|
invalidateGsapCache,
|
|
handleDomZIndexReorderCommitRef,
|
|
});
|
|
const handleTimelineElementsMove: TimelineMoveEditsHandler = useCallback(
|
|
async (edits, coalesceKey, operation: TimelineMoveOperation = "timing", coalesceMs) => {
|
|
const deps = { handleTimelineGroupMove: timelineEditing.handleTimelineGroupMove };
|
|
await persistTimelineMoveEditsAtomically(edits, coalesceKey, operation, deps, coalesceMs);
|
|
},
|
|
[timelineEditing.handleTimelineGroupMove],
|
|
);
|
|
const {
|
|
addAssetAtPlayhead: handleAddAssetAtPlayhead,
|
|
addCompositionAtPlayhead: handleAddCompositionAtPlayhead,
|
|
} = useTimelineAddAtPlayhead(
|
|
timelineEditing.handleTimelineAssetDrop,
|
|
timelineEditing.handleTimelineCompositionDrop,
|
|
);
|
|
const {
|
|
activeBlockParams,
|
|
setActiveBlockParams,
|
|
handleAddBlock,
|
|
handleTimelineBlockDrop,
|
|
handleAddMediaOverlay,
|
|
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);
|
|
type DomEditDelete = (s: DomEditSelection, o?: { expandGroup?: boolean }) => Promise<void>;
|
|
const handleDomEditElementDeleteRef = useRef<DomEditDelete>(async () => {});
|
|
const domEditDeleteBridge: DomEditDelete = (s, o) => handleDomEditElementDeleteRef.current(s, o);
|
|
const resetKeyframesRef = useRef<() => boolean>(() => false);
|
|
const deleteSelectedKeyframesRef = 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({
|
|
handleTimelineElementsDelete: timelineEditing.handleTimelineElementsDelete,
|
|
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: () => handleToggleRecordingRef.current(),
|
|
});
|
|
const sidebarTabRef = useRef({
|
|
select: (t: SidebarTab) => leftSidebarRef.current?.selectTab(t),
|
|
get: () => leftSidebarRef.current?.getTab() ?? "compositions",
|
|
});
|
|
const domEditSession = useDomEditSession({
|
|
projectId,
|
|
activeCompPath,
|
|
compIdToSrc,
|
|
captionEditMode,
|
|
compositionLoading,
|
|
previewIframeRef,
|
|
timelineElements,
|
|
getTimelineSelectionSet,
|
|
setSelectedTimelineElementId,
|
|
setTimelineSelectionSet,
|
|
setRightCollapsed: panelLayout.setRightCollapsed,
|
|
setRightPanelTab: panelLayout.setRightPanelTab,
|
|
showToast,
|
|
isRecordingRef: isGestureRecordingRef,
|
|
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,
|
|
syncPreviewHotkeys: appHotkeys.syncPreviewHotkeys,
|
|
reloadPreview,
|
|
setRefreshKey,
|
|
openSourceForSelection: fileManager.openSourceForSelection,
|
|
selectSidebarTab: sidebarTabRef.current.select,
|
|
getSidebarTab: sidebarTabRef.current.get,
|
|
sdkSession: editFlowSdkSession,
|
|
publishSdkSession: sdkHandle.publish,
|
|
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: activeCompPath
|
|
? `/api/projects/${projectId}/preview/comp/${activeCompPath}`
|
|
: null,
|
|
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 = useGlobalFileDrop(timelineEditing.handleTimelineFileDrop);
|
|
const handleToggleRecordingRef = useRef<() => void>(() => {});
|
|
const domEditSessionRef = useRef(domEditSession);
|
|
domEditSessionRef.current = domEditSession;
|
|
const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
|
|
domEditSessionRef,
|
|
previewIframeRef,
|
|
showToast,
|
|
isGestureRecordingRef,
|
|
});
|
|
handleToggleRecordingRef.current = handleToggleRecording;
|
|
const canvasRectRef = useRef<DOMRect | null>(null);
|
|
useLayoutEffect(() => {
|
|
if (gestureState !== "recording" || !previewIframe) {
|
|
canvasRectRef.current = null;
|
|
return;
|
|
}
|
|
canvasRectRef.current = previewIframe.getBoundingClientRect();
|
|
}, [gestureState, previewIframe]);
|
|
const handlePreviewIframeRef = useCallback(
|
|
(iframe: HTMLIFrameElement | null) => {
|
|
previewIframeRef.current = iframe;
|
|
setPreviewIframe(iframe);
|
|
appHotkeys.syncPreviewHotkeys(iframe);
|
|
resetConsoleErrors();
|
|
refreshPreviewDocumentVersion();
|
|
},
|
|
[appHotkeys, resetConsoleErrors, refreshPreviewDocumentVersion],
|
|
);
|
|
const { setEditingFile } = fileManager;
|
|
const handleSelectComposition = useCompositionContentLoader({
|
|
projectId,
|
|
setEditingFile,
|
|
setActiveCompPath,
|
|
showToast,
|
|
});
|
|
const {
|
|
designPanelActive,
|
|
inspectorPanelActive,
|
|
inspectorButtonActive,
|
|
shouldShowMotionPath,
|
|
shouldShowSelectedDomBounds,
|
|
} = useInspectorState(
|
|
panelLayout.rightPanelTab,
|
|
panelLayout.rightInspectorPanes,
|
|
panelLayout.effectiveRightCollapsed,
|
|
isPlaying,
|
|
domEditSession.domEditSelection,
|
|
gestureState === "recording",
|
|
);
|
|
useStudioUrlState({
|
|
projectId,
|
|
activeCompPath,
|
|
duration: effectiveTimelineDuration,
|
|
isPlaying,
|
|
compositionLoading,
|
|
refreshKey,
|
|
previewIframeRef,
|
|
rightPanelTab: panelLayout.rightPanelTab,
|
|
rightCollapsed: panelLayout.rightCollapsed,
|
|
activeCompPathHydrated,
|
|
domEditSelection: domEditSession.domEditSelection,
|
|
domEditGroupSelections: domEditSession.domEditGroupSelections,
|
|
applyMarqueeSelection: domEditSession.applyMarqueeSelection,
|
|
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,
|
|
});
|
|
const timelineToolbar = useMemo(
|
|
() => (
|
|
<TimelineToolbar
|
|
domEditSession={domEditSession}
|
|
onSplitElement={timelineEditing.handleTimelineElementSplit}
|
|
/>
|
|
),
|
|
[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 && !externalFileChanges.blocked && (
|
|
<SaveQueuePausedBanner
|
|
message={previewPersistence.domEditSaveQueuePaused}
|
|
onRetry={previewPersistence.resetDomEditSaveQueueBreaker}
|
|
/>
|
|
)}
|
|
<ExternalFileConflictBanner coordinator={externalFileChanges} />
|
|
{viewModeValue.viewMode === "storyboard" && (
|
|
<StoryboardView
|
|
projectId={projectId}
|
|
onSelectComposition={handleSelectComposition}
|
|
/>
|
|
)}
|
|
<EditorShell
|
|
hidden={viewModeValue.viewMode === "storyboard"}
|
|
left={
|
|
<StudioLeftSidebar
|
|
leftSidebarRef={leftSidebarRef}
|
|
onSelectComposition={handleSelectComposition}
|
|
onAddBlock={handleAddBlock}
|
|
onPreviewBlock={setBlockPreview}
|
|
onLint={handleLint}
|
|
linting={linting}
|
|
lintFindingCount={lintModal?.length ?? findingsByFile.size}
|
|
lintFindingsByFile={findingsByFile}
|
|
onAddAssetToTimeline={handleAddAssetAtPlayhead}
|
|
onAddCompositionToTimeline={handleAddCompositionAtPlayhead}
|
|
/>
|
|
}
|
|
right={
|
|
panelLayout.effectiveRightCollapsed ? null : (
|
|
<StudioRightPanel
|
|
designPanelActive={designPanelActive}
|
|
activeBlockParams={activeBlockParams}
|
|
onCloseBlockParams={() => {
|
|
setActiveBlockParams(null);
|
|
panelLayout.setRightPanelTab("design");
|
|
}}
|
|
recordingState={gestureState}
|
|
recordingDuration={gestureRecording.recordingDuration}
|
|
onToggleRecording={handleToggleRecording}
|
|
sdkSession={sdkHandle.session}
|
|
publishSdkSession={sdkHandle.publish}
|
|
forceReloadSdkSession={sdkHandle.forceReload}
|
|
reloadPreview={reloadPreview}
|
|
domEditSaveTimestampRef={domEditSaveTimestampRef}
|
|
recordEdit={editHistory.recordEdit}
|
|
onToggleElementHidden={timelineEditing.handleToggleElementHidden}
|
|
onAutoGroupCarveSources={timelineEditing.handleAutoGroupCarveSources}
|
|
onAddMediaOverlay={handleAddMediaOverlay}
|
|
/>
|
|
)
|
|
}
|
|
timelineToolbar={timelineToolbar}
|
|
renderClipContent={renderClipContent}
|
|
handleTimelineElementDelete={timelineEditing.handleTimelineElementDelete}
|
|
handleTimelineAssetDrop={timelineEditing.handleTimelineAssetDrop}
|
|
handleTimelineBlockDrop={handleTimelineBlockDrop}
|
|
handleTimelineCompositionDrop={timelineEditing.handleTimelineCompositionDrop}
|
|
handlePreviewBlockDrop={handlePreviewBlockDrop}
|
|
handleTimelineFileDrop={timelineEditing.handleTimelineFileDrop}
|
|
handleTimelineElementMove={timelineEditing.handleTimelineElementMove}
|
|
handleTimelineElementsMove={handleTimelineElementsMove}
|
|
handleTimelineElementResize={timelineEditing.handleTimelineElementResize}
|
|
handleTimelineGroupResize={timelineEditing.handleTimelineGroupResize}
|
|
handleToggleTrackHidden={timelineEditing.handleToggleTrackHidden}
|
|
setAudioGroupAttribute={timelineEditing.setAudioGroupAttribute}
|
|
handleGroupClips={timelineEditing.handleAutoGroupCarveSources}
|
|
setElementFxAttribute={timelineEditing.setElementFxAttribute}
|
|
handleBlockedTimelineEdit={timelineEditing.handleBlockedTimelineEdit}
|
|
handleTimelineElementSplit={timelineEditing.handleTimelineElementSplit}
|
|
handleRazorSplit={timelineEditing.handleRazorSplit}
|
|
handleRazorSplitAll={timelineEditing.handleRazorSplitAll}
|
|
setCompIdToSrc={setCompIdToSrc}
|
|
setCompositionLoading={setCompositionLoading}
|
|
shouldShowMotionPath={shouldShowMotionPath}
|
|
shouldShowSelectedDomBounds={shouldShowSelectedDomBounds}
|
|
isGestureRecording={gestureState === "recording"}
|
|
recordingState={gestureState}
|
|
onToggleRecording={handleToggleRecording}
|
|
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
|
|
}
|
|
/>
|
|
<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>
|
|
);
|
|
}
|