summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/use-editor-actions.ts
diff options
context:
space:
mode:
authorAnwarul Islam <anwaarulislaam@gmail.com>2025-07-18 07:08:46 +0600
committerAnwarul Islam <anwaarulislaam@gmail.com>2025-07-18 07:08:46 +0600
commit7eda7975fcc06a9fa3d88975c84ccc07748298cb (patch)
tree85c185dc84445b383d2a745e6dd2c4cfa66cd87b /apps/web/src/hooks/use-editor-actions.ts
parent1afc51eb9dfe1e26a670871fafca6df5a2fbd426 (diff)
downloadopencut-7eda7975fcc06a9fa3d88975c84ccc07748298cb.tar.gz
Implement editor actions for playback and timeline editing
Diffstat (limited to 'apps/web/src/hooks/use-editor-actions.ts')
-rw-r--r--apps/web/src/hooks/use-editor-actions.ts162
1 files changed, 162 insertions, 0 deletions
diff --git a/apps/web/src/hooks/use-editor-actions.ts b/apps/web/src/hooks/use-editor-actions.ts
new file mode 100644
index 00000000..e0d9b9eb
--- /dev/null
+++ b/apps/web/src/hooks/use-editor-actions.ts
@@ -0,0 +1,162 @@
+"use client";
+
+import { useEffect } from "react";
+import { useActionHandler } from "@/constants/actions";
+import { useTimelineStore } from "@/stores/timeline-store";
+import { usePlaybackStore } from "@/stores/playback-store";
+import { useProjectStore } from "@/stores/project-store";
+import { toast } from "sonner";
+
+export function useEditorActions() {
+ const {
+ tracks,
+ selectedElements,
+ clearSelectedElements,
+ setSelectedElements,
+ removeElementFromTrack,
+ splitElement,
+ addElementToTrack,
+ snappingEnabled,
+ toggleSnapping,
+ undo,
+ redo,
+ } = useTimelineStore();
+
+ const { currentTime, duration, isPlaying, toggle, seek } = usePlaybackStore();
+ const { activeProject } = useProjectStore();
+
+ // Playback actions
+ useActionHandler("toggle-play", () => {
+ toggle();
+ });
+
+ useActionHandler("stop-playback", () => {
+ if (isPlaying) {
+ toggle();
+ }
+ seek(0);
+ });
+
+ useActionHandler("seek-forward", (args) => {
+ const seconds = args?.seconds ?? 1;
+ seek(Math.min(duration, currentTime + seconds));
+ });
+
+ useActionHandler("seek-backward", (args) => {
+ const seconds = args?.seconds ?? 1;
+ seek(Math.max(0, currentTime - seconds));
+ });
+
+ useActionHandler("frame-step-forward", () => {
+ const projectFps = activeProject?.fps || 30;
+ seek(Math.min(duration, currentTime + 1 / projectFps));
+ });
+
+ useActionHandler("frame-step-backward", () => {
+ const projectFps = activeProject?.fps || 30;
+ seek(Math.max(0, currentTime - 1 / projectFps));
+ });
+
+ useActionHandler("jump-forward", (args) => {
+ const seconds = args?.seconds ?? 5;
+ seek(Math.min(duration, currentTime + seconds));
+ });
+
+ useActionHandler("jump-backward", (args) => {
+ const seconds = args?.seconds ?? 5;
+ seek(Math.max(0, currentTime - seconds));
+ });
+
+ useActionHandler("goto-start", () => {
+ seek(0);
+ });
+
+ useActionHandler("goto-end", () => {
+ seek(duration);
+ });
+
+ // Timeline editing actions
+ useActionHandler("split-element", () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element to split");
+ return;
+ }
+
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t: any) => t.id === trackId);
+ const element = track?.elements.find((el: any) => el.id === elementId);
+
+ if (element) {
+ const effectiveStart = element.startTime;
+ const effectiveEnd =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd);
+
+ if (currentTime > effectiveStart && currentTime < effectiveEnd) {
+ splitElement(trackId, elementId, currentTime);
+ } else {
+ toast.error("Playhead must be within selected element");
+ }
+ }
+ });
+
+ useActionHandler("delete-selected", () => {
+ if (selectedElements.length === 0) {
+ toast.error("No elements selected");
+ return;
+ }
+ selectedElements.forEach(
+ ({ trackId, elementId }: { trackId: string; elementId: string }) => {
+ removeElementFromTrack(trackId, elementId);
+ }
+ );
+ clearSelectedElements();
+ });
+
+ useActionHandler("select-all", () => {
+ const allElements = tracks.flatMap((track: any) =>
+ track.elements.map((element: any) => ({
+ trackId: track.id,
+ elementId: element.id,
+ }))
+ );
+ setSelectedElements(allElements);
+ });
+
+ useActionHandler("duplicate-selected", () => {
+ if (selectedElements.length !== 1) {
+ toast.error("Select exactly one element to duplicate");
+ return;
+ }
+
+ const { trackId, elementId } = selectedElements[0];
+ const track = tracks.find((t: any) => t.id === trackId);
+ const element = track?.elements.find((el: any) => el.id === elementId);
+
+ if (element) {
+ const newStartTime =
+ element.startTime +
+ (element.duration - element.trimStart - element.trimEnd) +
+ 0.1;
+ const { id, ...elementWithoutId } = element;
+
+ addElementToTrack(trackId, {
+ ...elementWithoutId,
+ startTime: newStartTime,
+ });
+ }
+ });
+
+ useActionHandler("toggle-snapping", () => {
+ toggleSnapping();
+ });
+
+ // History actions
+ useActionHandler("undo", () => {
+ undo();
+ });
+
+ useActionHandler("redo", () => {
+ redo();
+ });
+}