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