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