mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
Merge pull request #2111 from heygen-com/feat/timeline-multiselect
feat(studio): timeline multi-select (marquee) + relative group time editing
This commit is contained in:
@@ -25,6 +25,15 @@ import { TimelineEditProvider } from "../contexts/TimelineEditContext";
|
||||
import type { BlockPreviewInfo } from "./sidebar/BlocksTab";
|
||||
import { readStudioUiPreferences } from "../utils/studioUiPreferences";
|
||||
import type { GestureRecordingState } from "./editor/GestureRecordControl";
|
||||
import { useTimelineSelectionPreviewSync } from "../hooks/useTimelineSelectionPreviewSync";
|
||||
import type {
|
||||
TimelineGroupMoveChange,
|
||||
TimelineGroupResizeChange,
|
||||
} from "../hooks/useTimelineGroupEditing";
|
||||
import {
|
||||
formatTimelineAttributeNumber,
|
||||
patchIframeDomTiming,
|
||||
} from "../hooks/timelineEditingHelpers";
|
||||
|
||||
export interface StudioPreviewAreaProps {
|
||||
timelineToolbar: ReactNode;
|
||||
@@ -58,6 +67,8 @@ export interface StudioPreviewAreaProps {
|
||||
element: TimelineElement,
|
||||
updates: Pick<TimelineElement, "start" | "duration" | "playbackStart">,
|
||||
) => Promise<void> | void;
|
||||
handleTimelineGroupMove: (changes: TimelineGroupMoveChange[]) => Promise<void> | void;
|
||||
handleTimelineGroupResize: (changes: TimelineGroupResizeChange[]) => Promise<void> | void;
|
||||
handleToggleTrackHidden: (track: number, hidden: boolean) => Promise<void> | void;
|
||||
handleToggleElementHidden: (elementKey: string, hidden: boolean) => Promise<void> | void;
|
||||
handleBlockedTimelineEdit: (element: TimelineElement, intent: BlockedTimelineEditIntent) => void;
|
||||
@@ -85,6 +96,8 @@ export function StudioPreviewArea({
|
||||
handleTimelineFileDrop,
|
||||
handleTimelineElementMove,
|
||||
handleTimelineElementResize,
|
||||
handleTimelineGroupMove,
|
||||
handleTimelineGroupResize,
|
||||
handleToggleTrackHidden,
|
||||
handleToggleElementHidden,
|
||||
handleBlockedTimelineEdit,
|
||||
@@ -148,6 +161,9 @@ export function StudioPreviewArea({
|
||||
buildDomSelectionForTimelineElement,
|
||||
applyMarqueeSelection,
|
||||
} = useDomEditActionsContext();
|
||||
const selectedElementId = usePlayerStore((s) => s.selectedElementId);
|
||||
const selectedElementIds = usePlayerStore((s) => s.selectedElementIds);
|
||||
const timelineElements = usePlayerStore((s) => s.elements);
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const [snapPrefs, setSnapPrefs] = useState(() => {
|
||||
@@ -160,6 +176,18 @@ export function StudioPreviewArea({
|
||||
};
|
||||
});
|
||||
|
||||
useTimelineSelectionPreviewSync({
|
||||
selectedElementId,
|
||||
selectedElementIds,
|
||||
timelineElements,
|
||||
domEditSelection,
|
||||
domEditGroupSelections,
|
||||
activeCompPath,
|
||||
buildDomSelectionForTimelineElement,
|
||||
applyDomSelection,
|
||||
applyMarqueeSelection,
|
||||
});
|
||||
|
||||
// Resolve a timeline-diamond callback's clip-% to the keyframe's anim id + its
|
||||
// tween-relative percentage (shared by the delete/move keyframe callbacks): the
|
||||
// diamond reports a clip-% but the script ops key on the tween-%. Prefers the
|
||||
@@ -178,11 +206,47 @@ export function StudioPreviewArea({
|
||||
[domEditSelection?.id, selectedGsapAnimations],
|
||||
);
|
||||
|
||||
const handleTimelineGroupMovePreview = useCallback(
|
||||
(changes: TimelineGroupMoveChange[]) => {
|
||||
for (const change of changes) {
|
||||
patchIframeDomTiming(previewIframeRef.current, change.element, [
|
||||
["data-start", formatTimelineAttributeNumber(change.start)],
|
||||
]);
|
||||
}
|
||||
},
|
||||
[previewIframeRef],
|
||||
);
|
||||
|
||||
const handleTimelineGroupResizePreview = useCallback(
|
||||
(changes: TimelineGroupResizeChange[]) => {
|
||||
for (const change of changes) {
|
||||
const attrs: Array<[string, string]> = [
|
||||
["data-start", formatTimelineAttributeNumber(change.start)],
|
||||
["data-duration", formatTimelineAttributeNumber(change.duration)],
|
||||
];
|
||||
if (change.playbackStart != null) {
|
||||
attrs.push([
|
||||
change.element.playbackStartAttr === "playback-start"
|
||||
? "data-playback-start"
|
||||
: "data-media-start",
|
||||
formatTimelineAttributeNumber(change.playbackStart),
|
||||
]);
|
||||
}
|
||||
patchIframeDomTiming(previewIframeRef.current, change.element, attrs);
|
||||
}
|
||||
},
|
||||
[previewIframeRef],
|
||||
);
|
||||
|
||||
// fallow-ignore-next-line complexity
|
||||
const timelineEditCallbacks = useMemo(
|
||||
() => ({
|
||||
onMoveElement: handleTimelineElementMove,
|
||||
onResizeElement: handleTimelineElementResize,
|
||||
onMoveElements: handleTimelineGroupMove,
|
||||
onResizeElements: handleTimelineGroupResize,
|
||||
onPreviewMoveElements: handleTimelineGroupMovePreview,
|
||||
onPreviewResizeElements: handleTimelineGroupResizePreview,
|
||||
onToggleTrackHidden: handleToggleTrackHidden,
|
||||
onToggleElementHidden: handleToggleElementHidden,
|
||||
onBlockedEditAttempt: handleBlockedTimelineEdit,
|
||||
@@ -191,7 +255,7 @@ export function StudioPreviewArea({
|
||||
onRazorSplitAll: handleRazorSplitAll,
|
||||
onDeleteAllKeyframes: () => {
|
||||
// Hold the element where it is (collapse keyframes to a static set) rather
|
||||
// than deleting the whole animation — deleting strands a stale GSAP base
|
||||
// than deleting the whole animation, deleting strands a stale GSAP base
|
||||
// that the next drag adds to, flinging the element off-screen.
|
||||
const anim = selectedGsapAnimations.find((a) => a.keyframes);
|
||||
if (!anim) return;
|
||||
@@ -211,7 +275,7 @@ export function StudioPreviewArea({
|
||||
// absolute time (via the clip's timing basis) and let resolveKeyframeRetime
|
||||
// decide: a drop inside the tween window is a plain move (re-key tween-%); a
|
||||
// drop past the boundary (last keyframe past the end, first before the start)
|
||||
// resizes the tween — position/duration grow so the dragged keyframe lands at
|
||||
// resizes the tween, position/duration grow so the dragged keyframe lands at
|
||||
// the drop while every other keyframe keeps its absolute time (value+ease too).
|
||||
// fallow-ignore-next-line complexity
|
||||
onMoveKeyframe: (_elId: string, fromClipPct: number, toClipPct: number) => {
|
||||
@@ -284,6 +348,10 @@ export function StudioPreviewArea({
|
||||
[
|
||||
handleTimelineElementMove,
|
||||
handleTimelineElementResize,
|
||||
handleTimelineGroupMove,
|
||||
handleTimelineGroupMovePreview,
|
||||
handleTimelineGroupResize,
|
||||
handleTimelineGroupResizePreview,
|
||||
handleToggleTrackHidden,
|
||||
handleToggleElementHidden,
|
||||
handleBlockedTimelineEdit,
|
||||
@@ -327,7 +395,7 @@ export function StudioPreviewArea({
|
||||
onCompositionLoadingChange={setCompositionLoading}
|
||||
onCompositionChange={(compPath) => {
|
||||
// Sync activeCompPath when user drills down via timeline double-click
|
||||
// or navigates back via breadcrumb — keeps sidebar + thumbnails in sync.
|
||||
// or navigates back via breadcrumb, keeps sidebar + thumbnails in sync.
|
||||
// Guard against no-op updates to prevent circular refresh cascades
|
||||
// between activeCompPath → compositionStack → onCompositionChange.
|
||||
if (compPath !== activeCompPath) {
|
||||
|
||||
Reference in New Issue
Block a user