feat: disable inputs if peq dont use them

This commit is contained in:
2026-09-23 20:03:39 +02:00
parent e1d6398d57
commit d48155683b
12 changed files with 174 additions and 110 deletions
+7 -43
View File
@@ -113,25 +113,14 @@ export function BoltIcon(props: React.SVGProps<SVGSVGElement>) {
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle
cx="12"
cy="12"
r="4"
stroke="currentColor"
strokeWidth="2"
/>
<circle cx="12" cy="12" r="4" stroke="currentColor" strokeWidth="2" />
</svg>
);
}
export function GainIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M3 12H7M17 12H21"
stroke="currentColor"
@@ -152,12 +141,7 @@ export function GainIcon(props: React.SVGProps<SVGSVGElement>) {
export function GateIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M3 3V21H21"
stroke="currentColor"
@@ -179,12 +163,7 @@ export function GateIcon(props: React.SVGProps<SVGSVGElement>) {
export function CompressorIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M3 3V21H21"
stroke="currentColor"
@@ -206,12 +185,7 @@ export function CompressorIcon(props: React.SVGProps<SVGSVGElement>) {
export function LimiterIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M3 3V21H21"
stroke="currentColor"
@@ -233,12 +207,7 @@ export function LimiterIcon(props: React.SVGProps<SVGSVGElement>) {
export function DelayIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path
d="M4 5V19M12 8.5V15.5M20 11V13"
stroke="currentColor"
@@ -252,12 +221,7 @@ export function DelayIcon(props: React.SVGProps<SVGSVGElement>) {
export function MatrixIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" {...props}>
<path d="M3 7C11 7 11 4 21 4" />
<path d="M3 7C11 7 11 20 21 20" />
<path d="M3 17C11 17 11 12 21 12" />
+2 -2
View File
@@ -28,7 +28,7 @@
font-family: var(--font-ui);
font-size: 90%;
color: inherit;
cursor: pointer;
@@ -122,4 +122,4 @@
.choice-select__listbox--portal {
position: fixed;
z-index: 1000; /* clear of any local stacking contexts in the panel */
}
}
+43 -43
View File
@@ -40,35 +40,35 @@ function ChoiceSelect<T extends string>({
const listboxRef = useRef<HTMLUListElement>(null);
const suppressNextClick = useRef(false);
useEffect(() => {
function handleMouseDown(e: MouseEvent) {
const target = e.target as Node;
const clickedOutside =
rootRef.current &&
!rootRef.current.contains(target) &&
!listboxRef.current?.contains(target);
useEffect(() => {
function handleMouseDown(e: MouseEvent) {
const target = e.target as Node;
const clickedOutside =
rootRef.current &&
!rootRef.current.contains(target) &&
!listboxRef.current?.contains(target);
if (clickedOutside && open) {
setOpen(false);
suppressNextClick.current = true;
if (clickedOutside && open) {
setOpen(false);
suppressNextClick.current = true;
}
}
}
function handleClickCapture(e: MouseEvent) {
if (suppressNextClick.current) {
e.preventDefault();
e.stopPropagation();
suppressNextClick.current = false;
function handleClickCapture(e: MouseEvent) {
if (suppressNextClick.current) {
e.preventDefault();
e.stopPropagation();
suppressNextClick.current = false;
}
}
}
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('click', handleClickCapture, true); // capture phase
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('click', handleClickCapture, true);
};
}, [open]);
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('click', handleClickCapture, true); // capture phase
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('click', handleClickCapture, true);
};
}, [open]);
// Re-check placement/position on scroll/resize while open — the trigger's
// position can change (e.g. inside a scrollable panel), and since the
@@ -93,27 +93,27 @@ function ChoiceSelect<T extends string>({
};
function updatePlacement() {
const el = triggerRef.current;
if (!el) return;
const el = triggerRef.current;
if (!el) return;
const rect = el.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow;
setDropUp(needsFlip);
const rect = el.getBoundingClientRect();
const spaceBelow = window.innerHeight - rect.bottom;
const spaceAbove = rect.top;
const needsFlip = spaceBelow < LISTBOX_MAX_HEIGHT && spaceAbove > spaceBelow;
setDropUp(needsFlip);
// Capture the trigger's resolved font-size (its cqh has already resolved
// correctly here, since it's still inside the real container tree) and
// carry it as a plain px value into the portal, where cqh would break.
const fontSize = parseFloat(getComputedStyle(el).fontSize);
// Capture the trigger's resolved font-size (its cqh has already resolved
// correctly here, since it's still inside the real container tree) and
// carry it as a plain px value into the portal, where cqh would break.
const fontSize = parseFloat(getComputedStyle(el).fontSize);
setCoords({
top: needsFlip ? rect.top - LISTBOX_GAP : rect.bottom + LISTBOX_GAP,
left: rect.left,
width: rect.width,
fontSize,
});
}
setCoords({
top: needsFlip ? rect.top - LISTBOX_GAP : rect.bottom + LISTBOX_GAP,
left: rect.left,
width: rect.width,
fontSize,
});
}
function openList() {
if (disabled) return;
@@ -229,4 +229,4 @@ function ChoiceSelect<T extends string>({
);
}
export default ChoiceSelect;
export default ChoiceSelect;
@@ -78,9 +78,15 @@
}
.choice-stepper[data-disabled] {
opacity: 0.5;
opacity: 0.45;
filter: grayscale(1);
}
.choice-stepper__input:disabled {
cursor: not-allowed;
}
.choice-stepper[data-disabled] .choice-stepper__input,
.choice-stepper[data-disabled] .choice-stepper__arrow-btn {
color: var(--border-hairline);
}
@@ -146,3 +146,26 @@
font-family: inherit;
color: inherit;
}
.gauge-viewport.is-disabled {
opacity: 0.45;
filter: grayscale(1);
}
.gauge-viewport.is-disabled .slider {
cursor: not-allowed;
}
.gauge-viewport.is-disabled .thumb {
border-color: var(--border-hairline);
box-shadow: none;
}
.gauge-viewport.is-disabled .value {
color: var(--border-hairline);
box-shadow: none;
}
.value input:disabled {
cursor: not-allowed;
}
@@ -38,6 +38,9 @@ type VerticalSliderProps = {
className?: string;
style?: React.CSSProperties;
/** Greys out the slider and blocks all interaction. */
disabled?: boolean;
};
const defaultStep: StepFn = () => 0.1;
@@ -174,6 +177,7 @@ export default function VerticalSlider({
formatValue,
showTicks = true,
scale = 'linear',
disabled = false,
className,
style,
}: VerticalSliderProps) {
@@ -240,6 +244,26 @@ export default function VerticalSlider({
return () => observer.disconnect();
}, []);
useEffect(() => {
if (!disabled) return;
dragging.current = false;
keyboardEditing.current = false;
keyboardValue.current = null;
wheelEditing.current = false;
wheelValue.current = null;
if (wheelCommitTimeout.current) {
clearTimeout(wheelCommitTimeout.current);
wheelCommitTimeout.current = null;
}
setEditingText(null);
setDisplayValue(value);
dragValue.current = value;
onInput?.(value);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [disabled]);
const decimals = useMemo(() => {
if (listValues) return Math.min(3, getDecimals(displayValue));
return Math.min(3, getDecimals(step(displayValue)));
@@ -273,6 +297,8 @@ export default function VerticalSlider({
}
function handleKeyDown(e: React.KeyboardEvent<HTMLDivElement>) {
if (disabled) return;
if (!['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
return;
}
@@ -307,6 +333,8 @@ export default function VerticalSlider({
}
async function handleKeyUp() {
if (disabled) return;
if (keyboardValue.current == null) return;
const next = keyboardValue.current;
@@ -323,6 +351,7 @@ export default function VerticalSlider({
}
function handleWheel(e: WheelEvent) {
if (disabled) return;
e.preventDefault();
wheelEditing.current = true;
@@ -393,6 +422,8 @@ export default function VerticalSlider({
}
function startDrag(e: React.PointerEvent<HTMLDivElement>) {
if (disabled) return;
const target = e.currentTarget;
dragging.current = true;
target.setPointerCapture(e.pointerId);
@@ -427,6 +458,11 @@ export default function VerticalSlider({
}
async function commitEditing() {
if (disabled) {
setEditingText(null);
return;
}
if (editingText !== null && editingText !== '' && editingText !== '-') {
const parsed = Number(editingText);
@@ -452,7 +488,10 @@ export default function VerticalSlider({
const percent = valueToPercent(displayValue, min, max, scale, listValues);
return (
<div className={['gauge-viewport', className].filter(Boolean).join(' ')} style={style}>
<div
className={['gauge-viewport', disabled && 'is-disabled', className].filter(Boolean).join(' ')}
style={style}
>
<div
className={['gauge-track', !showTicks && 'no-ticks'].filter(Boolean).join(' ')}
ref={trackRef}
@@ -468,7 +507,12 @@ export default function VerticalSlider({
<div
className="slider"
ref={sliderRef}
tabIndex={0}
role="slider"
aria-disabled={disabled}
aria-valuemin={min}
aria-valuemax={max}
aria-valuenow={displayValue}
tabIndex={disabled ? -1 : 0}
onKeyDown={handleKeyDown}
onKeyUp={handleKeyUp}
onPointerDown={startDrag}
@@ -494,6 +538,7 @@ export default function VerticalSlider({
ref={inputRef}
type="text"
inputMode="decimal"
disabled={disabled}
value={editingText ?? formattedValue}
style={{ width: '100%' }}
onFocus={() => setEditingText(value.toFixed(decimals))} // plain number while editing
@@ -11,6 +11,8 @@ import {
PEQ_FILTERS_LABEL,
PEQ_Q_RANGES,
PEAK_Q_VALUES,
PEQ_Q_UNUSED,
PEQ_GAIN_USED,
} from '../../constants';
import { Channel, PEQBand, PEQFilter, PEQ, Crossover } from '../../types';
import { DSP408 as DSP } from '../../../../types';
@@ -128,6 +130,8 @@ function PeqPanelSliders({
onLiveValue: (value: PEQ) => void;
}) {
const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type];
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
const commit = (overrides: Partial<PEQ>) => {
const next = { ...peq, ...overrides };
@@ -168,6 +172,7 @@ function PeqPanelSliders({
value={peq.q}
onChange={(value) => commit({ q: value })}
onInput={(value) => onLiveValue({ ...peq, q: value })}
disabled={qDisabled}
style={{ width: '100%', flex: '1 1 auto' }}
/>
@@ -184,6 +189,7 @@ function PeqPanelSliders({
formatValue={(v, d) => (v > 0 ? `+${v.toFixed(d)}` : v.toFixed(d))}
onChange={(value) => commit({ gain: value })}
onInput={(value) => onLiveValue({ ...peq, gain: value })}
disabled={gainDisabled}
style={{ width: '100%', flex: '1 1 auto' }}
/>
@@ -335,6 +341,9 @@ function PeqBandList({
const [qMin, qMax] = PEQ_Q_RANGES[peq.filter_type];
const qValues = PEAK_Q_VALUES.filter((q) => q >= qMin && q <= qMax);
const qDisabled = PEQ_Q_UNUSED.has(peq.filter_type);
const gainDisabled = !PEQ_GAIN_USED.has(peq.filter_type);
return (
<div
className={`peq-band-list__row${isActive ? ' peq-band-list__row--active' : ''}`}
@@ -370,6 +379,7 @@ function PeqBandList({
value={peq.q}
values={qValues}
formatLabel={(v) => v.toFixed(2)}
disabled={qDisabled}
onChange={(value) => commit(band, { q: value })}
/>
@@ -380,6 +390,7 @@ function PeqBandList({
max={12}
step={0.1}
formatLabel={(v) => `${v > 0 ? '+' : ''}${v.toFixed(1)} dB`}
disabled={gainDisabled}
onChange={(value) => commit(band, { gain: value })}
/>
@@ -715,8 +726,8 @@ function ChannelSection({
<div className="confirm-modal" onClick={(e) => e.stopPropagation()}>
<h3>Reset {activeChannelState.name} PEQ?</h3>
<p>
This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB,
Peak). This action cannot be undone.
This will set all PEQ bands on {activeChannelState.name} back to flat (0 dB, Peak).
This action cannot be undone.
</p>
<div className="confirm-modal__actions">
@@ -1,6 +1,17 @@
import { useState } from 'react';
import { Sidebar, SidebarItem } from '../../../../components/Sidebar/Sidebar';
import { BoltIcon, CompressorIcon, DelayIcon, GainIcon, GateIcon, InfoIcon, LimiterIcon, MatrixIcon, SlidersVerticalIcon, TextIcon } from '../../../../assets/icons/AudioIcons';
import {
BoltIcon,
CompressorIcon,
DelayIcon,
GainIcon,
GateIcon,
InfoIcon,
LimiterIcon,
MatrixIcon,
SlidersVerticalIcon,
TextIcon,
} from '../../../../assets/icons/AudioIcons';
import ConnectionPanel from '../ConnectionSection/ConnectionSection';
import GainSection from '../GainSection/GainSection';
import CompressorSection from '../CompressorSection/CompressorSection';
@@ -97,9 +97,7 @@ function getInputChannelCurves(
): ChannelCurves {
const gain = liveGain ?? state.gain;
const geqValues = GEQ_BANDS_LABEL.map(
({ frequency }) => state.geq.gains[frequency] ?? 0
);
const geqValues = GEQ_BANDS_LABEL.map(({ frequency }) => state.geq.gains[frequency] ?? 0);
const geq = state.geq.bypass
? []
@@ -109,17 +107,9 @@ function getInputChannelCurves(
q: GEQ_CURVE_Q,
});
const peqCrossover = getPeqCrossoverPoints(
freqs,
state.peq_chain,
state.crossover
);
const peqCrossover = getPeqCrossoverPoints(freqs, state.peq_chain, state.crossover);
const total = sumCurvePoints([
geq,
peqCrossover,
getGainCurvePoints(freqs, gain),
]);
const total = sumCurvePoints([geq, peqCrossover, getGainCurvePoints(freqs, gain)]);
return { geq, peqCrossover, total };
}
@@ -112,7 +112,7 @@ function MiscellaneousPanel({
value={inputSource.type}
options={INPUT_TYPES}
onChange={handleInputTypeChange}
style={{color: 'var(--font-primary)'}}
style={{ color: 'var(--font-primary)' }}
/>
</div>
@@ -132,7 +132,7 @@ function MiscellaneousPanel({
return `${frequency.slice(1)} Hz`;
}}
onChange={handleFrequencyChange}
style={{color: 'var(--font-primary)'}}
style={{ color: 'var(--font-primary)' }}
/>
</div>
)}
+14
View File
@@ -225,6 +225,20 @@ export const PEQ_FILTERS_LABEL: { value: PEQFilter; label: string }[] = [
{ value: PEQFilter.AllPass2, label: 'All Pass 2' },
];
/** Filter types that ignore Q (first-order filters). */
export const PEQ_Q_UNUSED = new Set<PEQFilter>([
PEQFilter.Lp6Db,
PEQFilter.Hp6Db,
PEQFilter.AllPass1,
]);
/** Filter types that ignore gain (everything except Peak and the shelves). */
export const PEQ_GAIN_USED = new Set<PEQFilter>([
PEQFilter.Peak,
PEQFilter.LowShelf,
PEQFilter.HighShelf,
]);
export const FREQUENCY_MIN = 19.7;
export const FREQUENCY_MAX = 20160;
export const FREQUENCY_QUANTIZATION = 300;
+1 -1
View File
@@ -773,7 +773,7 @@ export function forDSP408(dsp: DSP408, setDsps: SetDsps, notify: NotifyFn) {
await updateDSPState();
notify('Channels copied successfully', 'success', dsp.name)
notify('Channels copied successfully', 'success', dsp.name);
return true;
} catch (err) {