Update locale files for English, Traditional Chinese, and Simplified Chinese to include new confirmation prompts for enabling audio effects; refactor EffectsPage and EQPage to integrate FeatureGate for managing effect states and user interactions.

This commit is contained in:
yangy
2026-05-19 13:33:10 +08:00
parent edd677c79d
commit 21839ff339
7 changed files with 315 additions and 43 deletions
+140 -21
View File
@@ -52,6 +52,39 @@ function volToDB(v: number) {
return `${db >= 0 ? "+" : ""}${db.toFixed(1)}dB`;
}
/** 耳机插孔彩蛋:大孔 → 上小孔 → 下小孔 */
const JACK_EGG_SEQUENCE = ["lg", "smTop", "smBot"] as const;
type JackId = (typeof JACK_EGG_SEQUENCE)[number];
const VU_BAR_COUNT = 20;
const EGG_DURATION_MS = 4200;
const EGG_BEAT_MS = 130;
const JACK_SEQUENCE_TIMEOUT_MS = 4000;
/** 旋律主音在 20 段 VU 上的位置(约 4.2s */
const EGG_MELODY_LEAD = [
0, 2, 4, 6, 8, 10, 12, 14, 16, 18, 19, 17, 15, 13, 11, 9, 7, 5, 3, 1,
0, 4, 8, 12, 16, 19, 16, 12, 8, 4, 0, 2, 5, 9, 13, 17, 19, 14, 10, 6, 2,
];
function idleVuBarHeights(): number[] {
return Array.from({ length: VU_BAR_COUNT }, (_, i) =>
Math.max(0.12, Math.sin((i / VU_BAR_COUNT) * Math.PI) * 0.65 + 0.2),
);
}
function vuHeightsForMelodyBeat(lead: number, pulse: number): number[] {
return Array.from({ length: VU_BAR_COUNT }, (_, i) => {
const dist = Math.abs(i - lead);
let amp = 0.14;
if (dist === 0) amp = 0.96;
else if (dist === 1) amp = 0.74;
else if (dist === 2) amp = 0.5;
else if (dist === 3) amp = 0.3;
return Math.min(1, amp * (0.82 + pulse * 0.18));
});
}
// ── iOS-style Toggle ──
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
return (
@@ -106,6 +139,11 @@ export default function Home() {
const effectLastTapRef = useRef<number | null>(null);
const effectNavigateTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const [powerConfirmOpen, setPowerConfirmOpen] = useState(false);
const [easterEggActive, setEasterEggActive] = useState(false);
const [vuHeights, setVuHeights] = useState<number[]>(idleVuBarHeights);
const jackProgressRef = useRef(0);
const jackResetTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const eggRafRef = useRef<number | null>(null);
const homeText = useMemo((): HomeLocale => {
if (!deviceState) return {} as HomeLocale;
@@ -174,10 +212,15 @@ export default function Home() {
hpEqNavigateTimerRef.current = setTimeout(() => {
hpEqNavigateTimerRef.current = null;
hpEqLastTapRef.current = null;
const on = (deviceState?.peqEnable ?? 0) === 1;
void updateSetting({ peqEnable: on ? 0 : 1 });
const bypassOn = (deviceState?.dsp_enable ?? 0) === 0;
if (bypassOn) {
void updateSetting({ peqEnable: 1, dsp_enable: 1 });
} else {
const on = (deviceState?.peqEnable ?? 0) === 1;
void updateSetting({ peqEnable: on ? 0 : 1 });
}
}, HP_EQ_DOUBLE_CLICK_MS);
}, [deviceState?.peqEnable, updateSetting, setLocation]);
}, [deviceState?.peqEnable, deviceState?.dsp_enable, updateSetting, setLocation]);
/** Effect 圆钮:单击短延迟后切换 audio_enable280ms 内第二次点击则进入 /effects */
const handleEffectCircleClick = useCallback(() => {
@@ -197,10 +240,15 @@ export default function Home() {
effectNavigateTimerRef.current = setTimeout(() => {
effectNavigateTimerRef.current = null;
effectLastTapRef.current = null;
const on = (deviceState?.audio_enable ?? 0) === 1;
void updateSetting({ audio_enable: on ? 0 : 1 });
const bypassOn = (deviceState?.dsp_enable ?? 0) === 0;
if (bypassOn) {
void updateSetting({ audio_enable: 1, dsp_enable: 1 });
} else {
const on = (deviceState?.audio_enable ?? 0) === 1;
void updateSetting({ audio_enable: on ? 0 : 1 });
}
}, HP_EQ_DOUBLE_CLICK_MS);
}, [deviceState?.audio_enable, updateSetting, setLocation]);
}, [deviceState?.audio_enable, deviceState?.dsp_enable, updateSetting, setLocation]);
useEffect(() => {
return () => {
@@ -209,6 +257,58 @@ export default function Home() {
};
}, []);
const triggerJackEasterEgg = useCallback(() => {
setEasterEggActive(true);
jackProgressRef.current = 0;
const start = performance.now();
const tick = (now: number) => {
const elapsed = now - start;
if (elapsed >= EGG_DURATION_MS) {
setEasterEggActive(false);
setVuHeights(idleVuBarHeights());
eggRafRef.current = null;
return;
}
const beat = Math.floor(elapsed / EGG_BEAT_MS) % EGG_MELODY_LEAD.length;
const pulse = 0.5 + 0.5 * Math.sin(elapsed * 0.02);
setVuHeights(vuHeightsForMelodyBeat(EGG_MELODY_LEAD[beat], pulse));
eggRafRef.current = requestAnimationFrame(tick);
};
if (eggRafRef.current !== null) cancelAnimationFrame(eggRafRef.current);
eggRafRef.current = requestAnimationFrame(tick);
}, []);
const onJackClick = useCallback(
(jack: JackId) => {
if (easterEggActive) return;
const expected = JACK_EGG_SEQUENCE[jackProgressRef.current];
if (jack === expected) {
jackProgressRef.current += 1;
if (jackProgressRef.current >= JACK_EGG_SEQUENCE.length) {
triggerJackEasterEgg();
}
} else {
jackProgressRef.current = jack === JACK_EGG_SEQUENCE[0] ? 1 : 0;
}
if (jackResetTimerRef.current) clearTimeout(jackResetTimerRef.current);
jackResetTimerRef.current = setTimeout(() => {
jackProgressRef.current = 0;
}, JACK_SEQUENCE_TIMEOUT_MS);
},
[easterEggActive, triggerJackEasterEgg],
);
useEffect(() => {
return () => {
if (jackResetTimerRef.current) clearTimeout(jackResetTimerRef.current);
if (eggRafRef.current !== null) cancelAnimationFrame(eggRafRef.current);
};
}, []);
if (!isConnected) return <ConnectScreen />;
const ds = deviceState!;
@@ -305,34 +405,43 @@ export default function Home() {
{/* Left ports */}
<div className="px-4">
<div className="flex items-center gap-3">
{/* Big hole (left) */}
<div
className="w-7 h-7 rounded-full border-2 bg-black/25 flex items-center justify-center"
{/* Big hole (left) — 彩蛋第 1 步 */}
<button
type="button"
className="w-7 h-7 rounded-full border-2 bg-black/25 flex items-center justify-center cursor-pointer active:scale-95 transition-transform"
style={{
borderColor: "rgba(245, 158, 11, 0.75)",
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.35)",
}}
aria-label="Headphone jack"
onClick={() => onJackClick("lg")}
>
<div
className="w-2 h-2 rounded-full"
className="w-2 h-2 rounded-full pointer-events-none"
style={{ background: "rgba(196, 126, 9, 0.9)" }}
/>
</div>
</button>
{/* Two small holes (right, stacked) */}
<div className="flex flex-col gap-2">
<div
className="w-5 h-5 rounded-full border-2 bg-black/25"
<button
type="button"
className="w-5 h-5 rounded-full border-2 bg-black/25 cursor-pointer active:scale-95 transition-transform"
style={{
borderColor: "rgba(245, 158, 11, 0.65)",
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.35)",
}}
aria-label="Headphone jack"
onClick={() => onJackClick("smTop")}
/>
<div
className="w-5 h-5 rounded-full border-2 bg-black/25"
<button
type="button"
className="w-5 h-5 rounded-full border-2 bg-black/25 cursor-pointer active:scale-95 transition-transform"
style={{
borderColor: "rgba(245, 158, 11, 0.65)",
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.35)",
}}
aria-label="Headphone jack"
onClick={() => onJackClick("smBot")}
/>
</div>
</div>
@@ -345,17 +454,27 @@ export default function Home() {
<span className="text-[9px] text-white/50">{volToDB(localVol)}</span>
</div>
{/* Mini VU bars */}
<div className="flex gap-0.5 px-2 pt-1 pb-2 h-16 items-end">
{Array.from({ length: 20 }).map((_, i) => {
const h = Math.max(0.1, Math.sin((i / 20) * Math.PI) * 0.8 + Math.random() * 0.2);
<div
className={cn(
"flex gap-0.5 px-2 pt-1 pb-2 h-16 items-end",
easterEggActive && "brightness-110",
)}
>
{vuHeights.map((h, i) => {
const isHot = i > 16;
return (
<div key={i} className="flex-1 rounded-sm"
<div
key={i}
className="flex-1 rounded-sm"
style={{
height: `${h * 100}%`,
background: isHot ? "#ef4444" : i > 13 ? "#f59e0b" : "#00FFF6",
opacity: 0.7,
}} />
opacity: easterEggActive ? 0.95 : 0.7,
transition: easterEggActive
? "height 90ms ease-out, opacity 120ms ease-out"
: "height 200ms ease-out",
}}
/>
);
})}
</div>