Files
TalkEdit/frontend/src/components/VolumePanel.tsx

175 lines
6.4 KiB
TypeScript
Raw Normal View History

2026-04-15 16:10:35 -06:00
import { useMemo, useState } from 'react';
import { useEditorStore } from '../store/editorStore';
import { Trash2, Volume2 } from 'lucide-react';
2026-04-15 16:36:21 -06:00
interface VolumePanelProps {
gainMode: boolean;
onToggleGainMode: () => void;
timelineGainDb: number;
onTimelineGainDbChange: (gainDb: number) => void;
}
export default function VolumePanel({
gainMode,
onToggleGainMode,
timelineGainDb,
onTimelineGainDbChange,
}: VolumePanelProps) {
2026-04-15 16:10:35 -06:00
const {
words,
selectedWordIndices,
globalGainDb,
gainRanges,
setGlobalGainDb,
addGainRange,
updateGainRange,
removeGainRange,
} = useEditorStore();
const [selectionGainDb, setSelectionGainDb] = useState(3);
const canApplySelection = selectedWordIndices.length > 0;
const selectedRange = useMemo(() => {
if (!canApplySelection) return null;
const sorted = [...selectedWordIndices].sort((a, b) => a - b);
const startWord = words[sorted[0]];
const endWord = words[sorted[sorted.length - 1]];
if (!startWord || !endWord) return null;
return {
start: startWord.start,
end: endWord.end,
};
}, [canApplySelection, selectedWordIndices, words]);
const applySelectionGain = () => {
if (!selectedRange) return;
addGainRange(selectedRange.start, selectedRange.end, selectionGainDb);
};
return (
<div className="p-4 space-y-4">
<div className="space-y-1">
<h3 className="text-sm font-semibold flex items-center gap-2">
<Volume2 className="w-4 h-4" />
Volume / Gain
</h3>
<p className="text-xs text-editor-text-muted">
Apply global gain or per-selection gain ranges.
</p>
</div>
<div className="space-y-2">
<label className="text-xs text-editor-text-muted font-medium">Global Gain (dB)</label>
<input
type="range"
min={-24}
max={24}
step={0.5}
value={globalGainDb}
onChange={(e) => setGlobalGainDb(Number(e.target.value))}
className="w-full"
/>
<div className="flex items-center justify-between text-xs">
<span className="text-editor-text-muted">-24 dB</span>
<span className="font-medium text-editor-text">{globalGainDb.toFixed(1)} dB</span>
<span className="text-editor-text-muted">+24 dB</span>
</div>
</div>
2026-04-15 16:36:21 -06:00
<div className="space-y-2 pt-1 border-t border-editor-border">
<label className="text-xs text-editor-text-muted font-medium">Timeline Gain Zone (dB)</label>
<div className="flex items-center gap-2">
<input
type="number"
min={-24}
max={24}
step={0.5}
value={timelineGainDb}
onChange={(e) => onTimelineGainDbChange(Math.max(-24, Math.min(24, Number(e.target.value) || 0)))}
className="w-24 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
/>
<button
onClick={onToggleGainMode}
className={`px-3 py-1.5 text-xs rounded transition-colors ${
gainMode
? 'bg-editor-accent text-white hover:bg-editor-accent-hover'
: 'bg-editor-accent/20 text-editor-accent hover:bg-editor-accent/30'
}`}
>
{gainMode ? 'Exit Zone Mode' : 'Add Gain Zones'}
</button>
</div>
<p className="text-[11px] text-editor-text-muted">
In gain zone mode, drag on the timeline to create a zone with this dB value.
</p>
</div>
2026-04-15 16:10:35 -06:00
<div className="space-y-2 pt-1 border-t border-editor-border">
<label className="text-xs text-editor-text-muted font-medium">Selection Gain (dB)</label>
<div className="flex items-center gap-2">
<input
type="number"
min={-24}
max={24}
step={0.5}
value={selectionGainDb}
onChange={(e) => setSelectionGainDb(Number(e.target.value) || 0)}
className="w-24 px-2 py-1.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
/>
<button
onClick={applySelectionGain}
disabled={!canApplySelection || !selectedRange}
className="px-3 py-1.5 text-xs rounded bg-editor-accent/20 text-editor-accent hover:bg-editor-accent/30 disabled:opacity-40"
>
Apply To Selection
</button>
</div>
<p className="text-[11px] text-editor-text-muted">
{canApplySelection
? `${selectedWordIndices.length} selected words${selectedRange ? ` (${selectedRange.start.toFixed(2)}s - ${selectedRange.end.toFixed(2)}s)` : ''}`
: 'Select transcript words to apply a gain range.'}
</p>
</div>
{gainRanges.length > 0 && (
<div className="space-y-2 pt-1 border-t border-editor-border">
<div className="text-xs font-medium">Gain Ranges</div>
<div className="max-h-56 overflow-y-auto space-y-1 pr-1">
{gainRanges.map((range) => (
<div
key={range.id}
className="px-2 py-1.5 rounded bg-editor-surface border border-editor-border text-xs flex items-center gap-2"
>
<div className="flex-1 min-w-0">
<div className="font-medium truncate">
{range.start.toFixed(2)}s - {range.end.toFixed(2)}s
</div>
<div className="text-editor-text-muted">{range.id}</div>
</div>
<input
type="number"
min={-24}
max={24}
step={0.5}
value={range.gainDb}
onChange={(e) => updateGainRange(range.id, Number(e.target.value) || 0)}
className="w-20 px-2 py-1 text-xs bg-editor-bg border border-editor-border rounded"
title="Gain dB"
/>
<button
onClick={() => removeGainRange(range.id)}
className="p-1 rounded hover:bg-editor-danger/20 text-editor-danger"
title="Delete gain range"
>
<Trash2 className="w-3.5 h-3.5" />
</button>
</div>
))}
</div>
</div>
)}
</div>
);
}