refactor(eq): Enhance EQPage and luxsinApi for improved loading states and debugging
- Introduced a loading indicator for raw curve data in the EQPage to enhance user experience during data fetching. - Added console logging for debugging purposes in luxsinApi when syncing PEQ and posting changes. - Updated related functions to manage loading states effectively, ensuring smoother interactions.
This commit is contained in:
@@ -262,7 +262,11 @@ export class LuxsinAPI {
|
|||||||
const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncPeq`);
|
const response = await fetch(`${this.baseUrl}/dev/info.cgi?action=syncPeq`);
|
||||||
const text = await response.text();
|
const text = await response.text();
|
||||||
const decoded = decodeCustomBase64(text.trim());
|
const decoded = decodeCustomBase64(text.trim());
|
||||||
return JSON.parse(decoded) as PeqState;
|
const parsed = JSON.parse(decoded) as PeqState;
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log("[syncPeq] decoded", parsed);
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
async setPeqFilters(filters: PeqFilter[]): Promise<void> {
|
async setPeqFilters(filters: PeqFilter[]): Promise<void> {
|
||||||
@@ -281,6 +285,11 @@ export class LuxsinAPI {
|
|||||||
|
|
||||||
/** POST `json=<custom-base64>` — matches legacy axios `upgradePeq`. */
|
/** POST `json=<custom-base64>` — matches legacy axios `upgradePeq`. */
|
||||||
private async postPeqJson(body: PeqChangePayload | PeqApplyPayload): Promise<void> {
|
private async postPeqJson(body: PeqChangePayload | PeqApplyPayload): Promise<void> {
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
const action =
|
||||||
|
"peqChange" in body ? "peqChange" : "peqApply" in body ? "peqApply" : "peq";
|
||||||
|
console.log(`[dev/info.cgi] POST ${action}`, body);
|
||||||
|
}
|
||||||
const encoded = encodeCustomBase64(JSON.stringify(body));
|
const encoded = encodeCustomBase64(JSON.stringify(body));
|
||||||
const form = new URLSearchParams();
|
const form = new URLSearchParams();
|
||||||
form.set("json", encoded);
|
form.set("json", encoded);
|
||||||
|
|||||||
+56
-15
@@ -84,10 +84,30 @@ import { CyanSlider } from "./eq/components/CyanSlider";
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
function LegendCurveIndicator({
|
||||||
|
loading,
|
||||||
|
color,
|
||||||
|
}: {
|
||||||
|
loading: boolean;
|
||||||
|
color: string;
|
||||||
|
}) {
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
className="inline-block w-3 h-3 rounded-full border-2 animate-spin flex-shrink-0"
|
||||||
|
style={{ borderColor: `${color}40`, borderTopColor: color }}
|
||||||
|
aria-hidden="true"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return <div className="w-3 h-[2px] rounded flex-shrink-0" style={{ background: color }} />;
|
||||||
|
}
|
||||||
|
|
||||||
/* ── Frequency Response Chart ── */
|
/* ── Frequency Response Chart ── */
|
||||||
function FreqChart({
|
function FreqChart({
|
||||||
bands,
|
bands,
|
||||||
rawCurve,
|
rawCurve,
|
||||||
|
rawCurveLoading = false,
|
||||||
selectedBand,
|
selectedBand,
|
||||||
abMode,
|
abMode,
|
||||||
onAbToggle,
|
onAbToggle,
|
||||||
@@ -100,6 +120,7 @@ function FreqChart({
|
|||||||
}: {
|
}: {
|
||||||
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
|
bands: Array<{ freq: number; gain: number; q: number; type: string; enabled: boolean }>;
|
||||||
rawCurve: number[] | null;
|
rawCurve: number[] | null;
|
||||||
|
rawCurveLoading?: boolean;
|
||||||
selectedBand: number;
|
selectedBand: number;
|
||||||
abMode: "A" | "B";
|
abMode: "A" | "B";
|
||||||
onAbToggle: (m: "A" | "B") => void;
|
onAbToggle: (m: "A" | "B") => void;
|
||||||
@@ -286,19 +307,29 @@ function FreqChart({
|
|||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
onClick={() => toggleCurveVisibility("raw")}
|
onClick={() => toggleCurveVisibility("raw")}
|
||||||
disabled={!hasRawCurve}
|
disabled={rawCurveLoading || !hasRawCurve}
|
||||||
>
|
>
|
||||||
<div className="w-3 h-[2px] rounded" style={{ background: "#ffffff" }} />
|
<LegendCurveIndicator loading={rawCurveLoading} color="#ffffff" />
|
||||||
<span className="text-[10px]" style={{ color: "#ffffff", opacity: showRaw ? 1 : 0.35 }}>Raw</span>
|
<span
|
||||||
|
className="text-[10px]"
|
||||||
|
style={{ color: "#ffffff", opacity: rawCurveLoading || showRaw ? 1 : 0.35 }}
|
||||||
|
>
|
||||||
|
Raw
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
className="flex items-center gap-1.5 active:opacity-80 transition-opacity disabled:opacity-40 disabled:cursor-not-allowed"
|
||||||
onClick={() => toggleCurveVisibility("equalized")}
|
onClick={() => toggleCurveVisibility("equalized")}
|
||||||
disabled={!hasEqualizedCurve}
|
disabled={rawCurveLoading || !hasEqualizedCurve}
|
||||||
>
|
>
|
||||||
<div className="w-3 h-[2px] rounded" style={{ background: "#23d2fe" }} />
|
<LegendCurveIndicator loading={rawCurveLoading} color="#23d2fe" />
|
||||||
<span className="text-[10px]" style={{ color: "#23d2fe", opacity: showEqualized ? 1 : 0.35 }}>Equalized</span>
|
<span
|
||||||
|
className="text-[10px]"
|
||||||
|
style={{ color: "#23d2fe", opacity: rawCurveLoading || showEqualized ? 1 : 0.35 }}
|
||||||
|
>
|
||||||
|
Equalized
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -578,6 +609,7 @@ export default function EQPage() {
|
|||||||
const [selectedCatalogTarget, setSelectedCatalogTarget] = useState<string>("");
|
const [selectedCatalogTarget, setSelectedCatalogTarget] = useState<string>("");
|
||||||
const [isConfirmingTarget, setIsConfirmingTarget] = useState(false);
|
const [isConfirmingTarget, setIsConfirmingTarget] = useState(false);
|
||||||
const [currentRawCurve, setCurrentRawCurve] = useState<number[] | null>(null);
|
const [currentRawCurve, setCurrentRawCurve] = useState<number[] | null>(null);
|
||||||
|
const [rawCurveLoading, setRawCurveLoading] = useState(false);
|
||||||
const allowPeqRemoteSyncRef = useRef(false);
|
const allowPeqRemoteSyncRef = useRef(false);
|
||||||
const lastPeqCatalogSyncKeyRef = useRef("");
|
const lastPeqCatalogSyncKeyRef = useRef("");
|
||||||
const syncingHeadphoneRef = useRef(false);
|
const syncingHeadphoneRef = useRef(false);
|
||||||
@@ -1023,9 +1055,13 @@ export default function EQPage() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const copyBrand = currentPeq?.brand?.trim() ?? "";
|
||||||
|
const copyModel = currentPeq?.model?.trim() ?? "";
|
||||||
const copyPayload: PeqChangePayload = {
|
const copyPayload: PeqChangePayload = {
|
||||||
peqChange: {
|
peqChange: {
|
||||||
name: nextName,
|
name: nextName,
|
||||||
|
...(copyBrand ? { brand: copyBrand } : {}),
|
||||||
|
...(copyModel ? { model: copyModel } : {}),
|
||||||
filters: bands.map(bandToPeqFilter),
|
filters: bands.map(bandToPeqFilter),
|
||||||
autoPre: currentPeq?.autoPre ?? 0,
|
autoPre: currentPeq?.autoPre ?? 0,
|
||||||
preamp: currentPeq?.preamp ?? 0,
|
preamp: currentPeq?.preamp ?? 0,
|
||||||
@@ -1064,6 +1100,8 @@ export default function EQPage() {
|
|||||||
...prev,
|
...prev,
|
||||||
{
|
{
|
||||||
name: nextName,
|
name: nextName,
|
||||||
|
...(addPresetMode === "copy" && copyBrand ? { brand: copyBrand } : {}),
|
||||||
|
...(addPresetMode === "copy" && copyModel ? { model: copyModel } : {}),
|
||||||
filters: localFilters,
|
filters: localFilters,
|
||||||
autoPre: localAutoPre,
|
autoPre: localAutoPre,
|
||||||
preamp: localPreamp,
|
preamp: localPreamp,
|
||||||
@@ -1177,15 +1215,8 @@ export default function EQPage() {
|
|||||||
|
|
||||||
const loadRawCurveForPeq = useCallback(
|
const loadRawCurveForPeq = useCallback(
|
||||||
async (peq: { brand?: string; model?: string; name?: string } | undefined): Promise<number[] | null> => {
|
async (peq: { brand?: string; model?: string; name?: string } | undefined): Promise<number[] | null> => {
|
||||||
let brand = peq?.brand?.trim() ?? "";
|
const brand = peq?.brand?.trim() ?? "";
|
||||||
let model = peq?.model?.trim() ?? "";
|
const model = peq?.model?.trim() ?? "";
|
||||||
|
|
||||||
// Some presets only have `name` (e.g. "Apple AirPods Pro") and miss explicit brand/model.
|
|
||||||
if ((!brand || !model) && peq?.name) {
|
|
||||||
const [first, ...rest] = peq.name.trim().split(/\s+/);
|
|
||||||
if (!brand && first) brand = first;
|
|
||||||
if (!model && rest.length > 0) model = rest.join(" ");
|
|
||||||
}
|
|
||||||
if (!brand || !model) return null;
|
if (!brand || !model) return null;
|
||||||
|
|
||||||
const modelCurve = await getModelCurve(brand, model);
|
const modelCurve = await getModelCurve(brand, model);
|
||||||
@@ -1326,11 +1357,20 @@ export default function EQPage() {
|
|||||||
syncingHeadphoneRef.current = true;
|
syncingHeadphoneRef.current = true;
|
||||||
setBandsForBothModes(nextBands);
|
setBandsForBothModes(nextBands);
|
||||||
setSelectedBandByMode({ A: 0, B: 0 });
|
setSelectedBandByMode({ A: 0, B: 0 });
|
||||||
|
const shouldFetchModelCurve = !!(peq.brand?.trim() && peq.model?.trim());
|
||||||
|
if (shouldFetchModelCurve) {
|
||||||
|
setRawCurveLoading(true);
|
||||||
|
setCurrentRawCurve(null);
|
||||||
|
} else {
|
||||||
|
setRawCurveLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const raw = await loadRawCurveForPeq(peq as { brand?: string; model?: string });
|
const raw = await loadRawCurveForPeq(peq as { brand?: string; model?: string });
|
||||||
if (cancelled) return;
|
if (cancelled) return;
|
||||||
setCurrentRawCurve(raw);
|
setCurrentRawCurve(raw);
|
||||||
|
setRawCurveLoading(false);
|
||||||
renderCharts(nextBands, raw, false);
|
renderCharts(nextBands, raw, false);
|
||||||
})();
|
})();
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -1890,6 +1930,7 @@ export default function EQPage() {
|
|||||||
<FreqChart
|
<FreqChart
|
||||||
bands={bands}
|
bands={bands}
|
||||||
rawCurve={currentRawCurve}
|
rawCurve={currentRawCurve}
|
||||||
|
rawCurveLoading={rawCurveLoading}
|
||||||
selectedBand={selectedBand}
|
selectedBand={selectedBand}
|
||||||
abMode={abMode}
|
abMode={abMode}
|
||||||
onAbToggle={handleAbToggle}
|
onAbToggle={handleAbToggle}
|
||||||
|
|||||||
Reference in New Issue
Block a user