forgot to add stuff
This commit is contained in:
237
frontend/src/components/ZoneEditor.tsx
Normal file
237
frontend/src/components/ZoneEditor.tsx
Normal file
@ -0,0 +1,237 @@
|
||||
import { useState } from 'react';
|
||||
import { useEditorStore } from '../store/editorStore';
|
||||
import { Trash2, Scissors, Volume2, SlidersHorizontal } from 'lucide-react';
|
||||
|
||||
export default function ZoneEditor() {
|
||||
const [viewMode, setViewMode] = useState<'all' | 'cut' | 'mute' | 'gain'>('all');
|
||||
|
||||
const {
|
||||
cutRanges,
|
||||
muteRanges,
|
||||
gainRanges,
|
||||
globalGainDb,
|
||||
setGlobalGainDb,
|
||||
removeCutRange,
|
||||
removeMuteRange,
|
||||
removeGainRange,
|
||||
updateGainRange,
|
||||
} = useEditorStore();
|
||||
|
||||
const totalZones = cutRanges.length + muteRanges.length + gainRanges.length;
|
||||
|
||||
const getZoneTypeColor = (type: 'cut' | 'mute' | 'gain') => {
|
||||
switch (type) {
|
||||
case 'cut':
|
||||
return 'border-red-500/40 bg-red-500/5';
|
||||
case 'mute':
|
||||
return 'border-orange-500/40 bg-orange-500/5';
|
||||
case 'gain':
|
||||
return 'border-amber-500/40 bg-amber-500/5';
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
|
||||
{/* View Mode Toggle */}
|
||||
<div className="flex items-center gap-1 rounded bg-editor-surface border border-editor-border p-1">
|
||||
<button
|
||||
onClick={() => setViewMode('all')}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
viewMode === 'all'
|
||||
? 'bg-editor-accent text-white'
|
||||
: 'text-editor-text-muted hover:text-editor-text'
|
||||
}`}
|
||||
>
|
||||
All
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('cut')}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
viewMode === 'cut'
|
||||
? 'bg-red-500/30 text-red-500'
|
||||
: 'text-editor-text-muted hover:text-editor-text'
|
||||
}`}
|
||||
>
|
||||
Cut
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('mute')}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
viewMode === 'mute'
|
||||
? 'bg-orange-500/30 text-orange-500'
|
||||
: 'text-editor-text-muted hover:text-editor-text'
|
||||
}`}
|
||||
>
|
||||
Mute
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setViewMode('gain')}
|
||||
className={`px-2 py-1 text-xs rounded transition-colors ${
|
||||
viewMode === 'gain'
|
||||
? 'bg-amber-500/30 text-amber-500'
|
||||
: 'text-editor-text-muted hover:text-editor-text'
|
||||
}`}
|
||||
>
|
||||
Gain
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{totalZones === 0 ? (
|
||||
<div className="p-4 rounded border border-dashed border-editor-border text-center">
|
||||
<p className="text-xs text-editor-text-muted">
|
||||
No zones yet. Create zones from the toolbar or by highlighting words.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{/* Cut Zones */}
|
||||
{(viewMode === 'all' || viewMode === 'cut') && cutRanges.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-red-500/80 flex items-center gap-2">
|
||||
<Scissors className="w-3.5 h-3.5" />
|
||||
Cut Zones ({cutRanges.length})
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{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')}`}
|
||||
>
|
||||
<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 text-[10px]">{range.id}</div>
|
||||
</div>
|
||||
<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"
|
||||
title="Delete cut zone"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Mute Zones */}
|
||||
{(viewMode === 'all' || viewMode === 'mute') && muteRanges.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-orange-500/80 flex items-center gap-2">
|
||||
<Volume2 className="w-3.5 h-3.5" />
|
||||
Mute Zones ({muteRanges.length})
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{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')}`}
|
||||
>
|
||||
<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 text-[10px]">{range.id}</div>
|
||||
</div>
|
||||
<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"
|
||||
title="Delete mute zone"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Gain Zones */}
|
||||
{(viewMode === 'all' || viewMode === 'gain') && gainRanges.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-xs font-semibold text-amber-500/80 flex items-center gap-2">
|
||||
<SlidersHorizontal className="w-3.5 h-3.5" />
|
||||
Gain Zones ({gainRanges.length})
|
||||
</div>
|
||||
|
||||
{/* Global Gain Slider */}
|
||||
<div className="px-2 py-2 rounded border border-amber-500/20 bg-amber-500/5 space-y-2">
|
||||
<label className="text-xs text-editor-text-muted font-medium">Global Gain</label>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="range"
|
||||
min={-24}
|
||||
max={24}
|
||||
step={0.5}
|
||||
value={globalGainDb}
|
||||
onChange={(e) => setGlobalGainDb(Number(e.target.value))}
|
||||
className="flex-1 h-1.5"
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
min={-24}
|
||||
max={24}
|
||||
step={0.5}
|
||||
value={globalGainDb}
|
||||
onChange={(e) => setGlobalGainDb(Math.max(-24, Math.min(24, Number(e.target.value) || 0)))}
|
||||
className="w-14 px-1.5 py-0.5 text-xs bg-editor-surface border border-editor-border rounded focus:border-editor-accent focus:outline-none"
|
||||
/>
|
||||
<span className="text-xs text-amber-500/80 font-medium w-6 text-right">dB</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
{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')}`}
|
||||
>
|
||||
<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 text-[10px]">
|
||||
{range.gainDb > 0 ? '+' : ''}{range.gainDb.toFixed(1)} dB
|
||||
</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-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"
|
||||
/>
|
||||
<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"
|
||||
title="Delete gain zone"
|
||||
>
|
||||
<Trash2 className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
frontend/src/store/editorStore.test.ts
Normal file
32
frontend/src/store/editorStore.test.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { useEditorStore } from './editorStore';
|
||||
|
||||
|
||||
describe('editorStore basics', () => {
|
||||
beforeEach(() => {
|
||||
useEditorStore.getState().reset();
|
||||
});
|
||||
|
||||
test('clamps global gain to valid bounds', () => {
|
||||
const state = useEditorStore.getState();
|
||||
|
||||
state.setGlobalGainDb(100);
|
||||
expect(useEditorStore.getState().globalGainDb).toBe(24);
|
||||
|
||||
state.setGlobalGainDb(-100);
|
||||
expect(useEditorStore.getState().globalGainDb).toBe(-24);
|
||||
});
|
||||
|
||||
test('adds gain range to store', () => {
|
||||
const state = useEditorStore.getState();
|
||||
|
||||
state.addGainRange(1.2, 2.4, 3.5);
|
||||
|
||||
const ranges = useEditorStore.getState().gainRanges;
|
||||
expect(ranges.length).toBe(1);
|
||||
expect(ranges[0].start).toBe(1.2);
|
||||
expect(ranges[0].end).toBe(2.4);
|
||||
expect(ranges[0].gainDb).toBe(3.5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user