123 lines
4.0 KiB
TypeScript
123 lines
4.0 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useAppStore } from "../stores/app-store";
|
|
|
|
interface MicButtonProps {
|
|
onStart: () => void;
|
|
onStop: () => void;
|
|
}
|
|
|
|
export function MicButton({ onStart, onStop }: MicButtonProps) {
|
|
const connected = useAppStore((s) => s.connectionStatus === "connected");
|
|
const micReady = useAppStore((s) => s.micReady);
|
|
const recording = useAppStore((s) => s.recording);
|
|
const pendingStart = useAppStore((s) => s.pendingStart);
|
|
const stopping = useAppStore((s) => s.stopping);
|
|
const weakNetwork = useAppStore((s) => s.weakNetwork);
|
|
const isActive = recording || pendingStart || stopping;
|
|
const disabled = !connected || !micReady || stopping;
|
|
|
|
const handlePointerDown = useCallback(
|
|
(e: React.PointerEvent<HTMLButtonElement>) => {
|
|
if (e.button !== 0) return;
|
|
e.preventDefault();
|
|
e.currentTarget.setPointerCapture(e.pointerId);
|
|
onStart();
|
|
},
|
|
[onStart],
|
|
);
|
|
|
|
const handlePointerUp = useCallback(
|
|
(e: React.PointerEvent<HTMLButtonElement>) => {
|
|
e.preventDefault();
|
|
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
onStop();
|
|
},
|
|
[onStop],
|
|
);
|
|
|
|
// Read latest state to avoid stale closures
|
|
const handlePointerLeave = useCallback(() => {
|
|
const s = useAppStore.getState();
|
|
if (s.recording || s.pendingStart) onStop();
|
|
}, [onStop]);
|
|
|
|
const handlePointerCancel = useCallback(() => {
|
|
const s = useAppStore.getState();
|
|
if (s.recording || s.pendingStart) onStop();
|
|
}, [onStop]);
|
|
|
|
const disabledClasses =
|
|
"cursor-not-allowed border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary opacity-30 shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
|
const activeClasses =
|
|
"animate-mic-breathe scale-[1.06] border-accent-hover bg-accent text-white shadow-[0_0_32px_rgba(99,102,241,0.35),0_0_80px_rgba(99,102,241,0.2)]";
|
|
const weakClasses =
|
|
"border-amber-400 bg-linear-to-br from-amber-400/15 to-surface text-amber-200 shadow-[0_0_22px_rgba(251,191,36,0.28)]";
|
|
const idleClasses =
|
|
"border-edge bg-linear-to-br from-surface-hover to-surface text-fg-secondary shadow-[0_2px_12px_rgba(0,0,0,0.3),inset_0_1px_0_rgba(255,255,255,0.04)]";
|
|
|
|
return (
|
|
<section className="flex shrink-0 flex-col items-center gap-3.5 pt-5 pb-4">
|
|
<div className="relative flex touch-none items-center justify-center">
|
|
<button
|
|
type="button"
|
|
disabled={disabled}
|
|
className={`relative z-1 flex size-24 cursor-pointer touch-none select-none items-center justify-center rounded-full border-2 transition-all duration-[250ms] ease-[cubic-bezier(0.4,0,0.2,1)] ${
|
|
disabled
|
|
? disabledClasses
|
|
: weakNetwork
|
|
? weakClasses
|
|
: isActive
|
|
? activeClasses
|
|
: idleClasses
|
|
}`}
|
|
onPointerDown={handlePointerDown}
|
|
onPointerUp={handlePointerUp}
|
|
onPointerLeave={handlePointerLeave}
|
|
onPointerCancel={handlePointerCancel}
|
|
onContextMenu={(e) => e.preventDefault()}
|
|
>
|
|
<svg
|
|
viewBox="0 0 24 24"
|
|
width={48}
|
|
height={48}
|
|
fill="currentColor"
|
|
aria-label="麦克风"
|
|
role="img"
|
|
>
|
|
<title>{"麦克风"}</title>
|
|
<path d="M12 14c1.66 0 3-1.34 3-3V5c0-1.66-1.34-3-3-3S9 3.34 9 5v6c0 1.66 1.34 3 3 3z" />
|
|
<path d="M17 11c0 2.76-2.24 5-5 5s-5-2.24-5-5H5c0 3.53 2.61 6.43 6 6.92V21h2v-3.08c3.39-.49 6-3.39 6-6.92h-2z" />
|
|
</svg>
|
|
</button>
|
|
|
|
{/* Wave rings */}
|
|
<div
|
|
className="pointer-events-none absolute inset-0"
|
|
aria-hidden="true"
|
|
>
|
|
{[0, 0.8, 1.6].map((delay) => (
|
|
<span
|
|
key={delay}
|
|
className={`absolute inset-0 rounded-full border-[1.5px] border-accent ${
|
|
isActive ? "animate-ring-expand" : "opacity-0"
|
|
}`}
|
|
style={isActive ? { animationDelay: `${delay}s` } : undefined}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<p className="font-medium text-fg-dim text-sm">
|
|
{!micReady
|
|
? "请先准备麦克风"
|
|
: !connected
|
|
? "连接中断,等待重连"
|
|
: stopping
|
|
? "收尾中…"
|
|
: weakNetwork
|
|
? "网络波动,已启用缓冲"
|
|
: "按住说话"}
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|