Update locale files for English, Traditional Chinese, and Simplified Chinese to include new effect titles and crossfeed options; refactor EffectsPage to utilize localized labels and improve state management for audio effects.
This commit is contained in:
@@ -36,6 +36,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"effect": {
|
"effect": {
|
||||||
|
"pageTitle": "Effects",
|
||||||
|
"selectStyleTitle": "Select sound style",
|
||||||
|
"selectCrossfeedTitle": "Select crossfeed preset",
|
||||||
"lowBass": "Bass",
|
"lowBass": "Bass",
|
||||||
"enterBass": "Mid",
|
"enterBass": "Mid",
|
||||||
"highBass": "Treble",
|
"highBass": "Treble",
|
||||||
@@ -47,7 +50,7 @@
|
|||||||
"mainframeCase": "Main speaker",
|
"mainframeCase": "Main speaker",
|
||||||
"subwooferUnit": "Subwoofer",
|
"subwooferUnit": "Subwoofer",
|
||||||
"style": {
|
"style": {
|
||||||
"label": "style",
|
"label": "Style",
|
||||||
"options": [
|
"options": [
|
||||||
{
|
{
|
||||||
"index": 0,
|
"index": 0,
|
||||||
@@ -119,7 +122,12 @@
|
|||||||
"label": "Stereo width"
|
"label": "Stereo width"
|
||||||
},
|
},
|
||||||
"crossfeed": {
|
"crossfeed": {
|
||||||
"label": "Crossfeed"
|
"label": "Crossfeed",
|
||||||
|
"options": [
|
||||||
|
"BS2B default (700 Hz, 4.5 dB)",
|
||||||
|
"BS2B popular (700 Hz, 6 dB)",
|
||||||
|
"BS2B relax (650 Hz, 9.5 dB)"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"subwoofer": {
|
"subwoofer": {
|
||||||
"label": "Subwoofer",
|
"label": "Subwoofer",
|
||||||
@@ -145,7 +153,7 @@
|
|||||||
"Standard": "Standard equalizer preset",
|
"Standard": "Standard equalizer preset",
|
||||||
"edit": "Batch Edit",
|
"edit": "Batch Edit",
|
||||||
"headset": "Add HeadSet",
|
"headset": "Add HeadSet",
|
||||||
"preamp": "Total gain",
|
"preamp": "Preamp",
|
||||||
"filters": {
|
"filters": {
|
||||||
"label": "Filters",
|
"label": "Filters",
|
||||||
"options": [
|
"options": [
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
"output": { "headset": "耳機" }
|
"output": { "headset": "耳機" }
|
||||||
},
|
},
|
||||||
"effect": {
|
"effect": {
|
||||||
|
"pageTitle": "音效",
|
||||||
|
"selectStyleTitle": "選擇音效風格",
|
||||||
|
"selectCrossfeedTitle": "選擇交叉回授模式",
|
||||||
"lowBass": "低頻",
|
"lowBass": "低頻",
|
||||||
"enterBass": "中頻",
|
"enterBass": "中頻",
|
||||||
"highBass": "高頻",
|
"highBass": "高頻",
|
||||||
@@ -60,7 +63,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"stereoWidth": { "label": "聲場寬度" },
|
"stereoWidth": { "label": "聲場寬度" },
|
||||||
"crossfeed": { "label": "交叉回授" },
|
"crossfeed": {
|
||||||
|
"label": "交叉回授",
|
||||||
|
"options": [
|
||||||
|
"BS2B default(700Hz,4.5dB)",
|
||||||
|
"BS2B polular(700Hz,6dB)",
|
||||||
|
"BS2B relax(650Hz,9.5dB)"
|
||||||
|
]
|
||||||
|
},
|
||||||
"subwoofer": {
|
"subwoofer": {
|
||||||
"label": "重低音輸出",
|
"label": "重低音輸出",
|
||||||
"freq": "低通頻率",
|
"freq": "低通頻率",
|
||||||
|
|||||||
@@ -29,6 +29,9 @@
|
|||||||
"output": { "headset": "耳机" }
|
"output": { "headset": "耳机" }
|
||||||
},
|
},
|
||||||
"effect": {
|
"effect": {
|
||||||
|
"pageTitle": "音效",
|
||||||
|
"selectStyleTitle": "选择音效风格",
|
||||||
|
"selectCrossfeedTitle": "选择交叉反馈模式",
|
||||||
"lowBass": "低音",
|
"lowBass": "低音",
|
||||||
"enterBass": "中音",
|
"enterBass": "中音",
|
||||||
"highBass": "高音",
|
"highBass": "高音",
|
||||||
@@ -39,7 +42,7 @@
|
|||||||
"mainframeCase": "主机箱",
|
"mainframeCase": "主机箱",
|
||||||
"subwooferUnit": "低音炮",
|
"subwooferUnit": "低音炮",
|
||||||
"style": {
|
"style": {
|
||||||
"label": "Style",
|
"label": "风格",
|
||||||
"options": [
|
"options": [
|
||||||
{ "index": 0, "label": "古典" },
|
{ "index": 0, "label": "古典" },
|
||||||
{ "index": 1, "label": "舞曲" },
|
{ "index": 1, "label": "舞曲" },
|
||||||
@@ -60,7 +63,14 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"stereoWidth": { "label": "声场宽度" },
|
"stereoWidth": { "label": "声场宽度" },
|
||||||
"crossfeed": { "label": "交叉反馈" },
|
"crossfeed": {
|
||||||
|
"label": "交叉反馈",
|
||||||
|
"options": [
|
||||||
|
"BS2B default(700Hz,4.5dB)",
|
||||||
|
"BS2B polular(700Hz,6dB)",
|
||||||
|
"BS2B relax(650Hz,9.5dB)"
|
||||||
|
]
|
||||||
|
},
|
||||||
"subwoofer": {
|
"subwoofer": {
|
||||||
"label": "低音炮输出",
|
"label": "低音炮输出",
|
||||||
"freq": "低通频率",
|
"freq": "低通频率",
|
||||||
|
|||||||
@@ -1225,6 +1225,15 @@ export default function EQPage() {
|
|||||||
loadHeadphones();
|
loadHeadphones();
|
||||||
}, [api, isDemoMode, loadRawCurveForPeq]);
|
}, [api, isDemoMode, loadRawCurveForPeq]);
|
||||||
|
|
||||||
|
/** 仅耳机/型号/filters 变化时变;preamp、autoPre 变化不触发,避免拖总增益时反复请求 modelCurve */
|
||||||
|
const modelCurveReloadKey = useMemo(() => {
|
||||||
|
const peq = peqItems[headphoneIdx];
|
||||||
|
if (!peq) return "";
|
||||||
|
const filtersKey =
|
||||||
|
typeof peq.filters === "string" ? peq.filters : JSON.stringify(peq.filters ?? []);
|
||||||
|
return [headphoneIdx, peq.name ?? "", peq.brand ?? "", peq.model ?? "", filtersKey].join("\u0001");
|
||||||
|
}, [headphoneIdx, peqItems]);
|
||||||
|
|
||||||
// 切换耳机型号后,立即用该型号 filters 刷新 10 个滤波器与曲线(暂停一次上报避免错写)
|
// 切换耳机型号后,立即用该型号 filters 刷新 10 个滤波器与曲线(暂停一次上报避免错写)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const peq = peqItems[headphoneIdx];
|
const peq = peqItems[headphoneIdx];
|
||||||
@@ -1247,7 +1256,7 @@ export default function EQPage() {
|
|||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
cancelled = true;
|
||||||
};
|
};
|
||||||
}, [headphoneIdx, peqItems, loadRawCurveForPeq]);
|
}, [modelCurveReloadKey, loadRawCurveForPeq]);
|
||||||
|
|
||||||
const schedulePeqSync = (
|
const schedulePeqSync = (
|
||||||
peq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined,
|
peq: { name: string; autoPre?: number; preamp?: number; canDel?: number } | undefined,
|
||||||
|
|||||||
@@ -9,9 +9,19 @@
|
|||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useState, useEffect, useRef } from "react";
|
import { useState, useEffect, useRef, useMemo } from "react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
import { navigateToSelect } from "./SelectPage";
|
import { navigateToSelect } from "./SelectPage";
|
||||||
|
import localeZh from "@/locales/data-zh.json";
|
||||||
|
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||||
|
import localeEn from "@/locales/data-en.json";
|
||||||
|
|
||||||
|
type EffectLocale = NonNullable<(typeof localeZh)["effect"]>;
|
||||||
|
|
||||||
|
function clampPickIndex(i: number, len: number) {
|
||||||
|
if (len <= 0) return 0;
|
||||||
|
return Math.min(Math.max(i, 0), len - 1);
|
||||||
|
}
|
||||||
|
|
||||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||||
return (
|
return (
|
||||||
@@ -22,10 +32,20 @@ function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: bool
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function CyanSlider({ value, min, max, step = 0.1, onChange, onMouseUp }: {
|
function CyanSlider({ value, min, max, step = 0.1, onChange, onRelease }: {
|
||||||
value: number; min: number; max: number; step?: number; onChange: (v: number) => void; onMouseUp?: () => void;
|
value: number;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
step?: number;
|
||||||
|
onChange: (v: number) => void;
|
||||||
|
/** Fires once when the user releases the thumb (pointer/mouse/touch). Read value from DOM to avoid stale React state. */
|
||||||
|
onRelease?: (v: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const fillPct = ((value - min) / (max - min)) * 100;
|
const fillPct = ((value - min) / (max - min)) * 100;
|
||||||
|
const emitRelease = (el: EventTarget | null) => {
|
||||||
|
if (!(el instanceof HTMLInputElement)) return;
|
||||||
|
onRelease?.(Number(el.value));
|
||||||
|
};
|
||||||
return (
|
return (
|
||||||
<div className="relative flex items-center w-full mt-3">
|
<div className="relative flex items-center w-full mt-3">
|
||||||
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
||||||
@@ -33,26 +53,37 @@ function CyanSlider({ value, min, max, step = 0.1, onChange, onMouseUp }: {
|
|||||||
<input type="range" min={min} max={max} step={step} value={value}
|
<input type="range" min={min} max={max} step={step} value={value}
|
||||||
className="cyan-slider relative z-10"
|
className="cyan-slider relative z-10"
|
||||||
onChange={(e) => onChange(Number(e.target.value))}
|
onChange={(e) => onChange(Number(e.target.value))}
|
||||||
onMouseUp={onMouseUp}
|
onPointerUp={(e) => emitRelease(e.currentTarget)}
|
||||||
onTouchEnd={onMouseUp} />
|
onPointerCancel={(e) => emitRelease(e.currentTarget)}
|
||||||
|
onKeyUp={(e) => {
|
||||||
|
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(e.key)) {
|
||||||
|
emitRelease(e.currentTarget);
|
||||||
|
}
|
||||||
|
}} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const SCENE_OPTIONS = [
|
|
||||||
"古典", "舞曲", "流行", "雷鬼", "现场", "摇滚", "柔和", "电子乐",
|
|
||||||
"俱乐部", "全低音", "全高音", "耳机", "大厅", "聚合", "斯卡", "慢摇"
|
|
||||||
];
|
|
||||||
const CROSSFEED_OPTIONS = [
|
|
||||||
"BS2B default(700Hz,4.5dB)",
|
|
||||||
"BS2B polular(700Hz,6dB)",
|
|
||||||
"BS2B relax(650Hz,9.5dB)",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function EffectsPage() {
|
export default function EffectsPage() {
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
const { deviceState, updateSetting } = useDevice();
|
const { deviceState, updateSetting } = useDevice();
|
||||||
|
|
||||||
|
const effectText = useMemo((): EffectLocale => {
|
||||||
|
const lang = deviceState?.language;
|
||||||
|
const pack = lang === 0 ? localeEn : lang === 1 ? localeZhHK : localeZh;
|
||||||
|
return (pack.effect ?? localeZh.effect) as EffectLocale;
|
||||||
|
}, [deviceState?.language]);
|
||||||
|
|
||||||
|
const sceneLabels = useMemo(() => {
|
||||||
|
const opts = effectText.style?.options ?? [];
|
||||||
|
return [...opts].sort((a, b) => a.index - b.index).map((o) => o.label);
|
||||||
|
}, [effectText]);
|
||||||
|
|
||||||
|
const crossfeedLabels = useMemo(
|
||||||
|
() => effectText.crossfeed?.options ?? [],
|
||||||
|
[effectText],
|
||||||
|
);
|
||||||
|
|
||||||
const ds = deviceState;
|
const ds = deviceState;
|
||||||
const effectOn = (ds?.audio_enable ?? 0) === 1;
|
const effectOn = (ds?.audio_enable ?? 0) === 1;
|
||||||
const widthOn = (ds?.width_enable ?? 0) === 1;
|
const widthOn = (ds?.width_enable ?? 0) === 1;
|
||||||
@@ -93,11 +124,11 @@ export default function EffectsPage() {
|
|||||||
if (!isDraggingLoudnessTreble.current) setLocalLoudnessTreble(loudnessTrebleGain);
|
if (!isDraggingLoudnessTreble.current) setLocalLoudnessTreble(loudnessTrebleGain);
|
||||||
}, [colorBassGain, colorMidGain, colorTrebleGain, loudnessThreshold, loudnessBassGain, loudnessTrebleGain]);
|
}, [colorBassGain, colorMidGain, colorTrebleGain, loudnessThreshold, loudnessBassGain, loudnessTrebleGain]);
|
||||||
|
|
||||||
const [localWidth, setLocalWidth] = useState(widthVal);
|
const [localWidth, setLocalWidth] = useState(() => Math.round(widthVal));
|
||||||
const isDragging = useRef(false);
|
const isDragging = useRef(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isDragging.current) setLocalWidth(widthVal);
|
if (!isDragging.current) setLocalWidth(Math.round(widthVal));
|
||||||
}, [widthVal]);
|
}, [widthVal]);
|
||||||
|
|
||||||
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
||||||
@@ -105,6 +136,11 @@ export default function EffectsPage() {
|
|||||||
setLocation("/select");
|
setLocation("/select");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sceneIdx = clampPickIndex(sceneVal, sceneLabels.length);
|
||||||
|
const sceneDisplay = sceneLabels[sceneIdx] ?? "";
|
||||||
|
const crossIdx = clampPickIndex(crossfeedVal, crossfeedLabels.length);
|
||||||
|
const crossfeedDisplay = crossfeedLabels[crossIdx] ?? "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-black">
|
<div className="min-h-screen bg-black">
|
||||||
<div style={{
|
<div style={{
|
||||||
@@ -115,7 +151,7 @@ export default function EffectsPage() {
|
|||||||
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
||||||
<ChevronLeft size={24} />
|
<ChevronLeft size={24} />
|
||||||
</button>
|
</button>
|
||||||
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">Effect</h1>
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">{effectText.pageTitle}</h1>
|
||||||
<IOSToggle checked={effectOn} onChange={(v) => updateSetting({ audio_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={effectOn} onChange={(v) => updateSetting({ audio_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -123,13 +159,13 @@ export default function EffectsPage() {
|
|||||||
{/* ── Style ── */}
|
{/* ── Style ── */}
|
||||||
<div className="ios-list-group p-4">
|
<div className="ios-list-group p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-[16px] font-medium text-white">Style</span>
|
<span className="text-[16px] font-medium text-white">{effectText.style.label}</span>
|
||||||
<IOSToggle checked={sceneOn} onChange={(v) => updateSetting({ effect_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={sceneOn} onChange={(v) => updateSetting({ effect_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
|
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
|
||||||
onClick={() => goSelect("选择音效风格", SCENE_OPTIONS, sceneVal, "effect_value")}>
|
onClick={() => goSelect(effectText.selectStyleTitle, sceneLabels, sceneVal, "effect_value")}>
|
||||||
<span className="text-[15px] text-white/55">{SCENE_OPTIONS[sceneVal]}</span>
|
<span className="text-[15px] text-white/55">{sceneDisplay}</span>
|
||||||
<ChevronRight size={16} className="ios-chevron" />
|
<ChevronRight size={16} className="ios-chevron" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,30 +174,38 @@ export default function EffectsPage() {
|
|||||||
<div className="ios-list-group p-4">
|
<div className="ios-list-group p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<span className="text-[16px] font-medium text-white">声场宽度</span>
|
<span className="text-[16px] font-medium text-white">{effectText.stereoWidth.label}</span>
|
||||||
<span className="text-[16px] font-bold" style={{ color: "#00FFF6" }}>{localWidth}</span>
|
<span className="text-[16px] font-bold" style={{ color: "#00FFF6" }}>{Math.round(localWidth)}</span>
|
||||||
</div>
|
</div>
|
||||||
<IOSToggle checked={widthOn} onChange={(v) => updateSetting({ width_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={widthOn} onChange={(v) => updateSetting({ width_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider value={localWidth} min={0} max={100}
|
<CyanSlider
|
||||||
|
value={localWidth}
|
||||||
|
min={0}
|
||||||
|
max={100}
|
||||||
|
step={1}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDragging.current = true;
|
isDragging.current = true;
|
||||||
setLocalWidth(v);
|
setLocalWidth(Math.round(v));
|
||||||
updateSetting({ width_value: v });
|
}}
|
||||||
setTimeout(() => { isDragging.current = false; }, 500);
|
onRelease={(v) => {
|
||||||
|
isDragging.current = false;
|
||||||
|
const w = Math.round(v);
|
||||||
|
setLocalWidth(w);
|
||||||
|
updateSetting({ width_value: w });
|
||||||
}} />
|
}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── 交叉反馈 ── */}
|
{/* ── 交叉反馈 ── */}
|
||||||
<div className="ios-list-group p-4">
|
<div className="ios-list-group p-4">
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<span className="text-[16px] font-medium text-white">交叉反馈</span>
|
<span className="text-[16px] font-medium text-white">{effectText.crossfeed.label}</span>
|
||||||
<IOSToggle checked={crossfeedOn} onChange={(v) => updateSetting({ crossfeed_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={crossfeedOn} onChange={(v) => updateSetting({ crossfeed_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
|
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
|
||||||
onClick={() => goSelect("选择交叉反馈模式", CROSSFEED_OPTIONS, crossfeedVal, "crossfeed_value")}>
|
onClick={() => goSelect(effectText.selectCrossfeedTitle, crossfeedLabels, crossfeedVal, "crossfeed_value")}>
|
||||||
<span className="text-[14px] text-white/45">{CROSSFEED_OPTIONS[crossfeedVal]}</span>
|
<span className="text-[14px] text-white/45">{crossfeedDisplay}</span>
|
||||||
<ChevronRight size={16} className="ios-chevron" />
|
<ChevronRight size={16} className="ios-chevron" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -169,7 +213,7 @@ export default function EffectsPage() {
|
|||||||
{/* ── 音调 ── */}
|
{/* ── 音调 ── */}
|
||||||
<div className="ios-list-group p-4">
|
<div className="ios-list-group p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[16px] font-medium text-white">音调</span>
|
<span className="text-[16px] font-medium text-white">{effectText.tone}</span>
|
||||||
<IOSToggle checked={colorOn} onChange={(v) => updateSetting({ color_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={colorOn} onChange={(v) => updateSetting({ color_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -178,20 +222,20 @@ export default function EffectsPage() {
|
|||||||
{/* 低音 */}
|
{/* 低音 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[14px] text-white/60">低音</span>
|
<span className="text-[14px] text-white/60">{effectText.lowBass}</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localBass.toFixed(1)}</span>
|
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localBass.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localBass}
|
value={localBass}
|
||||||
min={-10}
|
min={-10}
|
||||||
max={10}
|
max={10}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDraggingBass.current = true;
|
isDraggingBass.current = true;
|
||||||
setLocalBass(v);
|
setLocalBass(v);
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {
|
onRelease={(v) => {
|
||||||
isDraggingBass.current = false;
|
isDraggingBass.current = false;
|
||||||
updateSetting({ color_bass_gain: Math.round(localBass * 10) });
|
updateSetting({ color_bass_gain: Math.round(v * 10) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -199,20 +243,20 @@ export default function EffectsPage() {
|
|||||||
{/* 中音 */}
|
{/* 中音 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[14px] text-white/60">中音</span>
|
<span className="text-[14px] text-white/60">{effectText.enterBass}</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localMid.toFixed(1)}</span>
|
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localMid.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localMid}
|
value={localMid}
|
||||||
min={-10}
|
min={-10}
|
||||||
max={10}
|
max={10}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDraggingMid.current = true;
|
isDraggingMid.current = true;
|
||||||
setLocalMid(v);
|
setLocalMid(v);
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {
|
onRelease={(v) => {
|
||||||
isDraggingMid.current = false;
|
isDraggingMid.current = false;
|
||||||
updateSetting({ color_mid_gain: Math.round(localMid * 10) });
|
updateSetting({ color_mid_gain: Math.round(v * 10) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -220,20 +264,20 @@ export default function EffectsPage() {
|
|||||||
{/* 高音 */}
|
{/* 高音 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[14px] text-white/60">高音</span>
|
<span className="text-[14px] text-white/60">{effectText.highBass}</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localTreble.toFixed(1)}</span>
|
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localTreble.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localTreble}
|
value={localTreble}
|
||||||
min={-10}
|
min={-10}
|
||||||
max={10}
|
max={10}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDraggingTreble.current = true;
|
isDraggingTreble.current = true;
|
||||||
setLocalTreble(v);
|
setLocalTreble(v);
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {
|
onRelease={(v) => {
|
||||||
isDraggingTreble.current = false;
|
isDraggingTreble.current = false;
|
||||||
updateSetting({ color_treble_gain: Math.round(localTreble * 10) });
|
updateSetting({ color_treble_gain: Math.round(v * 10) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -244,7 +288,7 @@ export default function EffectsPage() {
|
|||||||
{/* ── 响度 ── */}
|
{/* ── 响度 ── */}
|
||||||
<div className="ios-list-group p-4">
|
<div className="ios-list-group p-4">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<span className="text-[16px] font-medium text-white">响度</span>
|
<span className="text-[16px] font-medium text-white">{effectText.loudness}</span>
|
||||||
<IOSToggle checked={loudnessOn} onChange={(v) => updateSetting({ loudness_enable: v ? 1 : 0 })} />
|
<IOSToggle checked={loudnessOn} onChange={(v) => updateSetting({ loudness_enable: v ? 1 : 0 })} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -253,65 +297,65 @@ export default function EffectsPage() {
|
|||||||
{/* 阈值 */}
|
{/* 阈值 */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[14px] text-white/60">阈值</span>
|
<span className="text-[14px] text-white/60">{effectText.threshold}</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLoudnessThreshold}</span>
|
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLoudnessThreshold}</span>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localLoudnessThreshold}
|
value={localLoudnessThreshold}
|
||||||
min={-30}
|
min={-30}
|
||||||
max={0}
|
max={0}
|
||||||
step={1}
|
step={1}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDraggingLoudnessThreshold.current = true;
|
isDraggingLoudnessThreshold.current = true;
|
||||||
setLocalLoudnessThreshold(v);
|
setLocalLoudnessThreshold(v);
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {
|
onRelease={(v) => {
|
||||||
isDraggingLoudnessThreshold.current = false;
|
isDraggingLoudnessThreshold.current = false;
|
||||||
updateSetting({ loudness_threshold_gain: Math.round(localLoudnessThreshold) });
|
updateSetting({ loudness_threshold_gain: Math.round(v) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 低音 */}
|
{/* Loudness bass */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[14px] text-white/60">低音</span>
|
<span className="text-[14px] text-white/60">{effectText.lowBass}</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLoudnessBass.toFixed(1)}</span>
|
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLoudnessBass.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localLoudnessBass}
|
value={localLoudnessBass}
|
||||||
min={0}
|
min={0}
|
||||||
max={10}
|
max={10}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDraggingLoudnessBass.current = true;
|
isDraggingLoudnessBass.current = true;
|
||||||
setLocalLoudnessBass(v);
|
setLocalLoudnessBass(v);
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {
|
onRelease={(v) => {
|
||||||
isDraggingLoudnessBass.current = false;
|
isDraggingLoudnessBass.current = false;
|
||||||
updateSetting({ loudness_bass_gain: Math.round(localLoudnessBass * 10) });
|
updateSetting({ loudness_bass_gain: Math.round(v * 10) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 高音 */}
|
{/* Loudness treble */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-2">
|
<div className="flex items-center justify-between mb-2">
|
||||||
<span className="text-[14px] text-white/60">高音</span>
|
<span className="text-[14px] text-white/60">{effectText.highBass}</span>
|
||||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLoudnessTreble.toFixed(1)}</span>
|
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLoudnessTreble.toFixed(1)}</span>
|
||||||
</div>
|
</div>
|
||||||
<CyanSlider
|
<CyanSlider
|
||||||
value={localLoudnessTreble}
|
value={localLoudnessTreble}
|
||||||
min={0}
|
min={0}
|
||||||
max={10}
|
max={10}
|
||||||
step={0.1}
|
step={0.1}
|
||||||
onChange={(v) => {
|
onChange={(v) => {
|
||||||
isDraggingLoudnessTreble.current = true;
|
isDraggingLoudnessTreble.current = true;
|
||||||
setLocalLoudnessTreble(v);
|
setLocalLoudnessTreble(v);
|
||||||
}}
|
}}
|
||||||
onMouseUp={() => {
|
onRelease={(v) => {
|
||||||
isDraggingLoudnessTreble.current = false;
|
isDraggingLoudnessTreble.current = false;
|
||||||
updateSetting({ loudness_treble_gain: Math.round(localLoudnessTreble * 10) });
|
updateSetting({ loudness_treble_gain: Math.round(v * 10) });
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ export default function Home() {
|
|||||||
}
|
}
|
||||||
}, [api, disconnect, isDemoMode, homeText]);
|
}, [api, disconnect, isDemoMode, homeText]);
|
||||||
|
|
||||||
/** HP-EQ 圆钮:单击进入 /eq;双击在 280ms 内第二次点击则切换 peqEnable(与 info.cgi?action=setting&peqEnable=0|1 一致) */
|
/** HP-EQ 圆钮:单击在短延迟后切换 peqEnable(避免与双击抢);280ms 内第二次点击则取消开关并进入 /eq */
|
||||||
const HP_EQ_DOUBLE_CLICK_MS = 280;
|
const HP_EQ_DOUBLE_CLICK_MS = 280;
|
||||||
const handleHpEqCircleClick = useCallback(() => {
|
const handleHpEqCircleClick = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -166,8 +166,7 @@ export default function Home() {
|
|||||||
clearTimeout(hpEqNavigateTimerRef.current);
|
clearTimeout(hpEqNavigateTimerRef.current);
|
||||||
hpEqNavigateTimerRef.current = null;
|
hpEqNavigateTimerRef.current = null;
|
||||||
}
|
}
|
||||||
const on = (deviceState?.peqEnable ?? 0) === 1;
|
setLocation("/eq");
|
||||||
void updateSetting({ peqEnable: on ? 0 : 1 });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hpEqLastTapRef.current = now;
|
hpEqLastTapRef.current = now;
|
||||||
@@ -175,11 +174,12 @@ export default function Home() {
|
|||||||
hpEqNavigateTimerRef.current = setTimeout(() => {
|
hpEqNavigateTimerRef.current = setTimeout(() => {
|
||||||
hpEqNavigateTimerRef.current = null;
|
hpEqNavigateTimerRef.current = null;
|
||||||
hpEqLastTapRef.current = null;
|
hpEqLastTapRef.current = null;
|
||||||
setLocation("/eq");
|
const on = (deviceState?.peqEnable ?? 0) === 1;
|
||||||
|
void updateSetting({ peqEnable: on ? 0 : 1 });
|
||||||
}, HP_EQ_DOUBLE_CLICK_MS);
|
}, HP_EQ_DOUBLE_CLICK_MS);
|
||||||
}, [deviceState?.peqEnable, updateSetting, setLocation]);
|
}, [deviceState?.peqEnable, updateSetting, setLocation]);
|
||||||
|
|
||||||
/** Effect 圆钮:单击进入 /effects;双击切换 audio_enable */
|
/** Effect 圆钮:单击短延迟后切换 audio_enable;280ms 内第二次点击则进入 /effects */
|
||||||
const handleEffectCircleClick = useCallback(() => {
|
const handleEffectCircleClick = useCallback(() => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const last = effectLastTapRef.current;
|
const last = effectLastTapRef.current;
|
||||||
@@ -189,8 +189,7 @@ export default function Home() {
|
|||||||
clearTimeout(effectNavigateTimerRef.current);
|
clearTimeout(effectNavigateTimerRef.current);
|
||||||
effectNavigateTimerRef.current = null;
|
effectNavigateTimerRef.current = null;
|
||||||
}
|
}
|
||||||
const on = (deviceState?.audio_enable ?? 0) === 1;
|
setLocation("/effects");
|
||||||
void updateSetting({ audio_enable: on ? 0 : 1 });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
effectLastTapRef.current = now;
|
effectLastTapRef.current = now;
|
||||||
@@ -198,7 +197,8 @@ export default function Home() {
|
|||||||
effectNavigateTimerRef.current = setTimeout(() => {
|
effectNavigateTimerRef.current = setTimeout(() => {
|
||||||
effectNavigateTimerRef.current = null;
|
effectNavigateTimerRef.current = null;
|
||||||
effectLastTapRef.current = null;
|
effectLastTapRef.current = null;
|
||||||
setLocation("/effects");
|
const on = (deviceState?.audio_enable ?? 0) === 1;
|
||||||
|
void updateSetting({ audio_enable: on ? 0 : 1 });
|
||||||
}, HP_EQ_DOUBLE_CLICK_MS);
|
}, HP_EQ_DOUBLE_CLICK_MS);
|
||||||
}, [deviceState?.audio_enable, updateSetting, setLocation]);
|
}, [deviceState?.audio_enable, updateSetting, setLocation]);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user