新增屏幕亮度
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 跟随设备语言。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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")} />
|
||||
|
||||
Reference in New Issue
Block a user