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:
+100
-18
@@ -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}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user