- SCP/N+1: new internal/scp downloads the community MASTER.SCP master list and a docked two-column widget shows, as you type a call, the known calls containing it (Partial) and the calls one edit away (N+1) — click to fix a busted call. Opt-in in Settings → General; top-bar toggle; queried debounced on callsign input. - Flex: OpsLog's GUI-client detection was too loose and could bind to "SmartSDR CAT" (or DAX) — both carry "smartsdr" in the program name — instead of the real GUI client, making SmartSDR CAT drop and reconnect in a loop while OpsLog was open. Now it binds only to a real SmartSDR/Maestro GUI client (e.g. a FLEX-8600M's integrated screen) and excludes cat/dax; dropped the risky empty-program fallback. - Telemetry: on a fresh install the callsign isn't set at launch, so the once-a-day heartbeat recorded the machine UUID. Now it waits (~10 min) for the operator to enter their callsign before sending, falling back to the UUID only if none appears.
64 lines
3.0 KiB
TypeScript
64 lines
3.0 KiB
TypeScript
import { SpellCheck, X } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import { useI18n } from '@/lib/i18n';
|
|
|
|
export type ScpResult = { partial?: string[]; nplus1?: string[] };
|
|
|
|
// ScpPanel — Super Check Partial + N+1 callsign helper, split in two columns.
|
|
// Left: master calls that CONTAIN what you've typed (spot/correct a call). Right:
|
|
// calls one edit away (busted-call check). Clicking a suggestion fills the entry.
|
|
export function ScpPanel({ result, currentCall, count, onPick, onClose }: {
|
|
result: ScpResult;
|
|
currentCall: string;
|
|
count: number; // master-list size (0 = list not downloaded yet)
|
|
onPick: (call: string) => void;
|
|
onClose: () => void;
|
|
}) {
|
|
const { t } = useI18n();
|
|
const cur = (currentCall || '').trim().toUpperCase();
|
|
const partial = result.partial ?? [];
|
|
const nplus1 = result.nplus1 ?? [];
|
|
|
|
const Col = ({ title, tone, calls, empty }: { title: string; tone: string; calls: string[]; empty: string }) => (
|
|
<div className="flex-1 min-w-0 flex flex-col">
|
|
<div className={cn('text-[10px] font-bold uppercase tracking-wider px-1.5 py-1 border-b border-border/50', tone)}>{title}</div>
|
|
<div className="flex-1 min-h-0 overflow-y-auto p-1 space-y-0.5">
|
|
{calls.length === 0 ? (
|
|
<div className="text-[10px] text-muted-foreground/70 italic px-1 py-1">{empty}</div>
|
|
) : calls.map((c) => (
|
|
<button key={c} type="button" onClick={() => onPick(c)}
|
|
title={t('scp.fill', { call: c })}
|
|
className={cn('w-full text-left rounded px-1.5 py-0.5 font-mono text-xs transition-colors',
|
|
c === cur ? 'bg-success/20 text-success font-bold'
|
|
: 'hover:bg-primary/15 text-foreground/90')}>
|
|
{c}
|
|
</button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
);
|
|
|
|
return (
|
|
<div className="h-full flex flex-col rounded-xl border border-border bg-gradient-to-b from-card to-muted/30 shadow-sm overflow-hidden">
|
|
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/40 shrink-0">
|
|
<SpellCheck className={cn('size-4', count > 0 ? 'text-primary' : 'text-muted-foreground')} />
|
|
<span className="text-xs font-bold uppercase tracking-[0.18em] text-foreground/80">{t('scp.title')}</span>
|
|
<span className="flex-1" />
|
|
<button type="button" onClick={onClose} className="text-muted-foreground hover:text-foreground transition-colors" title={t('scp.close')}>
|
|
<X className="size-3.5" />
|
|
</button>
|
|
</div>
|
|
{count === 0 ? (
|
|
<div className="flex-1 min-h-0 flex items-center justify-center text-[11px] text-muted-foreground italic text-center px-3">
|
|
{t('scp.noList')}
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 min-h-0 flex divide-x divide-border/60">
|
|
<Col title={t('scp.partial')} tone="text-primary" calls={partial} empty={t('scp.typeMore')} />
|
|
<Col title="N+1" tone="text-warning" calls={nplus1} empty={t('scp.none')} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|