From 4ca86ad60a6866a93ff04ae8804716d8bd1121ad Mon Sep 17 00:00:00 2001 From: yangy Date: Thu, 30 Apr 2026 17:02:13 +0800 Subject: [PATCH] Refactor EQPage to support dual mode (A/B) for equalizer settings; added visibility toggles for frequency response curves and copy functionality between modes. --- client/src/pages/EQPage.tsx | 118 ++++++++++++++++++++++++++++++------ 1 file changed, 100 insertions(+), 18 deletions(-) diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 1860c54..a4dd53f 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -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({ + @@ -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>({ + A: cloneBands(DEFAULT_BANDS), + B: cloneBands(DEFAULT_BANDS), + }); + const [selectedBandByMode, setSelectedBandByMode] = useState>({ + A: 0, + B: 0, + }); const [headphoneIdx, setHeadphoneIdx] = useState(deviceState?.peqSelect ?? 0); const [headphoneModels, setHeadphoneModels] = useState([]); const [peqItems, setPeqItems] = useState< @@ -550,6 +599,37 @@ export default function EQPage() { const peqSyncTimerRef = useRef(null); const headphoneMenuRef = useRef(null); const filterMenuRef = useRef(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} />