Refactor EQPage to support dual mode (A/B) for equalizer settings; added visibility toggles for frequency response curves and copy functionality between modes.

This commit is contained in:
yangy
2026-04-30 17:02:13 +08:00
parent 0262619d03
commit 4ca86ad60a
+100 -18
View File
@@ -94,13 +94,14 @@ function normalizeFilterType(type: string | number | undefined): string {
/* ── Frequency Response Chart ── */
function FreqChart({
bands, rawCurve, selectedBand, abMode, onAbToggle, onBandDrag, onBandSelect,
bands, rawCurve, selectedBand, abMode, onAbToggle, onCopyMode, onBandDrag, onBandSelect,
}: {
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
rawCurve: number[] | null;
selectedBand: number;
abMode: "A" | "B";
onAbToggle: (m: "A" | "B") => void;
onCopyMode: (from: "A" | "B", to: "A" | "B") => void;
onBandDrag: (idx: number, patch: Partial<{ freq: number; gain: number }>) => void;
onBandSelect: (idx: number) => void;
}) {
@@ -197,9 +198,27 @@ function FreqChart({
);
const pathD = curveData.pathD;
const fillD = curveData.fillD;
const [showEq, setShowEq] = useState(true);
const [showRaw, setShowRaw] = useState(true);
const [showEqualized, setShowEqualized] = useState(true);
const [curveVisibilityByMode, setCurveVisibilityByMode] = useState<
Record<"A" | "B", { eq: boolean; raw: boolean; equalized: boolean }>
>({
A: { eq: true, raw: true, equalized: true },
B: { eq: true, raw: false, equalized: false },
});
const showEq = curveVisibilityByMode[abMode].eq;
const showRaw = curveVisibilityByMode[abMode].raw;
const showEqualized = curveVisibilityByMode[abMode].equalized;
const toggleCurveVisibility = (key: "eq" | "raw" | "equalized") => {
setCurveVisibilityByMode((prev) => ({
...prev,
[abMode]: {
...prev[abMode],
[key]: !prev[abMode][key],
},
}));
};
const rawPathD = useMemo(() => {
if (!Array.isArray(rawCurve) || rawCurve.length !== curveData.points.length) return "";
return curveData.points
@@ -232,6 +251,9 @@ function FreqChart({
durationMs: 1350,
});
const targetMode: "A" | "B" = abMode === "A" ? "B" : "A";
const copyButtonText = abMode === "A" ? "复制到 B" : "复制到 A";
const freqLabels = [20, 50, 100, 200, 500, 1000, 2000, 5000, 10000, 20000];
const gainLabels = [20, 15, 10, 5, 0, -5, -10, -15, -20];
@@ -242,7 +264,7 @@ function FreqChart({
<button
type="button"
className="flex items-center gap-1.5 active:opacity-80 transition-opacity"
onClick={() => setShowEq((v) => !v)}
onClick={() => toggleCurveVisibility("eq")}
>
<div className="w-3 h-[2px] rounded" style={{ background: "#FFED00" }} />
<span className="text-[10px]" style={{ color: "#FFED00", opacity: showEq ? 1 : 0.35 }}>Equalizer</span>
@@ -250,7 +272,7 @@ function FreqChart({
<button
type="button"
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
onClick={() => setShowRaw((v) => !v)}
onClick={() => toggleCurveVisibility("raw")}
disabled={!hasRawCurve}
>
<div className="w-3 h-[2px] rounded" style={{ background: "#ffffff" }} />
@@ -259,7 +281,7 @@ function FreqChart({
<button
type="button"
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
onClick={() => setShowEqualized((v) => !v)}
onClick={() => toggleCurveVisibility("equalized")}
disabled={!hasEqualizedCurve}
>
<div className="w-3 h-[2px] rounded" style={{ background: "#23d2fe" }} />
@@ -286,8 +308,23 @@ function FreqChart({
<button
className="px-3 py-1 rounded-[8px] text-[11px] text-white/50 active:text-white transition-colors"
style={{ background: "rgba(44,44,46,0.7)", border: "1px solid rgba(255,255,255,0.08)" }}
onClick={() => toast.info("已复制到 B")}>
B
onClick={() => {
onCopyMode(abMode, targetMode);
setCurveVisibilityByMode((prev) => ({
...prev,
[targetMode]: { ...prev[abMode] },
}));
onAbToggle(targetMode);
toast.success(`已复制到 ${targetMode}`);
}}>
{copyButtonText}
</button>
<button
className="px-3 py-1 rounded-[8px] text-[11px] text-white/50 active:text-white transition-colors"
style={{ background: "rgba(44,44,46,0.7)", border: "1px solid rgba(255,255,255,0.08)" }}
onClick={() => toast.info("已加载至设备")}
>
</button>
</div>
@@ -436,6 +473,12 @@ const DEFAULT_BANDS = [
{ freq: 10000, gain: 0, q: 1.41, type: "HSHELF", enabled: true },
];
function cloneBands(
source: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>,
) {
return source.map((band) => ({ ...band }));
}
function freqLabel(f: number) {
return f >= 1000 ? `${(f / 1000).toFixed(1)}K` : `${f}`;
}
@@ -501,8 +544,14 @@ export default function EQPage() {
const { deviceState, updateSetting, api, isDemoMode, upgradePeqChange } = useDevice();
const eqOn = (deviceState?.peqEnable ?? 0) === 1;
const [bands, setBands] = useState(DEFAULT_BANDS);
const [selectedBand, setSelectedBand] = useState(0);
const [bandsByMode, setBandsByMode] = useState<Record<"A" | "B", typeof DEFAULT_BANDS>>({
A: cloneBands(DEFAULT_BANDS),
B: cloneBands(DEFAULT_BANDS),
});
const [selectedBandByMode, setSelectedBandByMode] = useState<Record<"A" | "B", number>>({
A: 0,
B: 0,
});
const [headphoneIdx, setHeadphoneIdx] = useState(deviceState?.peqSelect ?? 0);
const [headphoneModels, setHeadphoneModels] = useState<string[]>([]);
const [peqItems, setPeqItems] = useState<
@@ -550,6 +599,37 @@ export default function EQPage() {
const peqSyncTimerRef = useRef<number | null>(null);
const headphoneMenuRef = useRef<HTMLDivElement | null>(null);
const filterMenuRef = useRef<HTMLDivElement | null>(null);
const bands = bandsByMode[abMode];
const selectedBand = selectedBandByMode[abMode];
const setBandsForBothModes = useCallback((nextBands: typeof DEFAULT_BANDS) => {
const cloned = cloneBands(nextBands);
setBandsByMode({ A: cloneBands(cloned), B: cloneBands(cloned) });
}, []);
const setBands = useCallback(
(updater: typeof DEFAULT_BANDS | ((prev: typeof DEFAULT_BANDS) => typeof DEFAULT_BANDS)) => {
setBandsByMode((prev) => ({
...prev,
[abMode]:
typeof updater === "function"
? (updater as (prev: typeof DEFAULT_BANDS) => typeof DEFAULT_BANDS)(prev[abMode])
: cloneBands(updater),
}));
},
[abMode],
);
const setSelectedBand = useCallback(
(updater: number | ((prev: number) => number)) => {
setSelectedBandByMode((prev) => ({
...prev,
[abMode]: typeof updater === "function" ? (updater as (prev: number) => number)(prev[abMode]) : updater,
}));
},
[abMode],
);
const copyModeParams = useCallback((from: "A" | "B", to: "A" | "B") => {
setBandsByMode((prev) => ({ ...prev, [to]: cloneBands(prev[from]) }));
setSelectedBandByMode((prev) => ({ ...prev, [to]: prev[from] }));
}, []);
const currentPeq = peqItems[headphoneIdx];
const preampValue = Number(currentPeq?.preamp ?? 0);
const autoPreOn = (currentPeq?.autoPre ?? 0) === 1;
@@ -819,8 +899,8 @@ export default function EQPage() {
if (items.length === 0) {
setHeadphoneIdx(0);
setBands(DEFAULT_BANDS);
setSelectedBand(0);
setBandsForBothModes(DEFAULT_BANDS);
setSelectedBandByMode({ A: 0, B: 0 });
return;
}
@@ -828,9 +908,9 @@ export default function EQPage() {
setHeadphoneIdx(nextIdx);
const nextBands = normalizeFiltersFromPeq(items[nextIdx] as { filters?: any[] | string });
if (nextBands.length > 0) {
setBands(nextBands);
setBandsForBothModes(nextBands);
}
setSelectedBand(0);
setSelectedBandByMode({ A: 0, B: 0 });
};
const handleDeleteHeadphone = async () => {
@@ -1053,7 +1133,8 @@ export default function EQPage() {
// 更新 bands 状态
if (fixedFilters && fixedFilters.length > 0) {
setBands(fixedFilters);
setBandsForBothModes(fixedFilters);
setSelectedBandByMode({ A: 0, B: 0 });
}
// raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理,
@@ -1102,8 +1183,8 @@ export default function EQPage() {
const nextBands = normalizeFiltersFromPeq(peq);
if (!nextBands.length) return;
syncingHeadphoneRef.current = true;
setBands(nextBands);
setSelectedBand(0);
setBandsForBothModes(nextBands);
setSelectedBandByMode({ A: 0, B: 0 });
let cancelled = false;
void (async () => {
const raw = await loadRawCurveForPeq(peq as { brand?: string; model?: string });
@@ -1400,6 +1481,7 @@ export default function EQPage() {
selectedBand={selectedBand}
abMode={abMode}
onAbToggle={setAbMode}
onCopyMode={copyModeParams}
onBandDrag={(idx, patch) => setBands((prev) => prev.map((b, i) => i === idx ? { ...b, ...patch } : b))}
onBandSelect={setSelectedBand}
/>