From 2217adfec9a2121ae6cbca939c208f231398608e Mon Sep 17 00:00:00 2001 From: eafonyang Date: Thu, 24 Sep 2026 18:34:55 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20bh9=20=E5=AF=B9=E6=8E=A5?= =?UTF-8?q?=20webui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/components/BottomNav.tsx | 14 +- client/src/config/firmwareFeatures.ts | 4 +- client/src/contexts/DeviceContext.tsx | 10 +- client/src/lib/luxsinApi.ts | 30 ++++- client/src/locales/data-de.json | 33 ++++- client/src/locales/data-en.json | 33 ++++- client/src/locales/data-es.json | 33 ++++- client/src/locales/data-fr.json | 33 ++++- client/src/locales/data-zh-HK.json | 33 ++++- client/src/locales/data-zh.json | 33 ++++- client/src/pages/AudioPage.tsx | 113 +++++----------- client/src/pages/Dashboard.tsx | 47 +++---- client/src/pages/Home.tsx | 185 ++++++-------------------- client/src/pages/IOPage.tsx | 81 ++--------- client/src/pages/SelectPage.tsx | 4 + client/src/pages/SystemPage.tsx | 41 +----- 16 files changed, 353 insertions(+), 374 deletions(-) diff --git a/client/src/components/BottomNav.tsx b/client/src/components/BottomNav.tsx index e72dfda..4d6809d 100644 --- a/client/src/components/BottomNav.tsx +++ b/client/src/components/BottomNav.tsx @@ -1,7 +1,7 @@ /* ============================================================ BOTTOM NAV — Luxsin X9 Controller (Mobile) Design: iOS/Android App-style bottom tab bar - 5 tabs — labels from locales/bottomNav, language = deviceState.language (同系统页) + 3 个入口(社区/主页/我的 AI),文案来自 locales/bottomNav,语言跟随 deviceState.language - 社区:移动端进入 /community 引导复制链接;桌面(lg+)点击仍 window.open 论坛 - Active tab: cyan icon + top indicator bar - Inactive: muted icon + label @@ -13,13 +13,7 @@ import { useIsLgUp } from "@/hooks/useIsLgUp"; import localeZh from "@/locales/data-zh.json"; import localeEn from "@/locales/data-en.json"; import { resolveLocalePack } from "@/locales/resolveLocale"; -import { - Activity, - Bot, - Home, - MessageSquare, - Sliders, -} from "lucide-react"; +import { Bot, Home, MessageSquare } from "lucide-react"; import { Link, useLocation } from "wouter"; import { useMemo } from "react"; import { bottomNavInnerClass, bottomNavOuterClass } from "@/lib/appShell"; @@ -36,9 +30,7 @@ const FORUM_URL = "https://forum.zidoo.tv/index.php"; const NAV_ITEMS: NavItem[] = [ { path: "/community", icon: MessageSquare, labelKey: "community" }, - { path: "/eq", icon: Sliders, labelKey: "hpEq" }, { path: "/", icon: Home, labelKey: "home" }, - { path: "/effects", icon: Activity, labelKey: "effects" }, { path: "/ai", icon: Bot, labelKey: "myAi" }, ]; @@ -66,7 +58,7 @@ export default function BottomNav() { paddingBottom: "env(safe-area-inset-bottom, 8px)", }} > -
+
{NAV_ITEMS.map((item) => { const isActive = item.path === "/ai" diff --git a/client/src/config/firmwareFeatures.ts b/client/src/config/firmwareFeatures.ts index 91f9925..264957e 100644 --- a/client/src/config/firmwareFeatures.ts +++ b/client/src/config/firmwareFeatures.ts @@ -5,8 +5,8 @@ * Adjust thresholds here instead of hardcoding in page components. */ export const FIRMWARE_FEATURE_MIN_VERSION = { - /** Boot sound: indices 7–10 (-35…-50 dB); legacy firmware caps at index 6. */ - bootSoundExtendedSteps: 26, + /** BH9 与 BD9 一致,默认提供开机音量索引 7–10(-35…-50 dB),不按固件版本隐藏。 */ + bootSoundExtendedSteps: 0, /** Subwoofer LPF/HPF full-range toggles on the Effects page. */ subwooferFullRange: 2001, /** PEQ preset rename (peqRename) and reorder (peqSoft) in manage dialog. */ diff --git a/client/src/contexts/DeviceContext.tsx b/client/src/contexts/DeviceContext.tsx index 527f9cf..52a572f 100644 --- a/client/src/contexts/DeviceContext.tsx +++ b/client/src/contexts/DeviceContext.tsx @@ -1,6 +1,7 @@ import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from "react"; import { DeviceState, + getHomeVolumeMax, LuxsinAPI, MOCK_DEVICE_STATE, MOCK_PEQ_STATE, @@ -211,10 +212,15 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { }, [api, isDemoMode, applyPeqFiltersToState]); // Optimistic setters + const maxVolumeRef = useRef(getHomeVolumeMax(deviceState)); + maxVolumeRef.current = getHomeVolumeMax(deviceState); const setVolume = useCallback((v: number) => { - setDeviceState(prev => prev ? { ...prev, volume: v } : prev); + if (!Number.isFinite(v)) return; + // 拖动监听器也读取最新上限,乐观状态与实际下发值保持一致。 + const volume = Math.max(0, Math.min(maxVolumeRef.current, Math.round(v))); + setDeviceState(prev => prev ? { ...prev, volume } : prev); if (!isDemoMode && api) { - api.setVolume(v).catch(console.error); + api.setVolume(volume).catch(console.error); } }, [api, isDemoMode]); diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index 0d62bd8..800d0be 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -21,6 +21,28 @@ export function volumeToDb(vol: number): number { return (vol - 200) / 2; } +/** 耳机音量档位:缺失或非法值回退 0,其余截断并限制到合法范围。 */ +export function normalizeHpVolumeIndex(value: unknown, maxIndex: 7 | 10): number { + const index = Number(value ?? 0); + return Number.isFinite(index) ? Math.max(0, Math.min(maxIndex, Math.trunc(index))) : 0; +} + +/** 仅耳机输出(协议索引 2)应用最大音量;每档 5dB 对应 10 个设备音量单位。 */ +export function getHomeVolumeMax( + state: Pick | null | undefined, +): number { + if (state?.output !== 2) return 200; + return dbToVolume(-5 * normalizeHpVolumeIndex(state.hpMaxVolume, 10)); +} + +/** 耳机输出且设置低于 0dB 时返回提示值,其他情况不显示提示。 */ +export function getHeadphoneMaxVolumeDb( + state: Pick | null | undefined, +): number | null { + if (state?.output !== 2 || normalizeHpVolumeIndex(state.hpMaxVolume, 10) === 0) return null; + return volumeToDb(getHomeVolumeMax(state)); +} + export function decodeCustomBase64(encoded: string): string { let translated = ""; for (let i = 0; i < encoded.length; i++) { @@ -95,6 +117,10 @@ export interface DeviceState { dacGain: number; dacArc: number; dacImpedance: number; + /** 耳机最大音量:0 = 0dB,1 = -5dB … 10 = -50dB;旧固件可能不返回。 */ + hpMaxVolume?: number; + /** 耳机启动音量:0 = 默认,1 = -20dB … 7 = -50dB;旧固件可能不返回。 */ + hpStartupVolume?: number; dacVolumeDirect: number; analogGain: number; effect_enable: number; @@ -261,7 +287,7 @@ export interface PeqSoftPayload { // ============================================================ // Input/Output Labels // ============================================================ -export const INPUT_LABELS = ["USB", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA"]; +export const INPUT_LABELS = ["XLR", "RCA"]; export const OUTPUT_LABELS = ["XLR", "RCA", "Headset", "XLR/RCA"]; /** Device `input` index for Bluetooth source. */ @@ -582,6 +608,8 @@ export const MOCK_DEVICE_STATE: DeviceState = { dacGain: 0, dacArc: 0, dacImpedance: 0, + hpMaxVolume: 0, + hpStartupVolume: 0, dacVolumeDirect: 0, analogGain: 0, effect_enable: 0, diff --git a/client/src/locales/data-de.json b/client/src/locales/data-de.json index c0a479a..6eae18e 100644 --- a/client/src/locales/data-de.json +++ b/client/src/locales/data-de.json @@ -28,6 +28,7 @@ "volumePassthroughBlocked": "Lautstärke-Durchleitung ist aktiv ({mode}). Kopfhörerlautstärke ist fest. Schalten Sie die Durchleitung in den Audioeinstellungen aus.", "volumePassthroughMode0dB": "0dB", "volumePassthroughMode12dB": "-12dB", + "hpMaxVolumeHint": "Die maximale Kopfhörerlautstärke ist eingestellt. Maximum: {db}dB", "powerOffToastDemo": "Demomodus: Vom Gerät getrennt.", "powerOffToastSent": "Abschaltbefehl gesendet.", "doubleClickHint": "Doppelklick zum Eintreten", @@ -56,7 +57,7 @@ "pageTitle": "I/O", "inputSection": "Eingang", "outputSection": "Ausgang", - "inputOptions": ["USB-B", "USB-C", "Koaxial", "Optisch", "Bluetooth", "IIS", "RCA", "Lokaler Eingang"], + "inputOptions": ["Symmetrisch XLR", "Analog RCA"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "Kopfhörer"], "input": { "coaxial": "KOAXIAL", @@ -389,6 +390,7 @@ "dac": { "sectionBalanceSensitivity": "Balance & Empfindlichkeit", "sectionOutput": "Ausgangsmerkmale", + "sectionHpSettings": "Kopfhörer-Einstellungen", "sectionIisSettings": "IIS-Einstellungen", "sectionSystemInterface": "System & Schnittstelle", "balance": "Links-Rechts-Balance", @@ -435,6 +437,35 @@ ] }, "volumeStep": "Lautstärkestufe", + "hpMaxVolume": { + "label": "Max. Kopfhörerlautstärke", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Kopfhörer-Startlautstärke", + "options": [ + { "index": 0, "label": "Standard" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, "bootVolume": { "label": "Startlautstärke", "options": [ diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index edba42f..548f7e6 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -28,6 +28,7 @@ "volumePassthroughBlocked": "Volume passthrough is active ({mode}). Headphone volume is fixed. Turn passthrough off in Audio settings.", "volumePassthroughMode0dB": "0dB", "volumePassthroughMode12dB": "-12dB", + "hpMaxVolumeHint": "Headphone max volume is set. Maximum: {db}dB", "powerOffToastDemo": "Demo mode: disconnected from the device.", "powerOffToastSent": "Shutdown command sent.", "doubleClickHint": "Double-click to enter", @@ -56,7 +57,7 @@ "pageTitle": "I/O", "inputSection": "Input", "outputSection": "Output", - "inputOptions": ["USB-B", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA", "Local Input"], + "inputOptions": ["Balanced XLR", "Analog RCA"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "Headset"], "input": { "coaxial": "COAXIAL", @@ -389,6 +390,7 @@ "dac": { "sectionBalanceSensitivity": "Balance & sensitivity", "sectionOutput": "Output characteristics", + "sectionHpSettings": "Headphone settings", "sectionIisSettings": "IIS settings", "sectionSystemInterface": "System & interface", "balance": "Left-right balance", @@ -435,6 +437,35 @@ ] }, "volumeStep": "Volume step", + "hpMaxVolume": { + "label": "Max headphone volume", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Headphone startup volume", + "options": [ + { "index": 0, "label": "Default" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, "bootVolume": { "label": "Boot volume", "options": [ diff --git a/client/src/locales/data-es.json b/client/src/locales/data-es.json index 7fd1a53..2430cee 100644 --- a/client/src/locales/data-es.json +++ b/client/src/locales/data-es.json @@ -28,6 +28,7 @@ "volumePassthroughBlocked": "El paso de volumen está activo ({mode}). El volumen de los auriculares es fijo. Desactive el paso en la configuración de audio.", "volumePassthroughMode0dB": "0dB", "volumePassthroughMode12dB": "-12dB", + "hpMaxVolumeHint": "Se ha establecido el volumen máximo de los auriculares. Máximo: {db}dB", "powerOffToastDemo": "Modo demo: desconectado del dispositivo.", "powerOffToastSent": "Comando de apagado enviado.", "doubleClickHint": "Doble clic para entrar", @@ -56,7 +57,7 @@ "pageTitle": "E/S", "inputSection": "Entrada", "outputSection": "Salida", - "inputOptions": ["USB-B", "USB-C", "Coaxial", "Óptico", "Bluetooth", "IIS", "RCA", "Entrada local"], + "inputOptions": ["XLR balanceado", "RCA analógico"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "Auriculares"], "input": { "coaxial": "COAXIAL", @@ -389,6 +390,7 @@ "dac": { "sectionBalanceSensitivity": "Balance y sensibilidad", "sectionOutput": "Características de salida", + "sectionHpSettings": "Ajustes de auriculares", "sectionIisSettings": "Configuración IIS", "sectionSystemInterface": "Sistema e interfaz", "balance": "Balance izquierda-derecha", @@ -435,6 +437,35 @@ ] }, "volumeStep": "Paso de volumen", + "hpMaxVolume": { + "label": "Volumen máx. de auriculares", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Volumen de inicio de auriculares", + "options": [ + { "index": 0, "label": "Predeterminado" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, "bootVolume": { "label": "Volumen de inicio", "options": [ diff --git a/client/src/locales/data-fr.json b/client/src/locales/data-fr.json index 5665630..9e86a27 100644 --- a/client/src/locales/data-fr.json +++ b/client/src/locales/data-fr.json @@ -28,6 +28,7 @@ "volumePassthroughBlocked": "Le passage de volume est actif ({mode}). Le volume du casque est fixe. Désactivez le passage dans les réglages audio.", "volumePassthroughMode0dB": "0dB", "volumePassthroughMode12dB": "-12dB", + "hpMaxVolumeHint": "Le volume maximal du casque est réglé. Maximum : {db}dB", "powerOffToastDemo": "Mode démo : déconnecté de l'appareil.", "powerOffToastSent": "Commande d'extinction envoyée.", "doubleClickHint": "Double-clic pour entrer", @@ -56,7 +57,7 @@ "pageTitle": "E/S", "inputSection": "Entrée", "outputSection": "Sortie", - "inputOptions": ["USB-B", "USB-C", "Coaxial", "Optique", "Bluetooth", "IIS", "RCA", "Entrée locale"], + "inputOptions": ["XLR symétrique", "RCA analogique"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "Casque"], "input": { "coaxial": "COAXIAL", @@ -389,6 +390,7 @@ "dac": { "sectionBalanceSensitivity": "Balance et sensibilité", "sectionOutput": "Caractéristiques de sortie", + "sectionHpSettings": "Réglages du casque", "sectionIisSettings": "Paramètres IIS", "sectionSystemInterface": "Système et interface", "balance": "Balance gauche-droite", @@ -435,6 +437,35 @@ ] }, "volumeStep": "Pas de volume", + "hpMaxVolume": { + "label": "Volume max. du casque", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "Volume de démarrage du casque", + "options": [ + { "index": 0, "label": "Par défaut" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, "bootVolume": { "label": "Volume de démarrage", "options": [ diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index cb0a393..0073e81 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -28,6 +28,7 @@ "volumePassthroughBlocked": "目前為音量直通模式({mode}),無法調節耳機音量。請在音訊設定中關閉直通或改為「關閉」。", "volumePassthroughMode0dB": "0dB", "volumePassthroughMode12dB": "-12dB", + "hpMaxVolumeHint": "已設定耳機最大音量,最大音量為:{db}dB", "powerOffToastDemo": "演示模式:已中斷與裝置的連線", "powerOffToastSent": "已傳送關機指令", "doubleClickHint": "按兩下進入", @@ -56,7 +57,7 @@ "pageTitle": "輸入/輸出", "inputSection": "輸入", "outputSection": "輸出", - "inputOptions": ["USB-B", "USB-C", "同軸", "光纖", "藍牙", "IIS", "類比RCA", "本地輸入"], + "inputOptions": ["平衡 XLR", "類比 RCA"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "耳機"], "input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" }, "output": { "headset": "耳機" } @@ -383,6 +384,7 @@ "dac": { "sectionBalanceSensitivity": "平衡與靈敏度", "sectionOutput": "輸出特性", + "sectionHpSettings": "耳機設定", "sectionIisSettings": "IIS設定", "sectionSystemInterface": "系統與介面", "balance": "左右平衡", @@ -429,6 +431,35 @@ ] }, "volumeStep": "音量級距", + "hpMaxVolume": { + "label": "耳機最大音量", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "耳機啟動音量", + "options": [ + { "index": 0, "label": "預設" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, "bootVolume": { "label": "開機音量", "options": [ diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index a63a1d4..4c4602e 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -28,6 +28,7 @@ "volumePassthroughBlocked": "当前为音量直通模式({mode}),无法调节耳机音量。请在音频设置中关闭直通或改为「关闭」。", "volumePassthroughMode0dB": "0dB", "volumePassthroughMode12dB": "-12dB", + "hpMaxVolumeHint": "已设置耳机最大音量,最大音量为:{db}dB", "powerOffToastDemo": "演示模式:已断开与设备的连接", "powerOffToastSent": "已发送关机指令", "doubleClickHint": "双击进入", @@ -56,7 +57,7 @@ "pageTitle": "输入输出", "inputSection": "输入", "outputSection": "输出", - "inputOptions": ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "模拟RCA", "本地输入"], + "inputOptions": ["平衡 XLR", "模拟 RCA"], "outputOptions": ["XLR", "RCA", "XLR/RCA", "耳机"], "input": { "coaxial": "同轴", "optical": "光纤", "bluetooth": "蓝牙", "rca": "模拟RCA" }, "output": { "headset": "耳机" } @@ -383,6 +384,7 @@ "dac": { "sectionBalanceSensitivity": "平衡与灵敏度", "sectionOutput": "输出特性", + "sectionHpSettings": "耳机设定", "sectionIisSettings": "IIS设置", "sectionSystemInterface": "系统与接口", "balance": "左右平衡", @@ -429,6 +431,35 @@ ] }, "volumeStep": "音量幅度", + "hpMaxVolume": { + "label": "耳机最大音量", + "options": [ + { "index": 0, "label": "0dB" }, + { "index": 1, "label": "-5dB" }, + { "index": 2, "label": "-10dB" }, + { "index": 3, "label": "-15dB" }, + { "index": 4, "label": "-20dB" }, + { "index": 5, "label": "-25dB" }, + { "index": 6, "label": "-30dB" }, + { "index": 7, "label": "-35dB" }, + { "index": 8, "label": "-40dB" }, + { "index": 9, "label": "-45dB" }, + { "index": 10, "label": "-50dB" } + ] + }, + "hpStartupVolume": { + "label": "耳机启动音量", + "options": [ + { "index": 0, "label": "默认" }, + { "index": 1, "label": "-20dB" }, + { "index": 2, "label": "-25dB" }, + { "index": 3, "label": "-30dB" }, + { "index": 4, "label": "-35dB" }, + { "index": 5, "label": "-40dB" }, + { "index": 6, "label": "-45dB" }, + { "index": 7, "label": "-50dB" } + ] + }, "bootVolume": { "label": "开机音量", "options": [ diff --git a/client/src/pages/AudioPage.tsx b/client/src/pages/AudioPage.tsx index a67bfbc..e78e451 100644 --- a/client/src/pages/AudioPage.tsx +++ b/client/src/pages/AudioPage.tsx @@ -2,10 +2,10 @@ AUDIO PAGE — Audio Settings Design: Reference luxsin_x8_音频设置.png Sections: - - 平衡与灵敏度: 左右平衡, VU灵敏度, 模拟输入增益 sliders - - 输出特性: 滤波特性, 耳机增益, 音量幅度 - - IIS设置: IIS静音电平, IIS模式 - - 系统与接口: 开机音量, XLR极性 + - 平衡与灵敏度: 左右平衡, VU灵敏度 sliders + - 输出特性: 前级输出音量直通, 音量幅度 + - 耳机设定: 耳机增益, 自动检测耳机阻抗, 耳机最大音量, 耳机启动音量 + - 系统与接口: 开机音量 All list rows → navigate to /select page ============================================================ */ import { useDevice } from "@/contexts/DeviceContext"; @@ -17,6 +17,7 @@ import { navigateToSelect } from "./SelectPage"; import { getBootSoundMaxIndex, normalizeBootSound, + normalizeHpVolumeIndex, parseFirmwareVersion, readBootSoundFromState, } from "@/lib/luxsinApi"; @@ -111,22 +112,19 @@ type LocaleDac = { dac?: { sectionBalanceSensitivity?: string; sectionOutput?: string; - sectionIisSettings?: string; + sectionHpSettings?: string; sectionSystemInterface?: string; balance?: string; sensitivity?: string; - analogInputGain?: string; autoImpedanceDetection?: string; filters?: { label?: string; options?: Array<{ index: number; label: string }> }; dacGain?: { label?: string; options?: Array<{ index: number; label: string }> }; + hpMaxVolume?: { label?: string; options?: Array<{ index: number; label: string }> }; + hpStartupVolume?: { label?: string; options?: Array<{ index: number; label: string }> }; dacVolumeDirect?: { label?: string; options?: Array<{ index: number; label: string }> }; dacArc?: { label?: string; options?: Array<{ index: number; label: string }> }; volumeStep?: string; bootVolume?: string | { label?: string; options?: Array<{ index: number; label: string }> }; - xlr?: { label?: string; options?: Array<{ index: number; label: string }> }; - mutePolar?: { label?: string; options?: Array<{ index: number; label: string }> }; - IISMode?: { label?: string; options?: Array<{ index: number; label: string }> }; - uacVer?: { label?: string; options?: Array<{ index: number; label: string }> }; }; home?: { audioSet?: string }; }; @@ -191,22 +189,15 @@ export default function AudioPage() { const balanceVal = ds?.balance !== undefined ? ds.balance / 10 : 0; // vuSensor 从接口获取的是放大 2 倍的值,需要除以 2 显示(例如:20 → +10dB) const vuSensVal = vuSensor !== undefined ? vuSensor / 2 : 0; - // analogGain 从接口获取的是放大 2 倍的值,需要除以 2 显示(例如:-6 → -3dB) - const analogGainVal = ds?.analogGain !== undefined ? ds.analogGain / 2 : 0; const bootSoundFromApi = readBootSoundFromState(ds); const [localBootSound, setLocalBootSound] = useState(bootSoundFromApi); const [localBalance, setLocalBalance] = useState(balanceVal); const [localVuSens, setLocalVuSens] = useState(0); - const [localAnalogGain, setLocalAnalogGain] = useState(analogGainVal); const isBalanceDragging = useRef(false); const isVuDragging = useRef(false); - const isAnalogGainDragging = useRef(false); useEffect(() => { if (!isBalanceDragging.current) setLocalBalance(balanceVal); }, [balanceVal]); useEffect(() => { if (!isVuDragging.current) setLocalVuSens(vuSensVal); }, [vuSensVal]); - useEffect(() => { - if (!isAnalogGainDragging.current) setLocalAnalogGain(analogGainVal); - }, [analogGainVal]); useEffect(() => { setLocalBootSound(readBootSoundFromState(ds)); }, [ds?.bootSound]); @@ -225,14 +216,12 @@ export default function AudioPage() { const gainIdx = ds?.dacGain ?? 0; const autoImpedanceOn = (ds?.dacImpedance ?? 0) === 1; + const hpMaxVolumeIdx = normalizeHpVolumeIndex(ds?.hpMaxVolume, 10); + const hpStartupVolumeIdx = normalizeHpVolumeIndex(ds?.hpStartupVolume, 7); const dacVolumeDirectIdx = ds?.dacVolumeDirect ?? 0; const dacArcIdx = ds?.dacArc ?? 0; const stepIdx = ds?.soundStep ?? 1; const filterIdx = ds?.pcm ?? 0; - const xlrIdx = ds?.xlr ?? 0; - const mutePolarIdx = (ds as ({ hdmimutepolar?: number } | null))?.hdmimutepolar ?? 0; - const iisModeIdx = (ds as ({ hdmiType?: number } | null))?.hdmiType ?? 0; - const uacVerIdx = (ds as ({ uac_ver?: number } | null))?.uac_ver ?? 0; const bootSoundVal = normalizeBootSound(localBootSound); const firmwareVersion = parseFirmwareVersion(ds?.version); const bootSoundMaxIdx = getBootSoundMaxIndex(firmwareVersion); @@ -243,10 +232,12 @@ export default function AudioPage() { const GAIN_OPTIONS = optionLabels(dacText.dacGain?.options, ["低", "中", "高"]); const DAC_VOLUME_DIRECT_OPTIONS = optionLabels(dacText.dacVolumeDirect?.options, ["关闭", "0dB", "-12dB"]); const DAC_ARC_OPTIONS = optionLabels(dacText.dacArc?.options, ["ARC", "EARC"]); - const XLR_OPTIONS = optionLabels(dacText.xlr?.options, ["正常", "反转"]); - const MUTE_POLAR_OPTIONS = optionLabels(dacText.mutePolar?.options, ["低电平", "高电平"]); - const IIS_MODE_OPTIONS = optionLabels(dacText.IISMode?.options, ["模式1", "模式2", "模式3", "模式4", "模式5", "模式6", "模式7", "模式8"]); - const UAC_VER_OPTIONS = optionLabels(dacText.uacVer?.options, ["UAC 2.0", "UAC 1.0"]); + const HP_MAX_VOLUME_OPTIONS = optionLabels(dacText.hpMaxVolume?.options, [ + "0dB", "-5dB", "-10dB", "-15dB", "-20dB", "-25dB", "-30dB", "-35dB", "-40dB", "-45dB", "-50dB", + ]); + const HP_STARTUP_VOLUME_OPTIONS = optionLabels(dacText.hpStartupVolume?.options, [ + "默认", "-20dB", "-25dB", "-30dB", "-35dB", "-40dB", "-45dB", "-50dB", + ]); const pageTitle = usePageTitleWithConnection(localeData.home?.audioSet ?? "Audio"); @@ -318,30 +309,6 @@ export default function AudioPage() { }} />
-
-
-
- {dacText.analogInputGain ?? "模拟输入增益"} - - {localAnalogGain >= 0 ? `+${localAnalogGain.toFixed(1)}` : localAnalogGain.toFixed(1)} dB - -
- { - isAnalogGainDragging.current = true; - setLocalAnalogGain(v); - }} - onRelease={(v) => { - isAnalogGainDragging.current = false; - setLocalAnalogGain(v); - updateSetting({ analogGain: Math.round(v * 2) }); - }} - /> -
{/* ── 输出特性 ── */} @@ -354,17 +321,6 @@ export default function AudioPage() { onClick={() => goSelect(dacText.filters?.label ?? "选择滤波特性", FILTER_OPTIONS, filterIdx, "filterCharacteristic")} /> )} - goSelect(dacText.dacGain?.label ?? "选择耳机增益", GAIN_OPTIONS, gainIdx, "dacGain")} - /> - updateSetting({ dacImpedance: v ? 1 : 0 })} - />
- {/* ── IIS 设置 ── */} - + {/* ── 耳机设定 ── */} +
goSelect(dacText.mutePolar?.label ?? "选择 IIS 静音电平", MUTE_POLAR_OPTIONS, mutePolarIdx, "mutePolar")} + label={dacText.dacGain?.label ?? "耳机增益"} + value={GAIN_OPTIONS[gainIdx] ?? GAIN_OPTIONS[0] ?? "—"} + onClick={() => goSelect(dacText.dacGain?.label ?? "选择耳机增益", GAIN_OPTIONS, gainIdx, "dacGain")} /> goSelect(dacText.IISMode?.label ?? "选择 IIS 模式", IIS_MODE_OPTIONS, iisModeIdx, "IISMode")} + label={dacText.autoImpedanceDetection ?? "自动检测耳机阻抗"} + toggle + checked={autoImpedanceOn} + onToggle={(v) => updateSetting({ dacImpedance: v ? 1 : 0 })} + /> + goSelect(dacText.hpMaxVolume?.label ?? "耳机最大音量", HP_MAX_VOLUME_OPTIONS, hpMaxVolumeIdx, "hpMaxVolume")} + /> + goSelect(dacText.hpStartupVolume?.label ?? "耳机启动音量", HP_STARTUP_VOLUME_OPTIONS, hpStartupVolumeIdx, "hpStartupVolume")} />
@@ -428,16 +395,6 @@ export default function AudioPage() { ) } /> - goSelect(dacText.xlr?.label ?? "选择 XLR 极性", XLR_OPTIONS, xlrIdx, "xlr")} - /> - goSelect(dacText.uacVer?.label ?? "选择 UAC 版本", UAC_VER_OPTIONS, uacVerIdx, "uacVer")} - /> diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx index ecb0a84..eacacec 100644 --- a/client/src/pages/Dashboard.tsx +++ b/client/src/pages/Dashboard.tsx @@ -6,6 +6,7 @@ import Layout from "@/components/Layout"; import GlassToggle from "@/components/GlassToggle"; import { useDevice } from "@/contexts/DeviceContext"; import { + getHomeVolumeMax, INPUT_LABELS, OUTPUT_LABELS, } from "@/lib/luxsinApi"; @@ -23,29 +24,24 @@ import { Volume2, VolumeX, } from "lucide-react"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; // ---- Volume Knob ---- function VolumeKnob() { const { deviceState, setVolume } = useDevice(); - const volume = deviceState?.volume ?? 0; - const percent = volume / 200; + const maxVolume = getHomeVolumeMax(deviceState); + const maxVolumeRef = useRef(maxVolume); + maxVolumeRef.current = maxVolume; + const volume = Math.max(0, Math.min(maxVolume, deviceState?.volume ?? 0)); + const percent = volume / maxVolume; const isDragging = useRef(false); const startY = useRef(0); const startVol = useRef(0); - const handleMouseDown = useCallback((e: React.MouseEvent) => { - isDragging.current = true; - startY.current = e.clientY; - startVol.current = volume; - document.addEventListener("mousemove", handleMouseMove as EventListener); - document.addEventListener("mouseup", handleMouseUp); - }, [volume]); - const handleMouseMove = useCallback((e: MouseEvent) => { if (!isDragging.current) return; const delta = (startY.current - e.clientY) * 1.5; - const newVol = Math.max(0, Math.min(200, Math.round(startVol.current + delta))); + const newVol = Math.max(0, Math.min(maxVolumeRef.current, Math.round(startVol.current + delta))); setVolume(newVol); }, [setVolume]); @@ -55,12 +51,22 @@ function VolumeKnob() { document.removeEventListener("mouseup", handleMouseUp); }, [handleMouseMove]); + const handleMouseDown = useCallback((e: React.MouseEvent) => { + isDragging.current = true; + startY.current = e.clientY; + startVol.current = volume; + document.addEventListener("mousemove", handleMouseMove as EventListener); + document.addEventListener("mouseup", handleMouseUp); + }, [volume, handleMouseMove, handleMouseUp]); + + useEffect(() => () => handleMouseUp(), [handleMouseUp]); + const handleWheel = useCallback((e: React.WheelEvent) => { e.preventDefault(); const delta = e.deltaY > 0 ? -2 : 2; - const newVol = Math.max(0, Math.min(200, volume + delta)); + const newVol = Math.max(0, Math.min(maxVolume, volume + delta)); setVolume(newVol); - }, [volume, setVolume]); + }, [volume, maxVolume, setVolume]); // SVG arc calculation const size = 160; @@ -152,7 +158,7 @@ function VolumeKnob() { {Math.round(percent * 100)}% + {hpMaxVolumeHint && ( +

+ {hpMaxVolumeHint} +

+ )} + {/* Bluetooth info — only when input source is Bluetooth */} {ds.input === INPUT_BLUETOOTH_INDEX && ds.bt_status !== 0 && (
@@ -806,8 +753,8 @@ export default function Home() { )}
- {/* ── 4 quick-action circle buttons ── */} -
+ {/* ── 电源快捷按钮 ── */} +
{/* Power */}
- - {/* EQ */} -
- -
- HP-EQ - - {homeText.doubleClickHint ?? "Double-click to enter"} - -
-
- - {/* Effect */} -
- -
- Effect - - {homeText.doubleClickHint ?? "Double-click to enter"} - -
-
- - {/* Bypass (DSP) */} -
- - Bypass -
{/* ── iOS list rows ── */} @@ -937,8 +834,8 @@ export default function Home() { setLocalVol(0); applyVolume(0); } else if (volumeConfirm === "max") { - setLocalVol(200); - applyVolume(200); + setLocalVol(maxVolume); + applyVolume(maxVolume); } setVolumeConfirm(null); }} diff --git a/client/src/pages/IOPage.tsx b/client/src/pages/IOPage.tsx index acefef2..0d1f686 100644 --- a/client/src/pages/IOPage.tsx +++ b/client/src/pages/IOPage.tsx @@ -1,14 +1,14 @@ /* ============================================================ IO PAGE — Input / Output Selection Design: Reference luxsin_x8_输入输出.jpg - - Circular icon buttons in a grid (3 cols) + - 圆形图标按钮:输入区 2 列,输出区 3 列 - Active = cyan fill + glow - Inactive = dark gray circle ============================================================ */ import { useDevice } from "@/contexts/DeviceContext"; import { cn } from "@/lib/utils"; import BottomNav from "@/components/BottomNav"; -import { ChevronLeft, FolderInput } from "lucide-react"; +import { ChevronLeft } from "lucide-react"; import { useLocation } from "wouter"; import { useMemo } from "react"; import localeZh from "@/locales/data-zh.json"; @@ -33,29 +33,16 @@ const INPUT_OPTION_DEFS: IOOptionDef[] = [ { index: 0, icon: ( - - - - - - - - - - - + + + + + + ), }, { index: 1, - icon: ( - - - - ), - }, - { - index: 2, icon: ( @@ -63,52 +50,6 @@ const INPUT_OPTION_DEFS: IOOptionDef[] = [ ), }, - { - index: 3, - icon: ( - - - - - - ), - }, - { - index: 4, - icon: ( - - - - ), - }, - { - index: 5, - icon: ( - - - - - - - - - - - ), - }, - { - index: 6, - icon: ( - - - - - ), - }, - { - index: 7, - icon: , - }, ]; const OUTPUT_OPTION_DEFS: IOOptionDef[] = [ @@ -155,7 +96,7 @@ const OUTPUT_OPTION_DEFS: IOOptionDef[] = [ }, ]; -const FALLBACK_INPUT_LABELS = ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "RCA", "本地输入"]; +const FALLBACK_INPUT_LABELS = ["平衡 XLR", "模拟 RCA"]; const FALLBACK_OUTPUT_LABELS = ["XLR", "RCA", "XLR/RCA", "耳机"]; function withLabels(defs: IOOptionDef[], labels: string[] | undefined, fallback: string[]): IOOption[] { @@ -175,7 +116,7 @@ function IOCircleButton({ onSelect: () => void; }) { return ( -