Enhance DeviceContext and LuxsinAPI to support new PEQ apply functionality; update locale files for English and Chinese to include new UI strings for applying and saving PEQ settings. Refactor AudioPage and EQPage to integrate boot sound management and improve state handling.
This commit is contained in:
@@ -13,6 +13,12 @@ import { useLocation } from "wouter";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
import { navigateToSelect } from "./SelectPage";
|
||||
import {
|
||||
getBootSoundMaxIndex,
|
||||
normalizeBootSound,
|
||||
parseFirmwareVersion,
|
||||
readBootSoundFromState,
|
||||
} from "@/lib/luxsinApi";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
@@ -83,6 +89,7 @@ type LocaleDac = {
|
||||
filters?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
dacGain?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
volumeStep?: string;
|
||||
bootVolume?: string | { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
xlr?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
mutePolar?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
IISMode?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
@@ -104,8 +111,48 @@ function optionLabels(items: Array<{ index: number; label: string }> | undefined
|
||||
|
||||
const STEP_OPTIONS = ["0.5dB", "1dB", "2dB", "3dB"];
|
||||
|
||||
const BOOT_VOLUME_FALLBACK = [
|
||||
"Default",
|
||||
"-5dB",
|
||||
"-10dB",
|
||||
"-15dB",
|
||||
"-20dB",
|
||||
"-25dB",
|
||||
"-30dB",
|
||||
"-35dB",
|
||||
"-40dB",
|
||||
"-45dB",
|
||||
"-50dB",
|
||||
];
|
||||
|
||||
function bootVolumeLabel(dacText: LocaleDac["dac"], index: number): string {
|
||||
const boot = dacText?.bootVolume;
|
||||
if (boot && typeof boot === "object" && boot.options?.length) {
|
||||
const hit = boot.options.find((o) => o.index === index);
|
||||
if (hit) return hit.label;
|
||||
const sorted = [...boot.options].sort((a, b) => a.index - b.index);
|
||||
if (index >= 0 && index < sorted.length) return sorted[index]?.label ?? BOOT_VOLUME_FALLBACK[index] ?? "—";
|
||||
}
|
||||
return BOOT_VOLUME_FALLBACK[index] ?? "—";
|
||||
}
|
||||
|
||||
function bootVolumeOptions(dacText: LocaleDac["dac"], maxIndex: number): string[] {
|
||||
const labels: string[] = [];
|
||||
for (let i = 0; i <= maxIndex; i += 1) {
|
||||
labels.push(bootVolumeLabel(dacText, i));
|
||||
}
|
||||
return labels;
|
||||
}
|
||||
|
||||
function bootVolumeRowLabel(dacText: LocaleDac["dac"]): string {
|
||||
const boot = dacText?.bootVolume;
|
||||
if (boot && typeof boot === "object" && boot.label) return boot.label;
|
||||
if (typeof boot === "string") return boot;
|
||||
return "Boot volume";
|
||||
}
|
||||
|
||||
export default function AudioPage() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [location, setLocation] = useLocation();
|
||||
const { deviceState, updateSetting, refresh, isConnected, api } = useDevice();
|
||||
const ds = deviceState;
|
||||
const vuSensor = (ds as ({ vuSensor?: number } | null))?.vuSensor;
|
||||
@@ -117,6 +164,8 @@ export default function AudioPage() {
|
||||
const balanceVal = ds?.balance !== undefined ? ds.balance / 10 : 0;
|
||||
// vuSensor 从接口获取的是放大 2 倍的值,需要除以 2 显示(例如:20 → +10dB)
|
||||
const vuSensVal = vuSensor !== undefined ? vuSensor / 2 : 0;
|
||||
const bootSoundFromApi = readBootSoundFromState(ds);
|
||||
const [localBootSound, setLocalBootSound] = useState(bootSoundFromApi);
|
||||
const [localBalance, setLocalBalance] = useState(balanceVal);
|
||||
const [localVuSens, setLocalVuSens] = useState(0);
|
||||
const isBalanceDragging = useRef(false);
|
||||
@@ -125,11 +174,20 @@ export default function AudioPage() {
|
||||
useEffect(() => { if (!isBalanceDragging.current) setLocalBalance(balanceVal); }, [balanceVal]);
|
||||
useEffect(() => { if (!isVuDragging.current) setLocalVuSens(vuSensVal); }, [vuSensVal]);
|
||||
useEffect(() => {
|
||||
// Ensure we have the latest pcm value from syncData when entering this page
|
||||
setLocalBootSound(readBootSoundFromState(ds));
|
||||
}, [ds?.bootSound]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location !== "/audio") return;
|
||||
setLocalBootSound(readBootSoundFromState(ds));
|
||||
}, [location, ds?.bootSound, ds]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location !== "/audio") return;
|
||||
if (isConnected || api) {
|
||||
refresh();
|
||||
void refresh();
|
||||
}
|
||||
}, [isConnected, api, refresh]);
|
||||
}, [location, isConnected, api, refresh]);
|
||||
|
||||
const gainIdx = ds?.dacGain ?? 0;
|
||||
const stepIdx = ds?.soundStep ?? 1;
|
||||
@@ -137,6 +195,11 @@ export default function AudioPage() {
|
||||
const xlrIdx = ds?.xlr ?? 0;
|
||||
const mutePolarIdx = (ds as ({ hdmimutepolar?: number } | null))?.hdmimutepolar ?? 0;
|
||||
const iisModeIdx = (ds as ({ hdmiType?: number } | null))?.hdmiType ?? 0;
|
||||
const bootSoundVal = normalizeBootSound(localBootSound);
|
||||
const firmwareVersion = parseFirmwareVersion(ds?.version);
|
||||
const bootSoundMaxIdx = getBootSoundMaxIndex(firmwareVersion);
|
||||
const bootSoundSelectIdx = Math.min(bootSoundVal, bootSoundMaxIdx);
|
||||
const BOOT_VOLUME_OPTIONS = bootVolumeOptions(dacText, bootSoundMaxIdx);
|
||||
|
||||
const FILTER_OPTIONS = optionLabels(dacText.filters?.options, ["快速滚降", "慢速滚降", "短延迟快速滚降", "短延迟慢速滚降", "去重强调", "非过采样(NOS)"]);
|
||||
const GAIN_OPTIONS = optionLabels(dacText.dacGain?.options, ["低", "中", "高"]);
|
||||
@@ -237,6 +300,18 @@ export default function AudioPage() {
|
||||
{/* ── 系统与接口 ── */}
|
||||
<SectionHeader title={dacText.sectionSystemInterface ?? "系统与接口"} />
|
||||
<div className="ios-list-group">
|
||||
<ListRow
|
||||
label={bootVolumeRowLabel(dacText)}
|
||||
value={bootVolumeLabel(dacText, bootSoundVal)}
|
||||
onClick={() =>
|
||||
goSelect(
|
||||
bootVolumeRowLabel(dacText),
|
||||
BOOT_VOLUME_OPTIONS,
|
||||
bootSoundSelectIdx,
|
||||
"bootSound",
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ListRow
|
||||
label={dacText.xlr?.label ?? "XLR 端口极性"}
|
||||
value={XLR_OPTIONS[xlrIdx] ?? XLR_OPTIONS[0] ?? "—"}
|
||||
|
||||
+211
-16
@@ -28,7 +28,9 @@ import {
|
||||
type LuxsinAudioModelListItem,
|
||||
type LuxsinAudioModel,
|
||||
type PeqFilter,
|
||||
type PeqApplyPayload,
|
||||
type PeqChangePayload,
|
||||
type PeqPresetBody,
|
||||
} from "@/lib/luxsinApi";
|
||||
import * as echarts from "echarts";
|
||||
import {
|
||||
@@ -115,6 +117,8 @@ function FreqChart({
|
||||
abMode,
|
||||
onAbToggle,
|
||||
onCopyMode,
|
||||
onApplyB,
|
||||
onSaveB,
|
||||
onBandDrag,
|
||||
onBandSelect,
|
||||
eqUi,
|
||||
@@ -125,6 +129,8 @@ function FreqChart({
|
||||
abMode: "A" | "B";
|
||||
onAbToggle: (m: "A" | "B") => void;
|
||||
onCopyMode: (from: "A" | "B", to: "A" | "B") => void;
|
||||
onApplyB: () => void;
|
||||
onSaveB: () => void;
|
||||
onBandDrag: (idx: number, patch: Partial<{ freq: number; gain: number }>) => void;
|
||||
onBandSelect: (idx: number) => void;
|
||||
eqUi: PeqEqUi;
|
||||
@@ -321,7 +327,8 @@ function FreqChart({
|
||||
</div>
|
||||
|
||||
{/* A/B + DIFF controls */}
|
||||
<div className="flex items-center gap-2 mb-2 px-1">
|
||||
<div className="flex w-full items-center gap-2 mb-2 px-1">
|
||||
<div className="flex min-w-0 items-center gap-2">
|
||||
{/* A/B toggle pill */}
|
||||
<div className="flex items-center rounded-[8px] overflow-hidden"
|
||||
style={{ background: "rgba(44,44,46,0.7)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
||||
@@ -350,13 +357,25 @@ function FreqChart({
|
||||
}}>
|
||||
{copyButtonText}
|
||||
</button>
|
||||
</div>
|
||||
<div className="ml-auto flex shrink-0 items-center gap-2">
|
||||
<button
|
||||
type="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(eqUi.toastChartLoadedDevice)}
|
||||
onClick={() => onApplyB()}
|
||||
>
|
||||
{eqUi.chartLoadToDevice}
|
||||
{eqUi.chartApplyB}
|
||||
</button>
|
||||
<button
|
||||
type="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={() => onSaveB()}
|
||||
>
|
||||
{eqUi.chartSaveB}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SVG chart */}
|
||||
@@ -573,7 +592,7 @@ 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 } = useDevice();
|
||||
const { deviceState, updateSetting, api, isDemoMode, upgradePeqChange, upgradePeqApply } = useDevice();
|
||||
const eqOn = (deviceState?.peqEnable ?? 0) === 1;
|
||||
|
||||
const eqUi = useMemo((): PeqEqUi => {
|
||||
@@ -617,6 +636,8 @@ export default function EQPage() {
|
||||
const [addPresetMode, setAddPresetMode] = useState<"copy" | "flat">("copy");
|
||||
const [copyPresetName, setCopyPresetName] = useState("");
|
||||
const [flatPresetName, setFlatPresetName] = useState("");
|
||||
const [isSaveBDialogOpen, setIsSaveBDialogOpen] = useState(false);
|
||||
const [saveBPresetName, setSaveBPresetName] = useState("");
|
||||
const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false);
|
||||
const [batchEditText, setBatchEditText] = useState("");
|
||||
type BrandDrawerTab = "brands" | "models" | "target";
|
||||
@@ -642,6 +663,7 @@ export default function EQPage() {
|
||||
const allowPeqRemoteSyncRef = useRef(false);
|
||||
const syncingHeadphoneRef = useRef(false);
|
||||
const peqSyncTimerRef = useRef<number | null>(null);
|
||||
const skipPeqAutoSyncRef = useRef(false);
|
||||
const headphoneMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const filterMenuRef = useRef<HTMLDivElement | null>(null);
|
||||
const bands = bandsByMode[abMode];
|
||||
@@ -675,6 +697,7 @@ export default function EQPage() {
|
||||
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;
|
||||
@@ -995,6 +1018,13 @@ export default function EQPage() {
|
||||
setIsAddPresetDialogOpen(true);
|
||||
};
|
||||
|
||||
const openSaveBDialog = () => {
|
||||
const currentName = peqItems[headphoneIdx]?.name ?? headphoneModels[headphoneIdx] ?? "Preset";
|
||||
const defaultName = getUniquePresetName(`${currentName}_B`, headphoneModels);
|
||||
setSaveBPresetName(defaultName);
|
||||
setIsSaveBDialogOpen(true);
|
||||
};
|
||||
|
||||
const handleSaveAddPreset = async () => {
|
||||
const nextName = addPresetMode === "copy" ? copyPresetName.trim() : flatPresetName.trim();
|
||||
if (!nextName) {
|
||||
@@ -1062,6 +1092,60 @@ export default function EQPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveBAsPreset = async () => {
|
||||
const nextName = saveBPresetName.trim();
|
||||
if (!nextName) {
|
||||
toast.error(eqUi.addPresetNameEmpty);
|
||||
return;
|
||||
}
|
||||
if (headphoneModels.includes(nextName)) {
|
||||
toast.error(eqUi.addPresetNameExists);
|
||||
return;
|
||||
}
|
||||
|
||||
const bBands = cloneBands(bandsByMode.B);
|
||||
const filters = bBands.map(bandToPeqFilter);
|
||||
const payload: PeqChangePayload = {
|
||||
peqChange: {
|
||||
name: nextName,
|
||||
filters,
|
||||
autoPre: currentPeq?.autoPre ?? 0,
|
||||
preamp: currentPeq?.preamp ?? 0,
|
||||
canDel: currentPeq?.canDel ?? 1,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
await upgradePeqChange(payload);
|
||||
if (api && !isDemoMode) {
|
||||
const latest = await api.getPeqState();
|
||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||
const createdIndex = latest.peq?.findIndex((item) => item.name === nextName) ?? -1;
|
||||
if (createdIndex >= 0) {
|
||||
setHeadphoneIdx(createdIndex);
|
||||
updateSetting({ peqSelect: createdIndex });
|
||||
}
|
||||
} else {
|
||||
setHeadphoneModels((prev) => [...prev, nextName]);
|
||||
setPeqItems((prev) => [
|
||||
...prev,
|
||||
{
|
||||
name: nextName,
|
||||
filters,
|
||||
autoPre: currentPeq?.autoPre ?? 0,
|
||||
preamp: currentPeq?.preamp ?? 0,
|
||||
canDel: currentPeq?.canDel ?? 1,
|
||||
},
|
||||
]);
|
||||
setHeadphoneIdx(headphoneModels.length);
|
||||
}
|
||||
setIsSaveBDialogOpen(false);
|
||||
toast.success(eqUi.toastSaveBOk);
|
||||
} catch {
|
||||
toast.error(eqUi.toastSaveBFail);
|
||||
}
|
||||
};
|
||||
|
||||
// 同步 peqSelect 变化
|
||||
useEffect(() => {
|
||||
if (deviceState?.peqSelect !== undefined) {
|
||||
@@ -1261,7 +1345,7 @@ export default function EQPage() {
|
||||
const schedulePeqSync = (
|
||||
peq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined,
|
||||
filtersSource: typeof bands,
|
||||
delay = 320
|
||||
delay = 320,
|
||||
) => {
|
||||
if (!allowPeqRemoteSyncRef.current) return;
|
||||
if (!isDemoMode && !api) return;
|
||||
@@ -1271,33 +1355,40 @@ export default function EQPage() {
|
||||
window.clearTimeout(peqSyncTimerRef.current);
|
||||
}
|
||||
|
||||
const useApply = abMode === "B";
|
||||
|
||||
peqSyncTimerRef.current = window.setTimeout(() => {
|
||||
const filters = filtersSource.map(bandToPeqFilter);
|
||||
const payload: PeqChangePayload = {
|
||||
peqChange: {
|
||||
name: peq.name,
|
||||
filters,
|
||||
autoPre: peq.autoPre ?? 0,
|
||||
preamp: peq.preamp ?? 0,
|
||||
canDel: peq.canDel ?? 1,
|
||||
},
|
||||
const body: PeqPresetBody = {
|
||||
name: peq.name,
|
||||
filters,
|
||||
autoPre: peq.autoPre ?? 0,
|
||||
preamp: peq.preamp ?? 0,
|
||||
canDel: peq.canDel ?? 1,
|
||||
};
|
||||
upgradePeqChange(payload).catch((err) => {
|
||||
const request = useApply
|
||||
? upgradePeqApply({ peqApply: body } satisfies PeqApplyPayload)
|
||||
: upgradePeqChange({ peqChange: body } satisfies PeqChangePayload);
|
||||
request.catch(() => {
|
||||
toast.error("EQ 保存失败");
|
||||
});
|
||||
}, delay);
|
||||
};
|
||||
|
||||
// 参数变更(含拖动曲线)→ POST peqChange(防抖)
|
||||
// A 模式编辑 → peqChange;B 模式编辑(含切换后展示)→ peqApply(防抖)
|
||||
useEffect(() => {
|
||||
if (syncingHeadphoneRef.current) return;
|
||||
if (skipPeqAutoSyncRef.current) {
|
||||
skipPeqAutoSyncRef.current = false;
|
||||
return;
|
||||
}
|
||||
schedulePeqSync(peqItems[headphoneIdx], bands, 320);
|
||||
return () => {
|
||||
if (peqSyncTimerRef.current !== null) {
|
||||
window.clearTimeout(peqSyncTimerRef.current);
|
||||
}
|
||||
};
|
||||
}, [bands, headphoneIdx, peqItems, api, isDemoMode, upgradePeqChange]);
|
||||
}, [bands, abMode, headphoneIdx, peqItems, api, isDemoMode, upgradePeqChange, upgradePeqApply]);
|
||||
|
||||
useEffect(() => {
|
||||
const onPointerDown = (event: PointerEvent) => {
|
||||
@@ -1395,6 +1486,58 @@ export default function EQPage() {
|
||||
myChart.setOption(ops, true);
|
||||
};
|
||||
|
||||
const handleApplyB = useCallback(async () => {
|
||||
const peq = peqItems[headphoneIdx];
|
||||
if (!peq?.name) {
|
||||
toast.error(eqUi.toastApplyBFail);
|
||||
return;
|
||||
}
|
||||
|
||||
const bBands = cloneBands(bandsByMode.B);
|
||||
const filters = bBands.map(bandToPeqFilter);
|
||||
const payload: PeqChangePayload = {
|
||||
peqChange: {
|
||||
name: peq.name,
|
||||
filters,
|
||||
autoPre: peq.autoPre ?? 0,
|
||||
preamp: peq.preamp ?? 0,
|
||||
canDel: peq.canDel ?? 1,
|
||||
},
|
||||
};
|
||||
|
||||
skipPeqAutoSyncRef.current = true;
|
||||
setBandsByMode((prev) => ({ ...prev, A: cloneBands(bBands) }));
|
||||
setSelectedBandByMode((prev) => ({ ...prev, A: prev.B }));
|
||||
setPeqItems((prev) =>
|
||||
prev.map((item, i) => (i !== headphoneIdx ? item : { ...item, filters })),
|
||||
);
|
||||
setAbMode("A");
|
||||
|
||||
if (peqSyncTimerRef.current !== null) {
|
||||
window.clearTimeout(peqSyncTimerRef.current);
|
||||
peqSyncTimerRef.current = null;
|
||||
}
|
||||
|
||||
try {
|
||||
await upgradePeqChange(payload);
|
||||
if (peqItems.length > 0 && headphoneIdx < peqItems.length) {
|
||||
renderCharts(bBands, currentRawCurve, false);
|
||||
}
|
||||
toast.success(eqUi.toastApplyBSuccess);
|
||||
} catch {
|
||||
skipPeqAutoSyncRef.current = false;
|
||||
toast.error(eqUi.toastApplyBFail);
|
||||
}
|
||||
}, [
|
||||
bandsByMode.B,
|
||||
currentRawCurve,
|
||||
eqUi.toastApplyBFail,
|
||||
eqUi.toastApplyBSuccess,
|
||||
headphoneIdx,
|
||||
peqItems,
|
||||
upgradePeqChange,
|
||||
]);
|
||||
|
||||
const updateBand = (idx: number, patch: Partial<typeof bands[0]>) => {
|
||||
const nextBands = bands.map((b, i) => (i === idx ? { ...b, ...patch } : b));
|
||||
setBands(nextBands);
|
||||
@@ -1540,6 +1683,8 @@ export default function EQPage() {
|
||||
abMode={abMode}
|
||||
onAbToggle={setAbMode}
|
||||
onCopyMode={copyModeParams}
|
||||
onApplyB={() => void handleApplyB()}
|
||||
onSaveB={openSaveBDialog}
|
||||
onBandDrag={(idx, patch) => setBands((prev) => prev.map((b, i) => i === idx ? { ...b, ...patch } : b))}
|
||||
onBandSelect={setSelectedBand}
|
||||
eqUi={eqUi}
|
||||
@@ -1696,6 +1841,56 @@ export default function EQPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isSaveBDialogOpen && (
|
||||
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4">
|
||||
<div
|
||||
className="w-full max-w-[420px] rounded-[14px] p-5"
|
||||
style={{
|
||||
background: "linear-gradient(180deg, rgba(34,36,40,0.98) 0%, rgba(24,26,30,0.98) 100%)",
|
||||
border: "1px solid rgba(255,255,255,0.12)",
|
||||
boxShadow: "0 18px 48px rgba(0,0,0,0.55)",
|
||||
}}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-[18px] font-semibold leading-tight text-white/90">{eqUi.saveBTitle}</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-1.5 text-white/45 transition-colors hover:bg-white/10 hover:text-white/80"
|
||||
onClick={() => setIsSaveBDialogOpen(false)}
|
||||
aria-label={eqUi.closeDrawer}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input
|
||||
value={saveBPresetName}
|
||||
onChange={(e) => setSaveBPresetName(e.target.value)}
|
||||
className="h-11 w-full rounded-[10px] border border-white/12 bg-[#15171b] px-3 text-[15px] text-white/90 outline-none placeholder:text-white/30 focus:border-white/15"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="mt-6 flex items-center justify-center gap-4 sm:gap-8">
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-medium text-white/85 transition-colors bg-[#3f4349] hover:bg-[#4a4f56] active:scale-[0.98]"
|
||||
onClick={() => setIsSaveBDialogOpen(false)}
|
||||
>
|
||||
{eqUi.cancel}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="min-w-[112px] rounded-full px-5 py-2.5 text-[15px] font-semibold text-black transition-all bg-[#00FFF6] hover:brightness-95 active:scale-[0.98]"
|
||||
onClick={() => void handleSaveBAsPreset()}
|
||||
>
|
||||
{eqUi.save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isAddPresetDialogOpen && (
|
||||
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4">
|
||||
<div
|
||||
|
||||
@@ -22,6 +22,7 @@ const KEY_TO_SETTING: Record<string, string> = {
|
||||
language: "language",
|
||||
analogGain: "analogGain",
|
||||
soundStep: "soundStep",
|
||||
bootSound: "bootSound",
|
||||
filterCharacteristic: "pcm",
|
||||
mutePolar: "hdmimutepolar",
|
||||
IISMode: "hdmiType",
|
||||
@@ -63,7 +64,7 @@ export function navigateToSelect(title: string, options: string[], selected: num
|
||||
}
|
||||
|
||||
export default function SelectPage() {
|
||||
const [, setLocation] = useLocation();
|
||||
const [location, setLocation] = useLocation();
|
||||
const { updateSetting } = useDevice();
|
||||
const [state, setState] = useState<{
|
||||
title: string;
|
||||
@@ -71,14 +72,15 @@ export default function SelectPage() {
|
||||
selected: number;
|
||||
back: string;
|
||||
key: string;
|
||||
} | null>(selectPageState);
|
||||
} | null>(() => selectPageState);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// Clear state when component unmounts
|
||||
// Re-read global state whenever we enter /select (avoids stale key/options after remount)
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
selectPageState = null;
|
||||
};
|
||||
}, []);
|
||||
if (location === "/select" && selectPageState) {
|
||||
setState({ ...selectPageState });
|
||||
}
|
||||
}, [location]);
|
||||
|
||||
if (!state) {
|
||||
return null;
|
||||
@@ -86,16 +88,20 @@ export default function SelectPage() {
|
||||
|
||||
const { title, options, selected: selectedIdx, back, key } = state;
|
||||
|
||||
const handleSelect = (idx: number) => {
|
||||
// Keep selection result for pages that handle non-api local updates (e.g. EQ filter type)
|
||||
const handleSelect = async (idx: number) => {
|
||||
if (saving) return;
|
||||
selectPageResult = { key, selected: idx };
|
||||
|
||||
// Apply setting if key maps to an API field
|
||||
const apiField = KEY_TO_SETTING[key];
|
||||
if (apiField) {
|
||||
updateSetting({ [apiField]: idx } as Parameters<typeof updateSetting>[0]);
|
||||
setSaving(true);
|
||||
try {
|
||||
await updateSetting({ [apiField]: idx });
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
}
|
||||
// Navigate back
|
||||
selectPageState = null;
|
||||
setLocation(back);
|
||||
};
|
||||
|
||||
@@ -121,7 +127,7 @@ export default function SelectPage() {
|
||||
<button
|
||||
key={idx}
|
||||
className="ios-list-row w-full text-left active:bg-white/5 transition-colors"
|
||||
onClick={() => handleSelect(idx)}>
|
||||
onClick={() => void handleSelect(idx)}>
|
||||
<span className={`flex-1 text-[16px] ${isSelected ? "text-white font-medium" : "text-white/75"}`}>
|
||||
{opt}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user