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:
yangy
2026-05-26 10:20:05 +08:00
parent c34571392d
commit 8fcad50e7d
6 changed files with 536 additions and 75 deletions
+251 -16
View File
@@ -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