From 0c9e42b91c8d67cbc439c345a2749d0f904562d9 Mon Sep 17 00:00:00 2001 From: yangy Date: Tue, 26 May 2026 12:22:45 +0800 Subject: [PATCH] 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. --- client/src/contexts/DeviceContext.tsx | 15 ++++- client/src/pages/EQPage.tsx | 87 ++++++++++++++++++++------- 2 files changed, 78 insertions(+), 24 deletions(-) diff --git a/client/src/contexts/DeviceContext.tsx b/client/src/contexts/DeviceContext.tsx index 57f4fde..9e0daac 100644 --- a/client/src/contexts/DeviceContext.tsx +++ b/client/src/contexts/DeviceContext.tsx @@ -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) => { 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 ( = 1000 ? `${(f / 1000).toFixed(1)}K` : `${f}`; } +type PeqCatalogItem = NonNullable[number]; + +function buildPeqCatalogSyncKey( + remote: Pick | 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(null); const allowPeqRemoteSyncRef = useRef(false); + const lastPeqCatalogSyncKeyRef = useRef(""); const syncingHeadphoneRef = useRef(false); const peqSyncTimerRef = useRef(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);