新增屏幕亮度

This commit is contained in:
eafonyang
2026-11-04 10:43:43 +08:00
parent d9e05d34ad
commit 786c4e3e48
4 changed files with 122 additions and 38 deletions
+14
View File
@@ -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);
}
+13 -6
View File
@@ -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<void>;
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 跟随设备语言。
+46 -27
View File
@@ -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 {
+49 -5
View File
@@ -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() {
{/* ── 显示 ── */}
<SectionHeader title={ui.sectionDisplay} />
<div className="ios-list-group">
<ListRow label={settingsText.screenBrightness.label} description={ui.screenBrightnessDesc}
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
onClick={() => goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
{screenBrightnessSliderSupported ? (
<div className="ios-list-row">
<div className="flex-1 min-w-0">
<div className="flex items-start justify-between gap-2">
<div className="min-w-0">
<div className="text-[16px] text-white">{settingsText.screenBrightness.label}</div>
<div className="text-[12px] text-white/35 mt-0.5">{ui.screenBrightnessDesc}</div>
</div>
<span className="ios-row-value notranslate tabular-nums">{localScreenLight + 1}</span>
</div>
<CyanSlider
value={localScreenLight}
min={0}
max={9}
step={1}
onInteractionStart={() => { draggingScreenLight.current = true; }}
onChange={(v) => setLocalScreenLight(v)}
onInteractionEnd={() => {
draggingScreenLight.current = false;
if (localScreenLight !== screenLightValue) updateSetting({ screenLight: localScreenLight });
}}
/>
</div>
</div>
) : (
<ListRow label={settingsText.screenBrightness.label} description={ui.screenBrightnessDesc}
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
onClick={() => goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
)}
<ListRow label={settingsText.turnOffScreen.label} description={ui.turnOffScreenDesc}
value={SCREEN_OFF_OPTIONS[screenOffIdx]}
onClick={() => goSelect(settingsText.turnOffScreen.label, SCREEN_OFF_OPTIONS, screenOffIdx, "screenOff")} />