From b799615654eb5225365726fd736eeb0fbd5c776e Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Thu, 26 Jun 2025 23:40:56 +0530 Subject: feat: enhance playback controls with keyboard shortcuts for clip manipulation and audio separation --- apps/web/src/hooks/use-playback-controls.ts | 176 ++++++++++++++++++++++++++-- 1 file changed, 166 insertions(+), 10 deletions(-) (limited to 'apps/web/src/hooks/use-playback-controls.ts') diff --git a/apps/web/src/hooks/use-playback-controls.ts b/apps/web/src/hooks/use-playback-controls.ts index 5742e272..23bffe13 100644 --- a/apps/web/src/hooks/use-playback-controls.ts +++ b/apps/web/src/hooks/use-playback-controls.ts @@ -1,18 +1,174 @@ import { useEffect } from "react"; import { usePlaybackStore } from "@/stores/playback-store"; +import { useTimelineStore } from "@/stores/timeline-store"; +import { toast } from "sonner"; -export function usePlaybackControls() { - const { toggle } = usePlaybackStore(); +export const usePlaybackControls = () => { + const { isPlaying, currentTime, play, pause, seek } = usePlaybackStore(); + + const { + selectedClips, + tracks, + splitClip, + splitAndKeepLeft, + splitAndKeepRight, + separateAudio, + } = useTimelineStore(); useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.code === "Space" && e.target === document.body) { - e.preventDefault(); - toggle(); + const handleKeyPress = (e: KeyboardEvent) => { + if ( + e.target instanceof HTMLInputElement || + e.target instanceof HTMLTextAreaElement + ) { + return; } + + switch (e.key) { + case " ": + e.preventDefault(); + if (isPlaying) { + pause(); + } else { + play(); + } + break; + + case "s": + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + handleSplitSelectedClip(); + } + break; + + case "q": + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + handleSplitAndKeepLeft(); + } + break; + + case "w": + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + handleSplitAndKeepRight(); + } + break; + + case "d": + if (e.ctrlKey || e.metaKey) { + e.preventDefault(); + handleSeparateAudio(); + } + break; + } + }; + + const handleSplitSelectedClip = () => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one clip to split"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { + toast.error("Playhead must be within selected clip"); + return; + } + + splitClip(trackId, clipId, currentTime); + toast.success("Clip split at playhead"); + }; + + const handleSplitAndKeepLeft = () => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one clip"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { + toast.error("Playhead must be within selected clip"); + return; + } + + splitAndKeepLeft(trackId, clipId, currentTime); + toast.success("Split and kept left portion"); + }; + + const handleSplitAndKeepRight = () => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one clip"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { + toast.error("Playhead must be within selected clip"); + return; + } + + splitAndKeepRight(trackId, clipId, currentTime); + toast.success("Split and kept right portion"); + }; + + const handleSeparateAudio = () => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one video clip to separate audio"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + + if (!track || track.type !== "video") { + toast.error("Select a video clip to separate audio"); + return; + } + + separateAudio(trackId, clipId); + toast.success("Audio separated to audio track"); }; - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [toggle]); -} \ No newline at end of file + document.addEventListener("keydown", handleKeyPress); + return () => document.removeEventListener("keydown", handleKeyPress); + }, [ + isPlaying, + currentTime, + selectedClips, + tracks, + play, + pause, + splitClip, + splitAndKeepLeft, + splitAndKeepRight, + separateAudio, + ]); +}; -- cgit v1.3.1 From 3f0fe9d20e85a10e8615fd868e9ff8222dba0c29 Mon Sep 17 00:00:00 2001 From: DevloperAmanSingh Date: Fri, 27 Jun 2025 00:07:41 +0530 Subject: refactor: improve playback controls by replacing inline functions with useCallback for clip manipulation and audio separation --- apps/web/src/hooks/use-playback-controls.ts | 224 ++++++++++++++-------------- 1 file changed, 112 insertions(+), 112 deletions(-) (limited to 'apps/web/src/hooks/use-playback-controls.ts') diff --git a/apps/web/src/hooks/use-playback-controls.ts b/apps/web/src/hooks/use-playback-controls.ts index 23bffe13..2c689470 100644 --- a/apps/web/src/hooks/use-playback-controls.ts +++ b/apps/web/src/hooks/use-playback-controls.ts @@ -1,4 +1,4 @@ -import { useEffect } from "react"; +import { useEffect, useCallback } from "react"; import { usePlaybackStore } from "@/stores/playback-store"; import { useTimelineStore } from "@/stores/timeline-store"; import { toast } from "sonner"; @@ -15,8 +15,101 @@ export const usePlaybackControls = () => { separateAudio, } = useTimelineStore(); - useEffect(() => { - const handleKeyPress = (e: KeyboardEvent) => { + const handleSplitSelectedClip = useCallback(() => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one clip to split"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { + toast.error("Playhead must be within selected clip"); + return; + } + + splitClip(trackId, clipId, currentTime); + toast.success("Clip split at playhead"); + }, [selectedClips, tracks, currentTime, splitClip]); + + const handleSplitAndKeepLeftCallback = useCallback(() => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one clip"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { + toast.error("Playhead must be within selected clip"); + return; + } + + splitAndKeepLeft(trackId, clipId, currentTime); + toast.success("Split and kept left portion"); + }, [selectedClips, tracks, currentTime, splitAndKeepLeft]); + + const handleSplitAndKeepRightCallback = useCallback(() => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one clip"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + const clip = track?.clips.find((c) => c.id === clipId); + + if (!clip) return; + + const effectiveStart = clip.startTime; + const effectiveEnd = + clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); + + if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { + toast.error("Playhead must be within selected clip"); + return; + } + + splitAndKeepRight(trackId, clipId, currentTime); + toast.success("Split and kept right portion"); + }, [selectedClips, tracks, currentTime, splitAndKeepRight]); + + const handleSeparateAudioCallback = useCallback(() => { + if (selectedClips.length !== 1) { + toast.error("Select exactly one video clip to separate audio"); + return; + } + + const { trackId, clipId } = selectedClips[0]; + const track = tracks.find((t) => t.id === trackId); + + if (!track || track.type !== "video") { + toast.error("Select a video clip to separate audio"); + return; + } + + separateAudio(trackId, clipId); + toast.success("Audio separated to audio track"); + }, [selectedClips, tracks, separateAudio]); + + const handleKeyPress = useCallback( + (e: KeyboardEvent) => { if ( e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement @@ -44,131 +137,38 @@ export const usePlaybackControls = () => { case "q": if (e.ctrlKey || e.metaKey) { e.preventDefault(); - handleSplitAndKeepLeft(); + handleSplitAndKeepLeftCallback(); } break; case "w": if (e.ctrlKey || e.metaKey) { e.preventDefault(); - handleSplitAndKeepRight(); + handleSplitAndKeepRightCallback(); } break; case "d": if (e.ctrlKey || e.metaKey) { e.preventDefault(); - handleSeparateAudio(); + handleSeparateAudioCallback(); } break; } - }; - - const handleSplitSelectedClip = () => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one clip to split"); - return; - } - - const { trackId, clipId } = selectedClips[0]; - const track = tracks.find((t) => t.id === trackId); - const clip = track?.clips.find((c) => c.id === clipId); - - if (!clip) return; - - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected clip"); - return; - } - - splitClip(trackId, clipId, currentTime); - toast.success("Clip split at playhead"); - }; - - const handleSplitAndKeepLeft = () => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one clip"); - return; - } - - const { trackId, clipId } = selectedClips[0]; - const track = tracks.find((t) => t.id === trackId); - const clip = track?.clips.find((c) => c.id === clipId); - - if (!clip) return; - - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected clip"); - return; - } - - splitAndKeepLeft(trackId, clipId, currentTime); - toast.success("Split and kept left portion"); - }; - - const handleSplitAndKeepRight = () => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one clip"); - return; - } - - const { trackId, clipId } = selectedClips[0]; - const track = tracks.find((t) => t.id === trackId); - const clip = track?.clips.find((c) => c.id === clipId); - - if (!clip) return; - - const effectiveStart = clip.startTime; - const effectiveEnd = - clip.startTime + (clip.duration - clip.trimStart - clip.trimEnd); - - if (currentTime <= effectiveStart || currentTime >= effectiveEnd) { - toast.error("Playhead must be within selected clip"); - return; - } - - splitAndKeepRight(trackId, clipId, currentTime); - toast.success("Split and kept right portion"); - }; - - const handleSeparateAudio = () => { - if (selectedClips.length !== 1) { - toast.error("Select exactly one video clip to separate audio"); - return; - } - - const { trackId, clipId } = selectedClips[0]; - const track = tracks.find((t) => t.id === trackId); - - if (!track || track.type !== "video") { - toast.error("Select a video clip to separate audio"); - return; - } - - separateAudio(trackId, clipId); - toast.success("Audio separated to audio track"); - }; + }, + [ + isPlaying, + play, + pause, + handleSplitSelectedClip, + handleSplitAndKeepLeftCallback, + handleSplitAndKeepRightCallback, + handleSeparateAudioCallback, + ] + ); + useEffect(() => { document.addEventListener("keydown", handleKeyPress); return () => document.removeEventListener("keydown", handleKeyPress); - }, [ - isPlaying, - currentTime, - selectedClips, - tracks, - play, - pause, - splitClip, - splitAndKeepLeft, - splitAndKeepRight, - separateAudio, - ]); + }, [handleKeyPress]); }; -- cgit v1.3.1