UI improvements, moved file name and moved buttons left
This commit is contained in:
@ -108,11 +108,15 @@ export default function WaveformTimeline({
|
||||
muteMode,
|
||||
gainMode,
|
||||
gainModeDb,
|
||||
speedMode,
|
||||
speedModeValue,
|
||||
}: {
|
||||
cutMode: boolean;
|
||||
muteMode: boolean;
|
||||
gainMode: boolean;
|
||||
gainModeDb: number;
|
||||
speedMode: boolean;
|
||||
speedModeValue: number;
|
||||
}) {
|
||||
const waveCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const headCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -127,16 +131,20 @@ export default function WaveformTimeline({
|
||||
const cutRanges = useEditorStore((s) => s.cutRanges);
|
||||
const muteRanges = useEditorStore((s) => s.muteRanges);
|
||||
const gainRanges = useEditorStore((s) => s.gainRanges);
|
||||
const speedRanges = useEditorStore((s) => s.speedRanges);
|
||||
const setCurrentTime = useEditorStore((s) => s.setCurrentTime);
|
||||
const addCutRange = useEditorStore((s) => s.addCutRange);
|
||||
const addMuteRange = useEditorStore((s) => s.addMuteRange);
|
||||
const addGainRange = useEditorStore((s) => s.addGainRange);
|
||||
const addSpeedRange = useEditorStore((s) => s.addSpeedRange);
|
||||
const updateCutRange = useEditorStore((s) => s.updateCutRange);
|
||||
const updateMuteRange = useEditorStore((s) => s.updateMuteRange);
|
||||
const updateGainRangeBounds = useEditorStore((s) => s.updateGainRangeBounds);
|
||||
const updateSpeedRangeBounds = useEditorStore((s) => s.updateSpeedRangeBounds);
|
||||
const removeCutRange = useEditorStore((s) => s.removeCutRange);
|
||||
const removeMuteRange = useEditorStore((s) => s.removeMuteRange);
|
||||
const removeGainRange = useEditorStore((s) => s.removeGainRange);
|
||||
const removeSpeedRange = useEditorStore((s) => s.removeSpeedRange);
|
||||
|
||||
const waveformDataRef = useRef<WaveformData | null>(null);
|
||||
const zoomRef = useRef(1); // 1 = show all, >1 = zoomed in
|
||||
@ -150,12 +158,13 @@ export default function WaveformTimeline({
|
||||
const selectionEndRef = useRef<number | null>(null);
|
||||
const [selectionStart, setSelectionStart] = useState<number | null>(null);
|
||||
const [selectionEnd, setSelectionEnd] = useState<number | null>(null);
|
||||
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain', id: string} | null>(null);
|
||||
const [selectedZone, setSelectedZone] = useState<{type: 'cut' | 'mute' | 'gain' | 'speed', id: string} | null>(null);
|
||||
const [hoverCursor, setHoverCursor] = useState<string>('crosshair');
|
||||
const editingZoneRef = useRef<{type: 'cut' | 'mute' | 'gain', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
||||
const editingZoneRef = useRef<{type: 'cut' | 'mute' | 'gain' | 'speed', id: string, edge: 'start' | 'end' | 'move'} | null>(null);
|
||||
const [showCutZones, setShowCutZones] = useState(true);
|
||||
const [showMuteZones, setShowMuteZones] = useState(true);
|
||||
const [showGainZones, setShowGainZones] = useState(true);
|
||||
const [showSpeedZones, setShowSpeedZones] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoUrl || !videoPath) return;
|
||||
@ -394,16 +403,53 @@ export default function WaveformTimeline({
|
||||
}
|
||||
}
|
||||
|
||||
// Draw speed ranges (emerald overlays)
|
||||
for (const range of showSpeedZones ? speedRanges : []) {
|
||||
const x1 = (range.start - scroll) * pxPerSec;
|
||||
const x2 = (range.end - scroll) * pxPerSec;
|
||||
const isSelected = selectedZone?.type === 'speed' && selectedZone.id === range.id;
|
||||
|
||||
ctx.fillStyle = isSelected ? 'rgba(16, 185, 129, 0.55)' : 'rgba(16, 185, 129, 0.35)';
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
if (isSelected) {
|
||||
ctx.strokeStyle = '#10b981';
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
ctx.fillStyle = '#10b981';
|
||||
ctx.beginPath();
|
||||
ctx.arc(x1, waveTop + waveH / 2, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
ctx.beginPath();
|
||||
ctx.arc(x2, waveTop + waveH / 2, 4, 0, 2 * Math.PI);
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
const centerX = (x1 + x2) / 2;
|
||||
ctx.fillStyle = '#d1fae5';
|
||||
ctx.font = '10px sans-serif';
|
||||
ctx.textAlign = 'center';
|
||||
ctx.textBaseline = 'middle';
|
||||
if (centerX > 12 && centerX < width - 12) {
|
||||
ctx.fillText(`${range.speed.toFixed(2)}x`, centerX, waveTop + waveH / 2);
|
||||
}
|
||||
ctx.textAlign = 'start';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
}
|
||||
|
||||
// Draw selection overlay (when in zone mode)
|
||||
if ((cutMode || muteMode || gainMode) && selectionStart !== null && selectionEnd !== null) {
|
||||
if ((cutMode || muteMode || gainMode || speedMode) && selectionStart !== null && selectionEnd !== null) {
|
||||
const x1 = (Math.min(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
||||
const x2 = (Math.max(selectionStart, selectionEnd) - scroll) * pxPerSec;
|
||||
const fillColor = cutMode
|
||||
? 'rgba(239, 68, 68, 0.5)'
|
||||
: muteMode
|
||||
? 'rgba(59, 130, 246, 0.5)'
|
||||
: 'rgba(245, 158, 11, 0.5)';
|
||||
const strokeColor = cutMode ? '#ef4444' : muteMode ? '#3b82f6' : '#f59e0b';
|
||||
: gainMode
|
||||
? 'rgba(245, 158, 11, 0.5)'
|
||||
: 'rgba(16, 185, 129, 0.5)';
|
||||
const strokeColor = cutMode ? '#ef4444' : muteMode ? '#3b82f6' : gainMode ? '#f59e0b' : '#10b981';
|
||||
ctx.fillStyle = fillColor;
|
||||
ctx.fillRect(x1, waveTop, x2 - x1, waveH);
|
||||
|
||||
@ -441,15 +487,18 @@ export default function WaveformTimeline({
|
||||
cutRanges,
|
||||
muteRanges,
|
||||
gainRanges,
|
||||
speedRanges,
|
||||
selectionStart,
|
||||
selectionEnd,
|
||||
cutMode,
|
||||
muteMode,
|
||||
gainMode,
|
||||
speedMode,
|
||||
selectedZone,
|
||||
showCutZones,
|
||||
showMuteZones,
|
||||
showGainZones,
|
||||
showSpeedZones,
|
||||
]);
|
||||
|
||||
// Keep the ref in sync with the latest drawStaticWaveform closure
|
||||
@ -715,8 +764,43 @@ export default function WaveformTimeline({
|
||||
}
|
||||
}
|
||||
|
||||
// Check speed ranges
|
||||
for (const range of showSpeedZones ? speedRanges : []) {
|
||||
const rangeX1 = (range.start - scroll) * pxPerSec;
|
||||
const rangeX2 = (range.end - scroll) * pxPerSec;
|
||||
const isSelected = selectedZone?.type === 'speed' && selectedZone.id === range.id;
|
||||
|
||||
if (forHover && isSelected) {
|
||||
if (Math.abs(x - rangeX1) <= handleSize) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'start' as const };
|
||||
}
|
||||
if (Math.abs(x - rangeX2) <= handleSize) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'end' as const };
|
||||
}
|
||||
} else if (!forHover) {
|
||||
if (isSelected) {
|
||||
if (Math.abs(x - rangeX1) <= handleSize) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'start' as const };
|
||||
}
|
||||
if (Math.abs(x - rangeX2) <= handleSize) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'end' as const };
|
||||
}
|
||||
} else {
|
||||
if (Math.abs(x - rangeX1) <= handleSize && Math.abs(y - (waveTop + waveH / 2)) <= handleSize) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'start' as const };
|
||||
}
|
||||
if (Math.abs(x - rangeX2) <= handleSize && Math.abs(y - (waveTop + waveH / 2)) <= handleSize) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'end' as const };
|
||||
}
|
||||
}
|
||||
if (x >= rangeX1 && x <= rangeX2) {
|
||||
return { type: 'speed' as const, id: range.id, edge: 'move' as const };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [cutRanges, muteRanges, gainRanges, selectedZone, showCutZones, showMuteZones, showGainZones]);
|
||||
}, [cutRanges, muteRanges, gainRanges, speedRanges, selectedZone, showCutZones, showMuteZones, showGainZones, showSpeedZones]);
|
||||
|
||||
const handleMouseMove = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => {
|
||||
if (isDragging) return; // Don't change cursor while dragging
|
||||
@ -752,7 +836,9 @@ export default function WaveformTimeline({
|
||||
? cutRanges.find(r => r.id === zoneHit.id)
|
||||
: zoneHit.type === 'mute'
|
||||
? muteRanges.find(r => r.id === zoneHit.id)
|
||||
: gainRanges.find(r => r.id === zoneHit.id);
|
||||
: zoneHit.type === 'gain'
|
||||
? gainRanges.find(r => r.id === zoneHit.id)
|
||||
: speedRanges.find(r => r.id === zoneHit.id);
|
||||
|
||||
if (!originalRange) return;
|
||||
|
||||
@ -784,8 +870,10 @@ export default function WaveformTimeline({
|
||||
updateCutRange(editingZoneRef.current.id, newStart, newEnd);
|
||||
} else if (editingZoneRef.current.type === 'mute') {
|
||||
updateMuteRange(editingZoneRef.current.id, newStart, newEnd);
|
||||
} else {
|
||||
} else if (editingZoneRef.current.type === 'gain') {
|
||||
updateGainRangeBounds(editingZoneRef.current.id, newStart, newEnd);
|
||||
} else {
|
||||
updateSpeedRangeBounds(editingZoneRef.current.id, newStart, newEnd);
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -806,7 +894,7 @@ export default function WaveformTimeline({
|
||||
// Clear selection if clicking elsewhere
|
||||
setSelectedZone(null);
|
||||
|
||||
if (cutMode || muteMode || gainMode) {
|
||||
if (cutMode || muteMode || gainMode || speedMode) {
|
||||
// Range selection mode
|
||||
const startTime = clientXToTime(e.clientX);
|
||||
selectionStartRef.current = startTime;
|
||||
@ -838,6 +926,8 @@ export default function WaveformTimeline({
|
||||
addMuteRange(start, end);
|
||||
} else if (end - start >= minDuration && gainMode) {
|
||||
addGainRange(start, end, gainModeDb);
|
||||
} else if (end - start >= minDuration && speedMode) {
|
||||
addSpeedRange(start, end, speedModeValue);
|
||||
}
|
||||
}
|
||||
|
||||
@ -873,7 +963,7 @@ export default function WaveformTimeline({
|
||||
window.addEventListener('mouseup', onUp);
|
||||
}
|
||||
},
|
||||
[cutMode, muteMode, gainMode, gainModeDb, clientXToTime, seekToClientX, addCutRange, addMuteRange, addGainRange, getZoneAtPosition, cutRanges, muteRanges, gainRanges, duration, updateCutRange, updateMuteRange, updateGainRangeBounds],
|
||||
[cutMode, muteMode, gainMode, gainModeDb, speedMode, speedModeValue, clientXToTime, seekToClientX, addCutRange, addMuteRange, addGainRange, addSpeedRange, getZoneAtPosition, cutRanges, muteRanges, gainRanges, speedRanges, duration, updateCutRange, updateMuteRange, updateGainRangeBounds, updateSpeedRangeBounds],
|
||||
);
|
||||
|
||||
// Handle keyboard shortcuts for zone editing
|
||||
@ -896,8 +986,10 @@ export default function WaveformTimeline({
|
||||
removeCutRange(selectedZone.id);
|
||||
} else if (selectedZone.type === 'mute') {
|
||||
removeMuteRange(selectedZone.id);
|
||||
} else {
|
||||
} else if (selectedZone.type === 'gain') {
|
||||
removeGainRange(selectedZone.id);
|
||||
} else {
|
||||
removeSpeedRange(selectedZone.id);
|
||||
}
|
||||
setSelectedZone(null);
|
||||
editingZoneRef.current = null;
|
||||
@ -908,14 +1000,15 @@ export default function WaveformTimeline({
|
||||
// Capture phase ensures zone delete runs before app-level bubble shortcuts.
|
||||
window.addEventListener('keydown', handleKeyDown, { capture: true });
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, { capture: true });
|
||||
}, [selectedZone, removeCutRange, removeMuteRange, removeGainRange]);
|
||||
}, [selectedZone, removeCutRange, removeMuteRange, removeGainRange, removeSpeedRange]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!selectedZone) return;
|
||||
if (selectedZone.type === 'cut' && !showCutZones) setSelectedZone(null);
|
||||
if (selectedZone.type === 'mute' && !showMuteZones) setSelectedZone(null);
|
||||
if (selectedZone.type === 'gain' && !showGainZones) setSelectedZone(null);
|
||||
}, [selectedZone, showCutZones, showMuteZones, showGainZones]);
|
||||
if (selectedZone.type === 'speed' && !showSpeedZones) setSelectedZone(null);
|
||||
}, [selectedZone, showCutZones, showMuteZones, showGainZones, showSpeedZones]);
|
||||
|
||||
if (!videoUrl) {
|
||||
return (
|
||||
@ -935,6 +1028,7 @@ export default function WaveformTimeline({
|
||||
{cutMode && <span className="text-[10px] text-red-400">Cut mode</span>}
|
||||
{muteMode && <span className="text-[10px] text-blue-400">Mute mode</span>}
|
||||
{gainMode && <span className="text-[10px] text-amber-400">Gain mode ({gainModeDb.toFixed(1)} dB)</span>}
|
||||
{speedMode && <span className="text-[10px] text-emerald-400">Speed mode ({speedModeValue.toFixed(2)}x)</span>}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
@ -958,6 +1052,13 @@ export default function WaveformTimeline({
|
||||
>
|
||||
Gain
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setShowSpeedZones((v) => !v)}
|
||||
className={`px-1.5 py-0.5 rounded text-[10px] border ${showSpeedZones ? 'border-emerald-500/60 text-emerald-300 bg-emerald-500/10' : 'border-editor-border text-editor-text-muted'}`}
|
||||
title="Toggle speed zones"
|
||||
>
|
||||
Speed
|
||||
</button>
|
||||
<span className="text-[10px] text-editor-text-muted">
|
||||
Scroll · Ctrl+Scroll to zoom
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user