新增听力补偿,将有版本限制的功能,提取到配置文件,不再硬编码,优化部署文件参数
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Firmware version gates for UI features.
|
||||
*
|
||||
* Versions are compared using `parseFirmwareVersion` (e.g. 2007 → 2.0.0.7).
|
||||
* Adjust thresholds here instead of hardcoding in page components.
|
||||
*/
|
||||
export const FIRMWARE_FEATURE_MIN_VERSION = {
|
||||
/** Boot sound: indices 7–10 (-35…-50 dB); legacy firmware caps at index 6. */
|
||||
bootSoundExtendedSteps: 26,
|
||||
/** Subwoofer LPF/HPF full-range toggles on the Effects page. */
|
||||
subwooferFullRange: 2001,
|
||||
/** Hearing compensation section on the Effects page. */
|
||||
hearingCompensation: 2007,
|
||||
} as const;
|
||||
|
||||
export type FirmwareFeatureKey = keyof typeof FIRMWARE_FEATURE_MIN_VERSION;
|
||||
|
||||
export function isFirmwareFeatureAvailable(
|
||||
feature: FirmwareFeatureKey,
|
||||
firmwareVersion: number,
|
||||
): boolean {
|
||||
return firmwareVersion >= FIRMWARE_FEATURE_MIN_VERSION[feature];
|
||||
}
|
||||
|
||||
/** Max `bootSound` select index (0–6 legacy, 0–10 when extended steps are supported). */
|
||||
export function getBootSoundMaxIndex(firmwareVersion: number): number {
|
||||
return isFirmwareFeatureAvailable("bootSoundExtendedSteps", firmwareVersion) ? 10 : 6;
|
||||
}
|
||||
@@ -120,6 +120,9 @@ export interface DeviceState {
|
||||
loudness_bass_gain: number;
|
||||
loudness_treble_gain: number;
|
||||
loudness_threshold_gain: number;
|
||||
hearing_enable: number;
|
||||
hearing_select: number;
|
||||
hearing_data: HearingProfile[] | string;
|
||||
bt_status: number;
|
||||
bt_srcname: string;
|
||||
bt_title: string;
|
||||
@@ -127,6 +130,37 @@ export interface DeviceState {
|
||||
msgCount: number;
|
||||
}
|
||||
|
||||
export interface HearingProfile {
|
||||
n: string;
|
||||
l: number[];
|
||||
r: number[];
|
||||
}
|
||||
|
||||
/** Parse `hearing_data` from syncData (array or JSON string). */
|
||||
export function parseHearingData(raw: unknown): HearingProfile[] {
|
||||
if (raw == null || raw === "") return [];
|
||||
let value: unknown = raw;
|
||||
if (typeof raw === "string") {
|
||||
try {
|
||||
value = JSON.parse(raw);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
if (!Array.isArray(value)) return [];
|
||||
return value
|
||||
.map((item) => {
|
||||
if (!item || typeof item !== "object") return null;
|
||||
const row = item as Record<string, unknown>;
|
||||
const n = typeof row.n === "string" ? row.n.trim() : String(row.n ?? "").trim();
|
||||
if (!n) return null;
|
||||
const l = Array.isArray(row.l) ? row.l.map((v) => Number(v)) : [];
|
||||
const r = Array.isArray(row.r) ? row.r.map((v) => Number(v)) : [];
|
||||
return { n, l, r };
|
||||
})
|
||||
.filter((item): item is HearingProfile => item !== null);
|
||||
}
|
||||
|
||||
export interface PeqFilter {
|
||||
fc: number;
|
||||
gain: number;
|
||||
@@ -375,7 +409,7 @@ export class LuxsinAPI {
|
||||
setCrossfeedEnable(enable: boolean) { return this.setSetting({ crossfeed_enable: enable ? 1 : 0 }); }
|
||||
setXlrPolarity(reverse: boolean) { return this.setSetting({ xlr: reverse ? 1 : 0 }); }
|
||||
setDacArc(earc: boolean) { return this.setSetting({ dacArc: earc ? 1 : 0 }); }
|
||||
/** Boot volume level: 0 = default, 1..6 = -5..-30 dB, 7..10 = -35..-50 dB (firmware >= 26). */
|
||||
/** Boot volume level: 0 = default, 1..6 = -5..-30 dB, 7..10 = -35..-50 dB (see firmwareFeatures.bootSoundExtendedSteps). */
|
||||
setBootSound(level: number) {
|
||||
return this.setSetting({ bootSound: Math.max(0, Math.min(10, Math.round(level))) });
|
||||
}
|
||||
@@ -383,7 +417,7 @@ export class LuxsinAPI {
|
||||
powerOff() { return this.setSetting({ power: 0 }); }
|
||||
}
|
||||
|
||||
/** Parse firmware `version` for feature gating (e.g. bootSound extended steps). */
|
||||
/** Parse firmware `version` for feature gating (see `config/firmwareFeatures.ts`). */
|
||||
export function parseFirmwareVersion(version: unknown): number {
|
||||
if (version === null || version === undefined) return 0;
|
||||
if (typeof version === "number" && Number.isFinite(version)) return version;
|
||||
@@ -423,10 +457,7 @@ export function isHomeVolumeLockedByPassthrough(
|
||||
return (output ?? -1) !== OUTPUT_HEADSET_INDEX;
|
||||
}
|
||||
|
||||
/** Max `bootSound` index available for the current firmware. */
|
||||
export function getBootSoundMaxIndex(firmwareVersion: number): number {
|
||||
return firmwareVersion >= 26 ? 10 : 6;
|
||||
}
|
||||
export { getBootSoundMaxIndex } from "@/config/firmwareFeatures";
|
||||
|
||||
/** Normalize `bootSound` from syncData (0 = default … 10 = -50 dB). */
|
||||
export function normalizeBootSound(value: unknown): number {
|
||||
@@ -502,6 +533,15 @@ export const MOCK_DEVICE_STATE: DeviceState = {
|
||||
loudness_bass_gain: 3,
|
||||
loudness_treble_gain: 2,
|
||||
loudness_threshold_gain: 60,
|
||||
hearing_enable: 0,
|
||||
hearing_select: 0,
|
||||
hearing_data: [
|
||||
{
|
||||
n: "testing",
|
||||
l: [1.7, 0.8, 0.5, 0.5, 0.5, 0.5, 0.4],
|
||||
r: [0.4, 0.4, 0.3, 0.3, 0.2, 0.4, 0.3],
|
||||
},
|
||||
],
|
||||
bt_status: 1,
|
||||
bt_srcname: "iPhone 15 Pro",
|
||||
bt_title: "Bohemian Rhapsody",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Effekte",
|
||||
"selectStyleTitle": "Klangstil auswählen",
|
||||
"selectCrossfeedTitle": "Crossfeed-Voreinstellung auswählen",
|
||||
"selectHearingTitle": "Hörkompensationsprofil auswählen",
|
||||
"lowBass": "Bass",
|
||||
"enterBass": "Mitten",
|
||||
"highBass": "Höhen",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B Entspannt (650 Hz, 9,5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Hörkompensation",
|
||||
"emptyProfile": "Kein Profil verfügbar"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Subwoofer",
|
||||
"cutOffFrequency": "Trennfrequenz",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Effects",
|
||||
"selectStyleTitle": "Select sound style",
|
||||
"selectCrossfeedTitle": "Select crossfeed preset",
|
||||
"selectHearingTitle": "Select hearing compensation profile",
|
||||
"lowBass": "Bass",
|
||||
"enterBass": "Mid",
|
||||
"highBass": "Treble",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B relax (650 Hz, 9.5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Hearing compensation",
|
||||
"emptyProfile": "No profile available"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Subwoofer",
|
||||
"cutOffFrequency": "Cut off frequency",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Efectos",
|
||||
"selectStyleTitle": "Seleccionar estilo de sonido",
|
||||
"selectCrossfeedTitle": "Seleccionar preset de crossfeed",
|
||||
"selectHearingTitle": "Seleccionar perfil de compensación auditiva",
|
||||
"lowBass": "Graves",
|
||||
"enterBass": "Medios",
|
||||
"highBass": "Agudos",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B relajado (650 Hz, 9,5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Compensación auditiva",
|
||||
"emptyProfile": "No hay perfil disponible"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Subwoofer",
|
||||
"cutOffFrequency": "Frecuencia de corte",
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
"pageTitle": "Effets",
|
||||
"selectStyleTitle": "Sélectionner le style sonore",
|
||||
"selectCrossfeedTitle": "Sélectionner le préréglage crossfeed",
|
||||
"selectHearingTitle": "Sélectionner le profil de compensation auditive",
|
||||
"lowBass": "Basses",
|
||||
"enterBass": "Médiums",
|
||||
"highBass": "Aigus",
|
||||
@@ -110,6 +111,10 @@
|
||||
"BS2B relax (650 Hz, 9,5 dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "Compensation auditive",
|
||||
"emptyProfile": "Aucun profil disponible"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "Caisson de basses",
|
||||
"cutOffFrequency": "Fréquence de coupure",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"pageTitle": "音效",
|
||||
"selectStyleTitle": "選擇音效風格",
|
||||
"selectCrossfeedTitle": "選擇交叉回授模式",
|
||||
"selectHearingTitle": "選擇聽力補償配置",
|
||||
"lowBass": "低頻",
|
||||
"enterBass": "中頻",
|
||||
"highBass": "高頻",
|
||||
@@ -103,6 +104,10 @@
|
||||
"BS2B relax(650Hz,9.5dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "聽力補償",
|
||||
"emptyProfile": "無可用配置"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "低音炮輸出",
|
||||
"cutOffFrequency": "截止頻率",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"pageTitle": "音效",
|
||||
"selectStyleTitle": "选择音效风格",
|
||||
"selectCrossfeedTitle": "选择交叉反馈模式",
|
||||
"selectHearingTitle": "选择听力补偿配置",
|
||||
"lowBass": "低音",
|
||||
"enterBass": "中音",
|
||||
"highBass": "高音",
|
||||
@@ -103,6 +104,10 @@
|
||||
"BS2B relax(650Hz,9.5dB)"
|
||||
]
|
||||
},
|
||||
"hearing": {
|
||||
"label": "听力补偿",
|
||||
"emptyProfile": "无可用配置"
|
||||
},
|
||||
"subwoofer": {
|
||||
"label": "低音炮输出",
|
||||
"cutOffFrequency": "截止频率",
|
||||
|
||||
@@ -14,7 +14,8 @@ import BottomNav from "@/components/BottomNav";
|
||||
import SubwooferLpfChart from "@/components/SubwooferLpfChart";
|
||||
import { FeatureGate } from "@/components/FeatureGate";
|
||||
import { navigateToSelect } from "./SelectPage";
|
||||
import { parseFirmwareVersion } from "@/lib/luxsinApi";
|
||||
import { parseFirmwareVersion, parseHearingData } from "@/lib/luxsinApi";
|
||||
import { isFirmwareFeatureAvailable } from "@/config/firmwareFeatures";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
@@ -247,6 +248,16 @@ export default function EffectsPage() {
|
||||
const widthVal = ds?.width_value ?? 50;
|
||||
const crossfeedOn = (ds?.crossfeed_enable ?? 0) === 1;
|
||||
const crossfeedVal = ds?.crossfeed_value ?? 0;
|
||||
const hearingOn = (ds?.hearing_enable ?? 0) === 1;
|
||||
const hearingSelect = ds?.hearing_select ?? 0;
|
||||
const hearingProfiles = useMemo(
|
||||
() => parseHearingData(ds?.hearing_data),
|
||||
[ds?.hearing_data],
|
||||
);
|
||||
const hearingLabels = useMemo(
|
||||
() => hearingProfiles.map((profile) => profile.n),
|
||||
[hearingProfiles],
|
||||
);
|
||||
const sceneOn = (ds?.effect_enable ?? 0) === 1;
|
||||
const sceneVal = ds?.effect_value ?? 0;
|
||||
const colorOn = (ds?.color_enable ?? 0) === 1;
|
||||
@@ -265,7 +276,8 @@ export default function EffectsPage() {
|
||||
const subwooferLpfOn = (ds?.subwoofer_lpf_enable ?? 0) === 1;
|
||||
const subwooferHpfOn = (ds?.subwoofer_hpf_enable ?? 0) === 1;
|
||||
const firmwareVersion = parseFirmwareVersion(ds?.version);
|
||||
const showSubwooferFullRange = firmwareVersion >= 2001;
|
||||
const showSubwooferFullRange = isFirmwareFeatureAvailable("subwooferFullRange", firmwareVersion);
|
||||
const showHearingCompensation = isFirmwareFeatureAvailable("hearingCompensation", firmwareVersion);
|
||||
|
||||
const subwooferDelayMainL = clampSubwooferDelayValue(ds?.subwoofer_delay_main ?? 0);
|
||||
const subwooferDelaySubL = clampSubwooferDelayValue(ds?.subwoofer_delay ?? 0);
|
||||
@@ -393,6 +405,9 @@ export default function EffectsPage() {
|
||||
const sceneDisplay = sceneLabels[sceneIdx] ?? "";
|
||||
const crossIdx = clampPickIndex(crossfeedVal, crossfeedLabels.length);
|
||||
const crossfeedDisplay = crossfeedLabels[crossIdx] ?? "";
|
||||
const hearingIdx = clampPickIndex(hearingSelect, hearingLabels.length);
|
||||
const hearingDisplay =
|
||||
hearingLabels[hearingIdx] ?? effectText.hearing?.emptyProfile ?? "—";
|
||||
const subRateIdx = clampPickIndex(subwooferRate, subwooferRateLabels.length);
|
||||
const subRateDisplay = subwooferRateLabels[subRateIdx] ?? "";
|
||||
const subMixIdx = clampPickIndex(subwooferMixType, subwooferOutputLabels.length);
|
||||
@@ -870,6 +885,35 @@ export default function EffectsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ── 听力补偿 ── */}
|
||||
{showHearingCompensation && (
|
||||
<div className="ios-list-group p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className="text-[16px] font-medium text-white">{effectText.hearing?.label ?? "听力补偿"}</span>
|
||||
<IOSToggle checked={hearingOn} onChange={(v) => updateSetting({ hearing_enable: v ? 1 : 0 })} />
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
disabled={!hearingOn || hearingLabels.length === 0}
|
||||
className={`flex items-center justify-between w-full transition-opacity ${
|
||||
hearingOn && hearingLabels.length > 0 ? "active:opacity-70" : "cursor-default opacity-50"
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (!hearingOn || hearingLabels.length === 0) return;
|
||||
goSelect(
|
||||
effectText.selectHearingTitle ?? effectText.hearing?.label ?? "听力补偿",
|
||||
hearingLabels,
|
||||
hearingSelect,
|
||||
"hearing_select",
|
||||
);
|
||||
}}
|
||||
>
|
||||
<span className="text-[14px] text-white/45">{hearingDisplay}</span>
|
||||
<ChevronRight size={16} className="ios-chevron" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</FeatureGate>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ const KEY_TO_SETTING: Record<string, string> = {
|
||||
effect_value: "effect_value",
|
||||
|
||||
crossfeed_value: "crossfeed_value",
|
||||
|
||||
hearing_select: "hearing_select",
|
||||
subwoofer_rate: "subwoofer_rate",
|
||||
|
||||
subwoofer_mix_type: "subwoofer_mix_type",
|
||||
|
||||
+58
-20
@@ -1,20 +1,33 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================
|
||||
# 部署脚本 - 支持 test 环境与 images 目录排除
|
||||
#
|
||||
# 用法:
|
||||
# ./scripts/deploy.sh 上传至正式环境 (v2)
|
||||
# ./scripts/deploy.sh -test 上传至测试环境 (test)
|
||||
# ./scripts/deploy.sh -eximg 排除 images 目录,上传至正式环境
|
||||
# ./scripts/deploy.sh -eximg -test 排除 images 目录,上传至测试环境
|
||||
# -reload 参数:上传完成后刷新 CloudFront 缓存
|
||||
# ./scripts/deploy.sh -reload 上传正式环境并刷新缓存
|
||||
# ./scripts/deploy.sh -eximg -test -reload 排除 images,上传测试环境并刷新缓存
|
||||
# 部署脚本 - 支持 test/prod 环境与 images 目录排除
|
||||
# ============================================================
|
||||
|
||||
set -e
|
||||
|
||||
show_help() {
|
||||
cat <<EOF
|
||||
用法: ./scripts/deploy.sh [环境] [选项]
|
||||
|
||||
环境(可选,默认 prod):
|
||||
prod 上传至正式环境 (s3://.../v2/)
|
||||
test 上传至测试环境 (s3://.../test/)
|
||||
|
||||
选项(可合并,如 -ir):
|
||||
-i 排除 images 目录 (*images/*)
|
||||
-r 上传完成后刷新 CloudFront 缓存
|
||||
-h 显示此帮助
|
||||
|
||||
示例:
|
||||
./scripts/deploy.sh # prod,不排除 images,不刷新
|
||||
./scripts/deploy.sh test # test 环境
|
||||
./scripts/deploy.sh test -ir # test,排除 images,并刷新缓存
|
||||
./scripts/deploy.sh prod -i # prod,排除 images
|
||||
./scripts/deploy.sh -r # prod,刷新缓存
|
||||
EOF
|
||||
}
|
||||
|
||||
# 自动识别项目名称 (x8 / x9),依据脚本所在的项目根目录名
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_ROOT="$(basename "$(dirname "$SCRIPT_DIR")")"
|
||||
@@ -26,24 +39,49 @@ if [ -z "$PROJECT_NAME" ]; then
|
||||
fi
|
||||
|
||||
# 解析参数
|
||||
TARGET_ENV="prod" # 默认正式环境
|
||||
EXCLUDE_IMAGES=false # 默认不排除 images
|
||||
RELOAD=false # 默认不刷新 CloudFront 缓存
|
||||
TARGET_ENV="prod"
|
||||
EXCLUDE_IMAGES=false
|
||||
RELOAD=false
|
||||
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
-test)
|
||||
TARGET_ENV="test"
|
||||
-h|--help)
|
||||
show_help
|
||||
exit 0
|
||||
;;
|
||||
-eximg)
|
||||
EXCLUDE_IMAGES=true
|
||||
test|prod)
|
||||
TARGET_ENV="$arg"
|
||||
;;
|
||||
-reload)
|
||||
RELOAD=true
|
||||
-*)
|
||||
flags="${arg#-}"
|
||||
if [ -z "$flags" ]; then
|
||||
echo "未知参数: $arg"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
for ((i = 0; i < ${#flags}; i++)); do
|
||||
c="${flags:i:1}"
|
||||
case "$c" in
|
||||
i)
|
||||
EXCLUDE_IMAGES=true
|
||||
;;
|
||||
r)
|
||||
RELOAD=true
|
||||
;;
|
||||
*)
|
||||
echo "未知选项: -$c"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
;;
|
||||
*)
|
||||
echo "未知参数: $arg"
|
||||
echo "用法: ./scripts/deploy.sh [-test] [-eximg] [-reload]"
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
Reference in New Issue
Block a user