151 lines
5.5 KiB
TypeScript
151 lines
5.5 KiB
TypeScript
|
|
/* ============================================================
|
||
|
|
VU PAGE — VU Meter Gallery
|
||
|
|
Design: Reference luxsin_x8_VU表.png
|
||
|
|
- Card-based VU meter style selector
|
||
|
|
- Active = cyan check circle below card
|
||
|
|
- Inactive = empty circle
|
||
|
|
============================================================ */
|
||
|
|
import { useDevice } from "@/contexts/DeviceContext";
|
||
|
|
import { cn } from "@/lib/utils";
|
||
|
|
import { Check, ChevronLeft } from "lucide-react";
|
||
|
|
import { useLocation } from "wouter";
|
||
|
|
|
||
|
|
interface VUStyle {
|
||
|
|
index: number;
|
||
|
|
name: string;
|
||
|
|
description: string;
|
||
|
|
preview: React.ReactNode;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Classic analog VU meter preview
|
||
|
|
function ClassicVUPreview() {
|
||
|
|
return (
|
||
|
|
<div className="w-full h-28 rounded-xl overflow-hidden flex gap-1 p-2" style={{ background: "#1a1208" }}>
|
||
|
|
{["L", "R"].map((ch) => (
|
||
|
|
<div key={ch} className="flex-1 rounded-lg overflow-hidden relative" style={{ background: "#2a1e08" }}>
|
||
|
|
{/* Amber gradient arc area */}
|
||
|
|
<div className="absolute inset-0 flex items-end justify-center pb-2">
|
||
|
|
<div className="w-full h-full relative">
|
||
|
|
{/* Scale marks */}
|
||
|
|
<div className="absolute top-1 left-0 right-0 flex justify-around px-2">
|
||
|
|
{["-20","-10","-7","-5","-3","0","+3"].map((v) => (
|
||
|
|
<span key={v} className="text-[6px] text-amber-300/70">{v}</span>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
{/* Needle */}
|
||
|
|
<div className="absolute bottom-3 left-1/2 w-0.5 h-14 origin-bottom"
|
||
|
|
style={{ background: "linear-gradient(to top, #dc2626, #ef4444)", transform: "translateX(-50%) rotate(-15deg)", borderRadius: "1px" }} />
|
||
|
|
{/* VU label */}
|
||
|
|
<div className="absolute bottom-1 left-1/2 -translate-x-1/2 text-[8px] font-bold text-amber-400">VU</div>
|
||
|
|
{/* dB label */}
|
||
|
|
<div className="absolute bottom-1 right-2 text-[7px] text-amber-300/60">dB</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Modern flat LED bar preview
|
||
|
|
function ModernVUPreview() {
|
||
|
|
const bars = [0.9, 0.8, 0.75, 0.65, 0.55, 0.45, 0.35, 0.25];
|
||
|
|
return (
|
||
|
|
<div className="w-full h-28 rounded-xl overflow-hidden flex gap-1 p-3" style={{ background: "#0f1a1a" }}>
|
||
|
|
{["L", "R"].map((ch) => (
|
||
|
|
<div key={ch} className="flex-1 flex flex-col gap-1 justify-end">
|
||
|
|
<div className="text-[8px] text-white/40 mb-1">{ch}</div>
|
||
|
|
<div className="flex gap-0.5 items-end h-16">
|
||
|
|
{bars.map((h, i) => (
|
||
|
|
<div key={i} className="flex-1 rounded-sm"
|
||
|
|
style={{
|
||
|
|
height: `${h * 100}%`,
|
||
|
|
background: i < 6 ? "#00FFF6" : i === 6 ? "#f59e0b" : "#ef4444",
|
||
|
|
opacity: 0.8,
|
||
|
|
}} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
<div className="text-[8px] text-white/40 text-right">dB</div>
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const VU_STYLES: VUStyle[] = [
|
||
|
|
{
|
||
|
|
index: 0,
|
||
|
|
name: "经典复古",
|
||
|
|
description: "琥珀色指针表盘,经典模拟风格",
|
||
|
|
preview: <ClassicVUPreview />,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
index: 1,
|
||
|
|
name: "现代平面",
|
||
|
|
description: "LED 电平条,简洁现代风格",
|
||
|
|
preview: <ModernVUPreview />,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
export default function VUPage() {
|
||
|
|
const [, setLocation] = useLocation();
|
||
|
|
const { deviceState, updateSetting } = useDevice();
|
||
|
|
const currentVU = deviceState?.vu ?? 0;
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen bg-black">
|
||
|
|
<div className="page-header">
|
||
|
|
<button onClick={() => setLocation("/")} className="mr-4 text-white/60 active:text-white transition-colors">
|
||
|
|
<ChevronLeft size={24} />
|
||
|
|
</button>
|
||
|
|
<h1 className="flex-1 text-center text-[17px] font-semibold text-white">VU 表库</h1>
|
||
|
|
<div className="w-8" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div className="px-4 pb-32 pt-4 space-y-5">
|
||
|
|
{VU_STYLES.map((style) => {
|
||
|
|
const isActive = currentVU === style.index;
|
||
|
|
return (
|
||
|
|
<div key={style.index} className="space-y-3">
|
||
|
|
{/* Card */}
|
||
|
|
<div
|
||
|
|
className={cn(
|
||
|
|
"ios-list-group p-3 cursor-pointer transition-all duration-200",
|
||
|
|
isActive && "border-[#00FFF6]/30"
|
||
|
|
)}
|
||
|
|
style={isActive ? { borderColor: "rgba(0,255,246,0.3)", boxShadow: "0 0 20px rgba(0,255,246,0.08)" } : {}}
|
||
|
|
onClick={() => updateSetting({ vu: style.index })}
|
||
|
|
>
|
||
|
|
{style.preview}
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Selection indicator */}
|
||
|
|
<div className="flex justify-center">
|
||
|
|
<button
|
||
|
|
onClick={() => updateSetting({ vu: style.index })}
|
||
|
|
className={cn(
|
||
|
|
"w-7 h-7 rounded-full flex items-center justify-center transition-all duration-200",
|
||
|
|
isActive
|
||
|
|
? "bg-[#00FFF6] text-black shadow-[0_0_12px_rgba(0,255,246,0.5)]"
|
||
|
|
: "border-2 border-white/20 text-transparent"
|
||
|
|
)}
|
||
|
|
>
|
||
|
|
{isActive && <Check size={14} strokeWidth={3} />}
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Style name */}
|
||
|
|
<div className="text-center">
|
||
|
|
<p className={cn("text-[15px] font-medium", isActive ? "text-[#00FFF6]" : "text-white/60")}>
|
||
|
|
{style.name}
|
||
|
|
</p>
|
||
|
|
<p className="text-[12px] text-white/35 mt-0.5">{style.description}</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
})}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|