This commit is contained in:
yangy
2026-05-26 18:46:41 +08:00
parent 0c9e42b91c
commit 47ea7d7b56
13 changed files with 1370 additions and 121 deletions
+100 -22
View File
@@ -22,7 +22,7 @@ import {
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { INPUT_LABELS, OUTPUT_LABELS } from "@/lib/luxsinApi";
import { INPUT_LABELS, isHomeVolumeLockedByPassthrough, OUTPUT_LABELS } from "@/lib/luxsinApi";
import { cn } from "@/lib/utils";
import {
ChevronRight,
@@ -378,6 +378,38 @@ export default function Home() {
};
}, []);
const volumePassthroughActive = isHomeVolumeLockedByPassthrough(
deviceState?.dacVolumeDirect,
deviceState?.output,
);
const volumePassthroughModeLabel =
deviceState?.dacVolumeDirect === 1
? (homeText.volumePassthroughMode0dB ?? "0dB")
: deviceState?.dacVolumeDirect === 2
? (homeText.volumePassthroughMode12dB ?? "-12dB")
: "";
const notifyVolumePassthroughBlocked = useCallback(() => {
const template =
homeText.volumePassthroughBlocked ??
"当前为音量直通模式({mode}),无法调节耳机音量。请在音频设置中关闭直通或改为「关闭」。";
toast.info(template.replace("{mode}", volumePassthroughModeLabel));
}, [homeText, volumePassthroughModeLabel]);
const blockVolumeAdjust = useCallback(() => {
if (!volumePassthroughActive) return false;
notifyVolumePassthroughBlocked();
return true;
}, [volumePassthroughActive, notifyVolumePassthroughBlocked]);
const applyVolume = useCallback(
(v: number) => {
if (blockVolumeAdjust()) return;
setVolume(v);
},
[blockVolumeAdjust, setVolume],
);
if (!isConnected) return <ConnectionPlaceholder />;
const ds = deviceState!;
@@ -413,6 +445,7 @@ export default function Home() {
};
const beginKnobDrag = (e: React.PointerEvent<HTMLDivElement>) => {
if (blockVolumeAdjust()) return;
const el = e.currentTarget;
const rect = el.getBoundingClientRect();
knobDraggingRef.current = true;
@@ -442,7 +475,7 @@ export default function Home() {
knobDraggingRef.current = false;
knobActivePointerIdRef.current = null;
isDragging.current = false;
setVolume(knobLastVolRef.current);
applyVolume(knobLastVolRef.current);
try {
e.currentTarget.releasePointerCapture(e.pointerId);
} catch {
@@ -549,7 +582,7 @@ export default function Home() {
</div>
</div>
{/* Right knob */}
<div className="pr-4">
<div className={cn("pr-4", volumePassthroughActive && "opacity-40 pointer-events-none")}>
<div
className="w-12 h-12 rounded-full flex items-center justify-center touch-none select-none"
style={{
@@ -564,6 +597,7 @@ export default function Home() {
aria-valuemin={0}
aria-valuemax={200}
aria-valuenow={localVol}
aria-disabled={volumePassthroughActive}
onPointerDown={beginKnobDrag}
onPointerMove={moveKnobDrag}
onPointerUp={endKnobDrag}
@@ -583,12 +617,36 @@ export default function Home() {
</div>
{/* ── Volume slider ── */}
<div className="px-4 mb-5">
<div className="flex items-center gap-3">
<div
className={cn("px-4 mb-5", volumePassthroughActive && "opacity-45")}
onPointerDownCapture={(e) => {
if (volumePassthroughActive) {
e.preventDefault();
e.stopPropagation();
notifyVolumePassthroughBlocked();
}
}}
>
{volumePassthroughActive && (
<p className="text-center text-[13px] mb-3 px-2" style={{ color: "rgba(0,255,246,0.85)" }}>
{homeText.volumePassthroughHint ?? "当前为音量直通模式,音量不可调节"}
{volumePassthroughModeLabel ? ` (${volumePassthroughModeLabel})` : ""}
</p>
)}
<div
className={cn(
"flex items-center gap-3",
volumePassthroughActive && "pointer-events-none",
)}
>
<button
type="button"
onClick={() => setVolumeConfirm("min")}
className="text-white/50 active:text-white transition-colors flex-shrink-0"
disabled={volumePassthroughActive}
onClick={() => {
if (blockVolumeAdjust()) return;
setVolumeConfirm("min");
}}
className="text-white/50 active:text-white transition-colors flex-shrink-0 disabled:opacity-40"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
@@ -606,25 +664,28 @@ export default function Home() {
}} />
<input
type="range" min={0} max={200} value={localVol}
className="cyan-slider relative z-10"
disabled={volumePassthroughActive}
className="cyan-slider relative z-10 disabled:opacity-50"
onChange={(e) => {
if (volumePassthroughActive) return;
const v = Number(e.target.value);
setLocalVol(v);
}}
onPointerDown={() => {
if (volumePassthroughActive) return;
isDragging.current = true;
}}
onPointerUp={(e) => {
isDragging.current = false;
setVolume(Number(e.currentTarget.value));
applyVolume(Number(e.currentTarget.value));
}}
onPointerCancel={(e) => {
isDragging.current = false;
setVolume(Number(e.currentTarget.value));
applyVolume(Number(e.currentTarget.value));
}}
onBlur={(e) => {
isDragging.current = false;
setVolume(Number(e.currentTarget.value));
applyVolume(Number(e.currentTarget.value));
}}
onKeyUp={(e) => {
const k = e.key;
@@ -638,7 +699,7 @@ export default function Home() {
k === "PageUp" ||
k === "PageDown"
) {
setVolume(Number(e.currentTarget.value));
applyVolume(Number(e.currentTarget.value));
}
}}
/>
@@ -646,8 +707,12 @@ export default function Home() {
<button
type="button"
onClick={() => setVolumeConfirm("max")}
className="text-white/50 active:text-white transition-colors flex-shrink-0"
disabled={volumePassthroughActive}
onClick={() => {
if (blockVolumeAdjust()) return;
setVolumeConfirm("max");
}}
className="text-white/50 active:text-white transition-colors flex-shrink-0 disabled:opacity-40"
>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-6 h-6">
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
@@ -657,11 +722,16 @@ export default function Home() {
</button>
</div>
{/* dB value */}
<div className="mt-[6px] flex items-center justify-center gap-3">
<div
className={cn(
"mt-[6px] flex items-center justify-center gap-3",
volumePassthroughActive && "pointer-events-none",
)}
>
<button
type="button"
aria-label="Decrease volume"
disabled={localVol <= 0}
disabled={volumePassthroughActive || localVol <= 0}
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)",
@@ -669,22 +739,29 @@ export default function Home() {
color: "rgba(255,255,255,0.55)",
}}
onClick={() => {
if (blockVolumeAdjust()) return;
const next = Math.max(0, Math.min(200, localVol - fineVolumeStep));
setLocalVol(next);
setVolume(next);
applyVolume(next);
}}
>
<span className="text-[20px] leading-none">-</span>
</button>
<span className="text-[22px] font-bold tracking-tight" style={{ color: "#00FFF6" }}>
<span
className={cn(
"text-[22px] font-bold tracking-tight",
volumePassthroughActive ? "text-white/35" : "",
)}
style={volumePassthroughActive ? undefined : { color: "#00FFF6" }}
>
{volToDB(localVol)}
</span>
<button
type="button"
aria-label="Increase volume"
disabled={localVol >= 200}
disabled={volumePassthroughActive || localVol >= 200}
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)",
@@ -692,9 +769,10 @@ export default function Home() {
color: "rgba(255,255,255,0.55)",
}}
onClick={() => {
if (blockVolumeAdjust()) return;
const next = Math.max(0, Math.min(200, localVol + fineVolumeStep));
setLocalVol(next);
setVolume(next);
applyVolume(next);
}}
>
<span className="text-[20px] leading-none">+</span>
@@ -877,10 +955,10 @@ export default function Home() {
onClick={() => {
if (volumeConfirm === "min") {
setLocalVol(0);
setVolume(0);
applyVolume(0);
} else if (volumeConfirm === "max") {
setLocalVol(200);
setVolume(200);
applyVolume(200);
}
setVolumeConfirm(null);
}}