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 ── */
|
/* ── 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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user