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:
yangy
2026-05-13 15:18:07 +08:00
parent 651fb0b93e
commit 5285b886ea
6 changed files with 172 additions and 91 deletions
+120 -76
View File
@@ -9,9 +9,19 @@
import { useDevice } from "@/contexts/DeviceContext";
import { ChevronLeft, ChevronRight } from "lucide-react";
import { useLocation } from "wouter";
import { useState, useEffect, useRef } from "react";
import { useState, useEffect, useRef, useMemo } from "react";
import BottomNav from "@/components/BottomNav";
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 }) {
return (
@@ -22,10 +32,20 @@ function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: bool
);
}
function CyanSlider({ value, min, max, step = 0.1, onChange, onMouseUp }: {
value: number; min: number; max: number; step?: number; onChange: (v: number) => void; onMouseUp?: () => void;
function CyanSlider({ value, min, max, step = 0.1, onChange, onRelease }: {
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 emitRelease = (el: EventTarget | null) => {
if (!(el instanceof HTMLInputElement)) return;
onRelease?.(Number(el.value));
};
return (
<div className="relative flex items-center w-full mt-3">
<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}
className="cyan-slider relative z-10"
onChange={(e) => onChange(Number(e.target.value))}
onMouseUp={onMouseUp}
onTouchEnd={onMouseUp} />
onPointerUp={(e) => emitRelease(e.currentTarget)}
onPointerCancel={(e) => emitRelease(e.currentTarget)}
onKeyUp={(e) => {
if (["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "Home", "End", "PageUp", "PageDown"].includes(e.key)) {
emitRelease(e.currentTarget);
}
}} />
</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() {
const [, setLocation] = useLocation();
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 effectOn = (ds?.audio_enable ?? 0) === 1;
const widthOn = (ds?.width_enable ?? 0) === 1;
@@ -93,11 +124,11 @@ export default function EffectsPage() {
if (!isDraggingLoudnessTreble.current) setLocalLoudnessTreble(loudnessTrebleGain);
}, [colorBassGain, colorMidGain, colorTrebleGain, loudnessThreshold, loudnessBassGain, loudnessTrebleGain]);
const [localWidth, setLocalWidth] = useState(widthVal);
const [localWidth, setLocalWidth] = useState(() => Math.round(widthVal));
const isDragging = useRef(false);
useEffect(() => {
if (!isDragging.current) setLocalWidth(widthVal);
if (!isDragging.current) setLocalWidth(Math.round(widthVal));
}, [widthVal]);
const goSelect = (title: string, options: string[], selected: number, key: string) => {
@@ -105,6 +136,11 @@ export default function EffectsPage() {
setLocation("/select");
};
const sceneIdx = clampPickIndex(sceneVal, sceneLabels.length);
const sceneDisplay = sceneLabels[sceneIdx] ?? "";
const crossIdx = clampPickIndex(crossfeedVal, crossfeedLabels.length);
const crossfeedDisplay = crossfeedLabels[crossIdx] ?? "";
return (
<div className="min-h-screen bg-black">
<div style={{
@@ -115,7 +151,7 @@ export default function EffectsPage() {
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
<ChevronLeft size={24} />
</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 })} />
</div>
@@ -123,13 +159,13 @@ export default function EffectsPage() {
{/* ── Style ── */}
<div className="ios-list-group p-4">
<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 })} />
</div>
<button
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
onClick={() => goSelect("选择音效风格", SCENE_OPTIONS, sceneVal, "effect_value")}>
<span className="text-[15px] text-white/55">{SCENE_OPTIONS[sceneVal]}</span>
onClick={() => goSelect(effectText.selectStyleTitle, sceneLabels, sceneVal, "effect_value")}>
<span className="text-[15px] text-white/55">{sceneDisplay}</span>
<ChevronRight size={16} className="ios-chevron" />
</button>
</div>
@@ -138,30 +174,38 @@ export default function EffectsPage() {
<div className="ios-list-group p-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<span className="text-[16px] font-medium text-white"></span>
<span className="text-[16px] font-bold" style={{ color: "#00FFF6" }}>{localWidth}</span>
<span className="text-[16px] font-medium text-white">{effectText.stereoWidth.label}</span>
<span className="text-[16px] font-bold" style={{ color: "#00FFF6" }}>{Math.round(localWidth)}</span>
</div>
<IOSToggle checked={widthOn} onChange={(v) => updateSetting({ width_enable: v ? 1 : 0 })} />
</div>
<CyanSlider value={localWidth} min={0} max={100}
<CyanSlider
value={localWidth}
min={0}
max={100}
step={1}
onChange={(v) => {
isDragging.current = true;
setLocalWidth(v);
updateSetting({ width_value: v });
setTimeout(() => { isDragging.current = false; }, 500);
setLocalWidth(Math.round(v));
}}
onRelease={(v) => {
isDragging.current = false;
const w = Math.round(v);
setLocalWidth(w);
updateSetting({ width_value: w });
}} />
</div>
{/* ── 交叉反馈 ── */}
<div className="ios-list-group p-4">
<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 })} />
</div>
<button
className="flex items-center justify-between w-full active:opacity-70 transition-opacity"
onClick={() => goSelect("选择交叉反馈模式", CROSSFEED_OPTIONS, crossfeedVal, "crossfeed_value")}>
<span className="text-[14px] text-white/45">{CROSSFEED_OPTIONS[crossfeedVal]}</span>
onClick={() => goSelect(effectText.selectCrossfeedTitle, crossfeedLabels, crossfeedVal, "crossfeed_value")}>
<span className="text-[14px] text-white/45">{crossfeedDisplay}</span>
<ChevronRight size={16} className="ios-chevron" />
</button>
</div>
@@ -169,7 +213,7 @@ export default function EffectsPage() {
{/* ── 音调 ── */}
<div className="ios-list-group p-4">
<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 })} />
</div>
@@ -178,20 +222,20 @@ export default function EffectsPage() {
{/* 低音 */}
<div>
<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>
</div>
<CyanSlider
value={localBass}
min={-10}
max={10}
<CyanSlider
value={localBass}
min={-10}
max={10}
onChange={(v) => {
isDraggingBass.current = true;
setLocalBass(v);
}}
onMouseUp={() => {
onRelease={(v) => {
isDraggingBass.current = false;
updateSetting({ color_bass_gain: Math.round(localBass * 10) });
updateSetting({ color_bass_gain: Math.round(v * 10) });
}}
/>
</div>
@@ -199,20 +243,20 @@ export default function EffectsPage() {
{/* 中音 */}
<div>
<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>
</div>
<CyanSlider
value={localMid}
min={-10}
max={10}
<CyanSlider
value={localMid}
min={-10}
max={10}
onChange={(v) => {
isDraggingMid.current = true;
setLocalMid(v);
}}
onMouseUp={() => {
onRelease={(v) => {
isDraggingMid.current = false;
updateSetting({ color_mid_gain: Math.round(localMid * 10) });
updateSetting({ color_mid_gain: Math.round(v * 10) });
}}
/>
</div>
@@ -220,20 +264,20 @@ export default function EffectsPage() {
{/* 高音 */}
<div>
<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>
</div>
<CyanSlider
value={localTreble}
min={-10}
max={10}
<CyanSlider
value={localTreble}
min={-10}
max={10}
onChange={(v) => {
isDraggingTreble.current = true;
setLocalTreble(v);
}}
onMouseUp={() => {
onRelease={(v) => {
isDraggingTreble.current = false;
updateSetting({ color_treble_gain: Math.round(localTreble * 10) });
updateSetting({ color_treble_gain: Math.round(v * 10) });
}}
/>
</div>
@@ -244,7 +288,7 @@ export default function EffectsPage() {
{/* ── 响度 ── */}
<div className="ios-list-group p-4">
<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 })} />
</div>
@@ -253,65 +297,65 @@ export default function EffectsPage() {
{/* 阈值 */}
<div>
<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>
</div>
<CyanSlider
value={localLoudnessThreshold}
min={-30}
max={0}
<CyanSlider
value={localLoudnessThreshold}
min={-30}
max={0}
step={1}
onChange={(v) => {
isDraggingLoudnessThreshold.current = true;
setLocalLoudnessThreshold(v);
}}
onMouseUp={() => {
onRelease={(v) => {
isDraggingLoudnessThreshold.current = false;
updateSetting({ loudness_threshold_gain: Math.round(localLoudnessThreshold) });
updateSetting({ loudness_threshold_gain: Math.round(v) });
}}
/>
</div>
{/* 低音 */}
{/* Loudness bass */}
<div>
<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>
</div>
<CyanSlider
value={localLoudnessBass}
min={0}
max={10}
<CyanSlider
value={localLoudnessBass}
min={0}
max={10}
step={0.1}
onChange={(v) => {
isDraggingLoudnessBass.current = true;
setLocalLoudnessBass(v);
}}
onMouseUp={() => {
onRelease={(v) => {
isDraggingLoudnessBass.current = false;
updateSetting({ loudness_bass_gain: Math.round(localLoudnessBass * 10) });
updateSetting({ loudness_bass_gain: Math.round(v * 10) });
}}
/>
</div>
{/* 高音 */}
{/* Loudness treble */}
<div>
<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>
</div>
<CyanSlider
value={localLoudnessTreble}
min={0}
max={10}
<CyanSlider
value={localLoudnessTreble}
min={0}
max={10}
step={0.1}
onChange={(v) => {
isDraggingLoudnessTreble.current = true;
setLocalLoudnessTreble(v);
}}
onMouseUp={() => {
onRelease={(v) => {
isDraggingLoudnessTreble.current = false;
updateSetting({ loudness_treble_gain: Math.round(localLoudnessTreble * 10) });
updateSetting({ loudness_treble_gain: Math.round(v * 10) });
}}
/>
</div>