修复 bug:EQ 页面调节任意参数后,耳机会自动切换到别的耳机
This commit is contained in:
+124
-85
@@ -190,6 +190,10 @@ export default function EQPage() {
|
|||||||
const skipBandsSyncFromAbToggleRef = useRef(false);
|
const skipBandsSyncFromAbToggleRef = useRef(false);
|
||||||
/** 本地编辑过的预设(filters / preamp / autoPre);syncPeq 拉取不会覆盖 */
|
/** 本地编辑过的预设(filters / preamp / autoPre);syncPeq 拉取不会覆盖 */
|
||||||
const peqPresetCacheRef = useRef<Record<string, PeqPresetLocalCache>>({});
|
const peqPresetCacheRef = useRef<Record<string, PeqPresetLocalCache>>({});
|
||||||
|
/** Anchor UI selection by preset name so poll/catalog reorder won't mis-switch headphones. */
|
||||||
|
const selectedPeqNameRef = useRef("");
|
||||||
|
const devicePeqSelectRef = useRef(deviceState?.peqSelect);
|
||||||
|
devicePeqSelectRef.current = deviceState?.peqSelect;
|
||||||
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 bands = bandsByMode[abMode];
|
||||||
@@ -446,6 +450,11 @@ export default function EQPage() {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const selectHeadphone = useCallback((idx: number, name?: string) => {
|
||||||
|
selectedPeqNameRef.current = name ?? "";
|
||||||
|
setHeadphoneIdx(idx);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const syncPeqCatalogFromState = useCallback(
|
const syncPeqCatalogFromState = useCallback(
|
||||||
(
|
(
|
||||||
remote: { peq?: PeqCatalogItem[]; peqSelect?: number },
|
remote: { peq?: PeqCatalogItem[]; peqSelect?: number },
|
||||||
@@ -455,14 +464,24 @@ export default function EQPage() {
|
|||||||
setPeqItems(items as typeof peqItems);
|
setPeqItems(items as typeof peqItems);
|
||||||
setHeadphoneModels(items.map((item) => item.name));
|
setHeadphoneModels(items.map((item) => item.name));
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
|
selectedPeqNameRef.current = "";
|
||||||
setHeadphoneIdx(0);
|
setHeadphoneIdx(0);
|
||||||
return;
|
return 0;
|
||||||
}
|
}
|
||||||
const nextIdx = Math.min(
|
const preferred = selectedPeqNameRef.current;
|
||||||
Math.max(devicePeqSelect ?? remote.peqSelect ?? 0, 0),
|
let nextIdx =
|
||||||
items.length - 1,
|
preferred !== ""
|
||||||
);
|
? items.findIndex((item) => item.name === preferred)
|
||||||
|
: -1;
|
||||||
|
if (nextIdx < 0) {
|
||||||
|
nextIdx = Math.min(
|
||||||
|
Math.max(devicePeqSelect ?? remote.peqSelect ?? 0, 0),
|
||||||
|
items.length - 1,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
selectedPeqNameRef.current = items[nextIdx]?.name ?? "";
|
||||||
setHeadphoneIdx(nextIdx);
|
setHeadphoneIdx(nextIdx);
|
||||||
|
return nextIdx;
|
||||||
},
|
},
|
||||||
[mergeRemotePeqCatalog],
|
[mergeRemotePeqCatalog],
|
||||||
);
|
);
|
||||||
@@ -517,7 +536,7 @@ export default function EQPage() {
|
|||||||
window.clearTimeout(peqSyncTimerRef.current);
|
window.clearTimeout(peqSyncTimerRef.current);
|
||||||
peqSyncTimerRef.current = null;
|
peqSyncTimerRef.current = null;
|
||||||
}
|
}
|
||||||
setHeadphoneIdx(idx);
|
selectHeadphone(idx, peqItems[idx]?.name);
|
||||||
updateSetting({ peqSelect: idx });
|
updateSetting({ peqSelect: idx });
|
||||||
setIsHeadphoneMenuOpen(false);
|
setIsHeadphoneMenuOpen(false);
|
||||||
},
|
},
|
||||||
@@ -527,6 +546,7 @@ export default function EQPage() {
|
|||||||
peqItems,
|
peqItems,
|
||||||
persistPresetEditsToPeqItem,
|
persistPresetEditsToPeqItem,
|
||||||
rememberPeqPresetCache,
|
rememberPeqPresetCache,
|
||||||
|
selectHeadphone,
|
||||||
updateSetting,
|
updateSetting,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -545,20 +565,18 @@ export default function EQPage() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(remote, resolvedPeqSelect);
|
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(remote, resolvedPeqSelect);
|
||||||
syncPeqCatalogFromState(remote, resolvedPeqSelect);
|
const catalogIdx = syncPeqCatalogFromState(remote, resolvedPeqSelect);
|
||||||
|
|
||||||
const items = mergeRemotePeqCatalog((remote.peq ?? []) as PeqCatalogItem[]);
|
const items = remote.peq ?? [];
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
setBandsForBothModes(DEFAULT_BANDS);
|
setBandsForBothModes(DEFAULT_BANDS);
|
||||||
setSelectedBandByMode({ A: 0, B: 0 });
|
setSelectedBandByMode({ A: 0, B: 0 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nextIdx = Math.min(
|
const nextBands = normalizeFiltersFromPeq(
|
||||||
Math.max(resolvedPeqSelect, 0),
|
items[catalogIdx] as { filters?: any[] | string },
|
||||||
items.length - 1,
|
|
||||||
);
|
);
|
||||||
const nextBands = normalizeFiltersFromPeq(items[nextIdx] as { filters?: any[] | string });
|
|
||||||
if (nextBands.length > 0) {
|
if (nextBands.length > 0) {
|
||||||
setBandsForBothModes(nextBands);
|
setBandsForBothModes(nextBands);
|
||||||
}
|
}
|
||||||
@@ -664,7 +682,7 @@ export default function EQPage() {
|
|||||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||||
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
||||||
if (createdIndex >= 0) {
|
if (createdIndex >= 0) {
|
||||||
setHeadphoneIdx(createdIndex);
|
selectHeadphone(createdIndex, nextName);
|
||||||
updateSetting({ peqSelect: createdIndex });
|
updateSetting({ peqSelect: createdIndex });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -688,7 +706,7 @@ export default function EQPage() {
|
|||||||
canDel: 1,
|
canDel: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setHeadphoneIdx(headphoneModels.length);
|
selectHeadphone(headphoneModels.length, nextName);
|
||||||
}
|
}
|
||||||
setIsAddPresetDialogOpen(false);
|
setIsAddPresetDialogOpen(false);
|
||||||
toast.success(eqUi.toastAddPresetOk);
|
toast.success(eqUi.toastAddPresetOk);
|
||||||
@@ -737,7 +755,7 @@ export default function EQPage() {
|
|||||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||||
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
||||||
if (createdIndex >= 0) {
|
if (createdIndex >= 0) {
|
||||||
setHeadphoneIdx(createdIndex);
|
selectHeadphone(createdIndex, nextName);
|
||||||
updateSetting({ peqSelect: createdIndex });
|
updateSetting({ peqSelect: createdIndex });
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -752,7 +770,7 @@ export default function EQPage() {
|
|||||||
canDel: currentPeq?.canDel ?? 1,
|
canDel: currentPeq?.canDel ?? 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setHeadphoneIdx(headphoneModels.length);
|
selectHeadphone(headphoneModels.length, nextName);
|
||||||
}
|
}
|
||||||
setIsSaveBDialogOpen(false);
|
setIsSaveBDialogOpen(false);
|
||||||
toast.success(eqUi.toastSaveBOk);
|
toast.success(eqUi.toastSaveBOk);
|
||||||
@@ -822,7 +840,7 @@ export default function EQPage() {
|
|||||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||||
const targetIdx = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
const targetIdx = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
||||||
if (targetIdx >= 0) {
|
if (targetIdx >= 0) {
|
||||||
setHeadphoneIdx(targetIdx);
|
selectHeadphone(targetIdx, nextName);
|
||||||
updateSetting({ peqSelect: targetIdx });
|
updateSetting({ peqSelect: targetIdx });
|
||||||
}
|
}
|
||||||
} else if (isOverwrite) {
|
} else if (isOverwrite) {
|
||||||
@@ -844,7 +862,7 @@ export default function EQPage() {
|
|||||||
: item,
|
: item,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setHeadphoneIdx(existingIdx);
|
selectHeadphone(existingIdx, nextName);
|
||||||
} else {
|
} else {
|
||||||
setHeadphoneModels((prev) => [...prev, nextName]);
|
setHeadphoneModels((prev) => [...prev, nextName]);
|
||||||
setPeqItems((prev) => [
|
setPeqItems((prev) => [
|
||||||
@@ -861,7 +879,7 @@ export default function EQPage() {
|
|||||||
canDel: 1,
|
canDel: 1,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
setHeadphoneIdx(headphoneModels.length);
|
selectHeadphone(headphoneModels.length, nextName);
|
||||||
}
|
}
|
||||||
toast.success(eqUi.importSuccess);
|
toast.success(eqUi.importSuccess);
|
||||||
setIsShareDialogOpen(false);
|
setIsShareDialogOpen(false);
|
||||||
@@ -891,56 +909,15 @@ export default function EQPage() {
|
|||||||
|
|
||||||
// 加载耳机列表并初始化曲线
|
// 加载耳机列表并初始化曲线
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
let cancelled = false;
|
||||||
|
|
||||||
async function loadHeadphones() {
|
async function loadHeadphones() {
|
||||||
peqHydrationPendingRef.current = true;
|
peqHydrationPendingRef.current = true;
|
||||||
allowPeqRemoteSyncRef.current = false;
|
allowPeqRemoteSyncRef.current = false;
|
||||||
try {
|
try {
|
||||||
if (isDemoMode || !api) {
|
if (isDemoMode || !api) {
|
||||||
// 演示模式使用默认列表
|
if (cancelled) return;
|
||||||
const defaultModels = [
|
// 演示模式使用默认列表
|
||||||
"Sennheiser HD 650",
|
|
||||||
"Sennheiser HD 800",
|
|
||||||
"Sony WH-1000XM5",
|
|
||||||
"AKG K701",
|
|
||||||
"Beyerdynamic DT 990",
|
|
||||||
"Audio-Technica ATH-M50x",
|
|
||||||
];
|
|
||||||
setHeadphoneModels(defaultModels);
|
|
||||||
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
|
||||||
setCurrentRawCurve(null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const loadedPeq = await fetchEqSyncPeq(api, "loadHeadphones");
|
|
||||||
if (loadedPeq.peq && loadedPeq.peq.length > 0) {
|
|
||||||
const mergedPeq = mergeRemotePeqCatalog(loadedPeq.peq);
|
|
||||||
setHeadphoneModels(mergedPeq.map((h) => h.name));
|
|
||||||
setPeqItems(mergedPeq);
|
|
||||||
const nextIdx = Math.min(
|
|
||||||
Math.max(deviceState?.peqSelect ?? loadedPeq.peqSelect ?? 0, 0),
|
|
||||||
mergedPeq.length - 1,
|
|
||||||
);
|
|
||||||
setHeadphoneIdx(nextIdx);
|
|
||||||
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(loadedPeq, deviceState?.peqSelect);
|
|
||||||
|
|
||||||
// 初始化当前选中的耳机曲线
|
|
||||||
const currentPeq = mergedPeq[nextIdx];
|
|
||||||
if (currentPeq) {
|
|
||||||
// 修复 filters 数据(兼容不同的字段名)
|
|
||||||
const fixedFilters = normalizeFiltersFromPeq(currentPeq);
|
|
||||||
|
|
||||||
// 更新 bands 状态
|
|
||||||
if (fixedFilters && fixedFilters.length > 0) {
|
|
||||||
setBandsForBothModes(fixedFilters);
|
|
||||||
setSelectedBandByMode({ A: 0, B: 0 });
|
|
||||||
}
|
|
||||||
|
|
||||||
// raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理,
|
|
||||||
// 避免首次进入页面时重复请求 modelCurve。
|
|
||||||
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 如果没有数据,使用默认列表
|
|
||||||
const defaultModels = [
|
const defaultModels = [
|
||||||
"Sennheiser HD 650",
|
"Sennheiser HD 650",
|
||||||
"Sennheiser HD 800",
|
"Sennheiser HD 800",
|
||||||
@@ -951,32 +928,91 @@ export default function EQPage() {
|
|||||||
];
|
];
|
||||||
setHeadphoneModels(defaultModels);
|
setHeadphoneModels(defaultModels);
|
||||||
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
||||||
|
selectHeadphone(0, defaultModels[0]);
|
||||||
|
setCurrentRawCurve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const loadedPeq = await fetchEqSyncPeq(api, "loadHeadphones");
|
||||||
|
if (cancelled) return;
|
||||||
|
if (loadedPeq.peq && loadedPeq.peq.length > 0) {
|
||||||
|
const mergedPeq = mergeRemotePeqCatalog(loadedPeq.peq);
|
||||||
|
setHeadphoneModels(mergedPeq.map((h) => h.name));
|
||||||
|
setPeqItems(mergedPeq);
|
||||||
|
const nextIdx = Math.min(
|
||||||
|
Math.max(
|
||||||
|
devicePeqSelectRef.current ?? loadedPeq.peqSelect ?? 0,
|
||||||
|
0,
|
||||||
|
),
|
||||||
|
mergedPeq.length - 1,
|
||||||
|
);
|
||||||
|
selectHeadphone(nextIdx, mergedPeq[nextIdx]?.name);
|
||||||
|
lastPeqCatalogSyncKeyRef.current = buildPeqCatalogSyncKey(
|
||||||
|
loadedPeq,
|
||||||
|
devicePeqSelectRef.current ?? loadedPeq.peqSelect,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 初始化当前选中的耳机曲线
|
||||||
|
const currentPeq = mergedPeq[nextIdx];
|
||||||
|
if (currentPeq) {
|
||||||
|
// 修复 filters 数据(兼容不同的字段名)
|
||||||
|
const fixedFilters = normalizeFiltersFromPeq(currentPeq);
|
||||||
|
|
||||||
|
// 更新 bands 状态
|
||||||
|
if (fixedFilters && fixedFilters.length > 0) {
|
||||||
|
setBandsForBothModes(fixedFilters);
|
||||||
|
setSelectedBandByMode({ A: 0, B: 0 });
|
||||||
|
}
|
||||||
|
|
||||||
|
// raw 与图表渲染统一交给 headphoneIdx/peqItems 监听逻辑处理,
|
||||||
|
// 避免首次进入页面时重复请求 modelCurve。
|
||||||
|
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有数据,使用默认列表
|
||||||
|
const defaultModels = [
|
||||||
|
"Sennheiser HD 650",
|
||||||
|
"Sennheiser HD 800",
|
||||||
|
"Sony WH-1000XM5",
|
||||||
|
"AKG K701",
|
||||||
|
"Beyerdynamic DT 990",
|
||||||
|
"Audio-Technica ATH-M50x",
|
||||||
|
];
|
||||||
|
setHeadphoneModels(defaultModels);
|
||||||
|
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
||||||
|
selectHeadphone(0, defaultModels[0]);
|
||||||
|
setCurrentRawCurve(null);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
if (cancelled) return;
|
||||||
|
// 出错时使用默认列表
|
||||||
|
const defaultModels = [
|
||||||
|
"Sennheiser HD 650",
|
||||||
|
"Sennheiser HD 800",
|
||||||
|
"Sony WH-1000XM5",
|
||||||
|
"AKG K701",
|
||||||
|
"Beyerdynamic DT 990",
|
||||||
|
"Audio-Technica ATH-M50x",
|
||||||
|
];
|
||||||
|
setHeadphoneModels(defaultModels);
|
||||||
|
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
||||||
|
selectHeadphone(0, defaultModels[0]);
|
||||||
setCurrentRawCurve(null);
|
setCurrentRawCurve(null);
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// 出错时使用默认列表
|
|
||||||
const defaultModels = [
|
|
||||||
"Sennheiser HD 650",
|
|
||||||
"Sennheiser HD 800",
|
|
||||||
"Sony WH-1000XM5",
|
|
||||||
"AKG K701",
|
|
||||||
"Beyerdynamic DT 990",
|
|
||||||
"Audio-Technica ATH-M50x",
|
|
||||||
];
|
|
||||||
setHeadphoneModels(defaultModels);
|
|
||||||
setPeqItems(defaultModels.map(name => ({ name, filters: [] })));
|
|
||||||
setCurrentRawCurve(null);
|
|
||||||
}
|
|
||||||
} finally {
|
} finally {
|
||||||
|
if (cancelled) return;
|
||||||
allowPeqRemoteSyncRef.current = true;
|
allowPeqRemoteSyncRef.current = true;
|
||||||
skipPeqAutoSyncRef.current = true;
|
skipPeqAutoSyncRef.current = true;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
peqHydrationPendingRef.current = false;
|
if (!cancelled) peqHydrationPendingRef.current = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
loadHeadphones();
|
void loadHeadphones();
|
||||||
}, [api, isDemoMode, loadRawCurveForPeq, mergeRemotePeqCatalog, deviceState?.peqSelect]);
|
return () => {
|
||||||
|
cancelled = true;
|
||||||
|
};
|
||||||
|
}, [api, isDemoMode, mergeRemotePeqCatalog, selectHeadphone, setCurrentRawCurve]);
|
||||||
|
|
||||||
/** 切换耳机型号时变;不含 filters,避免编辑时写回 peqItems 把 B 试听曲线重置 */
|
/** 切换耳机型号时变;不含 filters,避免编辑时写回 peqItems 把 B 试听曲线重置 */
|
||||||
const headphoneSwitchKey = useMemo(() => {
|
const headphoneSwitchKey = useMemo(() => {
|
||||||
@@ -1786,7 +1822,7 @@ export default function EQPage() {
|
|||||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||||
const targetIdx = latest.peq?.findIndex((item) => item.name === createdName) ?? -1;
|
const targetIdx = latest.peq?.findIndex((item) => item.name === createdName) ?? -1;
|
||||||
if (targetIdx >= 0) {
|
if (targetIdx >= 0) {
|
||||||
setHeadphoneIdx(targetIdx);
|
selectHeadphone(targetIdx, createdName);
|
||||||
updateSetting({ peqSelect: targetIdx });
|
updateSetting({ peqSelect: targetIdx });
|
||||||
}
|
}
|
||||||
} else if (isOverwrite) {
|
} else if (isOverwrite) {
|
||||||
@@ -1808,7 +1844,7 @@ export default function EQPage() {
|
|||||||
: item,
|
: item,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
setHeadphoneIdx(existingIdx);
|
selectHeadphone(existingIdx, createdName);
|
||||||
} else {
|
} else {
|
||||||
let nextIndex = 0;
|
let nextIndex = 0;
|
||||||
setPeqItems((prev) => [
|
setPeqItems((prev) => [
|
||||||
@@ -1829,7 +1865,7 @@ export default function EQPage() {
|
|||||||
nextIndex = prev.length;
|
nextIndex = prev.length;
|
||||||
return [...prev, createdName];
|
return [...prev, createdName];
|
||||||
});
|
});
|
||||||
setHeadphoneIdx(nextIndex);
|
selectHeadphone(nextIndex, createdName);
|
||||||
}
|
}
|
||||||
setIsBrandDrawerOpen(false);
|
setIsBrandDrawerOpen(false);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -1909,6 +1945,9 @@ export default function EQPage() {
|
|||||||
}}
|
}}
|
||||||
onRenamePreset={async (oldName, newName, item) => {
|
onRenamePreset={async (oldName, newName, item) => {
|
||||||
try {
|
try {
|
||||||
|
if (selectedPeqNameRef.current === oldName) {
|
||||||
|
selectedPeqNameRef.current = newName;
|
||||||
|
}
|
||||||
if (isDemoMode || !api) {
|
if (isDemoMode || !api) {
|
||||||
const nextItems = peqItems.map(it =>
|
const nextItems = peqItems.map(it =>
|
||||||
it.name === oldName ? { ...it, name: newName } : it
|
it.name === oldName ? { ...it, name: newName } : it
|
||||||
|
|||||||
Reference in New Issue
Block a user