diff --git a/client/src/lib/luxsinApi.ts b/client/src/lib/luxsinApi.ts index b25959c..4243e2c 100644 --- a/client/src/lib/luxsinApi.ts +++ b/client/src/lib/luxsinApi.ts @@ -122,6 +122,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 PeqFilter { @@ -390,6 +394,19 @@ export function getBootSoundMaxIndex(firmwareVersion: number): number { return firmwareVersion >= 26 ? 10 : 6; } +/** Ambient LED block on System page (firmware build 28+). */ +export const AMBIENT_LED_MIN_FIRMWARE_VERSION = 28; + +export function supportsAmbientLed(firmwareVersion: number): boolean { + return firmwareVersion >= AMBIENT_LED_MIN_FIRMWARE_VERSION; +} + +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); @@ -408,7 +425,7 @@ export function readBootSoundFromState(state: DeviceState | null | undefined): n // ============================================================ export const MOCK_DEVICE_STATE: DeviceState = { device: "Luxsin-X8", - version: "1.2.3", + version: 28, mac: "AA:BB:CC:DD:EE:FF", language: 0, volume: 75, @@ -463,6 +480,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 = { diff --git a/client/src/locales/data-de.json b/client/src/locales/data-de.json index 941e14f..0e78bd9 100644 --- a/client/src/locales/data-de.json +++ b/client/src/locales/data-de.json @@ -274,6 +274,7 @@ "addPresetTitle": "Neue Voreinstellung", "addPresetNameEmpty": "Name darf nicht leer sein", "addPresetNameExists": "Dieser Name existiert bereits. Wählen Sie einen anderen.", + "addPresetNameTooLong": "Name darf maximal 80 Zeichen lang sein", "toastAddPresetOk": "Voreinstellung hinzugefügt", "toastAddPresetFail": "Hinzufügen der Voreinstellung fehlgeschlagen", "toastPresetLimitReached": "Voreinstellungslimit erreicht ({{max}}). Löschen Sie einige Voreinstellungen, bevor Sie weitere hinzufügen.", diff --git a/client/src/locales/data-en.json b/client/src/locales/data-en.json index 6efc2d9..259323d 100644 --- a/client/src/locales/data-en.json +++ b/client/src/locales/data-en.json @@ -274,6 +274,7 @@ "addPresetTitle": "New preset", "addPresetNameEmpty": "Name cannot be empty", "addPresetNameExists": "That name already exists. Choose another.", + "addPresetNameTooLong": "Name cannot exceed 80 characters", "toastAddPresetOk": "Preset added", "toastAddPresetFail": "Failed to add preset", "toastPresetLimitReached": "Preset limit reached ({{max}}). Delete some presets before adding more.", @@ -796,7 +797,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": { diff --git a/client/src/locales/data-es.json b/client/src/locales/data-es.json index 35d480b..364e4d1 100644 --- a/client/src/locales/data-es.json +++ b/client/src/locales/data-es.json @@ -274,6 +274,7 @@ "addPresetTitle": "Nuevo ajuste", "addPresetNameEmpty": "El nombre no puede estar vacío", "addPresetNameExists": "Ese nombre ya existe. Elija otro.", + "addPresetNameTooLong": "El nombre no puede superar los 80 caracteres", "toastAddPresetOk": "Ajuste agregado", "toastAddPresetFail": "Error al agregar el ajuste", "toastPresetLimitReached": "Límite de ajustes alcanzado ({{max}}). Elimine algunos ajustes antes de agregar más.", diff --git a/client/src/locales/data-fr.json b/client/src/locales/data-fr.json index 3dabbac..879e5a5 100644 --- a/client/src/locales/data-fr.json +++ b/client/src/locales/data-fr.json @@ -274,6 +274,7 @@ "addPresetTitle": "Nouveau préréglage", "addPresetNameEmpty": "Le nom ne peut pas être vide", "addPresetNameExists": "Ce nom existe déjà. Choisissez-en un autre.", + "addPresetNameTooLong": "Le nom ne peut pas dépasser 80 caractères", "toastAddPresetOk": "Préréglage ajouté", "toastAddPresetFail": "Échec de l'ajout du préréglage", "toastPresetLimitReached": "Limite de préréglages atteinte ({{max}}). Supprimez des préréglages avant d'en ajouter.", diff --git a/client/src/locales/data-zh-HK.json b/client/src/locales/data-zh-HK.json index 742aebe..90ea06c 100644 --- a/client/src/locales/data-zh-HK.json +++ b/client/src/locales/data-zh-HK.json @@ -274,6 +274,7 @@ "addPresetTitle": "新增預設", "addPresetNameEmpty": "名稱不能為空", "addPresetNameExists": "名稱已存在,請修改後再儲存", + "addPresetNameTooLong": "名稱不能超過 80 個字元", "toastAddPresetOk": "已新增預設", "toastAddPresetFail": "新增預設失敗", "toastPresetLimitReached": "預設已達上限({{max}} 個),請先清理後再操作", @@ -796,7 +797,12 @@ "deviceName": "裝置名稱", "firmwareVersion": "韌體版本", "macAddress": "MAC 位址", - "deviceIp": "裝置 IP" + "deviceIp": "裝置 IP", + "sectionAmbientLight": "氛圍燈", + "ambientLight": "總開關", + "ledRed": "紅", + "ledGreen": "綠", + "ledBlue": "藍" } }, "prompt": { diff --git a/client/src/locales/data-zh.json b/client/src/locales/data-zh.json index e2d700f..5e8f2c6 100644 --- a/client/src/locales/data-zh.json +++ b/client/src/locales/data-zh.json @@ -274,6 +274,7 @@ "addPresetTitle": "新增预设", "addPresetNameEmpty": "名称不能为空", "addPresetNameExists": "名称已存在,请修改后再保存", + "addPresetNameTooLong": "名称不能超过 80 个字符", "toastAddPresetOk": "已新增预设", "toastAddPresetFail": "新增预设失败", "toastPresetLimitReached": "预设已达上限({{max}} 个),请先清理后再操作", @@ -796,7 +797,12 @@ "deviceName": "设备名称", "firmwareVersion": "固件版本", "macAddress": "MAC 地址", - "deviceIp": "设备 IP" + "deviceIp": "设备 IP", + "sectionAmbientLight": "氛围灯", + "ambientLight": "总开关", + "ledRed": "红", + "ledGreen": "绿", + "ledBlue": "蓝" } }, "prompt": { diff --git a/client/src/pages/EQPage.tsx b/client/src/pages/EQPage.tsx index 1db38c9..24f86cd 100644 --- a/client/src/pages/EQPage.tsx +++ b/client/src/pages/EQPage.tsx @@ -42,6 +42,7 @@ import { BrandDrawer } from "./eq/components/BrandDrawer"; import { PeqOverwriteConfirmDialog } from "./eq/components/PeqOverwriteConfirmDialog"; import { PeqPresetManageDialog } from "./eq/components/PeqPresetManageDialog"; import { useRawCurve } from "./eq/hooks/useRawCurve"; +import { isPeqPresetNameTooLong } from "./eq/peqPresetName"; import { BAND_FREQ_MAX, BAND_FREQ_MIN, @@ -597,6 +598,10 @@ export default function EQPage() { toast.error(eqUi.addPresetNameEmpty); return; } + if (isPeqPresetNameTooLong(nextName)) { + toast.error(eqUi.addPresetNameTooLong); + return; + } if (headphoneModels.includes(nextName)) { toast.error(eqUi.addPresetNameExists); return; @@ -675,6 +680,10 @@ export default function EQPage() { toast.error(eqUi.addPresetNameEmpty); return; } + if (isPeqPresetNameTooLong(nextName)) { + toast.error(eqUi.addPresetNameTooLong); + return; + } if (headphoneModels.includes(nextName)) { toast.error(eqUi.addPresetNameExists); return; @@ -747,6 +756,10 @@ export default function EQPage() { toast.error(eqUi.addPresetNameEmpty); return; } + if (isPeqPresetNameTooLong(nextName)) { + toast.error(eqUi.addPresetNameTooLong); + return; + } if (!shareCode.trim()) { toast.error(eqUi.importCodeNotFound); return; diff --git a/client/src/pages/SystemPage.tsx b/client/src/pages/SystemPage.tsx index 00cd4f3..0170243 100644 --- a/client/src/pages/SystemPage.tsx +++ b/client/src/pages/SystemPage.tsx @@ -11,10 +11,15 @@ 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 { navigateToSelect } from "./SelectPage"; -import { formatFirmwareVersionDisplay } from "@/lib/luxsinApi"; +import { + clampLedChannel, + formatFirmwareVersionDisplay, + parseFirmwareVersion, + supportsAmbientLed, +} from "@/lib/luxsinApi"; import localeZh from "@/locales/data-zh.json"; import localeZhHK from "@/locales/data-zh-HK.json"; import localeEn from "@/locales/data-en.json"; @@ -36,6 +41,60 @@ function SectionHeader({ title }: { title: string }) { ); } +function CyanSlider({ + value, + min, + max, + step = 1, + onChange, + onRelease, +}: { + value: number; + min: number; + max: number; + step?: number; + onChange: (v: number) => void; + onRelease?: (v: number) => void; +}) { + const fillPct = ((value - min) / (max - min)) * 100; + const emitRelease = (el: EventTarget | null) => { + if (!(el instanceof HTMLInputElement)) return; + onRelease?.(Number(el.value)); + }; + return ( +