修复 bug
This commit is contained in:
@@ -145,6 +145,7 @@ export interface PeqState {
|
||||
canDel?: number;
|
||||
brand?: string;
|
||||
model?: string;
|
||||
form?: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
@@ -262,11 +263,7 @@ export class LuxsinAPI {
|
||||
const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncPeq`);
|
||||
const text = await response.text();
|
||||
const decoded = decodeCustomBase64(text.trim());
|
||||
const parsed = JSON.parse(decoded) as PeqState;
|
||||
if (import.meta.env.DEV) {
|
||||
console.log("[syncPeq] decoded", parsed);
|
||||
}
|
||||
return parsed;
|
||||
return JSON.parse(decoded) as PeqState;
|
||||
}
|
||||
|
||||
async setPeqFilters(filters: PeqFilter[]): Promise<void> {
|
||||
|
||||
@@ -288,6 +288,7 @@
|
||||
"addPresetNameExists": "That name already exists. Choose another.",
|
||||
"toastAddPresetOk": "Preset added",
|
||||
"toastAddPresetFail": "Failed to add preset",
|
||||
"toastPresetLimitReached": "Preset limit reached ({{max}}). Delete some presets before adding more.",
|
||||
"cancel": "Cancel",
|
||||
"save": "Save",
|
||||
"tabBrands": "Brands",
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
"addPresetNameExists": "名稱已存在,請修改後再儲存",
|
||||
"toastAddPresetOk": "已新增預設",
|
||||
"toastAddPresetFail": "新增預設失敗",
|
||||
"toastPresetLimitReached": "預設已達上限({{max}} 個),請先清理後再操作",
|
||||
"cancel": "取消",
|
||||
"save": "儲存",
|
||||
"tabBrands": "品牌",
|
||||
|
||||
@@ -224,6 +224,7 @@
|
||||
"addPresetNameExists": "名称已存在,请修改后再保存",
|
||||
"toastAddPresetOk": "已新增预设",
|
||||
"toastAddPresetFail": "新增预设失败",
|
||||
"toastPresetLimitReached": "预设已达上限({{max}} 个),请先清理后再操作",
|
||||
"cancel": "取消",
|
||||
"save": "保存",
|
||||
"tabBrands": "品牌",
|
||||
|
||||
+64
-18
@@ -58,6 +58,7 @@ import {
|
||||
BAND_PARAM_VALUE_BOX_STYLE,
|
||||
DEFAULT_BANDS,
|
||||
FLAT_PRESET_FILTERS,
|
||||
PEQ_PRESET_MAX,
|
||||
CATALOG_TARGETS,
|
||||
type BandParamKind,
|
||||
type PeqEqUi,
|
||||
@@ -78,6 +79,7 @@ import {
|
||||
buildPeqCatalogSyncKey,
|
||||
getUniquePresetName,
|
||||
bandToPeqFilter,
|
||||
fetchEqSyncPeq,
|
||||
} from "./eq/utils";
|
||||
import { IOSToggle } from "./eq/components/IOSToggle";
|
||||
import { CyanSlider } from "./eq/components/CyanSlider";
|
||||
@@ -589,6 +591,7 @@ export default function EQPage() {
|
||||
gain?: number;
|
||||
q?: number;
|
||||
} | null>(null);
|
||||
const bandSliderDragRef = useRef<BandParamKind | null>(null);
|
||||
const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false);
|
||||
const [batchEditText, setBatchEditText] = useState("");
|
||||
type BrandDrawerTab = "brands" | "models" | "target";
|
||||
@@ -729,7 +732,17 @@ export default function EQPage() {
|
||||
}
|
||||
}, [eqUi]);
|
||||
|
||||
const guardPeqPresetCapacity = () => {
|
||||
if (peqItems.length < PEQ_PRESET_MAX) return true;
|
||||
toast.error(
|
||||
eqInterp(eqUi.toastPresetLimitReached, { max: PEQ_PRESET_MAX })
|
||||
|| `Preset limit reached (${PEQ_PRESET_MAX}). Delete some presets first.`,
|
||||
);
|
||||
return false;
|
||||
};
|
||||
|
||||
const openAddHeadsetCatalog = () => {
|
||||
if (!guardPeqPresetCapacity()) return;
|
||||
setIsBrandDrawerOpen(true);
|
||||
setBrandDrawerTab("brands");
|
||||
setBrandSearchQuery("");
|
||||
@@ -1023,7 +1036,7 @@ export default function EQPage() {
|
||||
|
||||
forgetPeqPresetCache(target.name);
|
||||
await api.removePeq([target.name]);
|
||||
const latest = await api.getPeqState();
|
||||
const latest = await fetchEqSyncPeq(api, "deleteHeadphone");
|
||||
applyPeqStateToUI(latest);
|
||||
toast.success(eqUi.toastDeleted);
|
||||
} catch {
|
||||
@@ -1032,6 +1045,7 @@ export default function EQPage() {
|
||||
};
|
||||
|
||||
const openAddPresetDialog = () => {
|
||||
if (!guardPeqPresetCapacity()) return;
|
||||
const existingNames = headphoneModels;
|
||||
const currentName = headphoneModels[headphoneIdx] ?? "Preset";
|
||||
setCopyPresetName(getUniquePresetName(currentName, existingNames));
|
||||
@@ -1060,11 +1074,13 @@ export default function EQPage() {
|
||||
|
||||
const copyBrand = currentPeq?.brand?.trim() ?? "";
|
||||
const copyModel = currentPeq?.model?.trim() ?? "";
|
||||
const copyForm = currentPeq?.form?.trim() ?? "";
|
||||
const copyPayload: PeqChangePayload = {
|
||||
peqChange: {
|
||||
name: nextName,
|
||||
...(copyBrand ? { brand: copyBrand } : {}),
|
||||
...(copyModel ? { model: copyModel } : {}),
|
||||
...(copyForm ? { form: copyForm } : {}),
|
||||
filters: bands.map(bandToPeqFilter),
|
||||
autoPre: currentPeq?.autoPre ?? 0,
|
||||
preamp: currentPeq?.preamp ?? 0,
|
||||
@@ -1085,7 +1101,7 @@ export default function EQPage() {
|
||||
try {
|
||||
await upgradePeqChange(addPresetMode === "copy" ? copyPayload : flatPayload);
|
||||
if (api && !isDemoMode) {
|
||||
const latest = await api.getPeqState();
|
||||
const latest = await fetchEqSyncPeq(api, "addPreset");
|
||||
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) {
|
||||
@@ -1105,6 +1121,7 @@ export default function EQPage() {
|
||||
name: nextName,
|
||||
...(addPresetMode === "copy" && copyBrand ? { brand: copyBrand } : {}),
|
||||
...(addPresetMode === "copy" && copyModel ? { model: copyModel } : {}),
|
||||
...(addPresetMode === "copy" && copyForm ? { form: copyForm } : {}),
|
||||
filters: localFilters,
|
||||
autoPre: localAutoPre,
|
||||
preamp: localPreamp,
|
||||
@@ -1146,7 +1163,7 @@ export default function EQPage() {
|
||||
try {
|
||||
await upgradePeqChange(payload);
|
||||
if (api && !isDemoMode) {
|
||||
const latest = await api.getPeqState();
|
||||
const latest = await fetchEqSyncPeq(api, "saveBAsPreset");
|
||||
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) {
|
||||
@@ -1293,7 +1310,7 @@ export default function EQPage() {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const loadedPeq = await api.getPeqState();
|
||||
const loadedPeq = await fetchEqSyncPeq(api, "loadHeadphones");
|
||||
if (loadedPeq.peq && loadedPeq.peq.length > 0) {
|
||||
const mergedPeq = mergeRemotePeqCatalog(loadedPeq.peq);
|
||||
setHeadphoneModels(mergedPeq.map((h) => h.name));
|
||||
@@ -1714,6 +1731,37 @@ export default function EQPage() {
|
||||
[setBands],
|
||||
);
|
||||
|
||||
const clearBandSliderPreviewField = useCallback((kind: BandParamKind) => {
|
||||
setBandSliderPreview((prev) => {
|
||||
if (prev?.[kind] === undefined) return prev;
|
||||
const next = { ...prev };
|
||||
delete next[kind];
|
||||
return next.freq !== undefined || next.gain !== undefined || next.q !== undefined ? next : null;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const beginBandSliderDrag = useCallback((kind: BandParamKind) => {
|
||||
bandSliderDragRef.current = kind;
|
||||
}, []);
|
||||
|
||||
const endBandSliderDrag = useCallback(
|
||||
(kind: BandParamKind) => {
|
||||
if (bandSliderDragRef.current !== kind) return;
|
||||
bandSliderDragRef.current = null;
|
||||
clearBandSliderPreviewField(kind);
|
||||
},
|
||||
[clearBandSliderPreviewField],
|
||||
);
|
||||
|
||||
const handleBandSliderChange = useCallback(
|
||||
(kind: BandParamKind, value: number) => {
|
||||
if (bandSliderDragRef.current !== kind) return;
|
||||
setBandSliderPreview((prev) => ({ ...prev, [kind]: value }));
|
||||
updateBand(selectedBand, { [kind]: value });
|
||||
},
|
||||
[selectedBand, updateBand],
|
||||
);
|
||||
|
||||
const bandParamDialogMeta = useMemo(() => {
|
||||
if (!bandParamDialog) return null;
|
||||
const rangeMessages = {
|
||||
@@ -1783,9 +1831,11 @@ export default function EQPage() {
|
||||
toast.error(parsed.message);
|
||||
return;
|
||||
}
|
||||
bandSliderDragRef.current = null;
|
||||
if (bandParamDialog === "freq") updateBand(selectedBand, { freq: parsed.value });
|
||||
else if (bandParamDialog === "gain") updateBand(selectedBand, { gain: parsed.value });
|
||||
else updateBand(selectedBand, { q: parsed.value });
|
||||
setBandSliderPreview(null);
|
||||
closeBandParamDialog();
|
||||
};
|
||||
|
||||
@@ -2065,11 +2115,9 @@ export default function EQPage() {
|
||||
min={Math.log10(BAND_FREQ_MIN)}
|
||||
max={Math.log10(BAND_FREQ_MAX)}
|
||||
step={0.005}
|
||||
onChange={(v) => {
|
||||
const freq = Math.round(Math.pow(10, v));
|
||||
setBandSliderPreview((prev) => ({ ...prev, freq }));
|
||||
updateBand(selectedBand, { freq });
|
||||
}}
|
||||
onInteractionStart={() => beginBandSliderDrag("freq")}
|
||||
onInteractionEnd={() => endBandSliderDrag("freq")}
|
||||
onChange={(v) => handleBandSliderChange("freq", Math.round(Math.pow(10, v)))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2092,10 +2140,9 @@ export default function EQPage() {
|
||||
min={BAND_GAIN_MIN}
|
||||
max={BAND_GAIN_MAX}
|
||||
step={0.1}
|
||||
onChange={(v) => {
|
||||
setBandSliderPreview((prev) => ({ ...prev, gain: v }));
|
||||
updateBand(selectedBand, { gain: v });
|
||||
}}
|
||||
onInteractionStart={() => beginBandSliderDrag("gain")}
|
||||
onInteractionEnd={() => endBandSliderDrag("gain")}
|
||||
onChange={(v) => handleBandSliderChange("gain", v)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -2118,10 +2165,9 @@ export default function EQPage() {
|
||||
min={BAND_Q_MIN}
|
||||
max={BAND_Q_MAX}
|
||||
step={0.01}
|
||||
onChange={(v) => {
|
||||
setBandSliderPreview((prev) => ({ ...prev, q: v }));
|
||||
updateBand(selectedBand, { q: v });
|
||||
}}
|
||||
onInteractionStart={() => beginBandSliderDrag("q")}
|
||||
onInteractionEnd={() => endBandSliderDrag("q")}
|
||||
onChange={(v) => handleBandSliderChange("q", v)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2652,7 +2698,7 @@ export default function EQPage() {
|
||||
await upgradePeqChange(postPeq);
|
||||
const createdName = postPeq.peqChange.name;
|
||||
if (api && !isDemoMode) {
|
||||
const latest = await api.getPeqState();
|
||||
const latest = await fetchEqSyncPeq(api, "addCatalogPreset");
|
||||
applyPeqStateToUI(latest as { peq?: Array<{ name: string; filters?: any[] | string }>; peqSelect?: number });
|
||||
const createdIndex = latest.peq?.findIndex((item) => item.name === createdName) ?? -1;
|
||||
if (createdIndex >= 0) {
|
||||
|
||||
@@ -4,6 +4,8 @@ export function CyanSlider({
|
||||
max,
|
||||
step = 1,
|
||||
onChange,
|
||||
onInteractionStart,
|
||||
onInteractionEnd,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: number;
|
||||
@@ -11,6 +13,8 @@ export function CyanSlider({
|
||||
max: number;
|
||||
step?: number;
|
||||
onChange: (v: number) => void;
|
||||
onInteractionStart?: () => void;
|
||||
onInteractionEnd?: () => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const fillPct = Math.max(
|
||||
@@ -39,6 +43,11 @@ export function CyanSlider({
|
||||
value={value}
|
||||
className="cyan-slider relative z-10"
|
||||
disabled={disabled}
|
||||
onPointerDown={() => onInteractionStart?.()}
|
||||
onFocus={() => onInteractionStart?.()}
|
||||
onPointerUp={() => onInteractionEnd?.()}
|
||||
onPointerCancel={() => onInteractionEnd?.()}
|
||||
onBlur={() => onInteractionEnd?.()}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,9 @@ export const BAND_GAIN_MAX = 15;
|
||||
export const BAND_Q_MIN = 0.1;
|
||||
export const BAND_Q_MAX = 10;
|
||||
|
||||
/** Maximum headphone PEQ presets stored on device. */
|
||||
export const PEQ_PRESET_MAX = 50;
|
||||
|
||||
export type BandParamKind = "freq" | "gain" | "q";
|
||||
|
||||
/* ── Band param value box style ── */
|
||||
|
||||
@@ -158,6 +158,21 @@ export function freqLabel(freq: unknown) {
|
||||
return String(Math.round(hz));
|
||||
}
|
||||
|
||||
/** Dev: log decoded syncPeq JSON after EQ page fetches device PEQ state. */
|
||||
export function logEqSyncPeqDev(source: string, data: PeqState): void {
|
||||
if (!import.meta.env.DEV) return;
|
||||
console.log(`[EQ syncPeq] ${source}`, data);
|
||||
}
|
||||
|
||||
export async function fetchEqSyncPeq(
|
||||
api: { getPeqState: () => Promise<PeqState> },
|
||||
source: string,
|
||||
): Promise<PeqState> {
|
||||
const data = await api.getPeqState();
|
||||
logEqSyncPeqDev(source, data);
|
||||
return data;
|
||||
}
|
||||
|
||||
/* ── Build PEQ catalog sync key ── */
|
||||
export function buildPeqCatalogSyncKey(
|
||||
remote: Pick<PeqState, "peq" | "peqSelect"> | null | undefined,
|
||||
|
||||
Reference in New Issue
Block a user