chore: release v0.27.12
This commit is contained in:
@@ -19,6 +19,7 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { chaseAllows } from '@/lib/spotDisplay';
|
||||
import { markerColour, type SpotMarkerKey } from '@/lib/spotMarkers';
|
||||
import { writeUiPref } from '@/lib/uiPref';
|
||||
import { decoderName } from '@/lib/decoderName';
|
||||
|
||||
export type Decode = {
|
||||
call: string;
|
||||
@@ -99,6 +100,9 @@ interface Props {
|
||||
// the decoder announces — see the drift warning.
|
||||
rigBand?: string;
|
||||
onCall: (d: Decode) => void;
|
||||
// A single click: take the station without transmitting — fill the entry, and
|
||||
// point the panels at it. Absent, a click falls back to onCall.
|
||||
onSelect?: (d: Decode) => void;
|
||||
myCall?: string;
|
||||
// Drop every decode and transmit message held for this panel. The list is a
|
||||
// live view, not data — clearing it costs nothing but the seconds until the
|
||||
@@ -115,6 +119,12 @@ interface Props {
|
||||
// machine off is not something to go hunting through a settings tree for.
|
||||
autoCallOn?: boolean;
|
||||
onToggleAutoCall?: () => void;
|
||||
// The engine's own account of what it is doing, straight from the backend.
|
||||
autoCall?: { target: string; calls: number; max: number; misses: number; max_miss: number; stopped: boolean; reason: string };
|
||||
// The chase list, here as well as in Preferences: naming the station you are
|
||||
// waiting for is done WHILE watching the band, not in a settings tree.
|
||||
autoCallOnly?: string;
|
||||
onSetAutoCallOnly?: (list: string) => void;
|
||||
}
|
||||
|
||||
// The "new" categories, as toggle badges — the same idea and the same colours as
|
||||
@@ -542,12 +552,21 @@ function buildPeriods(filtered: Decode[], txMsgs: TxMsg[]) {
|
||||
}));
|
||||
}
|
||||
|
||||
export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, rigBand, onCall, myCall, onClear, onHalt, autoCallOn, onToggleAutoCall }: Props) {
|
||||
export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, rigBand, onCall, onSelect, myCall, onClear, onHalt, autoCallOn, onToggleAutoCall, autoCall, autoCallOnly, onSetAutoCallOnly }: Props) {
|
||||
const { t } = useI18n();
|
||||
// Column widths, dragged in the header and shared by every row. Persisted
|
||||
// through writeUiPref (not raw localStorage) so the layout travels with data/
|
||||
// like every other portable preference.
|
||||
const [colw, setColw] = useState<ColWidths>(loadWidths);
|
||||
// The chase list as TYPED. Re-seeded whenever the stored value changes —
|
||||
// from Preferences, or from another window — but never while the box has the
|
||||
// focus, or a status arriving mid-word would rewrite what is being typed.
|
||||
const [onlyText, setOnlyText] = useState(autoCallOnly ?? '');
|
||||
useEffect(() => {
|
||||
const el = document.activeElement as HTMLElement | null;
|
||||
if (el && el.tagName === 'INPUT' && el.getAttribute('placeholder') === t('dec.chasePh')) return;
|
||||
setOnlyText(autoCallOnly ?? '');
|
||||
}, [autoCallOnly, t]);
|
||||
const template = useMemo(() => COLS.map((c) => `${colw[c.key]}px`).join(' '), [colw]);
|
||||
const tableW = useMemo(() => COLS.reduce((s, c) => s + colw[c.key], 0), [colw]);
|
||||
const setColWidth = (key: ColKey, px: number) => {
|
||||
@@ -710,7 +729,8 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, r
|
||||
}
|
||||
return instances.map((inst) => ({
|
||||
key: inst,
|
||||
label: inst,
|
||||
// What the program is called, not the id it announces — see decoderName.
|
||||
label: decoderName(inst),
|
||||
tx: txStates?.[inst],
|
||||
periods: buildPeriods(
|
||||
filtered.filter((d) => (d.instance ?? '') === inst),
|
||||
@@ -757,7 +777,7 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, r
|
||||
className="inline-flex items-center gap-1 rounded-full border border-warning px-2 py-0.5 text-[11px] font-semibold text-warning">
|
||||
<AlertTriangle className="size-3.5" />
|
||||
{t('dec.bandDrift', {
|
||||
app: driftInstance || t('dec.bandDriftApp'),
|
||||
app: decoderName(driftInstance) || t('dec.bandDriftApp'),
|
||||
dec: decoderBand.toUpperCase(),
|
||||
rig: (rigBand ?? '').toUpperCase(),
|
||||
})}
|
||||
@@ -889,6 +909,54 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, r
|
||||
filter. Halt goes LAST — it is the one that must be findable without
|
||||
reading, and the end of the row is the one position that never moves
|
||||
as filters come and go. */}
|
||||
{/* Auto-call. Deliberately next to Halt: the two belong together, and
|
||||
what it is doing right now — which station, how many calls of how
|
||||
many — is on the button itself, because a thing that keys the
|
||||
transmitter must never be a switch with no readout. */}
|
||||
{onToggleAutoCall && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onToggleAutoCall}
|
||||
title={autoCall?.stopped ? t('dec.autoStoppedTip') : t('dec.autoCallTip')}
|
||||
className={cn('h-8 px-2.5 rounded-lg text-sm inline-flex items-center gap-1.5 border font-medium',
|
||||
!autoCallOn
|
||||
? 'border-border text-muted-foreground hover:bg-muted hover:text-foreground'
|
||||
: autoCall?.stopped
|
||||
? 'border-warning bg-warning text-warning-foreground'
|
||||
: 'border-success bg-success text-success-foreground')}
|
||||
>
|
||||
<Bot className="size-3.5" />
|
||||
{t('dec.autoCall')}
|
||||
{autoCallOn && autoCall?.target && (
|
||||
<span className="font-mono text-xs">
|
||||
{autoCall.target} {autoCall.calls}/{autoCall.max}
|
||||
{autoCall.misses > 0 ? ` ·${autoCall.misses}/${autoCall.max_miss}` : ''}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{/* The chase list. Raw text while typing, committed on blur or Enter:
|
||||
the stored value is upper-cased and trimmed, and binding the box to
|
||||
that makes the space bar look dead — in a field whose whole purpose
|
||||
is a list separated by spaces. */}
|
||||
{onSetAutoCallOnly && (
|
||||
<input
|
||||
type="text"
|
||||
value={onlyText}
|
||||
onChange={(e) => setOnlyText(e.target.value)}
|
||||
onBlur={() => { if (onlyText.toUpperCase() !== (autoCallOnly ?? '')) onSetAutoCallOnly(onlyText); }}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') (e.target as HTMLInputElement).blur();
|
||||
if (e.key === 'Escape') setOnlyText(autoCallOnly ?? '');
|
||||
}}
|
||||
placeholder={t('dec.chasePh')}
|
||||
title={t('dec.chaseTip')}
|
||||
className={cn('h-8 w-44 rounded-lg border px-2 text-sm font-mono uppercase bg-background',
|
||||
(autoCallOnly ?? '').trim()
|
||||
? 'border-primary text-foreground'
|
||||
: 'border-border text-muted-foreground')}
|
||||
/>
|
||||
)}
|
||||
{onHalt && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -955,7 +1023,7 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, r
|
||||
<span className="flex-1" />
|
||||
{txState.band && <span className="text-xs text-muted-foreground shrink-0">{txState.band}</span>}
|
||||
{txState.instance && instances.length > 1 && (
|
||||
<span className="text-xs text-muted-foreground shrink-0">{txState.instance}</span>
|
||||
<span className="text-xs text-muted-foreground shrink-0">{decoderName(txState.instance)}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
@@ -1095,7 +1163,12 @@ export function DecodesPanel({ decodes, txMsgs, txState, txStates, spotStatus, r
|
||||
<button
|
||||
key={`${d.call}-${d.freq_hz}-${i}`}
|
||||
type="button"
|
||||
onClick={() => onCall(d)}
|
||||
// ONE click selects, TWO transmit — the cluster's rule, and
|
||||
// the only safe one here: a single click used to hand the
|
||||
// decode straight to WSJT-X as a Reply, so brushing a row
|
||||
// while reading the band started calling a station.
|
||||
onClick={() => (onSelect ?? onCall)(d)}
|
||||
onDoubleClick={() => onCall(d)}
|
||||
title={t('dec.callTitle', { call: d.call })}
|
||||
style={{ gridTemplateColumns: template, width: tableW }}
|
||||
className={cn(ROW, 'text-left border-b border-border/20 transition-colors',
|
||||
|
||||
Reference in New Issue
Block a user