zone previews
This commit is contained in:
@ -1,15 +1,20 @@
|
||||
import { useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Trash2, Scissors, Volume2, SlidersHorizontal, Gauge } from 'lucide-react';
|
||||
import { Trash2, Scissors, Volume2, SlidersHorizontal, Gauge, Play } from 'lucide-react';
|
||||
|
||||
export default function ZoneEditor() {
|
||||
const [viewMode, setViewMode] = useState<'all' | 'cut' | 'mute' | 'gain' | 'speed'>('all');
|
||||
const previewFrameRef = useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
cutRanges,
|
||||
muteRanges,
|
||||
gainRanges,
|
||||
speedRanges,
|
||||
duration,
|
||||
setCurrentTime,
|
||||
zonePreviewPaddingSeconds,
|
||||
setZonePreviewPaddingSeconds,
|
||||
globalGainDb,
|
||||
setGlobalGainDb,
|
||||
removeCutRange,
|
||||
@ -20,6 +25,57 @@ export default function ZoneEditor() {
|
||||
updateSpeedRange,
|
||||
} = useEditorStore();
|
||||
|
||||
const stopPreviewLoop = useCallback(() => {
|
||||
if (previewFrameRef.current !== null) {
|
||||
cancelAnimationFrame(previewFrameRef.current);
|
||||
previewFrameRef.current = null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => stopPreviewLoop, [stopPreviewLoop]);
|
||||
|
||||
const previewZone = useCallback((start: number, end: number) => {
|
||||
const video = document.querySelector('video');
|
||||
if (!(video instanceof HTMLVideoElement)) return;
|
||||
|
||||
stopPreviewLoop();
|
||||
|
||||
const previewStart = Math.max(0, start - zonePreviewPaddingSeconds);
|
||||
const maxDuration = Number.isFinite(duration) && duration > 0 ? duration : video.duration;
|
||||
const previewEnd = Math.min(maxDuration || end + zonePreviewPaddingSeconds, end + zonePreviewPaddingSeconds);
|
||||
|
||||
video.currentTime = previewStart;
|
||||
setCurrentTime(previewStart);
|
||||
|
||||
const tick = () => {
|
||||
if (video.paused || video.ended) {
|
||||
previewFrameRef.current = null;
|
||||
return;
|
||||
}
|
||||
if (video.currentTime >= previewEnd) {
|
||||
video.pause();
|
||||
video.currentTime = previewEnd;
|
||||
setCurrentTime(previewEnd);
|
||||
previewFrameRef.current = null;
|
||||
return;
|
||||
}
|
||||
previewFrameRef.current = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
void video.play();
|
||||
previewFrameRef.current = requestAnimationFrame(tick);
|
||||
}, [duration, setCurrentTime, stopPreviewLoop, zonePreviewPaddingSeconds]);
|
||||
|
||||
const renderPreviewButton = (start: number, end: number, accentClass: string) => (
|
||||
<button
|
||||
onClick={() => previewZone(start, end)}
|
||||
className={`p-1 rounded opacity-0 group-hover:opacity-100 transition-opacity ${accentClass}`}
|
||||
title={`Play ${zonePreviewPaddingSeconds.toFixed(2)}s before and after zone`}
|
||||
>
|
||||
<Play className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
);
|
||||
|
||||
const totalZones = cutRanges.length + muteRanges.length + gainRanges.length + speedRanges.length;
|
||||
|
||||
const getZoneTypeColor = (type: 'cut' | 'mute' | 'gain' | 'speed') => {
|
||||
@ -39,13 +95,35 @@ export default function ZoneEditor() {
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-1">
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
<div className="w-4 h-4 rounded bg-editor-accent/30" />
|
||||
Zone Editor
|
||||
</h3>
|
||||
<p className="text-xs text-editor-text-muted">
|
||||
Manage all timeline zones ({totalZones} total)
|
||||
</p>
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold flex items-center gap-2">
|
||||
Zone Editor
|
||||
</h3>
|
||||
<p className="text-xs text-editor-text-muted">
|
||||
Manage all timeline zones ({totalZones} total)
|
||||
</p>
|
||||
</div>
|
||||
<div className="min-w-[160px] rounded border border-editor-border bg-editor-surface px-2 py-1.5">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="text-[10px] uppercase tracking-wide text-editor-text-muted">Preview</span>
|
||||
<span className="text-[10px] text-editor-text-muted">before/after</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-1.5">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={10}
|
||||
step={0.25}
|
||||
value={zonePreviewPaddingSeconds}
|
||||
onChange={(e) => setZonePreviewPaddingSeconds(Number(e.target.value) || 0)}
|
||||
className="w-16 px-2 py-1 bg-editor-bg border border-editor-border rounded text-xs text-editor-text focus:outline-none focus:border-editor-accent"
|
||||
title="Preview time before and after each zone"
|
||||
/>
|
||||
<span className="text-xs text-editor-text-muted">sec</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
@ -130,6 +208,7 @@ export default function ZoneEditor() {
|
||||
</div>
|
||||
<div className="text-editor-text-muted text-[10px]">{range.id}</div>
|
||||
</div>
|
||||
{renderPreviewButton(range.start, range.end, 'hover:bg-red-500/20 text-red-500/70 hover:text-red-500')}
|
||||
<button
|
||||
onClick={() => removeCutRange(range.id)}
|
||||
className="p-1 rounded hover:bg-red-500/20 text-red-500/70 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@ -162,6 +241,7 @@ export default function ZoneEditor() {
|
||||
</div>
|
||||
<div className="text-editor-text-muted text-[10px]">{range.id}</div>
|
||||
</div>
|
||||
{renderPreviewButton(range.start, range.end, 'hover:bg-orange-500/20 text-orange-500/70 hover:text-orange-500')}
|
||||
<button
|
||||
onClick={() => removeMuteRange(range.id)}
|
||||
className="p-1 rounded hover:bg-orange-500/20 text-orange-500/70 hover:text-orange-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@ -233,6 +313,7 @@ export default function ZoneEditor() {
|
||||
className="w-16 px-1.5 py-0.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
||||
title="Gain dB"
|
||||
/>
|
||||
{renderPreviewButton(range.start, range.end, 'hover:bg-amber-500/20 text-amber-500/70 hover:text-amber-500')}
|
||||
<button
|
||||
onClick={() => removeGainRange(range.id)}
|
||||
className="p-1 rounded hover:bg-amber-500/20 text-amber-500/70 hover:text-amber-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
@ -277,6 +358,7 @@ export default function ZoneEditor() {
|
||||
className="w-16 px-1.5 py-0.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
||||
title="Speed multiplier"
|
||||
/>
|
||||
{renderPreviewButton(range.start, range.end, 'hover:bg-emerald-500/20 text-emerald-500/70 hover:text-emerald-500')}
|
||||
<button
|
||||
onClick={() => removeSpeedRange(range.id)}
|
||||
className="p-1 rounded hover:bg-emerald-500/20 text-emerald-500/70 hover:text-emerald-500 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
|
||||
Reference in New Issue
Block a user