From 786c4e3e4836d8691b318f1d3e97619dcace273b Mon Sep 17 00:00:00 2001 From: eafonyang Date: Wed, 4 Nov 2026 10:43:43 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=B1=8F=E5=B9=95=E4=BA=AE?= =?UTF-8?q?=E5=BA=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/config/firmwareFeatures.ts | 14 +++++ client/src/contexts/DeviceContext.tsx | 19 ++++--- client/src/locales/resolveLocale.ts | 73 +++++++++++++++++---------- client/src/pages/SystemPage.tsx | 54 ++++++++++++++++++-- 4 files changed, 122 insertions(+), 38 deletions(-) diff --git a/client/src/config/firmwareFeatures.ts b/client/src/config/firmwareFeatures.ts index 815f434..a8d55b5 100644 --- a/client/src/config/firmwareFeatures.ts +++ b/client/src/config/firmwareFeatures.ts @@ -17,6 +17,10 @@ export const FIRMWARE_FEATURE_MIN_VERSION = { hpVolumeSettings: 2011, /** EQ 单个滤波器的 Bypass 开关(旁通时下发 type=10)。 */ peqBandBypass: 2012, + /** 设备语言扩展到 LangType 全枚举(12 种);低于此版本仅前 3 种(0/1/2)可下发设备。 */ + extendedDeviceLanguages: 2012, + /** System 页屏幕亮度改为 10 档拖动条(screenLight 下发 0–9);低于此版本保持三档选择列表。 */ + screenBrightnessSlider: 2012, } as const; export type FirmwareFeatureKey = keyof typeof FIRMWARE_FEATURE_MIN_VERSION; @@ -44,3 +48,13 @@ export function supportsHpVolumeSettings(firmwareVersion: number): boolean { export function supportsPeqBandBypass(firmwareVersion: number): boolean { return isFirmwareFeatureAvailable("peqBandBypass", firmwareVersion); } + +/** 固件是否支持把扩展设备语言(索引 3..11)下发到设备。 */ +export function supportsExtendedDeviceLanguages(firmwareVersion: number): boolean { + return isFirmwareFeatureAvailable("extendedDeviceLanguages", firmwareVersion); +} + +/** 固件是否支持屏幕亮度 10 档拖动条(screenLight 0–9,数值越大越亮)。 */ +export function supportsScreenBrightnessSlider(firmwareVersion: number): boolean { + return isFirmwareFeatureAvailable("screenBrightnessSlider", firmwareVersion); +} diff --git a/client/src/contexts/DeviceContext.tsx b/client/src/contexts/DeviceContext.tsx index 0012ec0..4154298 100644 --- a/client/src/contexts/DeviceContext.tsx +++ b/client/src/contexts/DeviceContext.tsx @@ -9,13 +9,15 @@ import { PeqChangePayload, PeqFilter, PeqState, + parseFirmwareVersion, } from "@/lib/luxsinApi"; import { getDeviceIpFromUrl, resolveInitialDeviceIp } from "@/lib/deviceIp"; import { isLikelyNetworkFetchError } from "@/lib/networkError"; +import { supportsExtendedDeviceLanguages } from "@/config/firmwareFeatures"; import { DEFAULT_DEVICE_LANGUAGE, getUiLanguageOverride, - isDeviceLanguage, + isSendableDeviceLanguage, setUiLanguageOverride, } from "@/locales/resolveLocale"; @@ -34,8 +36,8 @@ interface DeviceContextType { */ effectiveLanguage: number; /** - * 设置界面语言:选择设备语言(0/1/2)会下发设备并清除前端覆盖; - * 选择 de/es/fr(3/4/5)仅做前端切换并持久化,绝不下发设备。 + * 设置界面语言:当前固件可下发的语言(0/1/2 任意固件;3..11 需固件 >=2012)会下发设备并清除前端覆盖; + * 固件 <2012 时选择 3..11 仅做前端切换并持久化,不下发设备。 */ setUiLanguage: (index: number) => Promise; peqState: PeqState | null; @@ -200,18 +202,23 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) { // 生效的界面语言:前端覆盖优先,其次设备真实语言,最后默认。 const effectiveLanguage = uiLanguage ?? deviceState?.language ?? DEFAULT_DEVICE_LANGUAGE; + // 固件是否支持把扩展设备语言(3..11)下发到设备(>=2012)。 + const extendedLangsSupported = supportsExtendedDeviceLanguages( + parseFirmwareVersion(deviceState?.version), + ); + const setUiLanguage = useCallback(async (index: number) => { - if (isDeviceLanguage(index)) { + if (isSendableDeviceLanguage(index, extendedLangsSupported)) { // 设备支持的语言:清除前端覆盖并下发设备(沿用既有 setting 流程)。 setUiLanguageOverride(null); setUiLanguageState(null); await updateSetting({ language: index }); return; } - // de/es/fr:仅前端切换 + 持久化,不下发设备。 + // 固件 <2012 时的扩展语言(3..11):仅前端切换 + 持久化,不下发设备。 setUiLanguageOverride(index); setUiLanguageState(getUiLanguageOverride()); - }, [updateSetting]); + }, [updateSetting, extendedLangsSupported]); // 设备侧语言发生变化(用户在设备上切换语言,经 msgCount 轮询/刷新同步下来)时, // 清除前端语言覆盖,让 controller 跟随设备语言。 diff --git a/client/src/locales/resolveLocale.ts b/client/src/locales/resolveLocale.ts index 7e3c8db..21338c9 100644 --- a/client/src/locales/resolveLocale.ts +++ b/client/src/locales/resolveLocale.ts @@ -12,31 +12,32 @@ import localeRu from "./data-ru.json"; import localeJa from "./data-ja.json"; /** - * 语言索引映射: - * 0=en, 1=zh-HK, 2=zh-CN → 设备固件支持,选择后会下发 dev/info.cgi?action=setting&language= - * 3=de, 4=es, 5=fr, 6=ko, 7=vi, 8=pt, 9=pl, - * 10=ru, 11=ja → 设备不支持,仅在 controller 前端做界面语言切换,绝不下发设备 + * 语言索引映射(与设备固件 LangType 枚举顺序一致,值从 0 开始): + * 0=en, 1=zh-HK(繁中), 2=zh-CN(简中), 3=de, 4=fr, 5=ja, + * 6=ko, 7=pl, 8=pt, 9=ru, 10=es, 11=vi + * 0/1/2 → 所有固件均支持,选择后下发 dev/info.cgi?action=setting&language= + * 3..11 → 固件 >=2012 才支持下发设备;<2012 仅在 controller 前端做界面语言切换,不下发设备 */ export const DEFAULT_DEVICE_LANGUAGE = 0; -/** 设备固件真正支持的语言索引,只有这些允许下发到设备。 */ +/** 所有固件均支持、始终可下发设备的语言索引(0/1/2)。 */ export const DEVICE_LANGUAGE_INDICES = [0, 1, 2] as const; export type LocalePack = typeof localeEn; const LOCALE_PACKS = [ - localeEn, - localeZhHK, - localeZh, - localeDe, - localeEs, - localeFr, - localeKo, - localeVi, - localePt, - localePl, - localeRu, - localeJa, + localeEn, // 0 English + localeZhHK, // 1 Traditional Chinese + localeZh, // 2 Simplified Chinese + localeDe, // 3 German + localeFr, // 4 French + localeJa, // 5 Japanese + localeKo, // 6 Korean + localePl, // 7 Polish + localePt, // 8 Portuguese + localeRu, // 9 Russian + localeEs, // 10 Spanish + localeVi, // 11 Vietnamese ] as LocalePack[]; /** 语言选择器可展示的全部语言数量(含仅前端语言)。 */ @@ -44,28 +45,45 @@ export const LANGUAGE_COUNT = LOCALE_PACKS.length; /** * 语言选择器固定显示的「母语名称」,与当前界面语言无关。 - * 数组下标即语言索引(0=en … 11=ja),选择时直接作为语言值使用。 + * 数组下标即语言索引(0=en … 11=vi),与设备 LangType 枚举一致,选择时直接作为语言值使用。 */ export const LANGUAGE_NATIVE_LABELS = [ "English", "繁體中文", "简体中文", "Deutsch", - "Español", "Français", - "한국어", - "Tiếng Việt", - "Português (Portugal)", - "Polski", - "Русский", "日本語", + "한국어", + "Polski", + "Português (Portugal)", + "Русский", + "Español", + "Tiếng Việt", ]; -/** 是否为设备支持的语言(0/1/2),只有这些才会下发设备。 */ +/** 是否为设备支持的语言(0/1/2),这些在任何固件下都会下发设备。 */ export function isDeviceLanguage(index: number | undefined | null): boolean { return index === 0 || index === 1 || index === 2; } +/** 是否为「扩展设备语言」(3..11):固件 >=2012 时可下发设备,否则仅前端。 */ +export function isExtendedDeviceLanguage(index: number | undefined | null): boolean { + return typeof index === "number" && index >= 3 && index < LOCALE_PACKS.length; +} + +/** + * 是否为当前固件可下发设备的语言。 + * 0/1/2 始终可下发;3..11 仅当 extendedSupported(固件 >=2012)为真时可下发。 + */ +export function isSendableDeviceLanguage( + index: number | undefined | null, + extendedSupported: boolean, +): boolean { + if (isDeviceLanguage(index)) return true; + return extendedSupported && isExtendedDeviceLanguage(index); +} + /** 是否为已知的语言索引(0..LANGUAGE_COUNT-1)。 */ export function isKnownLanguage(index: number | undefined | null): boolean { return typeof index === "number" && index >= 0 && index < LOCALE_PACKS.length; @@ -76,8 +94,9 @@ export function resolveDeviceLanguage(language: number | undefined): number { return language ?? DEFAULT_DEVICE_LANGUAGE; } -/* ── 前端语言覆盖(仅非设备语言 3..11,持久化到 localStorage,绝不下发设备) ── */ -const UI_LANGUAGE_OVERRIDE_KEY = "luxsin.uiLanguageOverride"; +/* ── 前端语言覆盖(仅固件 <2012 时的非设备语言 3..11,持久化到 localStorage,不下发设备) ── + key 带 .v2 后缀:语言顺序已对齐设备 LangType 枚举,旧 key 存的索引含义已变化,需作废。 */ +const UI_LANGUAGE_OVERRIDE_KEY = "luxsin.uiLanguageOverride.v2"; function readStoredOverride(): number | null { try { diff --git a/client/src/pages/SystemPage.tsx b/client/src/pages/SystemPage.tsx index d25734c..5cdbacb 100644 --- a/client/src/pages/SystemPage.tsx +++ b/client/src/pages/SystemPage.tsx @@ -11,13 +11,16 @@ import { useDevice } from "@/contexts/DeviceContext"; import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react"; import { useLocation } from "wouter"; -import { useEffect, useState } from "react"; +import { useEffect, useRef, useState } from "react"; import BottomNav from "@/components/BottomNav"; import { fetchFirmwareVersionName, formatFirmwareVersionDisplay, + parseFirmwareVersion, readCachedFirmwareVersionName, } from "@/lib/luxsinApi"; +import { supportsScreenBrightnessSlider } from "@/config/firmwareFeatures"; +import { CyanSlider } from "./eq/components/CyanSlider"; import { navigateToSelect } from "./SelectPage"; import localeEn from "@/locales/data-en.json"; import { DEFAULT_DEVICE_LANGUAGE, isKnownLanguage, LANGUAGE_NATIVE_LABELS, resolveLocalePack } from "@/locales/resolveLocale"; @@ -106,6 +109,12 @@ function optionLabels(items: Array<{ index: number; label: string }> | undefined return sorted.map((item) => item.label); } +/** 屏幕亮度拖动条档位(固件 >=2012):设备值 0–9,数值越大越亮;界面显示 +1 即 1–10。 */ +function clampScreenLightLevel(v: number | undefined): number { + if (typeof v !== "number" || !Number.isFinite(v)) return 1; + return Math.min(9, Math.max(0, Math.round(v))); +} + export default function SystemPage() { const [, setLocation] = useLocation(); const { deviceState, updateSetting, refresh, isConnected, api, ip, effectiveLanguage } = useDevice(); @@ -160,6 +169,15 @@ export default function SystemPage() { const autoHomeIdx = ds?.autoHome ?? 1; const langIdx = isKnownLanguage(effectiveLanguage) ? effectiveLanguage : DEFAULT_DEVICE_LANGUAGE; const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0; + + // 固件 >=2012:屏幕亮度改为 10 档拖动条;<2012 保持三档选择列表不变。 + const screenBrightnessSliderSupported = supportsScreenBrightnessSlider(parseFirmwareVersion(ds?.version)); + const screenLightValue = clampScreenLightLevel(ds?.screenLight); + const [localScreenLight, setLocalScreenLight] = useState(() => clampScreenLightLevel(ds?.screenLight)); + const draggingScreenLight = useRef(false); + useEffect(() => { + if (!draggingScreenLight.current) setLocalScreenLight(clampScreenLightLevel(ds?.screenLight)); + }, [ds?.screenLight]); const localeData = resolveLocalePack(effectiveLanguage) as LocaleSettings; const settingsText = localeData.settings; const ui = settingsText.systemPage; @@ -169,7 +187,7 @@ export default function SystemPage() { const SLEEP_OPTIONS = optionLabels(settingsText.sleepTime?.options ?? settingsText.sleep?.options, ["关闭", "1分钟", "5分钟", "10分钟"]); const AUTO_HOME_OPTIONS = optionLabels(settingsText.Automatic?.options, ["关闭", "20秒", "40秒", "60秒"]); // 语言选项固定显示各语言的母语名称,与当前界面语言无关; - // 数组下标即语言索引(0=en … 7=vi),与 SelectPage 回传的值一致。 + // 数组下标即语言索引(0=en … 11=vi,与设备 LangType 枚举一致),与 SelectPage 回传的值一致。 const LANG_OPTIONS = [...LANGUAGE_NATIVE_LABELS]; const sleepRowLabel = @@ -199,9 +217,35 @@ export default function SystemPage() { {/* ── 显示 ── */}
- goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} /> + {screenBrightnessSliderSupported ? ( +
+
+
+
+
{settingsText.screenBrightness.label}
+
{ui.screenBrightnessDesc}
+
+ {localScreenLight + 1} +
+ { draggingScreenLight.current = true; }} + onChange={(v) => setLocalScreenLight(v)} + onInteractionEnd={() => { + draggingScreenLight.current = false; + if (localScreenLight !== screenLightValue) updateSetting({ screenLight: localScreenLight }); + }} + /> +
+
+ ) : ( + goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} /> + )} goSelect(settingsText.turnOffScreen.label, SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />