import { useState, useRef, useEffect, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Save, Trash2, RotateCcw } from 'lucide-react'; import CustomSelect from './CustomSelect'; import { useEqStore, EQ_BANDS, BUILTIN_PRESETS } from '../store/eqStore'; import { useThemeStore } from '../store/themeStore'; import { drawCurve } from '../utils/audio/eqCurve'; import VerticalFader from './equalizer/VerticalFader'; import AutoEqSection from './equalizer/AutoEqSection'; export default function Equalizer() { const { t } = useTranslation(); const gains = useEqStore(s => s.gains); const enabled = useEqStore(s => s.enabled); const preGain = useEqStore(s => s.preGain); const activePreset = useEqStore(s => s.activePreset); const customPresets = useEqStore(s => s.customPresets); const { setBandGain, setEnabled, setPreGain, applyPreset, saveCustomPreset, deleteCustomPreset } = useEqStore(); const [saveName, setSaveName] = useState(''); const [showSave, setShowSave] = useState(false); const canvasRef = useRef(null); const theme = useThemeStore(s => s.theme); const redraw = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; const style = getComputedStyle(document.documentElement); const accent = style.getPropertyValue('--accent').trim() || 'rgb(203, 166, 247)'; const bg = style.getPropertyValue('--bg-app').trim() || '#1e1e2e'; const text = style.getPropertyValue('--text-muted').trim() || 'rgba(255,255,255,0.4)'; drawCurve(canvas, gains, accent, bg, text); }, [gains, theme]); useEffect(() => { redraw(); }, [redraw]); useEffect(() => { const ro = new ResizeObserver(redraw); if (canvasRef.current) ro.observe(canvasRef.current); return () => ro.disconnect(); }, [redraw]); // ResizeObserver does not always fire for the `display: none → block` // transition the surrounding
causes when toggled, leaving the // canvas blank on the second open. Redraw explicitly when the parent //
opens; rAF waits one frame for the canvas to take its size. useEffect(() => { const details = canvasRef.current?.closest('details'); if (!details) return; const onToggle = () => { if (details.open) requestAnimationFrame(redraw); }; details.addEventListener('toggle', onToggle); return () => details.removeEventListener('toggle', onToggle); }, [redraw]); const isCustomSaved = activePreset && !BUILTIN_PRESETS.some(p => p.name === activePreset); const selectValue = activePreset ?? '__custom__'; const handleSave = () => { const name = saveName.trim(); if (!name) return; saveCustomPreset(name); setSaveName(''); setShowSave(false); }; return (
{/* Controls bar */}
applyPreset(v)} options={[ ...(activePreset === null ? [{ value: '__custom__', label: t('settings.eqPresetCustom'), disabled: true }] : []), ...BUILTIN_PRESETS.map(p => ({ value: p.name, label: p.name, group: t('settings.eqPresetBuiltin') })), ...customPresets.map(p => ({ value: p.name, label: p.name, group: t('settings.eqPresetCustomGroup') })), ]} /> {isCustomSaved && ( )}
{showSave && (
setSaveName(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleSave()} autoFocus style={{ flex: 1, padding: '6px 12px', fontSize: 13 }} />
)} {/* EQ panel */}
{/* Frequency response */} {/* Fader area */}
{/* dB scale */}
{[12, 6, 0, -6, -12].map(db => ( {db > 0 ? `+${db}` : db} ))}
{/* Bands */} {EQ_BANDS.map((band, i) => (
{gains[i] > 0 ? '+' : ''}{gains[i].toFixed(1)}
setBandGain(i, v)} />
{band.label}
))}
{/* Pre-gain row */}
{t('settings.eqPreGain')} setPreGain(parseFloat(e.target.value))} /> {preGain > 0 ? '+' : ''}{preGain.toFixed(1)} dB {preGain !== 0 && ( )}
); }