新增 bd9 对接 webui
This commit is contained in:
@@ -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,
|
||||
/** 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. */
|
||||
|
||||
@@ -132,6 +132,10 @@ export interface DeviceState {
|
||||
bt_title: string;
|
||||
bt_artist: string;
|
||||
msgCount: number;
|
||||
led_enable: number;
|
||||
led_red: number;
|
||||
led_green: number;
|
||||
led_blue: number;
|
||||
}
|
||||
|
||||
export interface HearingProfile {
|
||||
@@ -517,6 +521,14 @@ export function formatFirmwareVersionDisplay(version: unknown): string {
|
||||
return `${major}.${minor}.${patch}.${build}`;
|
||||
}
|
||||
|
||||
/** BD9 的 soundStep 档位按索引对应的 dB 幅度。 */
|
||||
export const VOLUME_STEP_DB = [0.5, 1, 1.5, 2] as const;
|
||||
|
||||
/** 将音量幅度档位换算为 volume 单位(1 单位 = 0.5dB)。 */
|
||||
export function getVolumeStep(soundStep: number | undefined): number {
|
||||
return (VOLUME_STEP_DB[soundStep ?? 1] ?? VOLUME_STEP_DB[0]) * 2;
|
||||
}
|
||||
|
||||
/** Pre-out volume passthrough mode selected (0dB or -12dB). */
|
||||
export function isVolumePassthroughActive(dacVolumeDirect: number | undefined): boolean {
|
||||
return dacVolumeDirect === 1 || dacVolumeDirect === 2;
|
||||
@@ -533,6 +545,13 @@ export function isHomeVolumeLockedByPassthrough(
|
||||
|
||||
export { getBootSoundMaxIndex } from "@/config/firmwareFeatures";
|
||||
|
||||
/** 将氛围灯 RGB 通道值归一化为 0–255 的整数。 */
|
||||
export function clampLedChannel(value: unknown): number {
|
||||
const n = typeof value === "number" ? value : Number(value);
|
||||
if (!Number.isFinite(n)) return 0;
|
||||
return Math.max(0, Math.min(255, Math.round(n)));
|
||||
}
|
||||
|
||||
/** Normalize `bootSound` from syncData (0 = default … 10 = -50 dB). */
|
||||
export function normalizeBootSound(value: unknown): number {
|
||||
const n = typeof value === "number" ? value : Number(value);
|
||||
@@ -623,6 +642,10 @@ export const MOCK_DEVICE_STATE: DeviceState = {
|
||||
bt_title: "Bohemian Rhapsody",
|
||||
bt_artist: "Queen",
|
||||
msgCount: 42,
|
||||
led_enable: 1,
|
||||
led_red: 128,
|
||||
led_green: 64,
|
||||
led_blue: 200,
|
||||
};
|
||||
|
||||
export const MOCK_PEQ_STATE: PeqState = {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"pageTitle": "I/O",
|
||||
"inputSection": "Eingang",
|
||||
"outputSection": "Ausgang",
|
||||
"inputOptions": ["USB-B", "USB-C", "Koaxial", "Optisch", "Bluetooth", "IIS", "RCA", "Lokaler Eingang"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA", "Kopfhörer"],
|
||||
"inputOptions": ["USB-B", "USB-C", "Koaxial", "Optisch", "Bluetooth", "IIS", "AES", "USB-Stick"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA"],
|
||||
"input": {
|
||||
"coaxial": "KOAXIAL",
|
||||
"optical": "OPTISCH",
|
||||
@@ -580,7 +580,12 @@
|
||||
"deviceName": "Gerätename",
|
||||
"firmwareVersion": "Firmware-Version",
|
||||
"macAddress": "MAC-Adresse",
|
||||
"deviceIp": "Geräte-IP"
|
||||
"deviceIp": "Geräte-IP",
|
||||
"sectionAmbientLight": "Ambientebeleuchtung",
|
||||
"ambientLight": "Hauptschalter",
|
||||
"ledRed": "Rot",
|
||||
"ledGreen": "Grün",
|
||||
"ledBlue": "Blau"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"pageTitle": "I/O",
|
||||
"inputSection": "Input",
|
||||
"outputSection": "Output",
|
||||
"inputOptions": ["USB-B", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "RCA", "Local Input"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA", "Headset"],
|
||||
"inputOptions": ["USB-B", "USB-C", "Coaxial", "Optical", "Bluetooth", "IIS", "AES", "USB Drive"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA"],
|
||||
"input": {
|
||||
"coaxial": "COAXIAL",
|
||||
"optical": "OPTICAL",
|
||||
@@ -580,7 +580,12 @@
|
||||
"deviceName": "Device name",
|
||||
"firmwareVersion": "Firmware version",
|
||||
"macAddress": "MAC address",
|
||||
"deviceIp": "Device IP"
|
||||
"deviceIp": "Device IP",
|
||||
"sectionAmbientLight": "Ambient light",
|
||||
"ambientLight": "Master switch",
|
||||
"ledRed": "Red",
|
||||
"ledGreen": "Green",
|
||||
"ledBlue": "Blue"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"pageTitle": "E/S",
|
||||
"inputSection": "Entrada",
|
||||
"outputSection": "Salida",
|
||||
"inputOptions": ["USB-B", "USB-C", "Coaxial", "Óptico", "Bluetooth", "IIS", "RCA", "Entrada local"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA", "Auriculares"],
|
||||
"inputOptions": ["USB-B", "USB-C", "Coaxial", "Óptico", "Bluetooth", "IIS", "AES", "Memoria USB"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA"],
|
||||
"input": {
|
||||
"coaxial": "COAXIAL",
|
||||
"optical": "ÓPTICO",
|
||||
@@ -580,7 +580,12 @@
|
||||
"deviceName": "Nombre del dispositivo",
|
||||
"firmwareVersion": "Versión de firmware",
|
||||
"macAddress": "Dirección MAC",
|
||||
"deviceIp": "IP del dispositivo"
|
||||
"deviceIp": "IP del dispositivo",
|
||||
"sectionAmbientLight": "Iluminación ambiental",
|
||||
"ambientLight": "Interruptor principal",
|
||||
"ledRed": "Rojo",
|
||||
"ledGreen": "Verde",
|
||||
"ledBlue": "Azul"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"pageTitle": "E/S",
|
||||
"inputSection": "Entrée",
|
||||
"outputSection": "Sortie",
|
||||
"inputOptions": ["USB-B", "USB-C", "Coaxial", "Optique", "Bluetooth", "IIS", "RCA", "Entrée locale"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA", "Casque"],
|
||||
"inputOptions": ["USB-B", "USB-C", "Coaxial", "Optique", "Bluetooth", "IIS", "AES", "Clé USB"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA"],
|
||||
"input": {
|
||||
"coaxial": "COAXIAL",
|
||||
"optical": "OPTIQUE",
|
||||
@@ -580,7 +580,12 @@
|
||||
"deviceName": "Nom de l'appareil",
|
||||
"firmwareVersion": "Version du firmware",
|
||||
"macAddress": "Adresse MAC",
|
||||
"deviceIp": "IP de l'appareil"
|
||||
"deviceIp": "IP de l'appareil",
|
||||
"sectionAmbientLight": "Éclairage d'ambiance",
|
||||
"ambientLight": "Interrupteur principal",
|
||||
"ledRed": "Rouge",
|
||||
"ledGreen": "Vert",
|
||||
"ledBlue": "Bleu"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"pageTitle": "輸入/輸出",
|
||||
"inputSection": "輸入",
|
||||
"outputSection": "輸出",
|
||||
"inputOptions": ["USB-B", "USB-C", "同軸", "光纖", "藍牙", "IIS", "類比RCA", "本地輸入"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA", "耳機"],
|
||||
"inputOptions": ["USB-B", "USB-C", "同軸", "光纖", "藍牙", "IIS", "AES", "U 盤"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA"],
|
||||
"input": { "coaxial": "同軸", "optical": "光纖", "bluetooth": "藍牙", "rca": "類比RCA" },
|
||||
"output": { "headset": "耳機" }
|
||||
},
|
||||
@@ -574,7 +574,12 @@
|
||||
"deviceName": "裝置名稱",
|
||||
"firmwareVersion": "韌體版本",
|
||||
"macAddress": "MAC 位址",
|
||||
"deviceIp": "裝置 IP"
|
||||
"deviceIp": "裝置 IP",
|
||||
"sectionAmbientLight": "氛圍燈",
|
||||
"ambientLight": "總開關",
|
||||
"ledRed": "紅",
|
||||
"ledGreen": "綠",
|
||||
"ledBlue": "藍"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -56,8 +56,8 @@
|
||||
"pageTitle": "输入输出",
|
||||
"inputSection": "输入",
|
||||
"outputSection": "输出",
|
||||
"inputOptions": ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "模拟RCA", "本地输入"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA", "耳机"],
|
||||
"inputOptions": ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "AES", "U 盘"],
|
||||
"outputOptions": ["XLR", "RCA", "XLR/RCA"],
|
||||
"input": { "coaxial": "同轴", "optical": "光纤", "bluetooth": "蓝牙", "rca": "模拟RCA" },
|
||||
"output": { "headset": "耳机" }
|
||||
},
|
||||
@@ -575,7 +575,12 @@
|
||||
"deviceName": "设备名称",
|
||||
"firmwareVersion": "固件版本",
|
||||
"macAddress": "MAC 地址",
|
||||
"deviceIp": "设备 IP"
|
||||
"deviceIp": "设备 IP",
|
||||
"sectionAmbientLight": "氛围灯",
|
||||
"ambientLight": "总开关",
|
||||
"ledRed": "红",
|
||||
"ledGreen": "绿",
|
||||
"ledBlue": "蓝"
|
||||
}
|
||||
},
|
||||
"prompt": {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
Design: Reference luxsin_x8_音频设置.png
|
||||
Sections:
|
||||
- 平衡与灵敏度: 左右平衡, VU灵敏度, 模拟输入增益 sliders
|
||||
- 输出特性: 滤波特性, 耳机增益, 音量幅度
|
||||
- 输出特性: 前级输出音量直通模式, 音量幅度
|
||||
- IIS设置: IIS静音电平, IIS模式
|
||||
- 系统与接口: 开机音量, XLR极性
|
||||
All list rows → navigate to /select page
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
normalizeBootSound,
|
||||
parseFirmwareVersion,
|
||||
readBootSoundFromState,
|
||||
VOLUME_STEP_DB,
|
||||
} from "@/lib/luxsinApi";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
import { resolveLocalePack } from "@/locales/resolveLocale";
|
||||
@@ -73,31 +74,11 @@ function SectionHeader({ title }: { title: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
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 ListRow({ label, value, onClick, toggle, checked, onToggle }: {
|
||||
function ListRow({ label, value, onClick }: {
|
||||
label: string;
|
||||
value?: string;
|
||||
onClick?: () => void;
|
||||
toggle?: boolean;
|
||||
checked?: boolean;
|
||||
onToggle?: (v: boolean) => void;
|
||||
}) {
|
||||
if (toggle) {
|
||||
return (
|
||||
<div className="ios-list-row">
|
||||
<span className="flex-1 text-[16px] text-white">{label}</span>
|
||||
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<button className="ios-list-row w-full text-left active:bg-white/5 transition-colors" onClick={onClick}>
|
||||
<span className="flex-1 text-[16px] text-white">{label}</span>
|
||||
@@ -116,9 +97,7 @@ type LocaleDac = {
|
||||
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 }> };
|
||||
dacVolumeDirect?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
dacArc?: { label?: string; options?: Array<{ index: number; label: string }> };
|
||||
volumeStep?: string;
|
||||
@@ -137,7 +116,7 @@ function optionLabels(items: Array<{ index: number; label: string }> | undefined
|
||||
return sorted.map((item) => item.label);
|
||||
}
|
||||
|
||||
const STEP_OPTIONS = ["0.5dB", "1dB", "2dB", "3dB"];
|
||||
const STEP_OPTIONS = VOLUME_STEP_DB.map((step) => `${step}dB`);
|
||||
|
||||
const BOOT_VOLUME_FALLBACK = [
|
||||
"Default",
|
||||
@@ -223,8 +202,6 @@ export default function AudioPage() {
|
||||
}
|
||||
}, [location, isConnected, api, refresh]);
|
||||
|
||||
const gainIdx = ds?.dacGain ?? 0;
|
||||
const autoImpedanceOn = (ds?.dacImpedance ?? 0) === 1;
|
||||
const dacVolumeDirectIdx = ds?.dacVolumeDirect ?? 0;
|
||||
const dacArcIdx = ds?.dacArc ?? 0;
|
||||
const stepIdx = ds?.soundStep ?? 1;
|
||||
@@ -240,7 +217,6 @@ export default function AudioPage() {
|
||||
const BOOT_VOLUME_OPTIONS = bootVolumeOptions(dacText, bootSoundMaxIdx);
|
||||
|
||||
const FILTER_OPTIONS = optionLabels(dacText.filters?.options, ["快速滚降", "慢速滚降", "短延迟快速滚降", "短延迟慢速滚降", "超慢速滚降", "低离散短延迟"]);
|
||||
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, ["正常", "反转"]);
|
||||
@@ -354,17 +330,6 @@ export default function AudioPage() {
|
||||
onClick={() => goSelect(dacText.filters?.label ?? "选择滤波特性", FILTER_OPTIONS, filterIdx, "filterCharacteristic")}
|
||||
/>
|
||||
)}
|
||||
<ListRow
|
||||
label={dacText.dacGain?.label ?? "耳机增益"}
|
||||
value={GAIN_OPTIONS[gainIdx] ?? GAIN_OPTIONS[0] ?? "—"}
|
||||
onClick={() => goSelect(dacText.dacGain?.label ?? "选择耳机增益", GAIN_OPTIONS, gainIdx, "dacGain")}
|
||||
/>
|
||||
<ListRow
|
||||
label={dacText.autoImpedanceDetection ?? "自动检测耳机阻抗"}
|
||||
toggle
|
||||
checked={autoImpedanceOn}
|
||||
onToggle={(v) => updateSetting({ dacImpedance: v ? 1 : 0 })}
|
||||
/>
|
||||
<ListRow
|
||||
label={dacText.dacVolumeDirect?.label ?? "前级输出音量直通模式"}
|
||||
value={DAC_VOLUME_DIRECT_OPTIONS[dacVolumeDirectIdx] ?? DAC_VOLUME_DIRECT_OPTIONS[0] ?? "—"}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Layout from "@/components/Layout";
|
||||
import GlassToggle from "@/components/GlassToggle";
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
import {
|
||||
getVolumeStep,
|
||||
INPUT_LABELS,
|
||||
OUTPUT_LABELS,
|
||||
} from "@/lib/luxsinApi";
|
||||
@@ -29,6 +30,7 @@ import { useCallback, useRef, useState } from "react";
|
||||
function VolumeKnob() {
|
||||
const { deviceState, setVolume } = useDevice();
|
||||
const volume = deviceState?.volume ?? 0;
|
||||
const volumeStep = getVolumeStep(deviceState?.soundStep);
|
||||
const percent = volume / 200;
|
||||
const isDragging = useRef(false);
|
||||
const startY = useRef(0);
|
||||
@@ -143,7 +145,7 @@ function VolumeKnob() {
|
||||
{/* Volume controls */}
|
||||
<div className="flex items-center gap-3 mt-2">
|
||||
<button
|
||||
onClick={() => setVolume(Math.max(0, volume - (deviceState?.soundStep ?? 2)))}
|
||||
onClick={() => setVolume(Math.max(0, volume - volumeStep))}
|
||||
className="btn-glass w-8 h-8 flex items-center justify-center"
|
||||
>
|
||||
<ChevronDown size={14} className="text-white/60" />
|
||||
@@ -152,7 +154,7 @@ function VolumeKnob() {
|
||||
{Math.round(percent * 100)}%
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setVolume(Math.min(200, volume + (deviceState?.soundStep ?? 2)))}
|
||||
onClick={() => setVolume(Math.min(200, volume + volumeStep))}
|
||||
className="btn-glass w-8 h-8 flex items-center justify-center"
|
||||
>
|
||||
<ChevronUp size={14} className="text-white/60" />
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
getVolumeStep,
|
||||
INPUT_BLUETOOTH_INDEX,
|
||||
INPUT_LABELS,
|
||||
isHomeVolumeLockedByPassthrough,
|
||||
@@ -433,11 +434,7 @@ export default function Home() {
|
||||
const vuLabel = `${homeText.vu ?? "VU表"}${(ds.vu ?? 0) + 1}`;
|
||||
const bypassOn = (ds.dsp_enable ?? 0) === 0;
|
||||
|
||||
// Home 的 volume 取值为 0..200,对应 dB = (v - 200) / 2
|
||||
// 因此 1 个 volume 单位 = 0.5dB
|
||||
// AudioPage 的 soundStep:0->0.5dB, 1->1dB, 2->2dB, 3->3dB
|
||||
// 换算为 volume 单位:0.5/1/2/3 dB -> 1/2/4/6
|
||||
const fineVolumeStep = [1, 2, 4, 6][ds.soundStep ?? 1] ?? 1;
|
||||
const fineVolumeStep = getVolumeStep(ds.soundStep);
|
||||
|
||||
// Slider fill % (0-200 → 0-100%)
|
||||
const fillPct = (localVol / 200) * 100;
|
||||
|
||||
@@ -106,7 +106,7 @@ const INPUT_OPTION_DEFS: IOOptionDef[] = [
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 7,
|
||||
index: 8,
|
||||
icon: <FolderInput size={28} />,
|
||||
},
|
||||
];
|
||||
@@ -143,20 +143,10 @@ const OUTPUT_OPTION_DEFS: IOOptionDef[] = [
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
index: 2,
|
||||
icon: (
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-7 h-7">
|
||||
<path d="M3 18v-6a9 9 0 0 1 18 0v6" />
|
||||
<path d="M21 19a2 2 0 0 1-2 2h-1a2 2 0 0 1-2-2v-3a2 2 0 0 1 2-2h3z" />
|
||||
<path d="M3 19a2 2 0 0 0 2 2h1a2 2 0 0 0 2-2v-3a2 2 0 0 0-2-2H3z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const FALLBACK_INPUT_LABELS = ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "RCA", "本地输入"];
|
||||
const FALLBACK_OUTPUT_LABELS = ["XLR", "RCA", "XLR/RCA", "耳机"];
|
||||
const FALLBACK_INPUT_LABELS = ["USB-B", "USB-C", "同轴", "光纤", "蓝牙", "IIS", "AES", "U 盘"];
|
||||
const FALLBACK_OUTPUT_LABELS = ["XLR", "RCA", "XLR/RCA"];
|
||||
|
||||
function withLabels(defs: IOOptionDef[], labels: string[] | undefined, fallback: string[]): IOOption[] {
|
||||
return defs.map((def, i) => ({
|
||||
@@ -225,7 +215,7 @@ export default function IOPage() {
|
||||
const outputSectionTitle = sourceText.outputSection ?? "输出";
|
||||
|
||||
const currentInput = deviceState?.input ?? 1;
|
||||
const currentOutput = deviceState?.output ?? 2;
|
||||
const currentOutput = deviceState?.output ?? 0;
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black">
|
||||
|
||||
+155
-28
@@ -2,7 +2,8 @@
|
||||
SYSTEM PAGE — Settings
|
||||
Design: Reference luxsin_x8_设置.png
|
||||
Sections:
|
||||
- 显示: 屏幕亮度, 自动锁屏, 旋钮亮度, 旋钮熄屏呼吸灯
|
||||
- 显示: 屏幕亮度, 自动锁屏
|
||||
- 氛围灯: 总开关, RGB 通道调节
|
||||
- 通用: 语言, 自动返回首页, 休眠
|
||||
- 声音: 开机提示音
|
||||
- 关于: 设备名, 固件版本, MAC
|
||||
@@ -11,18 +12,18 @@
|
||||
import { useDevice } from "@/contexts/DeviceContext";
|
||||
import { ChevronLeft, ChevronRight } from "lucide-react";
|
||||
import { useLocation } from "wouter";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import BottomNav from "@/components/BottomNav";
|
||||
import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
|
||||
import { clampLedChannel, formatFirmwareVersionDisplay } from "@/lib/luxsinApi";
|
||||
import { navigateToSelect } from "./SelectPage";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
import { DEFAULT_DEVICE_LANGUAGE, resolveDeviceLanguage, resolveLocalePack } from "@/locales/resolveLocale";
|
||||
import { usePageTitleWithConnection } from "@/hooks/usePageTitleWithConnection";
|
||||
|
||||
function IOSToggle({ checked, onChange }: { checked: boolean; onChange: (v: boolean) => void }) {
|
||||
function IOSToggle({ checked, onChange, label }: { checked: boolean; onChange: (v: boolean) => void; label: string }) {
|
||||
return (
|
||||
<label className="ios-toggle" onClick={(e) => e.stopPropagation()}>
|
||||
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
<input type="checkbox" aria-label={label} checked={checked} onChange={(e) => onChange(e.target.checked)} />
|
||||
<span className="ios-toggle-track"><span className="ios-toggle-thumb" /></span>
|
||||
</label>
|
||||
);
|
||||
@@ -36,21 +37,55 @@ function SectionHeader({ title }: { title: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
function ListRow({ label, description, value, onClick, toggle, checked, onToggle }: {
|
||||
label: string; description?: string; value?: string;
|
||||
onClick?: () => void; toggle?: boolean; checked?: boolean; onToggle?: (v: boolean) => void;
|
||||
function CyanSlider({ value, min, max, step = 1, label, onChange, onRelease }: {
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
step?: number;
|
||||
label: string;
|
||||
onChange: (v: number) => void;
|
||||
onRelease: (v: number) => void;
|
||||
}) {
|
||||
if (toggle) {
|
||||
const fillPct = ((value - min) / (max - min)) * 100;
|
||||
const emitRelease = (el: EventTarget | null) => {
|
||||
if (!(el instanceof HTMLInputElement)) return;
|
||||
onRelease(Number(el.value));
|
||||
};
|
||||
return (
|
||||
<div className="ios-list-row">
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-[16px] text-white">{label}</div>
|
||||
{description && <div className="text-[12px] text-white/35 mt-0.5">{description}</div>}
|
||||
</div>
|
||||
<IOSToggle checked={!!checked} onChange={onToggle ?? (() => {})} />
|
||||
<div className="relative flex items-center w-full mt-2">
|
||||
<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"
|
||||
aria-label={label}
|
||||
min={min}
|
||||
max={max}
|
||||
step={step}
|
||||
value={value}
|
||||
className="cyan-slider relative z-10"
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function ListRow({ label, description, value, onClick }: {
|
||||
label: string; description?: string; value?: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button className="ios-list-row w-full text-left active:bg-white/5 transition-colors" onClick={onClick}>
|
||||
<div className="flex-1 min-w-0">
|
||||
@@ -71,8 +106,6 @@ type LocaleSettings = {
|
||||
vuMeter?: string;
|
||||
screenBrightness: { label: string; options: Array<{ index: number; label: string }> };
|
||||
turnOffScreen: { label: string; options: Array<{ index: number; label: string }> };
|
||||
knobBrightness: { label: string; options: Array<{ index: number; label: string }> };
|
||||
buttonLight: { label: string; options: Array<{ index: number; label: string }> };
|
||||
language: { label: string; options: Array<{ index: number; label: string }> };
|
||||
Automatic: { label: string; options: Array<{ index: number; label: string }> };
|
||||
sleepTime?: { label: string; options: Array<{ index: number; label: string }> };
|
||||
@@ -83,8 +116,6 @@ type LocaleSettings = {
|
||||
sectionAbout: string;
|
||||
screenBrightnessDesc: string;
|
||||
turnOffScreenDesc: string;
|
||||
knobBrightnessDesc: string;
|
||||
buttonLightDesc: string;
|
||||
languageDesc: string;
|
||||
automaticDesc: string;
|
||||
sleepDesc: string;
|
||||
@@ -92,6 +123,11 @@ type LocaleSettings = {
|
||||
firmwareVersion: string;
|
||||
macAddress: string;
|
||||
deviceIp: string;
|
||||
sectionAmbientLight?: string;
|
||||
ambientLight?: string;
|
||||
ledRed?: string;
|
||||
ledGreen?: string;
|
||||
ledBlue?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
@@ -127,18 +163,35 @@ export default function SystemPage() {
|
||||
}, [isConnected, api]);
|
||||
|
||||
const screenLightIdx = ds?.screenLight ?? 1;
|
||||
const buttonLightIdx = ds?.buttonLight ?? 1;
|
||||
const screenOffIdx = ds?.screenOff ?? 2;
|
||||
const sleepIdx = ds?.sleep ?? 0;
|
||||
const autoHomeIdx = ds?.autoHome ?? 1;
|
||||
const rawLangIdx = resolveDeviceLanguage(ds?.language);
|
||||
const langIdx = SUPPORTED_LANGUAGE_INDICES.has(rawLangIdx) ? rawLangIdx : DEFAULT_DEVICE_LANGUAGE;
|
||||
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
|
||||
const ledOn = (ds?.led_enable ?? 0) === 1;
|
||||
const [localLedRed, setLocalLedRed] = useState(() => clampLedChannel(ds?.led_red));
|
||||
const [localLedGreen, setLocalLedGreen] = useState(() => clampLedChannel(ds?.led_green));
|
||||
const [localLedBlue, setLocalLedBlue] = useState(() => clampLedChannel(ds?.led_blue));
|
||||
const draggingLedRed = useRef(false);
|
||||
const draggingLedGreen = useRef(false);
|
||||
const draggingLedBlue = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draggingLedRed.current) setLocalLedRed(clampLedChannel(ds?.led_red));
|
||||
}, [ds?.led_red]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draggingLedGreen.current) setLocalLedGreen(clampLedChannel(ds?.led_green));
|
||||
}, [ds?.led_green]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!draggingLedBlue.current) setLocalLedBlue(clampLedChannel(ds?.led_blue));
|
||||
}, [ds?.led_blue]);
|
||||
|
||||
const localeData = resolveLocalePack(ds?.language) as LocaleSettings;
|
||||
const settingsText = localeData.settings;
|
||||
const ui = settingsText.systemPage;
|
||||
const SCREEN_LIGHT_OPTIONS = optionLabels(settingsText.screenBrightness?.options, ["亮", "中等", "暗"]);
|
||||
const KNOB_LIGHT_OPTIONS = optionLabels(settingsText.knobBrightness?.options, ["关闭", "亮", "中等", "暗"]);
|
||||
const SCREEN_OFF_OPTIONS = optionLabels(settingsText.turnOffScreen?.options, ["常亮", "30秒", "1分钟", "3分钟", "5分钟"]);
|
||||
const SLEEP_OPTIONS = optionLabels(settingsText.sleepTime?.options ?? settingsText.sleep?.options, ["关闭", "1分钟", "5分钟", "10分钟"]);
|
||||
const AUTO_HOME_OPTIONS = optionLabels(settingsText.Automatic?.options, ["关闭", "20秒", "40秒", "60秒"]);
|
||||
@@ -181,12 +234,86 @@ export default function SystemPage() {
|
||||
<ListRow label={settingsText.turnOffScreen.label} description={ui.turnOffScreenDesc}
|
||||
value={SCREEN_OFF_OPTIONS[screenOffIdx]}
|
||||
onClick={() => goSelect(settingsText.turnOffScreen.label, SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />
|
||||
<ListRow label={settingsText.knobBrightness.label} description={ui.knobBrightnessDesc}
|
||||
value={KNOB_LIGHT_OPTIONS[buttonLightIdx]}
|
||||
onClick={() => goSelect(settingsText.knobBrightness.label, KNOB_LIGHT_OPTIONS, buttonLightIdx, "buttonLight")} />
|
||||
<ListRow label={settingsText.buttonLight.label} description={ui.buttonLightDesc}
|
||||
toggle checked={knobBreathLightOn}
|
||||
onToggle={(v) => updateSetting({ knob_breathlight: v ? 0 : 1 })} />
|
||||
</div>
|
||||
|
||||
{/* ── 氛围灯 ── */}
|
||||
<SectionHeader title={ui.sectionAmbientLight ?? "氛围灯"} />
|
||||
<div className="ios-list-group">
|
||||
<div className="ios-list-row">
|
||||
<div className="flex-1" />
|
||||
<IOSToggle
|
||||
label={ui.ambientLight ?? "总开关"}
|
||||
checked={ledOn}
|
||||
onChange={(v) => updateSetting({ led_enable: v ? 1 : 0 })}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ledOn && (
|
||||
<div className="p-4 pt-0 space-y-4">
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px] text-white/60">{ui.ledRed ?? "红"}</span>
|
||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLedRed}</span>
|
||||
</div>
|
||||
<CyanSlider
|
||||
label={ui.ledRed ?? "红"}
|
||||
value={localLedRed}
|
||||
min={0}
|
||||
max={255}
|
||||
onChange={(v) => {
|
||||
draggingLedRed.current = true;
|
||||
setLocalLedRed(v);
|
||||
}}
|
||||
onRelease={(v) => {
|
||||
draggingLedRed.current = false;
|
||||
updateSetting({ led_red: clampLedChannel(v) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px] text-white/60">{ui.ledGreen ?? "绿"}</span>
|
||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLedGreen}</span>
|
||||
</div>
|
||||
<CyanSlider
|
||||
label={ui.ledGreen ?? "绿"}
|
||||
value={localLedGreen}
|
||||
min={0}
|
||||
max={255}
|
||||
onChange={(v) => {
|
||||
draggingLedGreen.current = true;
|
||||
setLocalLedGreen(v);
|
||||
}}
|
||||
onRelease={(v) => {
|
||||
draggingLedGreen.current = false;
|
||||
updateSetting({ led_green: clampLedChannel(v) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="text-[14px] text-white/60">{ui.ledBlue ?? "蓝"}</span>
|
||||
<span className="text-[14px] font-bold" style={{ color: "#00FFF6" }}>{localLedBlue}</span>
|
||||
</div>
|
||||
<CyanSlider
|
||||
label={ui.ledBlue ?? "蓝"}
|
||||
value={localLedBlue}
|
||||
min={0}
|
||||
max={255}
|
||||
onChange={(v) => {
|
||||
draggingLedBlue.current = true;
|
||||
setLocalLedBlue(v);
|
||||
}}
|
||||
onRelease={(v) => {
|
||||
draggingLedBlue.current = false;
|
||||
updateSetting({ led_blue: clampLedChannel(v) });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 通用 ── */}
|
||||
|
||||
Reference in New Issue
Block a user