diff options
Diffstat (limited to 'apps/web/src/hooks/actions')
| -rw-r--r-- | apps/web/src/hooks/actions/use-action-handler.ts | 52 | ||||
| -rw-r--r-- | apps/web/src/hooks/actions/use-editor-actions.ts | 312 |
2 files changed, 364 insertions, 0 deletions
diff --git a/apps/web/src/hooks/actions/use-action-handler.ts b/apps/web/src/hooks/actions/use-action-handler.ts new file mode 100644 index 00000000..3f6dbd21 --- /dev/null +++ b/apps/web/src/hooks/actions/use-action-handler.ts @@ -0,0 +1,52 @@ +import { useCallback, useEffect, useRef } from "react"; +import type { + TAction, + TActionFunc, + TActionHandlerOptions, + TArgOfAction, + TInvocationTrigger, +} from "@/lib/actions"; +import { bindAction, unbindAction } from "@/lib/actions"; + +export function useActionHandler<A extends TAction>( + action: A, + handler: TActionFunc<A>, + isActive: TActionHandlerOptions, +) { + const handlerRef = useRef<TActionFunc<A>>(handler); + const isBoundRef = useRef(false); + + useEffect(() => { + handlerRef.current = handler; + }, [handler]); + + const stableHandler = useCallback( + (...parameters: [TArgOfAction<A>, TInvocationTrigger?]) => { + ( + handlerRef.current as ( + ...handlerParameters: [TArgOfAction<A>, TInvocationTrigger?] + ) => void + )(...parameters); + }, + [], + ) as TActionFunc<A>; + + useEffect(() => { + const shouldBind = + isActive === undefined || + (typeof isActive === "boolean" ? isActive : isActive.current); + + if (shouldBind && !isBoundRef.current) { + bindAction(action, stableHandler); + isBoundRef.current = true; + } else if (!shouldBind && isBoundRef.current) { + unbindAction(action, stableHandler); + isBoundRef.current = false; + } + + return () => { + unbindAction(action, stableHandler); + isBoundRef.current = false; + }; + }, [action, stableHandler, isActive]); +} diff --git a/apps/web/src/hooks/actions/use-editor-actions.ts b/apps/web/src/hooks/actions/use-editor-actions.ts new file mode 100644 index 00000000..d1ff6c92 --- /dev/null +++ b/apps/web/src/hooks/actions/use-editor-actions.ts @@ -0,0 +1,312 @@ +"use client"; + +import { useTimelineStore } from "@/stores/timeline-store"; +import { useActionHandler } from "@/hooks/actions/use-action-handler"; +import { useEditor } from "../use-editor"; +import { useElementSelection } from "../timeline/element/use-element-selection"; +import { getElementsAtTime } from "@/lib/timeline"; + +export function useEditorActions() { + const editor = useEditor(); + const activeProject = editor.project.getActive(); + const { selectedElements, setElementSelection } = useElementSelection(); + const { clipboard, setClipboard, toggleSnapping } = useTimelineStore(); + + useActionHandler( + "toggle-play", + () => { + editor.playback.toggle(); + }, + undefined, + ); + + useActionHandler( + "stop-playback", + () => { + if (editor.playback.getIsPlaying()) { + editor.playback.toggle(); + } + editor.playback.seek({ time: 0 }); + }, + undefined, + ); + + useActionHandler( + "seek-forward", + (args) => { + const seconds = args?.seconds ?? 1; + editor.playback.seek({ + time: Math.min( + editor.timeline.getTotalDuration(), + editor.playback.getCurrentTime() + seconds, + ), + }); + }, + undefined, + ); + + useActionHandler( + "seek-backward", + (args) => { + const seconds = args?.seconds ?? 1; + editor.playback.seek({ + time: Math.max(0, editor.playback.getCurrentTime() - seconds), + }); + }, + undefined, + ); + + useActionHandler( + "frame-step-forward", + () => { + const fps = activeProject.settings.fps; + editor.playback.seek({ + time: Math.min( + editor.timeline.getTotalDuration(), + editor.playback.getCurrentTime() + 1 / fps, + ), + }); + }, + undefined, + ); + + useActionHandler( + "frame-step-backward", + () => { + const fps = activeProject.settings.fps; + editor.playback.seek({ + time: Math.max(0, editor.playback.getCurrentTime() - 1 / fps), + }); + }, + undefined, + ); + + useActionHandler( + "jump-forward", + (args) => { + const seconds = args?.seconds ?? 5; + editor.playback.seek({ + time: Math.min( + editor.timeline.getTotalDuration(), + editor.playback.getCurrentTime() + seconds, + ), + }); + }, + undefined, + ); + + useActionHandler( + "jump-backward", + (args) => { + const seconds = args?.seconds ?? 5; + editor.playback.seek({ + time: Math.max(0, editor.playback.getCurrentTime() - seconds), + }); + }, + undefined, + ); + + useActionHandler( + "goto-start", + () => { + editor.playback.seek({ time: 0 }); + }, + undefined, + ); + + useActionHandler( + "goto-end", + () => { + editor.playback.seek({ time: editor.timeline.getTotalDuration() }); + }, + undefined, + ); + + useActionHandler( + "split", + () => { + const currentTime = editor.playback.getCurrentTime(); + const elementsToSplit = + selectedElements.length > 0 + ? selectedElements + : getElementsAtTime({ + tracks: editor.timeline.getTracks(), + time: currentTime, + }); + + if (elementsToSplit.length === 0) return; + + editor.timeline.splitElements({ + elements: elementsToSplit, + splitTime: currentTime, + }); + }, + undefined, + ); + + useActionHandler( + "split-left", + () => { + const currentTime = editor.playback.getCurrentTime(); + const elementsToSplit = + selectedElements.length > 0 + ? selectedElements + : getElementsAtTime({ + tracks: editor.timeline.getTracks(), + time: currentTime, + }); + + if (elementsToSplit.length === 0) return; + + editor.timeline.splitElements({ + elements: elementsToSplit, + splitTime: currentTime, + retainSide: "right", + }); + }, + undefined, + ); + + useActionHandler( + "split-right", + () => { + const currentTime = editor.playback.getCurrentTime(); + const elementsToSplit = + selectedElements.length > 0 + ? selectedElements + : getElementsAtTime({ + tracks: editor.timeline.getTracks(), + time: currentTime, + }); + + if (elementsToSplit.length === 0) return; + + editor.timeline.splitElements({ + elements: elementsToSplit, + splitTime: currentTime, + retainSide: "left", + }); + }, + undefined, + ); + + useActionHandler( + "delete-selected", + () => { + if (selectedElements.length === 0) { + return; + } + editor.timeline.deleteElements({ + elements: selectedElements, + }); + }, + undefined, + ); + + useActionHandler( + "select-all", + () => { + const allElements = editor.timeline.getTracks().flatMap((track) => + track.elements.map((element) => ({ + trackId: track.id, + elementId: element.id, + })), + ); + setElementSelection({ elements: allElements }); + }, + undefined, + ); + + useActionHandler( + "duplicate-selected", + () => { + editor.timeline.duplicateElements({ + elements: selectedElements, + }); + }, + undefined, + ); + + useActionHandler( + "toggle-elements-muted-selected", + () => { + editor.timeline.toggleElementsMuted({ elements: selectedElements }); + }, + undefined, + ); + + useActionHandler( + "toggle-elements-visibility-selected", + () => { + editor.timeline.toggleElementsVisibility({ elements: selectedElements }); + }, + undefined, + ); + + useActionHandler( + "toggle-bookmark", + () => { + editor.scenes.toggleBookmark({ time: editor.playback.getCurrentTime() }); + }, + undefined, + ); + + useActionHandler( + "copy-selected", + () => { + if (selectedElements.length === 0) return; + + const results = editor.timeline.getElementsWithTracks({ + elements: selectedElements, + }); + const items = results.map(({ track, element }) => { + const { ...elementWithoutId } = element; + return { + trackId: track.id, + trackType: track.type, + element: elementWithoutId, + }; + }); + + setClipboard({ items }); + }, + undefined, + ); + + useActionHandler( + "paste-copied", + () => { + if (!clipboard?.items.length) return; + + editor.timeline.pasteAtTime({ + time: editor.playback.getCurrentTime(), + clipboardItems: clipboard.items, + }); + }, + undefined, + ); + + useActionHandler( + "toggle-snapping", + () => { + toggleSnapping(); + }, + undefined, + ); + + useActionHandler( + "undo", + () => { + editor.command.undo(); + }, + undefined, + ); + + useActionHandler( + "redo", + () => { + editor.command.redo(); + }, + undefined, + ); +} |
