OpenCut/apps/web/src/actions/use-editor-actions.ts

489 lines
10 KiB
TypeScript

"use client";
import { useEffect, useRef } from "react";
import { useTimelineStore } from "@/timeline/timeline-store";
import { useActionHandler } from "@/actions/use-action-handler";
import { useEditor } from "@/editor/use-editor";
import { useElementSelection } from "@/timeline/hooks/element/use-element-selection";
import { TICKS_PER_SECOND } from "@/wasm";
import { useKeyframeSelection } from "@/timeline/hooks/element/use-keyframe-selection";
import { getElementsAtTime, hasMediaId } from "@/timeline";
import { cancelInteraction } from "@/editor/cancel-interaction";
import { invokeAction } from "@/actions";
import { canToggleSourceAudio } from "@/timeline/audio-separation";
import {
activateScope,
clearActiveScope,
type ScopeEntry,
} from "@/selection/scope";
export function useEditorActions() {
const editor = useEditor();
const { selectedElements, setElementSelection } = useElementSelection();
const { selectedKeyframes, clearKeyframeSelection } = useKeyframeSelection();
const selectedMaskPointSelection = useEditor((e) =>
e.selection.getSelectedMaskPointSelection(),
);
const toggleSnapping = useTimelineStore((s) => s.toggleSnapping);
const rippleEditingEnabled = useTimelineStore((s) => s.rippleEditingEnabled);
const toggleRippleEditing = useTimelineStore((s) => s.toggleRippleEditing);
const hasTimelineSelectionRef = useRef(false);
const clearTimelineSelectionRef = useRef(() => {});
const clearTimelineActiveSelectionRef = useRef(() => {});
const timelineScopeRef = useRef<ScopeEntry | null>(null);
const hasTimelineSelection =
selectedElements.length > 0 ||
selectedKeyframes.length > 0 ||
selectedMaskPointSelection !== null;
hasTimelineSelectionRef.current = hasTimelineSelection;
clearTimelineSelectionRef.current = () => {
editor.selection.clearSelection();
};
clearTimelineActiveSelectionRef.current = () => {
editor.selection.clearMostSpecificSelection();
};
if (!timelineScopeRef.current) {
timelineScopeRef.current = {
hasSelection: () => hasTimelineSelectionRef.current,
clear: () => {
clearTimelineSelectionRef.current();
},
clearActive: () => {
clearTimelineActiveSelectionRef.current();
},
};
}
useEffect(() => {
if (!hasTimelineSelection) {
return;
}
const timelineScope = timelineScopeRef.current;
if (!timelineScope) {
return;
}
return activateScope({ entry: timelineScope });
}, [hasTimelineSelection]);
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 = editor.project.getActive().settings.fps;
const ticksPerFrame = Math.round(
(TICKS_PER_SECOND * fps.denominator) / fps.numerator,
);
editor.playback.seek({
time: Math.min(
editor.timeline.getTotalDuration(),
editor.playback.getCurrentTime() + ticksPerFrame,
),
});
},
undefined,
);
useActionHandler(
"frame-step-backward",
() => {
const fps = editor.project.getActive().settings.fps;
const ticksPerFrame = Math.round(
(TICKS_PER_SECOND * fps.denominator) / fps.numerator,
);
editor.playback.seek({
time: Math.max(0, editor.playback.getCurrentTime() - ticksPerFrame),
});
},
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 tracks = editor.scenes.getActiveScene().tracks;
const elementsToSplit =
selectedElements.length > 0
? selectedElements
: getElementsAtTime({
tracks,
time: currentTime,
});
if (elementsToSplit.length === 0) return;
editor.timeline.splitElements({
elements: elementsToSplit,
splitTime: currentTime,
});
},
undefined,
);
useActionHandler(
"split-left",
() => {
const currentTime = editor.playback.getCurrentTime();
const tracks = editor.scenes.getActiveScene().tracks;
const elementsToSplit =
selectedElements.length > 0
? selectedElements
: getElementsAtTime({
tracks,
time: currentTime,
});
if (elementsToSplit.length === 0) return;
const rightSideElements = editor.timeline.splitElements({
elements: elementsToSplit,
splitTime: currentTime,
retainSide: "right",
});
if (rippleEditingEnabled && rightSideElements.length > 0) {
const firstRightElement = editor.timeline.getElementsWithTracks({
elements: [rightSideElements[0]],
})[0];
if (firstRightElement) {
editor.playback.seek({ time: firstRightElement.element.startTime });
}
}
},
undefined,
);
useActionHandler(
"split-right",
() => {
const currentTime = editor.playback.getCurrentTime();
const tracks = editor.scenes.getActiveScene().tracks;
const elementsToSplit =
selectedElements.length > 0
? selectedElements
: getElementsAtTime({
tracks,
time: currentTime,
});
if (elementsToSplit.length === 0) return;
editor.timeline.splitElements({
elements: elementsToSplit,
splitTime: currentTime,
retainSide: "left",
});
},
undefined,
);
useActionHandler(
"delete-selected",
() => {
switch (editor.selection.getActiveSelectionKind()) {
case "mask-points":
if (!selectedMaskPointSelection) {
return;
}
editor.timeline.deleteCustomMaskPoints({
trackId: selectedMaskPointSelection.trackId,
elementId: selectedMaskPointSelection.elementId,
maskId: selectedMaskPointSelection.maskId,
pointIds: selectedMaskPointSelection.pointIds,
});
return;
case "keyframes":
if (selectedKeyframes.length === 0) {
return;
}
editor.timeline.removeKeyframes({ keyframes: selectedKeyframes });
clearKeyframeSelection();
return;
case "elements":
if (selectedElements.length === 0) {
return;
}
editor.timeline.deleteElements({
elements: selectedElements,
});
return;
default:
return;
}
},
undefined,
);
useActionHandler(
"toggle-source-audio",
() => {
if (selectedElements.length !== 1) {
return;
}
const selectedElement = editor.timeline.getElementsWithTracks({
elements: selectedElements,
})[0];
if (!selectedElement) {
return;
}
const mediaAsset = (() => {
const { element } = selectedElement;
if (!hasMediaId(element)) {
return null;
}
return (
editor.media
.getAssets()
.find((asset) => asset.id === element.mediaId) ?? null
);
})();
if (!canToggleSourceAudio(selectedElement.element, mediaAsset)) {
return;
}
editor.timeline.toggleSourceAudioSeparation({
trackId: selectedElement.track.id,
elementId: selectedElement.element.id,
});
},
undefined,
);
useActionHandler(
"select-all",
() => {
const scene = editor.scenes.getActiveScene();
const allElements = [
...scene.tracks.overlay,
scene.tracks.main,
...scene.tracks.audio,
].flatMap((track) =>
track.elements.map((element) => ({
trackId: track.id,
elementId: element.id,
})),
);
setElementSelection({ elements: allElements });
},
undefined,
);
useActionHandler(
"cancel-interaction",
() => {
if (!cancelInteraction()) {
invokeAction("deselect-all");
}
},
undefined,
);
useActionHandler(
"deselect-all",
() => {
if (!clearActiveScope()) {
editor.selection.clearMostSpecificSelection();
}
},
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",
() => {
editor.clipboard.copy();
},
undefined,
);
useActionHandler(
"paste-copied",
() => {
editor.clipboard.paste();
},
undefined,
);
useActionHandler(
"toggle-snapping",
() => {
toggleSnapping();
},
undefined,
);
useActionHandler(
"toggle-ripple-editing",
() => {
toggleRippleEditing();
},
undefined,
);
useActionHandler(
"undo",
() => {
editor.command.undo();
},
undefined,
);
useActionHandler(
"redo",
() => {
editor.command.redo();
},
undefined,
);
// todo: potnetially unify these two actions:
useActionHandler(
"remove-media-asset",
(args) => {
if (!args) return;
editor.media.removeMediaAsset({
projectId: args.projectId,
id: args.assetId,
});
},
undefined,
);
useActionHandler(
"remove-media-assets",
(args) => {
if (!args) return;
editor.media.removeMediaAssets({
projectId: args.projectId,
ids: args.assetIds,
});
},
undefined,
);
}