summaryrefslogtreecommitdiff
path: root/apps/web/src/actions/use-editor-actions.ts
diff options
context:
space:
mode:
authorcashmere <cashmere@autistici.org>2026-07-27 01:21:29 +0200
committercashmere <cashmere@autistici.org>2026-07-27 01:21:29 +0200
commit9c3b788b47cc639ee463dd07e1a4cce91e6376cc (patch)
treef7bec9cc3ca35ad3cb3eb5b79497b934513259ef /apps/web/src/actions/use-editor-actions.ts
parent783e9ac8462751adc14d78b070095fd63e45bda0 (diff)
downloadopencut-9c3b788b47cc639ee463dd07e1a4cce91e6376cc.tar.gz
feat: wire versioning/transfer/voice-over actions and header UI
Shared wiring for the six roadmap slices in one commit: action ids and handlers for create-checkpoint, toggle-versions-panel, export-project-archive, and voice-over; the freeze-frame handler awaits the duration prompt before capture; editor header gains the versions popover and export menu item (and loses the feedback popover and Discord item, part of the branding strip).
Diffstat (limited to 'apps/web/src/actions/use-editor-actions.ts')
-rw-r--r--apps/web/src/actions/use-editor-actions.ts125
1 files changed, 124 insertions, 1 deletions
diff --git a/apps/web/src/actions/use-editor-actions.ts b/apps/web/src/actions/use-editor-actions.ts
index 598e73b0..c99f65e2 100644
--- a/apps/web/src/actions/use-editor-actions.ts
+++ b/apps/web/src/actions/use-editor-actions.ts
@@ -28,6 +28,7 @@ import {
import { useCommittedRef } from "@/hooks/use-committed-ref";
import { toast } from "sonner";
import { generateImageThumbnail } from "@/media/processing";
+import { useFreezeFrameStore } from "@/actions/freeze-frame-store";
import {
AddMediaAssetCommand,
BatchCommand,
@@ -35,6 +36,16 @@ import {
ShiftSplitRemainderCommand,
SplitElementsCommand,
} from "@/commands";
+import { useVersionsStore } from "@/versions/versions-store";
+import {
+ cancelVoiceOverRecording,
+ getFileExtensionForMimeType,
+ getVoiceOverErrorMessage,
+ startVoiceOverRecording,
+ stopVoiceOverRecording,
+} from "@/voiceover/recorder";
+import type { VoiceOverRecordingResult } from "@/voiceover/recorder";
+import { useVoiceOverStore } from "@/voiceover/voiceover-store";
export function useEditorActions() {
const editor = useEditor();
@@ -46,6 +57,8 @@ export function useEditorActions() {
const toggleSnapping = useTimelineStore((s) => s.toggleSnapping);
const rippleEditingEnabled = useTimelineStore((s) => s.rippleEditingEnabled);
const toggleRippleEditing = useTimelineStore((s) => s.toggleRippleEditing);
+ const openCheckpointDialog = useVersionsStore((s) => s.openCheckpointDialog);
+ const toggleVersionsPanel = useVersionsStore((s) => s.togglePanel);
const hasTimelineSelection =
selectedElements.length > 0 ||
selectedKeyframes.length > 0 ||
@@ -408,6 +421,13 @@ export function useEditorActions() {
return;
}
+ const freezeDurationSeconds = await useFreezeFrameStore
+ .getState()
+ .requestDuration();
+ if (freezeDurationSeconds === null) {
+ return;
+ }
+
const blob = await editor.renderer.captureFrame();
if (!blob) {
toast.error("Failed to capture frame");
@@ -421,7 +441,9 @@ export function useEditorActions() {
imageFile: file,
});
- const freezeDuration = mediaTimeFromSeconds({ seconds: 3 });
+ const freezeDuration = mediaTimeFromSeconds({
+ seconds: freezeDurationSeconds,
+ });
const addMediaCommand = new AddMediaAssetCommand({
projectId: activeProject.metadata.id,
asset: {
@@ -611,4 +633,105 @@ export function useEditorActions() {
},
undefined,
);
+
+ useActionHandler(
+ "create-checkpoint",
+ () => {
+ openCheckpointDialog();
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "toggle-versions-panel",
+ () => {
+ toggleVersionsPanel();
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "voice-over",
+ async () => {
+ const voiceOverState = useVoiceOverStore.getState();
+
+ if (voiceOverState.status === "idle") {
+ try {
+ await startVoiceOverRecording();
+ } catch (error) {
+ toast.error(getVoiceOverErrorMessage({ error }));
+ return;
+ }
+ useVoiceOverStore.getState().startRecording({
+ playheadTime: editor.playback.getCurrentTime(),
+ });
+ return;
+ }
+
+ useVoiceOverStore.getState().stopRecording();
+
+ const activeProject = editor.project.getActive();
+ if (!activeProject) {
+ cancelVoiceOverRecording();
+ return;
+ }
+
+ let recording: VoiceOverRecordingResult;
+ try {
+ recording = await stopVoiceOverRecording();
+ } catch (error) {
+ toast.error(getVoiceOverErrorMessage({ error }));
+ return;
+ }
+
+ if (recording.blob.size === 0) {
+ toast.error("Voice-over recording is empty");
+ return;
+ }
+
+ const file = new File(
+ [recording.blob],
+ `voice-over.${getFileExtensionForMimeType({ mimeType: recording.mimeType })}`,
+ { type: recording.mimeType },
+ );
+ const addMediaCommand = new AddMediaAssetCommand({
+ projectId: activeProject.metadata.id,
+ asset: {
+ file,
+ name: "Voice over",
+ type: "audio",
+ url: URL.createObjectURL(file),
+ duration: recording.durationSeconds,
+ },
+ });
+ const insertCommand = new InsertElementCommand({
+ element: buildElementFromMedia({
+ mediaId: addMediaCommand.getAssetId(),
+ mediaType: "audio",
+ name: "Voice over",
+ duration: mediaTimeFromSeconds({
+ seconds: recording.durationSeconds,
+ }),
+ startTime:
+ voiceOverState.playheadStartTime ??
+ editor.playback.getCurrentTime(),
+ }),
+ placement: { mode: "auto", trackType: "audio" },
+ });
+ editor.command.execute({
+ command: new BatchCommand([addMediaCommand, insertCommand]),
+ });
+ },
+ undefined,
+ );
+
+ useActionHandler(
+ "export-project-archive",
+ () => {
+ const projectId = editor.project.getActiveOrNull()?.metadata.id;
+ if (!projectId) return;
+ void editor.project.exportArchive({ id: projectId });
+ },
+ undefined,
+ );
}