added save as
This commit is contained in:
@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
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 [focusedZone, setFocusedZone] = useState<{ type: 'cut' | 'mute' | 'gain' | 'speed'; id: string } | null>(null);
|
||||
const previewFrameRef = useRef<number | null>(null);
|
||||
|
||||
const {
|
||||
@ -68,7 +69,10 @@ export default function ZoneEditor() {
|
||||
|
||||
const renderPreviewButton = (start: number, end: number, accentClass: string) => (
|
||||
<button
|
||||
onClick={() => previewZone(start, end)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
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`}
|
||||
>
|
||||
@ -91,6 +95,53 @@ export default function ZoneEditor() {
|
||||
}
|
||||
};
|
||||
|
||||
const activeFocusedZone = useMemo(() => {
|
||||
if (!focusedZone) return null;
|
||||
const exists = focusedZone.type === 'cut'
|
||||
? cutRanges.some((range) => range.id === focusedZone.id)
|
||||
: focusedZone.type === 'mute'
|
||||
? muteRanges.some((range) => range.id === focusedZone.id)
|
||||
: focusedZone.type === 'gain'
|
||||
? gainRanges.some((range) => range.id === focusedZone.id)
|
||||
: speedRanges.some((range) => range.id === focusedZone.id);
|
||||
return exists ? focusedZone : null;
|
||||
}, [cutRanges, focusedZone, gainRanges, muteRanges, speedRanges]);
|
||||
|
||||
const isZoneFocused = useCallback(
|
||||
(type: 'cut' | 'mute' | 'gain' | 'speed', id: string) => activeFocusedZone?.type === type && activeFocusedZone.id === id,
|
||||
[activeFocusedZone],
|
||||
);
|
||||
|
||||
const removeZone = useCallback((type: 'cut' | 'mute' | 'gain' | 'speed', id: string) => {
|
||||
if (type === 'cut') removeCutRange(id);
|
||||
else if (type === 'mute') removeMuteRange(id);
|
||||
else if (type === 'gain') removeGainRange(id);
|
||||
else removeSpeedRange(id);
|
||||
setFocusedZone((current) => (current?.type === type && current.id === id ? null : current));
|
||||
}, [removeCutRange, removeGainRange, removeMuteRange, removeSpeedRange]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
const target = e.target as HTMLElement | null;
|
||||
if (target && (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.key === 'Escape') {
|
||||
setFocusedZone(null);
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.key === 'Delete' || e.key === 'Backspace') && activeFocusedZone) {
|
||||
e.preventDefault();
|
||||
removeZone(activeFocusedZone.type, activeFocusedZone.id);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
}, [activeFocusedZone, removeZone]);
|
||||
|
||||
return (
|
||||
<div className="p-4 space-y-4">
|
||||
<div className="space-y-2">
|
||||
@ -200,7 +251,8 @@ export default function ZoneEditor() {
|
||||
{cutRanges.map((range) => (
|
||||
<div
|
||||
key={range.id}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group ${getZoneTypeColor('cut')}`}
|
||||
onClick={() => setFocusedZone({ type: 'cut', id: range.id })}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group cursor-pointer transition-colors ${getZoneTypeColor('cut')} ${isZoneFocused('cut', range.id) ? 'ring-1 ring-red-400 border-red-400/80 bg-red-500/12' : ''}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
@ -210,7 +262,10 @@ export default function ZoneEditor() {
|
||||
</div>
|
||||
{renderPreviewButton(range.start, range.end, 'hover:bg-red-500/20 text-red-500/70 hover:text-red-500')}
|
||||
<button
|
||||
onClick={() => removeCutRange(range.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeZone('cut', 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"
|
||||
title="Delete cut zone"
|
||||
>
|
||||
@ -233,7 +288,8 @@ export default function ZoneEditor() {
|
||||
{muteRanges.map((range) => (
|
||||
<div
|
||||
key={range.id}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group ${getZoneTypeColor('mute')}`}
|
||||
onClick={() => setFocusedZone({ type: 'mute', id: range.id })}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group cursor-pointer transition-colors ${getZoneTypeColor('mute')} ${isZoneFocused('mute', range.id) ? 'ring-1 ring-orange-400 border-orange-400/80 bg-orange-500/12' : ''}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
@ -243,7 +299,10 @@ export default function ZoneEditor() {
|
||||
</div>
|
||||
{renderPreviewButton(range.start, range.end, 'hover:bg-orange-500/20 text-orange-500/70 hover:text-orange-500')}
|
||||
<button
|
||||
onClick={() => removeMuteRange(range.id)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeZone('mute', 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"
|
||||
title="Delete mute zone"
|
||||
>
|
||||
@ -293,7 +352,8 @@ export default function ZoneEditor() {
|
||||
{gainRanges.map((range) => (
|
||||
<div
|
||||
key={range.id}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group ${getZoneTypeColor('gain')}`}
|
||||
onClick={() => setFocusedZone({ type: 'gain', id: range.id })}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group cursor-pointer transition-colors ${getZoneTypeColor('gain')} ${isZoneFocused('gain', range.id) ? 'ring-1 ring-amber-400 border-amber-400/80 bg-amber-500/12' : ''}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
@ -309,13 +369,17 @@ export default function ZoneEditor() {
|
||||
max={24}
|
||||
step={0.5}
|
||||
value={range.gainDb}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => updateGainRange(range.id, Number(e.target.value) || 0)}
|
||||
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)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeZone('gain', 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"
|
||||
title="Delete gain zone"
|
||||
>
|
||||
@ -338,7 +402,8 @@ export default function ZoneEditor() {
|
||||
{speedRanges.map((range) => (
|
||||
<div
|
||||
key={range.id}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group ${getZoneTypeColor('speed')}`}
|
||||
onClick={() => setFocusedZone({ type: 'speed', id: range.id })}
|
||||
className={`px-2 py-1.5 rounded border text-xs flex items-center gap-2 group cursor-pointer transition-colors ${getZoneTypeColor('speed')} ${isZoneFocused('speed', range.id) ? 'ring-1 ring-emerald-400 border-emerald-400/80 bg-emerald-500/12' : ''}`}
|
||||
>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium truncate">
|
||||
@ -354,13 +419,17 @@ export default function ZoneEditor() {
|
||||
max={4}
|
||||
step={0.05}
|
||||
value={range.speed}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => updateSpeedRange(range.id, Number(e.target.value) || 1)}
|
||||
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)}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
removeZone('speed', 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"
|
||||
title="Delete speed zone"
|
||||
>
|
||||
|
||||
Reference in New Issue
Block a user