70 lines
3.4 KiB
TypeScript
70 lines
3.4 KiB
TypeScript
import { Mic, Square, X, Radio } from 'lucide-react';
|
||
import { Button } from '@/components/ui/button';
|
||
import { cn } from '@/lib/utils';
|
||
|
||
export type DVKMsg = { slot: number; label: string; has_audio: boolean; duration_sec: number };
|
||
export type DVKStat = { recording: boolean; playing: boolean; rec_slot: number };
|
||
|
||
type Props = {
|
||
messages: DVKMsg[];
|
||
status: DVKStat;
|
||
onPlay: (slot: number) => void;
|
||
onStop: () => void;
|
||
onClose: () => void;
|
||
};
|
||
|
||
// Operating panel for the Digital Voice Keyer — transmits the recorded F1–F6
|
||
// voice messages to the rig ("To Radio"). Mirrors the WinKeyer panel's slot in
|
||
// the reserved area. Recording/labeling lives in Settings → Audio.
|
||
export function DvkPanel({ messages, status, onPlay, onStop, onClose }: Props) {
|
||
const anyAudio = messages.some((m) => m.has_audio);
|
||
return (
|
||
<div className="h-full flex flex-col rounded-lg border border-border bg-card shadow-sm overflow-hidden">
|
||
<div className="flex items-center gap-2 px-3 py-1.5 border-b border-border bg-muted/40 shrink-0">
|
||
<Mic className="size-3.5 text-primary" />
|
||
<span className="text-[11px] font-semibold uppercase tracking-wider">Voice keyer</span>
|
||
<span className={cn('size-2 rounded-full', status.playing ? 'bg-amber-500 animate-pulse' : 'bg-emerald-500')} />
|
||
{status.playing && <span className="text-[10px] text-amber-600 font-medium">transmitting…</span>}
|
||
<div className="flex-1" />
|
||
<Button variant="ghost" size="sm" className="h-6 px-2 text-[11px]" onClick={onStop} disabled={!status.playing}>
|
||
<Square className="size-3" /> Stop
|
||
</Button>
|
||
<button className="text-muted-foreground hover:text-foreground" title="Disable voice keyer" onClick={onClose}>
|
||
<X className="size-3.5" />
|
||
</button>
|
||
</div>
|
||
|
||
<div className="flex-1 min-h-0 overflow-auto p-2">
|
||
{!anyAudio ? (
|
||
<div className="h-full flex flex-col items-center justify-center gap-1 text-center text-[11px] text-muted-foreground px-3">
|
||
<Radio className="size-5 opacity-50" />
|
||
No messages recorded yet. Open <strong>Settings → Audio devices & voice keyer</strong> to record F1–F6.
|
||
</div>
|
||
) : (
|
||
<div className="grid grid-cols-2 gap-1.5">
|
||
{messages.map((m) => (
|
||
<button
|
||
key={m.slot}
|
||
type="button"
|
||
disabled={!m.has_audio}
|
||
onClick={() => onPlay(m.slot)}
|
||
title={m.has_audio ? `Transmit F${m.slot}${m.label ? ' — ' + m.label : ''} (${m.duration_sec.toFixed(1)}s)` : `F${m.slot} — empty`}
|
||
className={cn(
|
||
'flex items-center gap-1.5 rounded-md border px-2 py-1.5 text-left transition-colors',
|
||
m.has_audio
|
||
? 'border-border bg-background hover:border-primary/60 hover:bg-accent/30 cursor-pointer'
|
||
: 'border-dashed border-border/60 text-muted-foreground/50 cursor-not-allowed',
|
||
)}
|
||
>
|
||
<span className="font-mono text-[11px] font-bold text-primary shrink-0">F{m.slot}</span>
|
||
<span className="text-xs truncate flex-1">{m.label || (m.has_audio ? 'message' : '—')}</span>
|
||
{m.has_audio && <span className="text-[9px] text-muted-foreground shrink-0">{m.duration_sec.toFixed(1)}s</span>}
|
||
</button>
|
||
))}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|