Initial commit: 提交控制器版本2核心代码
This commit is contained in:
@@ -0,0 +1,244 @@
|
||||
/* ============================================================
|
||||
EFFECTS PAGE — Sound Effect Settings
|
||||
Design: Reference luxsin_x8_音效.jpg
|
||||
Layout:
|
||||
- Page header with global Effect toggle
|
||||
- Glass cards: Style, 声场宽度, 交叉反馈, 音调, 响度
|
||||
All dropdowns → navigate to /select page
|
||||
============================================================ */
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
import { navigateToSelect } from "./SelectPage";
|
||||
|
||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
return (
|
||||
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
<span className="ios-toggle-track"><span className="ios-toggle-thumb" /></span>
|
||||
</label>
|
||||
);
|
||||
}
|
||||
|
||||
function CyanSlider({ value, min, max, step = 0.1, onChange, onMouseUp }: {
|
||||
value: number; min: number; max: number; step?: number; onChange: (v: number) => void; onMouseUp?: () => void;
|
||||
}) {
|
||||
const fillPct = ((value - min) / (max - min)) * 100;
|
||||
return (
|
||||
<div className="relative flex items-center w-full mt-3">
|
||||
<div className="absolute left-0 h-[4px] rounded-full pointer-events-none"
|
||||
style={{ width: `${fillPct}%`, background: "#00FFF6", boxShadow: "0 0 6px rgba(0,255,246,0.5)" }} />
|
||||
<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} />
|
||||
</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 ds = deviceState;
|
||||
const effectOn = (ds?.audio_enable ?? 0) === 1;
|
||||
const widthOn = (ds?.width_enable ?? 0) === 1;
|
||||
const widthVal = ds?.width_value ?? 50;
|
||||
const crossfeedOn = (ds?.crossfeed_enable ?? 0) === 1;
|
||||
const crossfeedVal = ds?.crossfeed_value ?? 0;
|
||||
const sceneOn = (ds?.effect_enable ?? 0) === 1;
|
||||
const sceneVal = ds?.effect_value ?? 0;
|
||||
const colorOn = (ds?.color_enable ?? 0) === 1;
|
||||
const colorBassGain = (ds?.color_bass_gain ?? 0) / 10;
|
||||
const colorMidGain = (ds?.color_mid_gain ?? 0) / 10;
|
||||
const colorTrebleGain = (ds?.color_treble_gain ?? 0) / 10;
|
||||
const loudnessOn = (ds?.loudness_enable ?? 0) === 1;
|
||||
|
||||
const [localBass, setLocalBass] = useState(colorBassGain);
|
||||
const [localMid, setLocalMid] = useState(colorMidGain);
|
||||
const [localTreble, setLocalTreble] = useState(colorTrebleGain);
|
||||
const isDraggingBass = useRef(false);
|
||||
const isDraggingMid = useRef(false);
|
||||
const isDraggingTreble = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDraggingBass.current) setLocalBass(colorBassGain);
|
||||
if (!isDraggingMid.current) setLocalMid(colorMidGain);
|
||||
if (!isDraggingTreble.current) setLocalTreble(colorTrebleGain);
|
||||
}, [colorBassGain, colorMidGain, colorTrebleGain]);
|
||||
|
||||
const [localWidth, setLocalWidth] = useState(widthVal);
|
||||
const isDragging = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging.current) setLocalWidth(widthVal);
|
||||
}, [widthVal]);
|
||||
|
||||
const goSelect = (title: string, options: string[], selected: number, key: string) => {
|
||||
navigateToSelect(title, options, selected, "/effects", key);
|
||||
setLocation("/select");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
<div style={{
|
||||
filter: effectOn ? "none" : "grayscale(1) opacity(0.4)",
|
||||
transition: "filter 0.3s ease-in-out"
|
||||
}}>
|
||||
<div className="page-header">
|
||||
<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>
|
||||
<IOSToggle checked={effectOn} onChange={(v) => updateSetting({ audio_enable: v ? 1 : 0 })} />
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-32 pt-3 space-y-3">
|
||||
{/* ── 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>
|
||||
<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>
|
||||
<ChevronRight size={16} className="ios-chevron" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── 声场宽度 ── */}
|
||||
<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>
|
||||
</div>
|
||||
<IOSToggle checked={widthOn} onChange={(v) => updateSetting({ width_enable: v ? 1 : 0 })} />
|
||||
</div>
|
||||
<CyanSlider value={localWidth} min={0} max={100}
|
||||
onChange={(v) => {
|
||||
isDragging.current = true;
|
||||
setLocalWidth(v);
|
||||
updateSetting({ width_value: v });
|
||||
setTimeout(() => { isDragging.current = false; }, 500);
|
||||
}} />
|
||||
</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>
|
||||
<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>
|
||||
<ChevronRight size={16} className="ios-chevron" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* ── 音调 ── */}
|
||||
<div className="ios-list-group p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[16px] font-medium text-white">音调</span>
|
||||
<IOSToggle checked={colorOn} onChange={(v) => updateSetting({ color_enable: v ? 1 : 0 })} />
|
||||
</div>
|
||||
|
||||
{colorOn && (
|
||||
<div className="space-y-4 mt-4">
|
||||
{/* 低音 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px] text-white/60">低音</span>
|
||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localBass.toFixed(1)}</span>
|
||||
</div>
|
||||
<CyanSlider
|
||||
value={localBass}
|
||||
min={-10}
|
||||
max={10}
|
||||
onChange={(v) => {
|
||||
isDraggingBass.current = true;
|
||||
setLocalBass(v);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
isDraggingBass.current = false;
|
||||
updateSetting({ color_bass_gain: Math.round(localBass * 10) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 中音 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px] text-white/60">中音</span>
|
||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localMid.toFixed(1)}</span>
|
||||
</div>
|
||||
<CyanSlider
|
||||
value={localMid}
|
||||
min={-10}
|
||||
max={10}
|
||||
onChange={(v) => {
|
||||
isDraggingMid.current = true;
|
||||
setLocalMid(v);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
isDraggingMid.current = false;
|
||||
updateSetting({ color_mid_gain: Math.round(localMid * 10) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 高音 */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px] text-white/60">高音</span>
|
||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localTreble.toFixed(1)}</span>
|
||||
</div>
|
||||
<CyanSlider
|
||||
value={localTreble}
|
||||
min={-10}
|
||||
max={10}
|
||||
onChange={(v) => {
|
||||
isDraggingTreble.current = true;
|
||||
setLocalTreble(v);
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
isDraggingTreble.current = false;
|
||||
updateSetting({ color_treble_gain: Math.round(localTreble * 10) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 响度 ── */}
|
||||
<div className="ios-list-group p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-[16px] font-medium text-white">响度</span>
|
||||
<IOSToggle checked={loudnessOn} onChange={(v) => updateSetting({ loudness_enable: v ? 1 : 0 })} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<BottomNav />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user