diff --git a/client/public/vu/vu1.png b/client/public/vu/vu1.png new file mode 100644 index 0000000..73d4119 Binary files /dev/null and b/client/public/vu/vu1.png differ diff --git a/client/public/vu/vu2.png b/client/public/vu/vu2.png new file mode 100644 index 0000000..5ed73a0 Binary files /dev/null and b/client/public/vu/vu2.png differ diff --git a/client/public/vu/vu3.png b/client/public/vu/vu3.png new file mode 100644 index 0000000..95ce949 Binary files /dev/null and b/client/public/vu/vu3.png differ diff --git a/client/public/vu/vu4.png b/client/public/vu/vu4.png new file mode 100644 index 0000000..00ee2f4 Binary files /dev/null and b/client/public/vu/vu4.png differ diff --git a/client/public/vu/vu5.png b/client/public/vu/vu5.png new file mode 100644 index 0000000..4179f7a Binary files /dev/null and b/client/public/vu/vu5.png differ diff --git a/client/public/vu/vu6.png b/client/public/vu/vu6.png new file mode 100644 index 0000000..8bdd881 Binary files /dev/null and b/client/public/vu/vu6.png differ diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index d15ff0c..7c466ae 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -114,6 +114,10 @@ export interface PeqFilter { export interface PeqState { filters: PeqFilter[]; + peq?: Array<{ + name: string; + filters?: PeqFilter[]; + }>; } // ============================================================ diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index a4a332c..4bd4313 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -13,7 +13,7 @@ import { useDevice } from "@/contexts/DeviceContext"; import { ChevronLeft, ChevronDown, Minus, Plus, Edit3, Headphones } from "lucide-react"; import { useLocation } from "wouter"; -import { useState, useMemo } from "react"; +import { useState, useMemo, useEffect } from "react"; import { cn } from "@/lib/utils"; import BottomNav from "@/components/BottomNav"; import { toast } from "sonner"; @@ -47,14 +47,6 @@ function CyanSlider({ value, min, max, step = 1, onChange }: { /* ── Constants ── */ const FILTER_TYPES = ["PEAK", "LSHELF", "HSHELF", "NOTCH", "LPF", "HPF"]; -const HEADPHONE_MODELS = [ - "Sennheiser HD 650", - "Sennheiser HD 800", - "Sony WH-1000XM5", - "AKG K701", - "Beyerdynamic DT 990", - "Audio-Technica ATH-M50x", -]; /* ── Frequency Response Chart ── */ function FreqChart({ @@ -211,15 +203,69 @@ function freqLabel(f: number) { /* ── Main Component ── */ export default function EQPage() { const [, setLocation] = useLocation(); - const { deviceState, updateSetting } = useDevice(); + const { deviceState, updateSetting, api, isDemoMode } = useDevice(); const eqOn = (deviceState?.peqEnable ?? 0) === 1; const [bands, setBands] = useState(DEFAULT_BANDS); const [selectedBand, setSelectedBand] = useState(3); - const [headphoneIdx, setHeadphoneIdx] = useState(0); + const [headphoneIdx, setHeadphoneIdx] = useState(deviceState?.peqSelect ?? 0); + const [headphoneModels, setHeadphoneModels] = useState([]); const [abMode, setAbMode] = useState<"A" | "B">("A"); const [autoGain, setAutoGain] = useState(false); + // 同步 peqSelect 变化 + useEffect(() => { + if (deviceState?.peqSelect !== undefined) { + setHeadphoneIdx(deviceState.peqSelect); + } + }, [deviceState?.peqSelect]); + + // 加载耳机列表 + useEffect(() => { + async function loadHeadphones() { + if (isDemoMode || !api) { + // 演示模式使用默认列表 + setHeadphoneModels([ + "Sennheiser HD 650", + "Sennheiser HD 800", + "Sony WH-1000XM5", + "AKG K701", + "Beyerdynamic DT 990", + "Audio-Technica ATH-M50x", + ]); + return; + } + try { + const peqState = await api.getPeqState(); + if (peqState.peq && peqState.peq.length > 0) { + setHeadphoneModels(peqState.peq.map(h => h.name)); + } else { + // 如果没有数据,使用默认列表 + setHeadphoneModels([ + "Sennheiser HD 650", + "Sennheiser HD 800", + "Sony WH-1000XM5", + "AKG K701", + "Beyerdynamic DT 990", + "Audio-Technica ATH-M50x", + ]); + } + } catch (error) { + console.error("Failed to load headphone models:", error); + // 出错时使用默认列表 + setHeadphoneModels([ + "Sennheiser HD 650", + "Sennheiser HD 800", + "Sony WH-1000XM5", + "AKG K701", + "Beyerdynamic DT 990", + "Audio-Technica ATH-M50x", + ]); + } + } + loadHeadphones(); + }, [api, isDemoMode]); + const band = bands[selectedBand]; const updateBand = (idx: number, patch: Partial) => { @@ -271,10 +317,10 @@ export default function EQPage() {
@@ -287,7 +333,7 @@ export default function EQPage() {
diff --git a/client/src/pages/EffectsPage.tsx b/client/src/pages/EffectsPage.tsx index 92c28a6..42e0281 100644 --- a/client/src/pages/EffectsPage.tsx +++ b/client/src/pages/EffectsPage.tsx @@ -66,6 +66,9 @@ export default function EffectsPage() { const colorMidGain = (ds?.color_mid_gain ?? 0) / 10; const colorTrebleGain = (ds?.color_treble_gain ?? 0) / 10; const loudnessOn = (ds?.loudness_enable ?? 0) === 1; + const loudnessThreshold = ds?.loudness_threshold_gain ?? 0; + const loudnessBassGain = (ds?.loudness_bass_gain ?? 0) / 10; + const loudnessTrebleGain = (ds?.loudness_treble_gain ?? 0) / 10; const [localBass, setLocalBass] = useState(colorBassGain); const [localMid, setLocalMid] = useState(colorMidGain); @@ -74,11 +77,21 @@ export default function EffectsPage() { const isDraggingMid = useRef(false); const isDraggingTreble = useRef(false); + const [localLoudnessThreshold, setLocalLoudnessThreshold] = useState(loudnessThreshold); + const [localLoudnessBass, setLocalLoudnessBass] = useState(loudnessBassGain); + const [localLoudnessTreble, setLocalLoudnessTreble] = useState(loudnessTrebleGain); + const isDraggingLoudnessThreshold = useRef(false); + const isDraggingLoudnessBass = useRef(false); + const isDraggingLoudnessTreble = useRef(false); + useEffect(() => { if (!isDraggingBass.current) setLocalBass(colorBassGain); if (!isDraggingMid.current) setLocalMid(colorMidGain); if (!isDraggingTreble.current) setLocalTreble(colorTrebleGain); - }, [colorBassGain, colorMidGain, colorTrebleGain]); + if (!isDraggingLoudnessThreshold.current) setLocalLoudnessThreshold(loudnessThreshold); + if (!isDraggingLoudnessBass.current) setLocalLoudnessBass(loudnessBassGain); + if (!isDraggingLoudnessTreble.current) setLocalLoudnessTreble(loudnessTrebleGain); + }, [colorBassGain, colorMidGain, colorTrebleGain, loudnessThreshold, loudnessBassGain, loudnessTrebleGain]); const [localWidth, setLocalWidth] = useState(widthVal); const isDragging = useRef(false); @@ -234,6 +247,76 @@ export default function EffectsPage() { 响度 updateSetting({ loudness_enable: v ? 1 : 0 })} /> + + {loudnessOn && ( +
+ {/* 阈值 */} +
+
+ 阈值 + {localLoudnessThreshold} +
+ { + isDraggingLoudnessThreshold.current = true; + setLocalLoudnessThreshold(v); + }} + onMouseUp={() => { + isDraggingLoudnessThreshold.current = false; + updateSetting({ loudness_threshold_gain: Math.round(localLoudnessThreshold) }); + }} + /> +
+ + {/* 低音 */} +
+
+ 低音 + {localLoudnessBass.toFixed(1)} +
+ { + isDraggingLoudnessBass.current = true; + setLocalLoudnessBass(v); + }} + onMouseUp={() => { + isDraggingLoudnessBass.current = false; + updateSetting({ loudness_bass_gain: Math.round(localLoudnessBass * 10) }); + }} + /> +
+ + {/* 高音 */} +
+
+ 高音 + {localLoudnessTreble.toFixed(1)} +
+ { + isDraggingLoudnessTreble.current = true; + setLocalLoudnessTreble(v); + }} + onMouseUp={() => { + isDraggingLoudnessTreble.current = false; + updateSetting({ loudness_treble_gain: Math.round(localLoudnessTreble * 10) }); + }} + /> +
+
+ )} diff --git a/client/src/pages/SelectPage.tsx b/client/src/pages/SelectPage.tsx index ad289eb..1c1faf0 100644 --- a/client/src/pages/SelectPage.tsx +++ b/client/src/pages/SelectPage.tsx @@ -33,6 +33,7 @@ const KEY_TO_SETTING: Record = { buttonLight: "buttonLight", autoHome: "autoHome", sleep: "sleep", + headphone: "peqSelect", }; // Global state for select page (to avoid URL params) diff --git a/client/src/pages/VUPage.tsx b/client/src/pages/VUPage.tsx index 467fb2f..e2d8f9c 100644 --- a/client/src/pages/VUPage.tsx +++ b/client/src/pages/VUPage.tsx @@ -14,62 +14,7 @@ interface VUStyle { index: number; name: string; description: string; - preview: React.ReactNode; -} - -// Classic analog VU meter preview -function ClassicVUPreview() { - return ( -
- {["L", "R"].map((ch) => ( -
- {/* Amber gradient arc area */} -
-
- {/* Scale marks */} -
- {["-20","-10","-7","-5","-3","0","+3"].map((v) => ( - {v} - ))} -
- {/* Needle */} -
- {/* VU label */} -
VU
- {/* dB label */} -
dB
-
-
-
- ))} -
- ); -} - -// Modern flat LED bar preview -function ModernVUPreview() { - const bars = [0.9, 0.8, 0.75, 0.65, 0.55, 0.45, 0.35, 0.25]; - return ( -
- {["L", "R"].map((ch) => ( -
-
{ch}
-
- {bars.map((h, i) => ( -
- ))} -
-
dB
-
- ))} -
- ); + preview: string; } const VU_STYLES: VUStyle[] = [ @@ -77,13 +22,37 @@ const VU_STYLES: VUStyle[] = [ index: 0, name: "经典复古", description: "琥珀色指针表盘,经典模拟风格", - preview: , + preview: "/vu/vu1.png", }, { index: 1, name: "现代平面", description: "LED 电平条,简洁现代风格", - preview: , + preview: "/vu/vu2.png", + }, + { + index: 2, + name: "数字显示", + description: "数字电平显示,精准直观", + preview: "/vu/vu3.png", + }, + { + index: 3, + name: "条形图", + description: "垂直条形图,专业风格", + preview: "/vu/vu4.png", + }, + { + index: 4, + name: "渐变光谱", + description: "彩虹渐变光谱,炫酷动感", + preview: "/vu/vu5.png", + }, + { + index: 5, + name: "频谱分析", + description: "实时频谱分析,专业监测", + preview: "/vu/vu6.png", }, ]; @@ -116,7 +85,11 @@ export default function VUPage() { style={isActive ? { borderColor: "rgba(0,255,246,0.3)", boxShadow: "0 0 20px rgba(0,255,246,0.08)" } : {}} onClick={() => updateSetting({ vu: style.index })} > - {style.preview} + {style.name}
{/* Selection indicator */}