feat: Station Control amp parity + all amps, Help→Send log to F4BPO, bulk-edit frequency, fix Cluster midnight sort
- Station Control: the amplifier panel is now the exact same card as the FlexRadio panel (extracted shared AmpCard: OPERATE/STANDBY, ON/OFF, power level, output-power bar, live FWD/ID/temp meters). Every configured amp gets its own card — no dropdown. - Help → "Send log to F4BPO" (only when SMTP is configured): e-mails opslog.log + previous session + crash log to the developer. New email.SendFiles for multi-attachment. - Bulk edit: new "Frequency (MHz)" field sets freq_hz AND recomputes band; out-of-band values rejected. Fixes a run logged on a stale/default freq after CAT dropped. - Cluster: Time column sorted lexically on the HHMMZ string, so spots after 0000Z sorted below 2359Z and looked frozen at the UTC rollover. Now sorts by real arrival time. - Also folds in the DVK auto-CQ/2-column layout and Station Control dashboard batch (changelog 0.20.10, EN+FR).
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { Mic, Square, X, Radio } from 'lucide-react';
|
||||
import { Mic, Square, X, Radio, Repeat } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
@@ -12,14 +12,20 @@ type Props = {
|
||||
onPlay: (slot: number) => void;
|
||||
onStop: () => void;
|
||||
onClose: () => void;
|
||||
autoCq: boolean;
|
||||
autoCqSecs: number;
|
||||
onToggleAutoCq: (on: boolean) => void;
|
||||
onSetAutoCqSecs: (n: number) => void;
|
||||
phoneOk: boolean; // false when the rig is on a non-phone mode → DVK TX blocked
|
||||
};
|
||||
|
||||
// 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) {
|
||||
export function DvkPanel({ messages, status, onPlay, onStop, onClose, autoCq, autoCqSecs, onToggleAutoCq, onSetAutoCqSecs, phoneOk }: Props) {
|
||||
const { t } = useI18n();
|
||||
const anyAudio = messages.some((m) => m.has_audio);
|
||||
const recorded = messages.filter((m) => m.has_audio);
|
||||
const anyAudio = recorded.length > 0;
|
||||
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">
|
||||
@@ -43,29 +49,54 @@ export function DvkPanel({ messages, status, onPlay, onStop, onClose }: Props) {
|
||||
{t('dvkp.noMsgPre')} <strong>{t('dvkp.settingsPath')}</strong> {t('dvkp.noMsgPost')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 gap-1">
|
||||
{messages.map((m) => (
|
||||
// Only the recorded slots are shown. Two columns filling top-to-bottom
|
||||
// (rows sized to the count) — use the width instead of scrolling, so the
|
||||
// panel keeps the same height as the other reserved-area widgets.
|
||||
<div className="grid grid-flow-col gap-1"
|
||||
style={{ gridTemplateRows: `repeat(${Math.max(1, Math.ceil(recorded.length / 2))}, minmax(0, auto))` }}>
|
||||
{recorded.map((m) => (
|
||||
<button
|
||||
key={m.slot}
|
||||
type="button"
|
||||
disabled={!m.has_audio}
|
||||
disabled={!phoneOk}
|
||||
onClick={() => onPlay(m.slot)}
|
||||
title={m.has_audio ? t('dvkp.transmit', { slot: m.slot, label: m.label ? ' — ' + m.label : '', dur: m.duration_sec.toFixed(1) }) : t('dvkp.empty', { slot: m.slot })}
|
||||
title={!phoneOk ? t('dvkp.notPhone') : t('dvkp.transmit', { slot: m.slot, label: m.label ? ' — ' + m.label : '', dur: m.duration_sec.toFixed(1) })}
|
||||
className={cn(
|
||||
'flex items-center gap-1.5 rounded-md border px-2 py-1 text-left transition-colors',
|
||||
m.has_audio
|
||||
phoneOk
|
||||
? '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 ? t('dvkp.message') : '—')}</span>
|
||||
{m.has_audio && <span className="text-[9px] text-muted-foreground shrink-0">{m.duration_sec.toFixed(1)}s</span>}
|
||||
<span className="text-xs truncate flex-1">{m.label || t('dvkp.message')}</span>
|
||||
{(m.label || '').toUpperCase().includes('CQ') && autoCq && <Repeat className="size-3 text-primary shrink-0" />}
|
||||
<span className="text-[9px] text-muted-foreground shrink-0">{m.duration_sec.toFixed(1)}s</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Auto-CQ footer — repeats a CQ-labelled slot on a timer. Greyed out on a
|
||||
non-phone mode (the DVK won't transmit there). */}
|
||||
<div className="shrink-0 border-t border-border bg-muted/30 px-2 py-1.5 flex items-center gap-2">
|
||||
<label className={cn('flex items-center gap-1.5 text-[11px] cursor-pointer', !phoneOk && 'opacity-50')} title={phoneOk ? t('dvkp.autoCqHint') : t('dvkp.notPhone')}>
|
||||
<input type="checkbox" className="accent-primary" checked={autoCq} disabled={!phoneOk}
|
||||
onChange={(e) => onToggleAutoCq(e.target.checked)} />
|
||||
<Repeat className="size-3" /> {t('dvkp.autoCq')}
|
||||
</label>
|
||||
<div className="flex-1" />
|
||||
<span className="text-[10px] text-muted-foreground">{t('dvkp.gap')}</span>
|
||||
<input type="number" min={0} max={120} value={autoCqSecs}
|
||||
onChange={(e) => onSetAutoCqSecs(parseInt(e.target.value, 10))}
|
||||
className="w-12 h-6 rounded border border-input bg-background px-1 text-[11px] font-mono text-center" />
|
||||
<span className="text-[10px] text-muted-foreground">s</span>
|
||||
</div>
|
||||
|
||||
{!phoneOk && (
|
||||
<div className="shrink-0 bg-warning-muted text-warning-muted-foreground text-[10px] px-2 py-1 text-center">{t('dvkp.notPhone')}</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user