2025-06-23 09:18:15 +03:00
|
|
|
import { useEffect } from "react";
|
|
|
|
|
import { usePlaybackStore } from "@/stores/playback-store";
|
2025-06-26 23:40:56 +05:30
|
|
|
import { useTimelineStore } from "@/stores/timeline-store";
|
|
|
|
|
import { toast } from "sonner";
|
2025-06-23 09:18:15 +03:00
|
|
|
|
2025-06-26 23:40:56 +05:30
|
|
|
export const usePlaybackControls = () => {
|
|
|
|
|
const { isPlaying, currentTime, play, pause, seek } = usePlaybackStore();
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
selectedClips,
|
|
|
|
|
tracks,
|
|
|
|
|
splitClip,
|
|
|
|
|
splitAndKeepLeft,
|
|
|
|
|
splitAndKeepRight,
|
|
|
|
|
separateAudio,
|
|
|
|
|
} = useTimelineStore();
|
2025-06-23 09:18:15 +03:00
|
|
|
|
|
|
|
|
useEffect(() => {
|
2025-06-26 23:40:56 +05:30
|
|
|
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;
|
2025-06-23 09:18:15 +03:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2025-06-26 23:40:56 +05:30
|
|
|
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", handleKeyPress);
|
|
|
|
|
return () => document.removeEventListener("keydown", handleKeyPress);
|
|
|
|
|
}, [
|
|
|
|
|
isPlaying,
|
|
|
|
|
currentTime,
|
|
|
|
|
selectedClips,
|
|
|
|
|
tracks,
|
|
|
|
|
play,
|
|
|
|
|
pause,
|
|
|
|
|
splitClip,
|
|
|
|
|
splitAndKeepLeft,
|
|
|
|
|
splitAndKeepRight,
|
|
|
|
|
separateAudio,
|
|
|
|
|
]);
|
|
|
|
|
};
|