volume panel; copilot instructions
This commit is contained in:
134
frontend/src/components/VolumePanel.tsx
Normal file
134
frontend/src/components/VolumePanel.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Trash2, Volume2 } from 'lucide-react';
|
||||
|
||||
export default function VolumePanel() {
|
||||
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>
|
||||
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user