feat(device): 增加耳机最大音量限制及显示提示
- 新增 getHomeVolumeMax 函数,计算耳机输出的最大允许音量 - 新增 getHeadphoneMaxVolumeDb 函数,获取耳机最大音量的 dB 值用于提示显示 - 修改 setVolume 函数,限制音量在动态最大值范围内,防止首页入口发送超限音量 - Home 页面音量滑块、旋钮角度及音量范围由固定 200 调整为当前设备最大音量 - 添加耳机最大音量提示文本,根据用户设置动态显示最大 dB 值 - 各语言文件新增耳机最大音量提示相关文案 - 优化实现,防止拖动时音量显示超出最大音量限制
This commit is contained in:
+42
-18
@@ -24,6 +24,8 @@ import {
|
||||
AlertDialogTitle,
|
||||
} from "@/components/ui/alert-dialog";
|
||||
import {
|
||||
getHeadphoneMaxVolumeDb,
|
||||
getHomeVolumeMax,
|
||||
INPUT_BLUETOOTH_INDEX,
|
||||
INPUT_LABELS,
|
||||
isHomeVolumeLockedByPassthrough,
|
||||
@@ -253,7 +255,10 @@ export default function Home() {
|
||||
return null;
|
||||
}, [volumeConfirm, homeText]);
|
||||
|
||||
const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100);
|
||||
const maxVolume = getHomeVolumeMax(deviceState);
|
||||
const [volumeDraft, setLocalVol] = useState(deviceState?.volume ?? 100);
|
||||
// 上限或输出切换时立即约束显示,拖动中也不能保留超限值。
|
||||
const localVol = Math.max(0, Math.min(maxVolume, volumeDraft));
|
||||
const isDragging = useRef(false);
|
||||
const knobDraggingRef = useRef(false);
|
||||
const knobActivePointerIdRef = useRef<number | null>(null);
|
||||
@@ -262,10 +267,10 @@ export default function Home() {
|
||||
const knobLastVolRef = useRef<number>(localVol);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging.current && deviceState?.volume !== undefined) {
|
||||
setLocalVol(deviceState.volume);
|
||||
}
|
||||
}, [deviceState?.volume]);
|
||||
setLocalVol(current => Math.max(0, Math.min(maxVolume,
|
||||
!isDragging.current && deviceState?.volume !== undefined ? deviceState.volume : current,
|
||||
)));
|
||||
}, [deviceState?.volume, maxVolume]);
|
||||
|
||||
useEffect(() => {
|
||||
knobLastVolRef.current = localVol;
|
||||
@@ -456,16 +461,25 @@ export default function Home() {
|
||||
const vuLabel = `${homeText.vu ?? "VU表"}${(ds.vu ?? 0) + 1}`;
|
||||
const bypassOn = (ds.dsp_enable ?? 0) === 0;
|
||||
|
||||
// 耳机输出且已设置耳机最大音量时,提示当前上限,避免用户误以为还能加到 0dB。
|
||||
const hpMaxVolumeDb = getHeadphoneMaxVolumeDb(ds);
|
||||
const hpMaxVolumeHint =
|
||||
hpMaxVolumeDb === null
|
||||
? null
|
||||
: (homeText.hpMaxVolumeHint ?? "已设置耳机最大音量,最大音量为:{db}dB").replace(
|
||||
"{db}",
|
||||
String(hpMaxVolumeDb),
|
||||
);
|
||||
|
||||
// Home 的 volume 取值为 0..200,对应 dB = (v - 200) / 2
|
||||
// 因此 1 个 volume 单位 = 0.5dB
|
||||
// AudioPage 的 soundStep:0->0.5dB, 1->1dB, 2->2dB, 3->3dB
|
||||
// 换算为 volume 单位:0.5/1/2/3 dB -> 1/2/4/6
|
||||
const fineVolumeStep = [1, 2, 4, 6][ds.soundStep ?? 1] ?? 1;
|
||||
|
||||
// Slider fill % (0-200 → 0-100%)
|
||||
const fillPct = (localVol / 200) * 100;
|
||||
// Knob angle (map 0..200 -> -135..+135 degrees)
|
||||
const knobAngle = -135 + (localVol / 200) * 270;
|
||||
// 滑条填充和旋钮角度均按当前输出的最大音量映射。
|
||||
const fillPct = (localVol / maxVolume) * 100;
|
||||
const knobAngle = -135 + (localVol / maxVolume) * 270;
|
||||
|
||||
const pointAngle = (clientX: number, clientY: number, rect: DOMRect) => {
|
||||
const cx = rect.left + rect.width / 2;
|
||||
@@ -500,9 +514,9 @@ export default function Home() {
|
||||
const rect = el.getBoundingClientRect();
|
||||
const a = pointAngle(e.clientX, e.clientY, rect);
|
||||
const delta = normalizeDelta(a - knobStartAngleRef.current);
|
||||
// 270deg travel -> full scale; 1 rad maps to ~200/(1.5*pi) volume steps
|
||||
const stepsPerRad = 200 / (Math.PI * 1.5);
|
||||
const next = Math.max(0, Math.min(200, Math.round(knobStartVolRef.current + delta * stepsPerRad)));
|
||||
// 270 度行程对应当前允许的完整音量范围。
|
||||
const stepsPerRad = maxVolume / (Math.PI * 1.5);
|
||||
const next = Math.max(0, Math.min(maxVolume, Math.round(knobStartVolRef.current + delta * stepsPerRad)));
|
||||
setLocalVol(next);
|
||||
knobLastVolRef.current = next;
|
||||
};
|
||||
@@ -691,7 +705,7 @@ export default function Home() {
|
||||
}} />
|
||||
<ThumbOnlyRangeSlider
|
||||
min={0}
|
||||
max={200}
|
||||
max={maxVolume}
|
||||
value={localVol}
|
||||
disabled={volumePassthroughActive}
|
||||
className={volumePassthroughActive ? "opacity-50" : undefined}
|
||||
@@ -748,7 +762,7 @@ export default function Home() {
|
||||
}}
|
||||
onClick={() => {
|
||||
if (blockVolumeAdjust()) return;
|
||||
const next = Math.max(0, Math.min(200, localVol - fineVolumeStep));
|
||||
const next = Math.max(0, Math.min(maxVolume, localVol - fineVolumeStep));
|
||||
setLocalVol(next);
|
||||
applyVolume(next);
|
||||
}}
|
||||
@@ -771,7 +785,7 @@ export default function Home() {
|
||||
<button
|
||||
type="button"
|
||||
aria-label="Increase volume"
|
||||
disabled={volumePassthroughActive || localVol >= 200}
|
||||
disabled={volumePassthroughActive || localVol >= maxVolume}
|
||||
className="w-10 h-10 rounded-full flex items-center justify-center transition-all active:scale-95 disabled:opacity-30 disabled:cursor-not-allowed"
|
||||
style={{
|
||||
background: "rgba(44,44,46,0.6)",
|
||||
@@ -780,7 +794,7 @@ export default function Home() {
|
||||
}}
|
||||
onClick={() => {
|
||||
if (blockVolumeAdjust()) return;
|
||||
const next = Math.max(0, Math.min(200, localVol + fineVolumeStep));
|
||||
const next = Math.max(0, Math.min(maxVolume, localVol + fineVolumeStep));
|
||||
setLocalVol(next);
|
||||
applyVolume(next);
|
||||
}}
|
||||
@@ -791,6 +805,16 @@ export default function Home() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{hpMaxVolumeHint && (
|
||||
<p
|
||||
className="mt-2 text-center text-[12px] leading-relaxed"
|
||||
style={{ color: "rgba(255,255,255,0.5)" }}
|
||||
role="status"
|
||||
>
|
||||
{hpMaxVolumeHint}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Bluetooth info — only when input source is Bluetooth */}
|
||||
{ds.input === INPUT_BLUETOOTH_INDEX && ds.bt_status !== 0 && (
|
||||
<div className="mt-3 px-3 py-3 text-center">
|
||||
@@ -961,8 +985,8 @@ export default function Home() {
|
||||
setLocalVol(0);
|
||||
applyVolume(0);
|
||||
} else if (volumeConfirm === "max") {
|
||||
setLocalVol(200);
|
||||
applyVolume(200);
|
||||
setLocalVol(maxVolume);
|
||||
applyVolume(maxVolume);
|
||||
}
|
||||
setVolumeConfirm(null);
|
||||
}}
|
||||
|
||||
Reference in New Issue
Block a user