Update locale files for English, Traditional Chinese, and Simplified Chinese to add new confirmation prompts for volume adjustments; refactor Home and EQPage components to integrate volume confirmation dialogs and enhance user interaction for setting volume levels.
This commit is contained in:
+251
-16
@@ -91,6 +91,77 @@ function CyanSlider({ value, min, max, step = 1, onChange, disabled = false }: {
|
||||
|
||||
/* ── Constants ── */
|
||||
const FILTER_TYPES = ["LPF", "HPF", "BPF", "NOTCH", "PEAK", "LSHELF", "HSHELF", "APF"];
|
||||
const BAND_FREQ_MIN = 20;
|
||||
const BAND_FREQ_MAX = 20000;
|
||||
const BAND_GAIN_MIN = -15;
|
||||
const BAND_GAIN_MAX = 15;
|
||||
const BAND_Q_MIN = 0.1;
|
||||
const BAND_Q_MAX = 10;
|
||||
|
||||
type BandParamKind = "freq" | "gain" | "q";
|
||||
|
||||
const BAND_PARAM_VALUE_BOX_STYLE: React.CSSProperties = {
|
||||
background: "rgba(44,44,46,0.9)",
|
||||
border: "1px solid rgba(255,255,255,0.08)",
|
||||
};
|
||||
|
||||
function formatBandFreqDisplay(freq: number) {
|
||||
return freq >= 1000
|
||||
? `${(freq / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz`
|
||||
: `${freq} Hz`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
function normalizeFilterType(type: string | number | undefined): string {
|
||||
if (type === undefined || type === null) return "PEAK";
|
||||
@@ -642,6 +713,8 @@ export default function EQPage() {
|
||||
const [flatPresetName, setFlatPresetName] = useState("");
|
||||
const [isSaveBDialogOpen, setIsSaveBDialogOpen] = useState(false);
|
||||
const [saveBPresetName, setSaveBPresetName] = useState("");
|
||||
const [bandParamDialog, setBandParamDialog] = useState<BandParamKind | null>(null);
|
||||
const [bandParamInput, setBandParamInput] = useState("");
|
||||
const [isBatchEditDialogOpen, setIsBatchEditDialogOpen] = useState(false);
|
||||
const [batchEditText, setBatchEditText] = useState("");
|
||||
type BrandDrawerTab = "brands" | "models" | "target";
|
||||
@@ -1547,6 +1620,81 @@ export default function EQPage() {
|
||||
setBands(nextBands);
|
||||
};
|
||||
|
||||
const bandParamDialogMeta = useMemo(() => {
|
||||
if (!bandParamDialog) return null;
|
||||
const rangeMessages = {
|
||||
invalid: eqUi.paramInputInvalid ?? "请输入有效数值",
|
||||
outOfRange: "",
|
||||
};
|
||||
switch (bandParamDialog) {
|
||||
case "freq":
|
||||
return {
|
||||
title: eqUi.paramInputTitleFreq ?? "设置频率",
|
||||
hint: eqUi.paramInputHintFreq ?? "范围:20 – 20000 Hz,可使用 k 表示 kHz(如 9.5k)",
|
||||
placeholder: "9500",
|
||||
inputMode: "decimal" as const,
|
||||
rangeMessages: {
|
||||
...rangeMessages,
|
||||
outOfRange: eqInterp(eqUi.paramInputOutOfRangeFreq, {
|
||||
min: String(BAND_FREQ_MIN),
|
||||
max: String(BAND_FREQ_MAX),
|
||||
}) || `频率需在 ${BAND_FREQ_MIN} – ${BAND_FREQ_MAX} Hz 之间`,
|
||||
},
|
||||
};
|
||||
case "gain":
|
||||
return {
|
||||
title: eqUi.paramInputTitleGain ?? "设置增益",
|
||||
hint: eqUi.paramInputHintGain ?? "范围:-15.0 – +15.0 dB",
|
||||
placeholder: "0.0",
|
||||
inputMode: "decimal" as const,
|
||||
rangeMessages: {
|
||||
...rangeMessages,
|
||||
outOfRange: eqInterp(eqUi.paramInputOutOfRangeGain, {
|
||||
min: BAND_GAIN_MIN.toFixed(1),
|
||||
max: BAND_GAIN_MAX.toFixed(1),
|
||||
}) || `增益需在 ${BAND_GAIN_MIN} – ${BAND_GAIN_MAX} dB 之间`,
|
||||
},
|
||||
};
|
||||
case "q":
|
||||
return {
|
||||
title: eqUi.paramInputTitleQ ?? "设置 Q 值",
|
||||
hint: eqUi.paramInputHintQ ?? "范围:0.10 – 10.00",
|
||||
placeholder: "1.41",
|
||||
inputMode: "decimal" as const,
|
||||
rangeMessages: {
|
||||
...rangeMessages,
|
||||
outOfRange: eqInterp(eqUi.paramInputOutOfRangeQ, {
|
||||
min: BAND_Q_MIN.toFixed(2),
|
||||
max: BAND_Q_MAX.toFixed(2),
|
||||
}) || `Q 值需在 ${BAND_Q_MIN} – ${BAND_Q_MAX} 之间`,
|
||||
},
|
||||
};
|
||||
}
|
||||
}, [bandParamDialog, eqUi]);
|
||||
|
||||
const openBandParamDialog = (kind: BandParamKind) => {
|
||||
setBandParamDialog(kind);
|
||||
setBandParamInput(formatBandParamForInput(kind, band));
|
||||
};
|
||||
|
||||
const closeBandParamDialog = () => {
|
||||
setBandParamDialog(null);
|
||||
setBandParamInput("");
|
||||
};
|
||||
|
||||
const applyBandParamDialog = () => {
|
||||
if (!bandParamDialog || !bandParamDialogMeta) return;
|
||||
const parsed = parseBandParamInput(bandParamDialog, bandParamInput, bandParamDialogMeta.rangeMessages);
|
||||
if (!parsed.ok) {
|
||||
toast.error(parsed.message);
|
||||
return;
|
||||
}
|
||||
if (bandParamDialog === "freq") updateBand(selectedBand, { freq: parsed.value });
|
||||
else if (bandParamDialog === "gain") updateBand(selectedBand, { gain: parsed.value });
|
||||
else updateBand(selectedBand, { q: parsed.value });
|
||||
closeBandParamDialog();
|
||||
};
|
||||
|
||||
const updateCurrentPeqMeta = (patch: Partial<{ autoPre: number; preamp: number }>) => {
|
||||
let updatedPeq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined;
|
||||
setPeqItems((prev) => {
|
||||
@@ -1799,39 +1947,66 @@ export default function EQPage() {
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[13px] text-white/45 font-medium tracking-wider">FREQ</span>
|
||||
<span className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white"
|
||||
style={{ background: "rgba(44,44,46,0.9)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
||||
{band.freq >= 1000 ? `${(band.freq / 1000).toFixed(2).replace(/\.?0+$/, "")} kHz` : `${band.freq} Hz`}
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openBandParamDialog("freq")}
|
||||
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
||||
style={BAND_PARAM_VALUE_BOX_STYLE}
|
||||
>
|
||||
{formatBandFreqDisplay(band.freq)}
|
||||
</button>
|
||||
</div>
|
||||
<CyanSlider value={Math.log10(band.freq)} min={Math.log10(20)} max={Math.log10(20000)} step={0.005}
|
||||
onChange={(v) => updateBand(selectedBand, { freq: Math.round(Math.pow(10, v)) })} />
|
||||
<CyanSlider
|
||||
value={Math.log10(band.freq)}
|
||||
min={Math.log10(BAND_FREQ_MIN)}
|
||||
max={Math.log10(BAND_FREQ_MAX)}
|
||||
step={0.005}
|
||||
onChange={(v) => updateBand(selectedBand, { freq: Math.round(Math.pow(10, v)) })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* GAIN */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[13px] text-white/45 font-medium tracking-wider">GAIN</span>
|
||||
<span className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white"
|
||||
style={{ background: "rgba(44,44,46,0.9)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openBandParamDialog("gain")}
|
||||
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
||||
style={BAND_PARAM_VALUE_BOX_STYLE}
|
||||
>
|
||||
{band.gain >= 0 ? `+${band.gain.toFixed(1)}` : band.gain.toFixed(1)} dB
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CyanSlider value={band.gain} min={-15} max={15} step={0.1}
|
||||
onChange={(v) => updateBand(selectedBand, { gain: v })} />
|
||||
<CyanSlider
|
||||
value={band.gain}
|
||||
min={BAND_GAIN_MIN}
|
||||
max={BAND_GAIN_MAX}
|
||||
step={0.1}
|
||||
onChange={(v) => updateBand(selectedBand, { gain: v })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Q 值 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[13px] text-white/45 font-medium">{peqCardLabels.q ?? "Q值"}</span>
|
||||
<span className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white"
|
||||
style={{ background: "rgba(44,44,46,0.9)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => openBandParamDialog("q")}
|
||||
className="text-[13px] font-semibold px-2.5 py-1 rounded-[8px] text-white active:scale-95 transition-transform"
|
||||
style={BAND_PARAM_VALUE_BOX_STYLE}
|
||||
>
|
||||
{band.q.toFixed(2)}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<CyanSlider value={band.q} min={0.1} max={10} step={0.01}
|
||||
onChange={(v) => updateBand(selectedBand, { q: v })} />
|
||||
<CyanSlider
|
||||
value={band.q}
|
||||
min={BAND_Q_MIN}
|
||||
max={BAND_Q_MAX}
|
||||
step={0.01}
|
||||
onChange={(v) => updateBand(selectedBand, { q: v })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1865,6 +2040,66 @@ export default function EQPage() {
|
||||
</FeatureGate>
|
||||
</div>
|
||||
|
||||
{bandParamDialog && bandParamDialogMeta && (
|
||||
<div
|
||||
className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4"
|
||||
onClick={closeBandParamDialog}
|
||||
>
|
||||
<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-2">
|
||||
<h3 className="text-[18px] font-semibold leading-tight text-white/90">{bandParamDialogMeta.title}</h3>
|
||||
<button
|
||||
type="button"
|
||||
className="rounded-full p-1.5 text-white/45 transition-colors hover:bg-white/10 hover:text-white/80"
|
||||
onClick={closeBandParamDialog}
|
||||
aria-label={eqUi.closeDrawer}
|
||||
>
|
||||
<X size={20} />
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-4 text-[13px] leading-relaxed text-white/45">{bandParamDialogMeta.hint}</p>
|
||||
<input
|
||||
value={bandParamInput}
|
||||
onChange={(e) => setBandParamInput(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
applyBandParamDialog();
|
||||
}
|
||||
}}
|
||||
inputMode={bandParamDialogMeta.inputMode}
|
||||
placeholder={bandParamDialogMeta.placeholder}
|
||||
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-[#00FFF6]/40"
|
||||
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={closeBandParamDialog}
|
||||
>
|
||||
{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={applyBandParamDialog}
|
||||
>
|
||||
{eqUi.confirmButton ?? eqUi.save}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSaveBDialogOpen && (
|
||||
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-black/65 px-4">
|
||||
<div
|
||||
|
||||
Reference in New Issue
Block a user