Enhance DeviceContext to fetch and manage PEQ state; update EQPage to synchronize PEQ catalog and improve headphone selection logic based on PEQ state changes. Refactor related functions for better state handling and synchronization.

This commit is contained in:
yangy
2026-05-26 12:22:45 +08:00
parent 8fcad50e7d
commit 0c9e42b91c
2 changed files with 78 additions and 24 deletions
+12 -3
View File
@@ -67,6 +67,11 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
lastMsgCountRef.current = state.msgCount;
}, []);
const fetchPeqState = useCallback(async (apiInstance: LuxsinAPI) => {
const peq = await apiInstance.getPeqState();
setPeqState(peq);
}, []);
const connect = useCallback(async (forceDemo?: boolean, connectIp?: string) => {
const useDemo = forceDemo !== undefined ? forceDemo : isDemoMode;
const targetIp = (connectIp ?? ip).trim();
@@ -138,10 +143,11 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
if (!apiInstance) return;
try {
await fetchState(apiInstance);
await fetchPeqState(apiInstance).catch(() => undefined);
} catch (e) {
setError(`Refresh failed: ${e instanceof Error ? e.message : String(e)}`);
}
}, [api, isConnected, ip, isDemoMode, fetchState]);
}, [api, isConnected, ip, isDemoMode, fetchState, fetchPeqState]);
const updateSetting = useCallback(async (params: Record<string, string | number>) => {
if (isDemoMode) {
@@ -235,7 +241,10 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
try {
const count = await api.getMsgCount();
if (count !== lastMsgCountRef.current) {
await fetchState(api);
await Promise.all([
fetchState(api),
fetchPeqState(api).catch(() => undefined),
]);
}
} catch {
// Silently fail polling
@@ -246,7 +255,7 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
return () => {
if (pollIntervalRef.current) clearInterval(pollIntervalRef.current);
};
}, [isConnected, isDemoMode, api, fetchState]);
}, [isConnected, isDemoMode, api, fetchState, fetchPeqState]);
return (
<DeviceContext.Provider value={{
+66 -21
View File
@@ -32,6 +32,7 @@ import {
type PeqApplyPayload,
type PeqChangePayload,
type PeqPresetBody,
type PeqState,
} from "@/lib/luxsinApi";
import * as echarts from "echarts";
import {
@@ -607,6 +608,18 @@ function freqLabel(f: number) {
return f >= 1000 ? `${(f / 1000).toFixed(1)}K` : `${f}`;
}
type PeqCatalogItem = NonNullable<PeqState["peq"]>[number];
function buildPeqCatalogSyncKey(
remote: Pick<PeqState, "peq" | "peqSelect"> | null | undefined,
devicePeqSelect?: number,
): string {
const items = remote?.peq;
if (!items?.length) return items ? "empty" : "";
const select = devicePeqSelect ?? remote?.peqSelect ?? 0;
return `${select}|${items.map((p) => p.name).join("\u0001")}`;
}
function getUniquePresetName(base: string, existingNames: string[]) {
if (!existingNames.includes(base)) return base;
let index = 1;
@@ -665,7 +678,8 @@ function bandToPeqFilter(b: { freq: number; gain: number; q: number; type: strin
/* ── Main Component ── */
export default function EQPage() {
const [, setLocation] = useLocation();
const { deviceState, updateSetting, api, isDemoMode, upgradePeqChange, upgradePeqApply } = useDevice();
const { deviceState, peqState, updateSetting, api, isDemoMode, upgradePeqChange, upgradePeqApply } =
useDevice();
const bypassOn = (deviceState?.dsp_enable ?? 0) === 0;
const peqOn = (deviceState?.peqEnable ?? 0) === 1;
const eqOn = peqOn && !bypassOn;
@@ -738,6 +752,7 @@ export default function EQPage() {
const [isConfirmingTarget, setIsConfirmingTarget] = useState(false);
const [currentRawCurve, setCurrentRawCurve] = useState<number[] | null>(null);
const allowPeqRemoteSyncRef = useRef(false);
const lastPeqCatalogSyncKeyRef = useRef("");
const syncingHeadphoneRef = useRef(false);
const peqSyncTimerRef = useRef<number | null>(null);
const skipPeqAutoSyncRef = useRef(false);
@@ -1039,22 +1054,44 @@ export default function EQPage() {
}
};
const applyPeqStateToUI = (
peqState: { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number },
) => {
const items = peqState.peq ?? [];
setPeqItems(items as typeof peqItems);
setHeadphoneModels(items.map((item) => item.name));
const syncPeqCatalogFromState = useCallback(
(
remote: { peq?: PeqCatalogItem[]; peqSelect?: number },
devicePeqSelect?: number,
) => {
const items = remote.peq ?? [];
setPeqItems(items as typeof peqItems);
setHeadphoneModels(items.map((item) => item.name));
if (items.length === 0) {
setHeadphoneIdx(0);
return;
}
const nextIdx = Math.min(
Math.max(devicePeqSelect ?? remote.peqSelect ?? 0, 0),
items.length - 1,
);
setHeadphoneIdx(nextIdx);
},
[],
);
const applyPeqStateToUI = (
remote: { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number },
) => {
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(remote, deviceState?.peqSelect);
syncPeqCatalogFromState(remote, deviceState?.peqSelect);
const items = remote.peq ?? [];
if (items.length === 0) {
setHeadphoneIdx(0);
setBandsForBothModes(DEFAULT_BANDS);
setSelectedBandByMode({ A: 0, B: 0 });
return;
}
const nextIdx = Math.min(Math.max(peqState.peqSelect ?? 0, 0), items.length - 1);
setHeadphoneIdx(nextIdx);
const nextIdx = Math.min(
Math.max(deviceState?.peqSelect ?? remote.peqSelect ?? 0, 0),
items.length - 1,
);
const nextBands = normalizeFiltersFromPeq(items[nextIdx] as { filters?: any[] | string });
if (nextBands.length > 0) {
setBandsForBothModes(nextBands);
@@ -1223,12 +1260,14 @@ export default function EQPage() {
}
};
// 同步 peqSelect 变化
// msgCount 轮询更新 peqState 后,同步耳机列表与当前选中项
useEffect(() => {
if (deviceState?.peqSelect !== undefined) {
setHeadphoneIdx(deviceState.peqSelect);
}
}, [deviceState?.peqSelect]);
if (!peqState?.peq) return;
const key = buildPeqCatalogSyncKey(peqState, deviceState?.peqSelect);
if (!key || key === lastPeqCatalogSyncKeyRef.current) return;
lastPeqCatalogSyncKeyRef.current = key;
syncPeqCatalogFromState(peqState, deviceState?.peqSelect);
}, [peqState, deviceState?.peqSelect, syncPeqCatalogFromState]);
// 切换耳机时默认回到 A 组对比
useEffect(() => {
@@ -1335,13 +1374,19 @@ export default function EQPage() {
return;
}
try {
const peqState = await api.getPeqState();
if (peqState.peq && peqState.peq.length > 0) {
setHeadphoneModels(peqState.peq.map(h => h.name));
setPeqItems(peqState.peq);
const loadedPeq = await api.getPeqState();
if (loadedPeq.peq && loadedPeq.peq.length > 0) {
setHeadphoneModels(loadedPeq.peq.map((h) => h.name));
setPeqItems(loadedPeq.peq);
const nextIdx = Math.min(
Math.max(deviceState?.peqSelect ?? loadedPeq.peqSelect ?? 0, 0),
loadedPeq.peq.length - 1,
);
setHeadphoneIdx(nextIdx);
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(loadedPeq, deviceState?.peqSelect);
// 初始化当前选中的耳机曲线
const currentPeq = peqState.peq[peqState.peqSelect || 0];
const currentPeq = loadedPeq.peq[nextIdx];
if (currentPeq) {
// 修复 filters 数据(兼容不同的字段名)
const fixedFilters = normalizeFiltersFromPeq(currentPeq);