This commit is contained in:
2026-06-07 17:45:08 +02:00
parent 7d80d26bbd
commit 3dd9620cca
7 changed files with 133 additions and 77 deletions
+41 -14
View File
@@ -27,7 +27,7 @@ import {
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts, GetWinkeyerStatus,
WinkeyerConnect, WinkeyerDisconnect, WinkeyerSend, WinkeyerStop, WinkeyerSetSpeed, WinkeyerBackspace,
GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop,
QSOAudioBegin, QSOAudioCancel,
QSOAudioBegin, QSOAudioCancel, QSOAudioRestart,
GetAwardDefs,
} from '../wailsjs/go/main/App';
import { Combobox } from '@/components/ui/combobox';
@@ -654,6 +654,11 @@ export default function App() {
// tell whether an incoming DX call actually changed anything.
const callsignValRef = useRef('');
useEffect(() => { callsignValRef.current = callsign; }, [callsign]);
// True while the operator is typing in the Call field. A call change that
// arrives while it's NOT focused is programmatic (clicked spot / external app
// via UDP) → we (re)start the recording immediately; typed changes wait for
// blur so we don't restart on every keystroke.
const callFocusedRef = useRef(false);
// When the entered callsign turns out to be worked-before, jump to the
// Worked-before tab so the history is front-and-centre. Only once per call,
@@ -817,6 +822,16 @@ export default function App() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// applyModeFromSpot updates the mode AND its RST default for a fresh target
// (clicked spot / rig-driven mode change). Unlike a manual mode tweak, this
// is a new contact, so we clear the "user edited RST" flag first — otherwise
// a 599 left from a CW QSO would stick when jumping to an SSB spot.
function applyModeFromSpot(m: string) {
if (!m) return;
setMode(m);
rstUserEditedRef.current = false;
applyModePreset(m);
}
function applyModePreset(m: string) {
if (rstUserEditedRef.current) return;
// Prefer the user's configured preset RST; otherwise fall back to the mode
@@ -876,12 +891,15 @@ export default function App() {
// 3. Else trust CAT (SSB, CW, AM, FM…).
if (!lk.mode) {
const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
if (inferred) {
setMode(inferred);
} else if (s.mode === 'DATA') {
setMode(digitalDefaultRef.current || 'FT8');
} else if (s.mode) {
setMode(s.mode);
let nextMode = '';
if (inferred) nextMode = inferred;
else if (s.mode === 'DATA') nextMode = digitalDefaultRef.current || 'FT8';
else if (s.mode) nextMode = s.mode;
if (nextMode) {
setMode(nextMode);
// Flip the RST default (599↔59) when the rig changes mode. Respects a
// user-edited RST (applyModePreset early-returns when edited).
applyModePreset(nextMode);
}
}
});
@@ -1394,6 +1412,13 @@ export default function App() {
}
setCallsign(v);
scheduleLookup(v);
// Programmatic call change (clicked spot, or external app via UDP) for a new
// non-empty target → (re)start the recording now, even if one was already
// running for the previous contact. Typed changes (field focused) wait for
// blur so we don't restart per keystroke.
if (v.trim() !== '' && !callFocusedRef.current) {
QSOAudioRestart().then(setRecording).catch(() => {});
}
}
function markEdited(field: string) { userEditedRef.current.add(field); }
@@ -1615,10 +1640,11 @@ export default function App() {
ref={callsignRef}
className="font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card"
value={callsign}
onFocus={() => { callFocusedRef.current = true; }}
onChange={(e) => onCallsignInput(e.target.value)}
// Start the QSO recording when leaving the callsign field (the pre-roll
// covers the seconds before). No-op when the recorder is off.
onBlur={() => { if (callsign.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); }}
onBlur={() => { callFocusedRef.current = false; if (callsign.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); }}
/>
</div>
</div>
@@ -2585,17 +2611,18 @@ export default function App() {
} else {
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
if (s.band) setBand(s.band);
if (m) setMode(m);
}
// Set mode + flip the RST default (599↔59) for the new
// target — a plain setMode skipped the RST preset.
if (m) applyModeFromSpot(m);
onCallsignInput(s.dx_call);
// A POTA spot carries the park ref — pre-fill the POTA
// award reference (like the State→RAC auto-match) so it's
// logged without re-typing. n-fer refs (comma-separated)
// become one POTA@ entry each.
applySpotPOTA((s as any).pota_ref);
// Clicking a spot fills the call programmatically (no blur
// on the call field), so start the QSO recording here too.
if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {});
// (recording (re)starts inside onCallsignInput — the call
// changed programmatically with the field unfocused.)
}}
/>
);
@@ -2808,11 +2835,11 @@ export default function App() {
} else {
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
if (s.band) setBand(s.band);
if (m) setMode(m);
}
if (m) applyModeFromSpot(m);
onCallsignInput(s.dx_call);
applySpotPOTA((s as any).pota_ref);
if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {});
// (recording (re)starts inside onCallsignInput — programmatic call change)
}}
onClose={() => setShowBandMap(false)}
/>
+30 -56
View File
@@ -105,6 +105,14 @@ function fmtDateTimeUTC(s: any): string {
type ColEntry = ColDef<ClusterSpot> & { group: string; label: string; defaultVisible?: boolean };
// statusFor resolves the precomputed spot status (new / new-band / new-slot /
// worked-call) for an ag-Grid cell's row.
function statusFor(p: any): SpotStatusEntry | undefined {
return p?.context?.spotStatus?.[
spotStatusKey(p.data?.dx_call, p.data?.band ?? '', p.data?.comment ?? '', p.data?.freq_hz)
];
}
const COL_CATALOG: ColEntry[] = [
{
group: 'Spot', label: 'Time', colId: 'time',
@@ -117,28 +125,15 @@ const COL_CATALOG: ColEntry[] = [
group: 'Spot', label: 'Call', colId: 'call',
headerName: 'Call', field: 'dx_call' as any, width: 120,
defaultVisible: true,
cellRenderer: (p: any) => {
if (!p.value) return '';
const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[
spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz)
];
const isNew = status?.status === 'new';
const workedCall = !!status?.worked_call;
const style: any = {
fontFamily: 'ui-monospace, monospace', fontWeight: 700, fontSize: 12,
};
if (isNew) {
// New DXCC entity — soft rose pill, no clashing border.
style.backgroundColor = '#ffe4e6';
style.color = '#be123c';
style.padding = '1px 7px';
style.borderRadius = 4;
} else if (workedCall) {
style.color = '#0369a1'; // already worked this exact call
} else {
style.color = '#b8410c'; // new call in a worked entity
}
return <span style={style} title={isNew ? `NEW DXCC: ${status?.country ?? ''}` : workedCall ? 'Already worked this call' : undefined}>{p.value}</span>;
cellClass: 'font-mono',
// New DXCC entity → fill the whole cell (no padded pill, so calls stay
// aligned with non-new rows). Text colour also flags worked-call vs new-call.
cellStyle: (p: any): any => (statusFor(p)?.status === 'new'
? { backgroundColor: '#ffe4e6', color: '#be123c', fontWeight: 700 }
: { color: statusFor(p)?.worked_call ? '#0369a1' : '#b8410c', fontWeight: 700 }),
tooltipValueGetter: (p: any) => {
const s = statusFor(p);
return s?.status === 'new' ? `NEW DXCC: ${s?.country ?? ''}` : s?.worked_call ? 'Already worked this call' : undefined;
},
},
{
@@ -159,46 +154,25 @@ const COL_CATALOG: ColEntry[] = [
group: 'Spot', label: 'Band', colId: 'band',
headerName: 'Band', field: 'band' as any, width: 75,
defaultVisible: true,
cellClass: 'flex items-center',
cellRenderer: (p: any) => {
const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[
spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz)
];
const newBand = status?.status === 'new-band';
return p.value
? <span
style={{
fontFamily: 'ui-monospace, monospace', fontSize: 12,
fontWeight: newBand ? 700 : 400,
...(newBand ? { backgroundColor: '#fde68a', color: '#92400e', padding: '1px 7px', borderRadius: 4 } : {}),
}}
title={newBand ? 'NEW BAND for this entity' : undefined}
>{p.value}</span>
: '';
},
cellClass: 'font-mono',
// NEW BAND for this entity → fill the cell (keeps the band text aligned).
cellStyle: (p: any) => (statusFor(p)?.status === 'new-band'
? { backgroundColor: '#fde68a', color: '#92400e', fontWeight: 700 }
: undefined),
tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-band' ? 'NEW BAND for this entity' : undefined),
},
{
group: 'Spot', label: 'Mode', colId: 'mode',
headerName: 'Mode', colSpan: undefined, width: 80,
defaultVisible: true,
cellClass: 'flex items-center',
cellClass: 'font-mono',
valueGetter: (p: any) => p.data ? inferSpotMode(p.data.comment ?? '', p.data.freq_hz) : '',
cellRenderer: (p: any) => {
const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[
spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz)
];
const newSlot = status?.status === 'new-slot';
return p.value
? <span
style={{
fontFamily: 'ui-monospace, monospace', fontSize: 12,
fontWeight: newSlot ? 700 : 400,
...(newSlot ? { backgroundColor: '#fef08a', color: '#854d0e', padding: '1px 7px', borderRadius: 4 } : {}),
}}
title={newSlot ? 'NEW SLOT (mode not yet worked on this band)' : undefined}
>{p.value}</span>
: <span style={{ color: '#a8a29e', fontSize: 10 }}></span>;
},
// NEW SLOT (mode not yet worked on this band) → fill the cell.
cellStyle: (p: any) => (statusFor(p)?.status === 'new-slot'
? { backgroundColor: '#fef08a', color: '#854d0e', fontWeight: 700 }
: undefined),
cellRenderer: (p: any) => p.value ? p.value : <span style={{ color: '#a8a29e', fontSize: 10 }}></span>,
tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-slot' ? 'NEW SLOT (mode not yet worked on this band)' : undefined),
},
{
group: 'Spot', label: 'Pfx', colId: 'pfx',
+2
View File
@@ -237,6 +237,8 @@ export function QSOAudioBegin():Promise<boolean>;
export function QSOAudioCancel():Promise<void>;
export function QSOAudioRestart():Promise<boolean>;
export function QuitApp():Promise<void>;
export function RefreshCtyDat():Promise<main.CtyDatInfo>;
+4
View File
@@ -446,6 +446,10 @@ export function QSOAudioCancel() {
return window['go']['main']['App']['QSOAudioCancel']();
}
export function QSOAudioRestart() {
return window['go']['main']['App']['QSOAudioRestart']();
}
export function QuitApp() {
return window['go']['main']['App']['QuitApp']();
}