新增屏幕亮度
This commit is contained in:
@@ -17,6 +17,10 @@ export const FIRMWARE_FEATURE_MIN_VERSION = {
|
|||||||
hpVolumeSettings: 2011,
|
hpVolumeSettings: 2011,
|
||||||
/** EQ 单个滤波器的 Bypass 开关(旁通时下发 type=10)。 */
|
/** EQ 单个滤波器的 Bypass 开关(旁通时下发 type=10)。 */
|
||||||
peqBandBypass: 2012,
|
peqBandBypass: 2012,
|
||||||
|
/** 设备语言扩展到 LangType 全枚举(12 种);低于此版本仅前 3 种(0/1/2)可下发设备。 */
|
||||||
|
extendedDeviceLanguages: 2012,
|
||||||
|
/** System 页屏幕亮度改为 10 档拖动条(screenLight 下发 0–9);低于此版本保持三档选择列表。 */
|
||||||
|
screenBrightnessSlider: 2012,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FirmwareFeatureKey = keyof typeof FIRMWARE_FEATURE_MIN_VERSION;
|
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 {
|
export function supportsPeqBandBypass(firmwareVersion: number): boolean {
|
||||||
return isFirmwareFeatureAvailable("peqBandBypass", firmwareVersion);
|
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,
|
PeqChangePayload,
|
||||||
PeqFilter,
|
PeqFilter,
|
||||||
PeqState,
|
PeqState,
|
||||||
|
parseFirmwareVersion,
|
||||||
} from "@/lib/luxsinApi";
|
} from "@/lib/luxsinApi";
|
||||||
import { getDeviceIpFromUrl, resolveInitialDeviceIp } from "@/lib/deviceIp";
|
import { getDeviceIpFromUrl, resolveInitialDeviceIp } from "@/lib/deviceIp";
|
||||||
import { isLikelyNetworkFetchError } from "@/lib/networkError";
|
import { isLikelyNetworkFetchError } from "@/lib/networkError";
|
||||||
|
import { supportsExtendedDeviceLanguages } from "@/config/firmwareFeatures";
|
||||||
import {
|
import {
|
||||||
DEFAULT_DEVICE_LANGUAGE,
|
DEFAULT_DEVICE_LANGUAGE,
|
||||||
getUiLanguageOverride,
|
getUiLanguageOverride,
|
||||||
isDeviceLanguage,
|
isSendableDeviceLanguage,
|
||||||
setUiLanguageOverride,
|
setUiLanguageOverride,
|
||||||
} from "@/locales/resolveLocale";
|
} from "@/locales/resolveLocale";
|
||||||
|
|
||||||
@@ -34,8 +36,8 @@ interface DeviceContextType {
|
|||||||
*/
|
*/
|
||||||
effectiveLanguage: number;
|
effectiveLanguage: number;
|
||||||
/**
|
/**
|
||||||
* 设置界面语言:选择设备语言(0/1/2)会下发设备并清除前端覆盖;
|
* 设置界面语言:当前固件可下发的语言(0/1/2 任意固件;3..11 需固件 >=2012)会下发设备并清除前端覆盖;
|
||||||
* 选择 de/es/fr(3/4/5)仅做前端切换并持久化,绝不下发设备。
|
* 固件 <2012 时选择 3..11 仅做前端切换并持久化,不下发设备。
|
||||||
*/
|
*/
|
||||||
setUiLanguage: (index: number) => Promise<void>;
|
setUiLanguage: (index: number) => Promise<void>;
|
||||||
peqState: PeqState | null;
|
peqState: PeqState | null;
|
||||||
@@ -200,18 +202,23 @@ export function DeviceProvider({ children }: { children: React.ReactNode }) {
|
|||||||
// 生效的界面语言:前端覆盖优先,其次设备真实语言,最后默认。
|
// 生效的界面语言:前端覆盖优先,其次设备真实语言,最后默认。
|
||||||
const effectiveLanguage = uiLanguage ?? deviceState?.language ?? DEFAULT_DEVICE_LANGUAGE;
|
const effectiveLanguage = uiLanguage ?? deviceState?.language ?? DEFAULT_DEVICE_LANGUAGE;
|
||||||
|
|
||||||
|
// 固件是否支持把扩展设备语言(3..11)下发到设备(>=2012)。
|
||||||
|
const extendedLangsSupported = supportsExtendedDeviceLanguages(
|
||||||
|
parseFirmwareVersion(deviceState?.version),
|
||||||
|
);
|
||||||
|
|
||||||
const setUiLanguage = useCallback(async (index: number) => {
|
const setUiLanguage = useCallback(async (index: number) => {
|
||||||
if (isDeviceLanguage(index)) {
|
if (isSendableDeviceLanguage(index, extendedLangsSupported)) {
|
||||||
// 设备支持的语言:清除前端覆盖并下发设备(沿用既有 setting 流程)。
|
// 设备支持的语言:清除前端覆盖并下发设备(沿用既有 setting 流程)。
|
||||||
setUiLanguageOverride(null);
|
setUiLanguageOverride(null);
|
||||||
setUiLanguageState(null);
|
setUiLanguageState(null);
|
||||||
await updateSetting({ language: index });
|
await updateSetting({ language: index });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// de/es/fr:仅前端切换 + 持久化,不下发设备。
|
// 固件 <2012 时的扩展语言(3..11):仅前端切换 + 持久化,不下发设备。
|
||||||
setUiLanguageOverride(index);
|
setUiLanguageOverride(index);
|
||||||
setUiLanguageState(getUiLanguageOverride());
|
setUiLanguageState(getUiLanguageOverride());
|
||||||
}, [updateSetting]);
|
}, [updateSetting, extendedLangsSupported]);
|
||||||
|
|
||||||
// 设备侧语言发生变化(用户在设备上切换语言,经 msgCount 轮询/刷新同步下来)时,
|
// 设备侧语言发生变化(用户在设备上切换语言,经 msgCount 轮询/刷新同步下来)时,
|
||||||
// 清除前端语言覆盖,让 controller 跟随设备语言。
|
// 清除前端语言覆盖,让 controller 跟随设备语言。
|
||||||
|
|||||||
@@ -12,31 +12,32 @@ import localeRu from "./data-ru.json";
|
|||||||
import localeJa from "./data-ja.json";
|
import localeJa from "./data-ja.json";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 语言索引映射:
|
* 语言索引映射(与设备固件 LangType 枚举顺序一致,值从 0 开始):
|
||||||
* 0=en, 1=zh-HK, 2=zh-CN → 设备固件支持,选择后会下发 dev/info.cgi?action=setting&language=
|
* 0=en, 1=zh-HK(繁中), 2=zh-CN(简中), 3=de, 4=fr, 5=ja,
|
||||||
* 3=de, 4=es, 5=fr, 6=ko, 7=vi, 8=pt, 9=pl,
|
* 6=ko, 7=pl, 8=pt, 9=ru, 10=es, 11=vi
|
||||||
* 10=ru, 11=ja → 设备不支持,仅在 controller 前端做界面语言切换,绝不下发设备
|
* 0/1/2 → 所有固件均支持,选择后下发 dev/info.cgi?action=setting&language=
|
||||||
|
* 3..11 → 固件 >=2012 才支持下发设备;<2012 仅在 controller 前端做界面语言切换,不下发设备
|
||||||
*/
|
*/
|
||||||
export const DEFAULT_DEVICE_LANGUAGE = 0;
|
export const DEFAULT_DEVICE_LANGUAGE = 0;
|
||||||
|
|
||||||
/** 设备固件真正支持的语言索引,只有这些允许下发到设备。 */
|
/** 所有固件均支持、始终可下发设备的语言索引(0/1/2)。 */
|
||||||
export const DEVICE_LANGUAGE_INDICES = [0, 1, 2] as const;
|
export const DEVICE_LANGUAGE_INDICES = [0, 1, 2] as const;
|
||||||
|
|
||||||
export type LocalePack = typeof localeEn;
|
export type LocalePack = typeof localeEn;
|
||||||
|
|
||||||
const LOCALE_PACKS = [
|
const LOCALE_PACKS = [
|
||||||
localeEn,
|
localeEn, // 0 English
|
||||||
localeZhHK,
|
localeZhHK, // 1 Traditional Chinese
|
||||||
localeZh,
|
localeZh, // 2 Simplified Chinese
|
||||||
localeDe,
|
localeDe, // 3 German
|
||||||
localeEs,
|
localeFr, // 4 French
|
||||||
localeFr,
|
localeJa, // 5 Japanese
|
||||||
localeKo,
|
localeKo, // 6 Korean
|
||||||
localeVi,
|
localePl, // 7 Polish
|
||||||
localePt,
|
localePt, // 8 Portuguese
|
||||||
localePl,
|
localeRu, // 9 Russian
|
||||||
localeRu,
|
localeEs, // 10 Spanish
|
||||||
localeJa,
|
localeVi, // 11 Vietnamese
|
||||||
] as LocalePack[];
|
] 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 = [
|
export const LANGUAGE_NATIVE_LABELS = [
|
||||||
"English",
|
"English",
|
||||||
"繁體中文",
|
"繁體中文",
|
||||||
"简体中文",
|
"简体中文",
|
||||||
"Deutsch",
|
"Deutsch",
|
||||||
"Español",
|
|
||||||
"Français",
|
"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 {
|
export function isDeviceLanguage(index: number | undefined | null): boolean {
|
||||||
return index === 0 || index === 1 || index === 2;
|
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)。 */
|
/** 是否为已知的语言索引(0..LANGUAGE_COUNT-1)。 */
|
||||||
export function isKnownLanguage(index: number | undefined | null): boolean {
|
export function isKnownLanguage(index: number | undefined | null): boolean {
|
||||||
return typeof index === "number" && index >= 0 && index < LOCALE_PACKS.length;
|
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;
|
return language ?? DEFAULT_DEVICE_LANGUAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── 前端语言覆盖(仅非设备语言 3..11,持久化到 localStorage,绝不下发设备) ── */
|
/* ── 前端语言覆盖(仅固件 <2012 时的非设备语言 3..11,持久化到 localStorage,不下发设备) ──
|
||||||
const UI_LANGUAGE_OVERRIDE_KEY = "luxsin.uiLanguageOverride";
|
key 带 .v2 后缀:语言顺序已对齐设备 LangType 枚举,旧 key 存的索引含义已变化,需作废。 */
|
||||||
|
const UI_LANGUAGE_OVERRIDE_KEY = "luxsin.uiLanguageOverride.v2";
|
||||||
|
|
||||||
function readStoredOverride(): number | null {
|
function readStoredOverride(): number | null {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -11,13 +11,16 @@
|
|||||||
import { useDevice } from "@/contexts/DeviceContext";
|
import { useDevice } from "@/contexts/DeviceContext";
|
||||||
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
|
import { ChevronLeft, ChevronRight, Loader2 } from "lucide-react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import BottomNav from "@/components/BottomNav";
|
import BottomNav from "@/components/BottomNav";
|
||||||
import {
|
import {
|
||||||
fetchFirmwareVersionName,
|
fetchFirmwareVersionName,
|
||||||
formatFirmwareVersionDisplay,
|
formatFirmwareVersionDisplay,
|
||||||
|
parseFirmwareVersion,
|
||||||
readCachedFirmwareVersionName,
|
readCachedFirmwareVersionName,
|
||||||
} from "@/lib/luxsinApi";
|
} from "@/lib/luxsinApi";
|
||||||
|
import { supportsScreenBrightnessSlider } from "@/config/firmwareFeatures";
|
||||||
|
import { CyanSlider } from "./eq/components/CyanSlider";
|
||||||
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, isKnownLanguage, LANGUAGE_NATIVE_LABELS, resolveLocalePack } from "@/locales/resolveLocale";
|
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);
|
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() {
|
export default function SystemPage() {
|
||||||
const [, setLocation] = useLocation();
|
const [, setLocation] = useLocation();
|
||||||
const { deviceState, updateSetting, refresh, isConnected, api, ip, effectiveLanguage } = useDevice();
|
const { deviceState, updateSetting, refresh, isConnected, api, ip, effectiveLanguage } = useDevice();
|
||||||
@@ -160,6 +169,15 @@ export default function SystemPage() {
|
|||||||
const autoHomeIdx = ds?.autoHome ?? 1;
|
const autoHomeIdx = ds?.autoHome ?? 1;
|
||||||
const langIdx = isKnownLanguage(effectiveLanguage) ? effectiveLanguage : DEFAULT_DEVICE_LANGUAGE;
|
const langIdx = isKnownLanguage(effectiveLanguage) ? effectiveLanguage : DEFAULT_DEVICE_LANGUAGE;
|
||||||
const knobBreathLightOn = (ds?.knob_breathlight ?? 1) === 0;
|
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 localeData = resolveLocalePack(effectiveLanguage) as LocaleSettings;
|
||||||
const settingsText = localeData.settings;
|
const settingsText = localeData.settings;
|
||||||
const ui = settingsText.systemPage;
|
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 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秒"]);
|
||||||
// 语言选项固定显示各语言的母语名称,与当前界面语言无关;
|
// 语言选项固定显示各语言的母语名称,与当前界面语言无关;
|
||||||
// 数组下标即语言索引(0=en … 7=vi),与 SelectPage 回传的值一致。
|
// 数组下标即语言索引(0=en … 11=vi,与设备 LangType 枚举一致),与 SelectPage 回传的值一致。
|
||||||
const LANG_OPTIONS = [...LANGUAGE_NATIVE_LABELS];
|
const LANG_OPTIONS = [...LANGUAGE_NATIVE_LABELS];
|
||||||
|
|
||||||
const sleepRowLabel =
|
const sleepRowLabel =
|
||||||
@@ -199,9 +217,35 @@ export default function SystemPage() {
|
|||||||
{/* ── 显示 ── */}
|
{/* ── 显示 ── */}
|
||||||
<SectionHeader title={ui.sectionDisplay} />
|
<SectionHeader title={ui.sectionDisplay} />
|
||||||
<div className="ios-list-group">
|
<div className="ios-list-group">
|
||||||
<ListRow label={settingsText.screenBrightness.label} description={ui.screenBrightnessDesc}
|
{screenBrightnessSliderSupported ? (
|
||||||
value={SCREEN_LIGHT_OPTIONS[screenLightIdx]}
|
<div className="ios-list-row">
|
||||||
onClick={() => goSelect(settingsText.screenBrightness.label, SCREEN_LIGHT_OPTIONS, screenLightIdx, "screenLight")} />
|
<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}
|
<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")} />
|
||||||
|
|||||||
Reference in New Issue
Block a user