Refactor Vite configuration for improved chunking strategy and caching. Enhance DeviceContext with new upgradePeqChange method for PEQ updates. Update audio page to support localized labels and dynamic volume control. Introduce new filter types and improve EQPage functionality with enhanced frequency response visualization.
This commit is contained in:
+121
-13
@@ -29,6 +29,9 @@ import {
|
||||
import { useLocation } from "wouter";
|
||||
import { useRef, useState, useEffect, useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
import localeZh from "@/locales/data-zh.json";
|
||||
import localeZhHK from "@/locales/data-zh-HK.json";
|
||||
import localeEn from "@/locales/data-en.json";
|
||||
|
||||
// ── Volume in dB (0-200 → -100dB to 0dB) ──
|
||||
function volToDB(v: number) {
|
||||
@@ -88,6 +91,10 @@ export default function Home() {
|
||||
|
||||
const [localVol, setLocalVol] = useState(deviceState?.volume ?? 100);
|
||||
const isDragging = useRef(false);
|
||||
const knobDraggingRef = useRef(false);
|
||||
const knobStartAngleRef = useRef(0);
|
||||
const knobStartVolRef = useRef(0);
|
||||
const knobLastVolRef = useRef<number>(localVol);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isDragging.current && deviceState?.volume !== undefined) {
|
||||
@@ -95,6 +102,10 @@ export default function Home() {
|
||||
}
|
||||
}, [deviceState?.volume]);
|
||||
|
||||
useEffect(() => {
|
||||
knobLastVolRef.current = localVol;
|
||||
}, [localVol]);
|
||||
|
||||
const handleVolChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const v = Number(e.target.value);
|
||||
setLocalVol(v);
|
||||
@@ -104,15 +115,67 @@ export default function Home() {
|
||||
if (!isConnected) return <ConnectScreen />;
|
||||
|
||||
const ds = deviceState!;
|
||||
const localeData = ds.language === 0 ? localeEn : ds.language === 1 ? localeZhHK : localeZh;
|
||||
const homeText = localeData.home ?? {};
|
||||
const inputLabel = INPUT_LABELS[ds.input] ?? "USB-C";
|
||||
const outputLabel = OUTPUT_LABELS[ds.output] ?? "耳机";
|
||||
const outputLabel = OUTPUT_LABELS[ds.output] ?? "Headset";
|
||||
const effectOn = ds.audio_enable === 1;
|
||||
const audioOn = ds.audio_enable === 1;
|
||||
const vuLabel = `VU表${(ds.vu ?? 0) + 1}`;
|
||||
const vuLabel = `${homeText.vu ?? "VU表"}${(ds.vu ?? 0) + 1}`;
|
||||
const bypassOn = (ds.dsp_enable ?? 0) === 0;
|
||||
|
||||
// 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 pointAngle = (clientX: number, clientY: number, rect: DOMRect) => {
|
||||
const cx = rect.left + rect.width / 2;
|
||||
const cy = rect.top + rect.height / 2;
|
||||
return Math.atan2(clientY - cy, clientX - cx);
|
||||
};
|
||||
|
||||
const normalizeDelta = (delta: number) => {
|
||||
// Wrap to [-PI, PI] to avoid jumps across the -PI/PI boundary.
|
||||
const pi2 = Math.PI * 2;
|
||||
let d = ((delta + Math.PI) % pi2) - Math.PI;
|
||||
if (d < -Math.PI) d += pi2;
|
||||
return d;
|
||||
};
|
||||
|
||||
const beginKnobDrag = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
const el = e.currentTarget;
|
||||
const rect = el.getBoundingClientRect();
|
||||
knobDraggingRef.current = true;
|
||||
knobStartAngleRef.current = pointAngle(e.clientX, e.clientY, rect);
|
||||
knobStartVolRef.current = knobLastVolRef.current;
|
||||
isDragging.current = true;
|
||||
el.setPointerCapture(e.pointerId);
|
||||
};
|
||||
|
||||
const moveKnobDrag = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!knobDraggingRef.current) return;
|
||||
const el = e.currentTarget;
|
||||
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)));
|
||||
setLocalVol(next);
|
||||
setVolume(next);
|
||||
};
|
||||
|
||||
const endKnobDrag = (e: React.PointerEvent<HTMLDivElement>) => {
|
||||
if (!knobDraggingRef.current) return;
|
||||
knobDraggingRef.current = false;
|
||||
isDragging.current = false;
|
||||
try {
|
||||
e.currentTarget.releasePointerCapture(e.pointerId);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black flex flex-col">
|
||||
@@ -135,11 +198,39 @@ export default function Home() {
|
||||
<div className="absolute inset-x-4 inset-y-3 rounded-xl flex items-center"
|
||||
style={{ background: "linear-gradient(135deg, #1a1a1c 0%, #2a2a2e 50%, #1a1a1c 100%)", border: "1px solid rgba(255,255,255,0.08)" }}>
|
||||
{/* Left ports */}
|
||||
<div className="flex flex-col gap-2 px-4">
|
||||
<div className="w-6 h-6 rounded-full border-2 border-white/20 flex items-center justify-center">
|
||||
<div className="w-2 h-2 rounded-full bg-white/30" />
|
||||
<div className="px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Big hole (left) */}
|
||||
<div
|
||||
className="w-7 h-7 rounded-full border-2 bg-black/25 flex items-center justify-center"
|
||||
style={{
|
||||
borderColor: "rgba(245, 158, 11, 0.75)",
|
||||
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.35)",
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="w-2 h-2 rounded-full"
|
||||
style={{ background: "rgba(196, 126, 9, 0.9)" }}
|
||||
/>
|
||||
</div>
|
||||
{/* Two small holes (right, stacked) */}
|
||||
<div className="flex flex-col gap-2">
|
||||
<div
|
||||
className="w-5 h-5 rounded-full border-2 bg-black/25"
|
||||
style={{
|
||||
borderColor: "rgba(245, 158, 11, 0.65)",
|
||||
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.35)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
className="w-5 h-5 rounded-full border-2 bg-black/25"
|
||||
style={{
|
||||
borderColor: "rgba(245, 158, 11, 0.65)",
|
||||
boxShadow: "inset 0 0 0 1px rgba(0,0,0,0.35)",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-5 h-5 rounded-full border-2 border-amber-400/40" />
|
||||
</div>
|
||||
{/* VU display area */}
|
||||
<div className="flex-1 mx-3 rounded-lg overflow-hidden"
|
||||
@@ -166,8 +257,25 @@ export default function Home() {
|
||||
</div>
|
||||
{/* Right knob */}
|
||||
<div className="pr-4">
|
||||
<div className="w-12 h-12 rounded-full flex items-center justify-center"
|
||||
style={{ background: "radial-gradient(circle at 35% 35%, #4a4a4e, #1a1a1c)", border: "2px solid rgba(255,255,255,0.12)", boxShadow: "inset 0 2px 4px rgba(0,0,0,0.5)" }}>
|
||||
<div
|
||||
className="w-12 h-12 rounded-full flex items-center justify-center touch-none select-none"
|
||||
style={{
|
||||
background: "radial-gradient(circle at 35% 35%, #4a4a4e, #1a1a1c)",
|
||||
border: "2px solid rgba(255,255,255,0.12)",
|
||||
boxShadow: "inset 0 2px 4px rgba(0,0,0,0.5)",
|
||||
transform: `rotate(${knobAngle}deg)`,
|
||||
transition: knobDraggingRef.current ? "none" : "transform 120ms ease-out",
|
||||
}}
|
||||
role="slider"
|
||||
aria-label="Volume"
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={200}
|
||||
aria-valuenow={localVol}
|
||||
onPointerDown={beginKnobDrag}
|
||||
onPointerMove={moveKnobDrag}
|
||||
onPointerUp={endKnobDrag}
|
||||
onPointerCancel={endKnobDrag}
|
||||
>
|
||||
<div className="w-1 h-4 rounded-full bg-white/40" style={{ transform: "rotate(-30deg)", transformOrigin: "bottom center" }} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -278,7 +386,7 @@ export default function Home() {
|
||||
>
|
||||
<Power size={22} className="text-white/60" />
|
||||
</button>
|
||||
<span className="text-[11px] text-white/35">电源</span>
|
||||
<span className="text-[11px] text-white/35">{homeText.power ?? "电源"}</span>
|
||||
</div>
|
||||
|
||||
{/* EQ */}
|
||||
@@ -320,13 +428,13 @@ export default function Home() {
|
||||
<div className="ios-list-group">
|
||||
<ListRow
|
||||
icon={<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4"><path d="M9 3H5a2 2 0 0 0-2 2v4m6-6h10a2 2 0 0 1 2 2v4M9 3v18m0 0h10a2 2 0 0 0 2-2V9M9 21H5a2 2 0 0 1-2-2V9m0 0h18"/></svg>}
|
||||
label="输入源"
|
||||
label={homeText.input ?? "输入源"}
|
||||
value={inputLabel}
|
||||
onClick={() => setLocation("/io")}
|
||||
/>
|
||||
<ListRow
|
||||
icon={<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" className="w-4 h-4"><path d="M15 3H9a2 2 0 0 0-2 2v4m8-6h4a2 2 0 0 1 2 2v4M15 3v18m0 0H9a2 2 0 0 1-2-2V9m8 12h4a2 2 0 0 0 2-2V9M7 9H3"/></svg>}
|
||||
label="输出端口"
|
||||
label={homeText.output ?? "输出端口"}
|
||||
value={outputLabel}
|
||||
onClick={() => setLocation("/io")}
|
||||
/>
|
||||
@@ -340,12 +448,12 @@ export default function Home() {
|
||||
/>
|
||||
<ListRow
|
||||
icon={<Settings size={16} />}
|
||||
label="音频设置"
|
||||
label={homeText.audioSet ?? "音频设置"}
|
||||
onClick={() => setLocation("/audio")}
|
||||
/>
|
||||
<ListRow
|
||||
icon={<Gauge size={16} />}
|
||||
label="VU表"
|
||||
label={homeText.vu ?? "VU表"}
|
||||
value={vuLabel}
|
||||
onClick={() => setLocation("/vu")}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user