Skip to content

Commit 81416ab

Browse files
refactor(studio): split oversized files to pass 600-line check (#1313)
1 parent d13ae13 commit 81416ab

17 files changed

Lines changed: 1380 additions & 1055 deletions

packages/studio/src/App.tsx

Lines changed: 21 additions & 190 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,9 @@ import { usePanelLayout } from "./hooks/usePanelLayout";
1010
import { useFileManager } from "./hooks/useFileManager";
1111
import { usePreviewPersistence } from "./hooks/usePreviewPersistence";
1212
import { useTimelineEditing } from "./hooks/useTimelineEditing";
13-
import { addBlockToProject } from "./utils/blockInstaller";
14-
import type { BlockParam } from "@hyperframes/core/registry";
1513
import type { BlockPreviewInfo } from "./components/sidebar/BlocksTab";
1614
import { useDomEditSession } from "./hooks/useDomEditSession";
15+
import { useBlockHandlers } from "./hooks/useBlockHandlers";
1716
import { useAppHotkeys } from "./hooks/useAppHotkeys";
1817
import { useClipboard } from "./hooks/useClipboard";
1918
import { readStudioUiPreferences, writeStudioUiPreferences } from "./utils/studioUiPreferences";
@@ -35,8 +34,7 @@ import type { DomEditSelection } from "./components/editor/domEditing";
3534
import { AskAgentModal } from "./components/AskAgentModal";
3635
import { StudioGlobalDragOverlay } from "./components/StudioGlobalDragOverlay";
3736
import { StudioHeader } from "./components/StudioHeader";
38-
import { useGestureRecording } from "./hooks/useGestureRecording";
39-
import { simplifyGestureSamples } from "./utils/rdpSimplify";
37+
import { useGestureCommit } from "./hooks/useGestureCommit";
4038

4139
import { GestureTrailOverlay } from "./components/editor/GestureTrailOverlay";
4240
import { StudioLeftSidebar } from "./components/StudioLeftSidebar";
@@ -82,12 +80,6 @@ export function StudioApp() {
8280
const [compositionLoading, setCompositionLoading] = useState(true);
8381
const [refreshKey, setRefreshKey] = useState(0);
8482
const [, setPreviewDocumentVersion] = useState(0);
85-
const [activeBlockParams, setActiveBlockParams] = useState<{
86-
blockName: string;
87-
blockTitle: string;
88-
params: BlockParam[];
89-
compositionPath: string;
90-
} | null>(null);
9183
const [blockPreview, setBlockPreview] = useState<BlockPreviewInfo | null>(null);
9284

9385
const previewIframeRef = useRef<HTMLIFrameElement | null>(null);
@@ -192,8 +184,15 @@ export function StudioApp() {
192184
isRecordingRef: isGestureRecordingRef,
193185
});
194186

195-
const blockCtx = useMemo(
196-
() => ({
187+
const {
188+
activeBlockParams,
189+
setActiveBlockParams,
190+
handleAddBlock,
191+
handleTimelineBlockDrop,
192+
handlePreviewBlockDrop,
193+
} = useBlockHandlers({
194+
projectId,
195+
blockCtxDeps: {
197196
activeCompPath,
198197
timelineElements,
199198
readProjectFile: fileManager.readProjectFile,
@@ -202,70 +201,11 @@ export function StudioApp() {
202201
refreshFileTree: fileManager.refreshFileTree,
203202
reloadPreview,
204203
showToast,
205-
}),
206-
[
207-
activeCompPath,
208-
timelineElements,
209-
fileManager,
210-
editHistory.recordEdit,
211-
reloadPreview,
212-
showToast,
213-
],
214-
);
215-
const handleAddBlock = useCallback(
216-
(blockName: string) => {
217-
if (!projectId) return;
218-
void (async () => {
219-
const result = await addBlockToProject({
220-
projectId,
221-
blockName,
222-
...blockCtx,
223-
previewIframe: previewIframeRef.current,
224-
currentTime: usePlayerStore.getState().currentTime,
225-
});
226-
const params = result?.block.type === "hyperframes:block" ? result.block.params : undefined;
227-
if (params?.length) {
228-
setActiveBlockParams({
229-
blockName: result!.block.name,
230-
blockTitle: result!.block.title,
231-
params,
232-
compositionPath: result!.compositionPath,
233-
});
234-
panelLayout.setRightCollapsed(false);
235-
panelLayout.setRightPanelTab("block-params");
236-
}
237-
})();
238204
},
239-
[projectId, blockCtx, panelLayout],
240-
);
241-
const handleTimelineBlockDrop = useCallback(
242-
(blockName: string, placement: { start: number; track: number }) => {
243-
if (!projectId) return;
244-
void addBlockToProject({
245-
projectId,
246-
blockName,
247-
placement,
248-
...blockCtx,
249-
previewIframe: previewIframeRef.current,
250-
currentTime: usePlayerStore.getState().currentTime,
251-
});
252-
},
253-
[projectId, blockCtx],
254-
);
255-
const handlePreviewBlockDrop = useCallback(
256-
(blockName: string, position: { left: number; top: number }) => {
257-
if (!projectId) return;
258-
void addBlockToProject({
259-
projectId,
260-
blockName,
261-
visualPosition: position,
262-
...blockCtx,
263-
previewIframe: previewIframeRef.current,
264-
currentTime: usePlayerStore.getState().currentTime,
265-
});
266-
},
267-
[projectId, blockCtx],
268-
);
205+
previewIframeRef,
206+
setRightCollapsed: panelLayout.setRightCollapsed,
207+
setRightPanelTab: panelLayout.setRightPanelTab,
208+
});
269209

270210
const clearDomSelectionRef = useRef<() => void>(() => {});
271211
const domEditSelectionBridgeRef = useRef<DomEditSelection | null>(null);
@@ -406,125 +346,16 @@ export function StudioApp() {
406346
const dragOverlay = useDragOverlay(fileManager.handleImportFiles);
407347

408348
// Gesture recording
409-
const gestureRecording = useGestureRecording();
410-
const [gestureState, setGestureState] = useState<"idle" | "recording">("idle");
411-
// Synchronous mirror of gestureState — immune to React batching.
412-
// Prevents double-R-press within a single render cycle from swallowing the stop.
413-
const gestureStateRef = useRef<"idle" | "recording">("idle");
414-
const recordingAutoStopRef = useRef<ReturnType<typeof setInterval>>(undefined);
415-
const recordingStartTimeRef = useRef(0);
416-
const commitInFlightRef = useRef(false);
417349
const handleToggleRecordingRef = useRef<() => void>(() => {});
418350
const domEditSessionRef = useRef(domEditSession);
419351
domEditSessionRef.current = domEditSession;
420352

421-
// Unmount: clear auto-stop interval
422-
useEffect(() => () => clearInterval(recordingAutoStopRef.current), []);
423-
424-
// fallow-ignore-next-line complexity
425-
const stopAndCommitRecording = useCallback(async () => {
426-
clearInterval(recordingAutoStopRef.current);
427-
if (commitInFlightRef.current) return;
428-
commitInFlightRef.current = true;
429-
gestureStateRef.current = "idle";
430-
isGestureRecordingRef.current = false;
431-
const frozenSamples = gestureRecording.stopRecording();
432-
const store = usePlayerStore.getState();
433-
store.setIsPlaying(false);
434-
try {
435-
const liveSession = domEditSessionRef.current;
436-
const sel = liveSession.domEditSelection;
437-
if (!sel) {
438-
if (frozenSamples.length > 2) {
439-
showToast("Selection lost during recording", "error");
440-
}
441-
return;
442-
}
443-
const duration = frozenSamples.length > 0 ? frozenSamples[frozenSamples.length - 1]!.time : 0;
444-
445-
if (frozenSamples.length <= 2) {
446-
showToast("No gesture detected — move the pointer while recording", "error");
447-
return;
448-
}
449-
if (duration <= 0) {
450-
showToast("Recording too short — try again", "error");
451-
return;
452-
}
453-
454-
const simplified = simplifyGestureSamples(frozenSamples, duration, 5);
455-
const sortedPcts = Array.from(simplified.keys()).sort((a, b) => a - b);
456-
457-
// Always create a new tween scoped to the recording range.
458-
// Injecting into an existing tween creates keyframes before the recording
459-
// start (from the convert-to-keyframes step), causing wrong positions.
460-
const selector = sel.id ? `#${sel.id}` : sel.selector;
461-
if (!selector) {
462-
showToast("Cannot save — element has no selector", "error");
463-
return;
464-
}
465-
if (liveSession.commitMutation) {
466-
const recStart = recordingStartTimeRef.current;
467-
const keyframes = sortedPcts.map((pct) => ({
468-
percentage: pct,
469-
properties: simplified.get(pct) as Record<string, number | string>,
470-
}));
471-
472-
await liveSession.commitMutation(
473-
{
474-
type: "add-with-keyframes",
475-
targetSelector: selector,
476-
position: Math.round(recStart * 1000) / 1000,
477-
duration: Math.round(duration * 1000) / 1000,
478-
keyframes,
479-
},
480-
{ label: "Gesture recording", softReload: true },
481-
);
482-
}
483-
showToast(`Recorded ${sortedPcts.length} keyframes`, "info");
484-
} finally {
485-
store.requestSeek(recordingStartTimeRef.current);
486-
gestureRecording.clearSamples();
487-
setGestureState("idle");
488-
commitInFlightRef.current = false;
489-
}
490-
}, [gestureRecording, showToast]);
491-
492-
const handleToggleRecording = useCallback(() => {
493-
if (gestureStateRef.current === "recording") {
494-
void stopAndCommitRecording();
495-
return;
496-
}
497-
const sel = domEditSessionRef.current.domEditSelection;
498-
if (!sel) {
499-
showToast("Select an element first", "error");
500-
return;
501-
}
502-
const iframe = previewIframeRef.current;
503-
if (!iframe) {
504-
showToast("Preview not ready — try again", "error");
505-
return;
506-
}
507-
508-
const store = usePlayerStore.getState();
509-
recordingStartTimeRef.current = store.currentTime;
510-
const elStart = Number.parseFloat(sel.dataAttributes?.start ?? "0") || 0;
511-
const elDur = Number.parseFloat(sel.dataAttributes?.duration ?? "0") || 0;
512-
const elementEnd = elDur > 0 ? elStart + elDur : undefined;
513-
gestureRecording.startRecording(sel.element, iframe, elementEnd);
514-
gestureStateRef.current = "recording";
515-
isGestureRecordingRef.current = true;
516-
setGestureState("recording");
517-
518-
clearInterval(recordingAutoStopRef.current);
519-
const autoStopAt = elementEnd ?? Infinity;
520-
recordingAutoStopRef.current = setInterval(() => {
521-
const { currentTime: t, duration: d } = usePlayerStore.getState();
522-
const limit = Math.min(autoStopAt, d);
523-
if (limit > 0 && t >= limit - 0.05) {
524-
void stopAndCommitRecording();
525-
}
526-
}, 100);
527-
}, [gestureRecording, showToast, stopAndCommitRecording]);
353+
const { gestureState, gestureRecording, handleToggleRecording } = useGestureCommit({
354+
domEditSessionRef,
355+
previewIframeRef,
356+
showToast,
357+
isGestureRecordingRef,
358+
});
528359
handleToggleRecordingRef.current = handleToggleRecording;
529360

530361
const handlePreviewIframeRef = useCallback(

0 commit comments

Comments
 (0)