refactor(eq): 重构EQ页面及相关代码以优化PEQ管理与同步
- 禁用Vite配置中的manus debug collector插件,减少无用请求 - 对DeviceContext添加syncPeqCatalog方法用于同步PEQ状态 - 重构luxsinApi.ts中PEQ请求相关代码,统一POST请求封装,增强错误处理 - 将EQPage相关常量、工具和组件抽取至独立模块,简化主文件结构 - 使用缓存机制记忆预设滤波器列表,减少不必要的重复计算 - 优化PEQ状态同步逻辑,支持按模式(A/B)分别保存与应用滤波器 - 实现选择耳机型号时自动保存并更新对应滤波器缓存和显示 - 修改A/B切换逻辑,使用防抖同步和固定的peqApply调用 - 为音量滑块添加thumb-only滑动样式及拖拽区域限制,提升交互体验 - Home页面ListRow组件支持显示自定义缩略图 - 增加eq/constants.ts定义滤波器类型、参数范围和样式,统一管理配置 - 解决多处EQ页面组件交互和状态切换的潜在同步问题,提高代码维护性和可读性
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
export function CyanSlider({
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
step = 1,
|
||||
onChange,
|
||||
disabled = false,
|
||||
}: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
onChange: (v: number) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
const fillPct = Math.max(
|
||||
0,
|
||||
Math.min(100, ((value - min) / (max - min)) * 100),
|
||||
);
|
||||
return (
|
||||
<div
|
||||
className={`relative flex items-center w-full mt-2 ${disabled ? "opacity-60" : ""}`}
|
||||
>
|
||||
<div
|
||||
className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
||||
style={{
|
||||
width: `${fillPct}%`,
|
||||
background: disabled ? "rgba(148,163,184,0.7)" : "#00FFF6",
|
||||
boxShadow: disabled
|
||||
? "none"
|
||||
: "0 0 6px rgba(0,255,246,0.45)",
|
||||
}}
|
||||
/>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
className="cyan-slider relative z-10"
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export function IOSToggle({
|
||||
checked,
|
||||
onChange,
|
||||
}: {
|
||||
checked: boolean;
|
||||
onChange: (v: boolean) => void;
|
||||
}) {
|
||||
return (
|
||||
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => onChange(e.target.checked)}
|
||||
/>
|
||||
<span className="ios-toggle-track">
|
||||
<span className="ios-toggle-thumb" />
|
||||
</span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
import type { CSSProperties } from "react";
|
||||
import type { PeqFilter, PeqState } from "@/lib/luxsinApi";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
|
||||
/* ── Filter types ── */
|
||||
export const FILTER_TYPES = [
|
||||
"LPF",
|
||||
"HPF",
|
||||
"BPF",
|
||||
"NOTCH",
|
||||
"PEAK",
|
||||
"LSHELF",
|
||||
"HSHELF",
|
||||
"APF",
|
||||
];
|
||||
|
||||
/* ── Band parameter ranges ── */
|
||||
export const BAND_FREQ_MIN = 20;
|
||||
export const BAND_FREQ_MAX = 20000;
|
||||
export const BAND_GAIN_MIN = -15;
|
||||
export const BAND_GAIN_MAX = 15;
|
||||
export const BAND_Q_MIN = 0.1;
|
||||
export const BAND_Q_MAX = 10;
|
||||
|
||||
export type BandParamKind = "freq" | "gain" | "q";
|
||||
|
||||
/* ── Band param value box style ── */
|
||||
export const BAND_PARAM_VALUE_BOX_STYLE: CSSProperties = {
|
||||
background: "rgba(44,44,46,0.9)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
};
|
||||
|
||||
/* ── EQ UI locale type ── */
|
||||
export type PeqEqUi = NonNullable<
|
||||
NonNullable<(typeof localeZh)["peq"]>["eqUi"]
|
||||
>;
|
||||
|
||||
/* ── Default bands matching reference image ── */
|
||||
export const DEFAULT_BANDS = [
|
||||
{ freq: 9500, gain: 0, q: 1.41, type: "LSHELF", enabled: true },
|
||||
{ freq: 9200, gain: -2, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 220, gain: 1, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 500, gain: -3, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 1200, gain: 0, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 13800, gain: -1, q: 1.41, type: "NOTCH", enabled: true },
|
||||
{ freq: 11000, gain: -2, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 7400, gain: 1, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 8200, gain: -1, q: 1.41, type: "PEAK", enabled: true },
|
||||
{ freq: 10000, gain: 0, q: 1.41, type: "HSHELF", enabled: true },
|
||||
];
|
||||
|
||||
/* ── PEQ catalog item type ── */
|
||||
export type PeqCatalogItem = NonNullable<PeqState["peq"]>[number];
|
||||
|
||||
/* ── Flat preset filters ── */
|
||||
export const FLAT_PRESET_FILTERS: PeqFilter[] = [
|
||||
{ type: 4, fc: 80, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 150, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 350, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 750, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 1500, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 3000, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 6000, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 10000, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 14000, gain: 0, q: 0.1 },
|
||||
{ type: 4, fc: 18000, gain: 0, q: 0.1 },
|
||||
];
|
||||
|
||||
/* ── Catalog target type & data ── */
|
||||
export type CatalogTarget = {
|
||||
name: string;
|
||||
bassBoost: { fc: number; q: number; gain: number };
|
||||
ear: "in" | "over" | "all";
|
||||
};
|
||||
|
||||
export const CATALOG_TARGETS: CatalogTarget[] = [
|
||||
{
|
||||
name: "Harman over-ear 2018",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "HMS II.3 Harman over-ear 2018",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "crinacle EARS + 711 Harman over-ear 2018",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "Harman in-ear 2019",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 9.5 },
|
||||
ear: "in",
|
||||
},
|
||||
{
|
||||
name: "AutoEq in-ear",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 8 },
|
||||
ear: "in",
|
||||
},
|
||||
{
|
||||
name: "HMS II.3 AutoEq in-ear",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 8 },
|
||||
ear: "in",
|
||||
},
|
||||
{
|
||||
name: "HMS II.3 Harman in-ear 2019",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 9.5 },
|
||||
ear: "in",
|
||||
},
|
||||
{
|
||||
name: "Diffuse Field 5128 (-1 dB/oct)",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 0 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "LMG 5128 0.6 without bass",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "JM-1 with Harman filters",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6.5 },
|
||||
ear: "all",
|
||||
},
|
||||
{
|
||||
name: "oratory1990 in-ear",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 9.5 },
|
||||
ear: "in",
|
||||
},
|
||||
{
|
||||
name: "oratory1990 over-ear",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "Harman over-ear 2013",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 6 },
|
||||
ear: "over",
|
||||
},
|
||||
{
|
||||
name: "Flat",
|
||||
bassBoost: { fc: 105, q: 0.7, gain: 0 },
|
||||
ear: "all",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,176 @@
|
||||
import type { PeqFilter, PeqState } from "@/lib/luxsinApi";
|
||||
import { getFilterShortName, getFilterType } from "@/lib/peqAudio";
|
||||
import {
|
||||
BAND_FREQ_MIN,
|
||||
BAND_FREQ_MAX,
|
||||
BAND_GAIN_MIN,
|
||||
BAND_GAIN_MAX,
|
||||
BAND_Q_MIN,
|
||||
BAND_Q_MAX,
|
||||
type BandParamKind,
|
||||
type PeqCatalogItem,
|
||||
} from "./constants";
|
||||
|
||||
/* ── Template interpolation ── */
|
||||
export function eqInterp(
|
||||
template: string | undefined,
|
||||
vars: Record<string, string | number>,
|
||||
): string {
|
||||
if (!template) return "";
|
||||
let s = template;
|
||||
for (const [k, v] of Object.entries(vars)) {
|
||||
s = s.split(`{{${k}}}`).join(String(v));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/* ── Band frequency display formatting ── */
|
||||
export function formatBandFreqDisplay(freq: number) {
|
||||
return freq >= 1000
|
||||
? `${(freq / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz`
|
||||
: `${freq} Hz`;
|
||||
}
|
||||
|
||||
/* ── Band param value for input field ── */
|
||||
export function formatBandParamForInput(
|
||||
kind: BandParamKind,
|
||||
band: { freq: number; gain: number; q: number },
|
||||
) {
|
||||
switch (kind) {
|
||||
case "freq":
|
||||
return String(band.freq);
|
||||
case "gain":
|
||||
return band.gain.toFixed(1);
|
||||
case "q":
|
||||
return band.q.toFixed(2);
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Parse band param input with validation ── */
|
||||
export function parseBandParamInput(
|
||||
kind: BandParamKind,
|
||||
raw: string,
|
||||
messages: { invalid: string; outOfRange: string },
|
||||
): { ok: true; value: number } | { ok: false; message: string } {
|
||||
const trimmed = raw.trim();
|
||||
if (!trimmed) return { ok: false, message: messages.invalid };
|
||||
|
||||
if (kind === "freq") {
|
||||
let s = trimmed.replace(/\s+/g, "").toLowerCase().replace(/hz$/, "");
|
||||
const kHz = /k(hz)?$/.test(s);
|
||||
if (kHz) s = s.replace(/k(hz)?$/, "");
|
||||
const num = Number.parseFloat(s);
|
||||
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
|
||||
const hz = Math.round(kHz ? num * 1000 : num);
|
||||
if (hz < BAND_FREQ_MIN || hz > BAND_FREQ_MAX) {
|
||||
return { ok: false, message: messages.outOfRange };
|
||||
}
|
||||
return { ok: true, value: hz };
|
||||
}
|
||||
|
||||
if (kind === "gain") {
|
||||
const s = trimmed.replace(/\s*dB\s*$/i, "").trim();
|
||||
const num = Number.parseFloat(s);
|
||||
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
|
||||
const gain = Number(num.toFixed(1));
|
||||
if (gain < BAND_GAIN_MIN || gain > BAND_GAIN_MAX) {
|
||||
return { ok: false, message: messages.outOfRange };
|
||||
}
|
||||
return { ok: true, value: gain };
|
||||
}
|
||||
|
||||
const num = Number.parseFloat(trimmed);
|
||||
if (!Number.isFinite(num)) return { ok: false, message: messages.invalid };
|
||||
const q = Number(num.toFixed(2));
|
||||
if (q < BAND_Q_MIN || q > BAND_Q_MAX) {
|
||||
return { ok: false, message: messages.outOfRange };
|
||||
}
|
||||
return { ok: true, value: q };
|
||||
}
|
||||
|
||||
/* ── Normalize filter type from string or number ── */
|
||||
export function normalizeFilterType(
|
||||
type: string | number | undefined,
|
||||
): string {
|
||||
if (type === undefined || type === null) return "PEAK";
|
||||
if (typeof type === "number") {
|
||||
switch (type) {
|
||||
case 0:
|
||||
return "LPF";
|
||||
case 1:
|
||||
return "HPF";
|
||||
case 2:
|
||||
return "BPF";
|
||||
case 3:
|
||||
return "NOTCH";
|
||||
case 4:
|
||||
return "PEAK";
|
||||
case 5:
|
||||
return "LSHELF";
|
||||
case 6:
|
||||
return "HSHELF";
|
||||
case 7:
|
||||
return "APF";
|
||||
default:
|
||||
return "PEAK";
|
||||
}
|
||||
}
|
||||
return getFilterShortName(type);
|
||||
}
|
||||
|
||||
/* ── Clone bands array ── */
|
||||
export function cloneBands(
|
||||
source: Array<{
|
||||
freq: number;
|
||||
gain: number;
|
||||
q: number;
|
||||
type: string;
|
||||
enabled: boolean;
|
||||
}>,
|
||||
) {
|
||||
return source.map((band) => ({ ...band }));
|
||||
}
|
||||
|
||||
/* ── Frequency label for band grid ── */
|
||||
export function freqLabel(f: number) {
|
||||
return f >= 1000 ? `${(f / 1000).toFixed(1)}K` : `${f}`;
|
||||
}
|
||||
|
||||
/* ── Build PEQ catalog sync key ── */
|
||||
export function buildPeqCatalogSyncKey(
|
||||
remote: Pick<PeqState, "peq" | "peqSelect"> | 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")}`;
|
||||
}
|
||||
|
||||
/* ── Generate unique preset name ── */
|
||||
export function getUniquePresetName(
|
||||
base: string,
|
||||
existingNames: string[],
|
||||
) {
|
||||
if (!existingNames.includes(base)) return base;
|
||||
let index = 1;
|
||||
while (existingNames.includes(`${base}_${index}`)) {
|
||||
index += 1;
|
||||
}
|
||||
return `${base}_${index}`;
|
||||
}
|
||||
|
||||
/* ── Convert UI band → device PeqFilter ── */
|
||||
export function bandToPeqFilter(b: {
|
||||
freq: number;
|
||||
gain: number;
|
||||
q: number;
|
||||
type: string | number;
|
||||
}): PeqFilter {
|
||||
return {
|
||||
fc: b.freq,
|
||||
gain: b.gain,
|
||||
q: b.q,
|
||||
type: getFilterType(b.type),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user