up
This commit is contained in:
Binary file not shown.
@@ -3457,6 +3457,16 @@ func (a *App) QSOAudioBegin() bool {
|
|||||||
return a.qsoRec.Active()
|
return a.qsoRec.Active()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QSOAudioRestart starts a fresh recording for a new target even if one is
|
||||||
|
// already in progress (new call+freq from a clicked spot or external app).
|
||||||
|
func (a *App) QSOAudioRestart() bool {
|
||||||
|
if a.qsoRec == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
a.qsoRec.RestartQSO()
|
||||||
|
return a.qsoRec.Active()
|
||||||
|
}
|
||||||
|
|
||||||
// QSOAudioCancel drops the in-progress recording (callsign cleared, QSO
|
// QSOAudioCancel drops the in-progress recording (callsign cleared, QSO
|
||||||
// abandoned without logging).
|
// abandoned without logging).
|
||||||
func (a *App) QSOAudioCancel() {
|
func (a *App) QSOAudioCancel() {
|
||||||
@@ -4587,6 +4597,7 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
// that count for ARRL awards) so each incoming one is flagged NEW.
|
// that count for ARRL awards) so each incoming one is flagged NEW.
|
||||||
sets, _ := a.qso.ConfirmedSlots(ctx, []string{"lotw_rcvd", "qsl_rcvd"})
|
sets, _ := a.qso.ConfirmedSlots(ctx, []string{"lotw_rcvd", "qsl_rcvd"})
|
||||||
var items []ConfirmationItem
|
var items []ConfirmationItem
|
||||||
|
var unmatched []string
|
||||||
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
|
perr := adif.Parse(strings.NewReader(adifText), func(rec adif.Record) error {
|
||||||
q, ok := adif.RecordToQSO(rec)
|
q, ok := adif.RecordToQSO(rec)
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -4611,6 +4622,12 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
keyIDs[key] = newID // guard against dup records in the report
|
keyIDs[key] = newID // guard against dup records in the report
|
||||||
added++
|
added++
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// No local QSO matched this confirmation on (call, minute, band,
|
||||||
|
// mode). Record the specifics so the user can see WHICH one and
|
||||||
|
// why (time off by a minute, FT4 logged as MFSK, portable call…).
|
||||||
|
unmatched = append(unmatched, fmt.Sprintf("%s · %s · %s · %s",
|
||||||
|
q.Callsign, q.QSODate.UTC().Format("2006-01-02 15:04Z"), q.Band, q.Mode))
|
||||||
}
|
}
|
||||||
// Build the result row + NEW flags (vs the pre-download snapshot),
|
// Build the result row + NEW flags (vs the pre-download snapshot),
|
||||||
// then fold this slot into the sets so a repeat in the same batch
|
// then fold this slot into the sets so a repeat in the same batch
|
||||||
@@ -4648,6 +4665,12 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
} else {
|
} else {
|
||||||
emit(fmt.Sprintf("Matched %d of %d confirmed QSO(s)", matched, total))
|
emit(fmt.Sprintf("Matched %d of %d confirmed QSO(s)", matched, total))
|
||||||
}
|
}
|
||||||
|
// Surface confirmations with no local match so the user sees WHICH one
|
||||||
|
// and why (time off by a minute, FT4 logged as MFSK, portable call, or
|
||||||
|
// never logged). Tick "Add not-found" to import them instead.
|
||||||
|
for _, u := range unmatched {
|
||||||
|
emit(" ⚠ no local QSO for: " + u)
|
||||||
|
}
|
||||||
// Remember today so the next pull is incremental (per active profile).
|
// Remember today so the next pull is incremental (per active profile).
|
||||||
if a.settings != nil {
|
if a.settings != nil {
|
||||||
_ = a.settings.Set(ctx, a.profileScope()+keyExtLoTWLastDownload, time.Now().UTC().Format("2006-01-02"))
|
_ = a.settings.Set(ctx, a.profileScope()+keyExtLoTWLastDownload, time.Now().UTC().Format("2006-01-02"))
|
||||||
@@ -4658,10 +4681,11 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
// and (when a window is requested) skip records older than sinceDate by
|
// and (when a window is requested) skip records older than sinceDate by
|
||||||
// QSO date. sinceDate is "YYYY-MM-DD".
|
// QSO date. sinceDate is "YYYY-MM-DD".
|
||||||
sinceDate := resolveSince(keyExtQRZLastDownload)
|
sinceDate := resolveSince(keyExtQRZLastDownload)
|
||||||
|
emit(fmt.Sprintf("Window: since=%q → resolved date=%q (key %s%s)", since, sinceDate, a.profileScope(), keyExtQRZLastDownload))
|
||||||
if sinceDate != "" {
|
if sinceDate != "" {
|
||||||
emit("Fetching QRZ.com logbook (QSOs since " + sinceDate + ")…")
|
emit("Fetching QRZ.com logbook (will skip QSOs before " + sinceDate + ")…")
|
||||||
} else {
|
} else {
|
||||||
emit("Fetching QRZ.com logbook…")
|
emit("Fetching QRZ.com logbook (full — no since date)…")
|
||||||
}
|
}
|
||||||
fr, err := extsvc.FetchQRZ(ctx, nil, cfg.QRZ.APIKey, "ALL")
|
fr, err := extsvc.FetchQRZ(ctx, nil, cfg.QRZ.APIKey, "ALL")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -4671,6 +4695,14 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
}
|
}
|
||||||
adifText := fr.ADIF
|
adifText := fr.ADIF
|
||||||
emit(fmt.Sprintf("QRZ RESULT=%s COUNT=%s, ADIF %d bytes", fr.Result, fr.Count, len(adifText)))
|
emit(fmt.Sprintf("QRZ RESULT=%s COUNT=%s, ADIF %d bytes", fr.Result, fr.Count, len(adifText)))
|
||||||
|
// Persist the last-download date NOW (right after a successful fetch),
|
||||||
|
// not at the end: the QRZ logbook can be huge (tens of thousands of
|
||||||
|
// records) and the user may close the panel mid-processing — storing it
|
||||||
|
// late meant the date was never saved, so "since last download" kept
|
||||||
|
// resolving to empty and re-pulled everything.
|
||||||
|
if a.settings != nil {
|
||||||
|
_ = a.settings.Set(ctx, a.profileScope()+keyExtQRZLastDownload, time.Now().UTC().Format("2006-01-02"))
|
||||||
|
}
|
||||||
if snip := strings.TrimSpace(adifText); snip != "" {
|
if snip := strings.TrimSpace(adifText); snip != "" {
|
||||||
if len(snip) > 300 {
|
if len(snip) > 300 {
|
||||||
snip = snip[:300]
|
snip = snip[:300]
|
||||||
@@ -4781,10 +4813,7 @@ func (a *App) runDownloadConfirmations(svc extsvc.Service, cfg extsvc.ExternalSe
|
|||||||
sort.Strings(keys)
|
sort.Strings(keys)
|
||||||
emit(fmt.Sprintf("Parsed %d record(s). Fields seen: %s", parsed, strings.Join(keys, ", ")))
|
emit(fmt.Sprintf("Parsed %d record(s). Fields seen: %s", parsed, strings.Join(keys, ", ")))
|
||||||
emit(fmt.Sprintf("Confirmed %d, added %d (of %d returned)", matched, added, total))
|
emit(fmt.Sprintf("Confirmed %d, added %d (of %d returned)", matched, added, total))
|
||||||
// Remember today so a later "since last download" pull is incremental.
|
// (last-download date already stored right after the fetch above)
|
||||||
if a.settings != nil {
|
|
||||||
_ = a.settings.Set(ctx, a.profileScope()+keyExtQRZLastDownload, time.Now().UTC().Format("2006-01-02"))
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
emit(fmt.Sprintf("Confirmation download isn't available for %s yet.", svc))
|
emit(fmt.Sprintf("Confirmation download isn't available for %s yet.", svc))
|
||||||
@@ -6296,7 +6325,12 @@ func (a *App) GetWinkeyerSettings() (WinkeyerSettings, error) {
|
|||||||
out.AutoSpace = v == "1"
|
out.AutoSpace = v == "1"
|
||||||
}
|
}
|
||||||
out.UsePTT = m[keyWKUsePTT] == "1"
|
out.UsePTT = m[keyWKUsePTT] == "1"
|
||||||
out.SerialEcho = m[keyWKSerialEcho] == "1"
|
// Only override the default (true) when the key is actually stored — otherwise
|
||||||
|
// settings saved before serial_echo existed would silently disable the echo,
|
||||||
|
// and the TX text would stop showing as it's keyed.
|
||||||
|
if v := m[keyWKSerialEcho]; v != "" {
|
||||||
|
out.SerialEcho = v == "1"
|
||||||
|
}
|
||||||
if v := m[keyWKMacros]; v != "" {
|
if v := m[keyWKMacros]; v != "" {
|
||||||
var mac []WKMacro
|
var mac []WKMacro
|
||||||
if json.Unmarshal([]byte(v), &mac) == nil && len(mac) > 0 {
|
if json.Unmarshal([]byte(v), &mac) == nil && len(mac) > 0 {
|
||||||
|
|||||||
+41
-14
@@ -27,7 +27,7 @@ import {
|
|||||||
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts, GetWinkeyerStatus,
|
GetWinkeyerSettings, SaveWinkeyerSettings, ListSerialPorts, GetWinkeyerStatus,
|
||||||
WinkeyerConnect, WinkeyerDisconnect, WinkeyerSend, WinkeyerStop, WinkeyerSetSpeed, WinkeyerBackspace,
|
WinkeyerConnect, WinkeyerDisconnect, WinkeyerSend, WinkeyerStop, WinkeyerSetSpeed, WinkeyerBackspace,
|
||||||
GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop,
|
GetDVKMessages, GetDVKStatus, DVKPlay, DVKStop,
|
||||||
QSOAudioBegin, QSOAudioCancel,
|
QSOAudioBegin, QSOAudioCancel, QSOAudioRestart,
|
||||||
GetAwardDefs,
|
GetAwardDefs,
|
||||||
} from '../wailsjs/go/main/App';
|
} from '../wailsjs/go/main/App';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
@@ -654,6 +654,11 @@ export default function App() {
|
|||||||
// tell whether an incoming DX call actually changed anything.
|
// tell whether an incoming DX call actually changed anything.
|
||||||
const callsignValRef = useRef('');
|
const callsignValRef = useRef('');
|
||||||
useEffect(() => { callsignValRef.current = callsign; }, [callsign]);
|
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
|
// 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,
|
// 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
|
// 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) {
|
function applyModePreset(m: string) {
|
||||||
if (rstUserEditedRef.current) return;
|
if (rstUserEditedRef.current) return;
|
||||||
// Prefer the user's configured preset RST; otherwise fall back to the mode
|
// 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…).
|
// 3. Else trust CAT (SSB, CW, AM, FM…).
|
||||||
if (!lk.mode) {
|
if (!lk.mode) {
|
||||||
const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
|
const inferred = s.freq_hz ? inferDigitalMode(s.freq_hz) : '';
|
||||||
if (inferred) {
|
let nextMode = '';
|
||||||
setMode(inferred);
|
if (inferred) nextMode = inferred;
|
||||||
} else if (s.mode === 'DATA') {
|
else if (s.mode === 'DATA') nextMode = digitalDefaultRef.current || 'FT8';
|
||||||
setMode(digitalDefaultRef.current || 'FT8');
|
else if (s.mode) nextMode = s.mode;
|
||||||
} else if (s.mode) {
|
if (nextMode) {
|
||||||
setMode(s.mode);
|
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);
|
setCallsign(v);
|
||||||
scheduleLookup(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); }
|
function markEdited(field: string) { userEditedRef.current.add(field); }
|
||||||
|
|
||||||
@@ -1615,10 +1640,11 @@ export default function App() {
|
|||||||
ref={callsignRef}
|
ref={callsignRef}
|
||||||
className="font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card"
|
className="font-mono text-base font-bold tracking-wider uppercase h-9 bg-muted/40 focus:bg-card"
|
||||||
value={callsign}
|
value={callsign}
|
||||||
|
onFocus={() => { callFocusedRef.current = true; }}
|
||||||
onChange={(e) => onCallsignInput(e.target.value)}
|
onChange={(e) => onCallsignInput(e.target.value)}
|
||||||
// Start the QSO recording when leaving the callsign field (the pre-roll
|
// Start the QSO recording when leaving the callsign field (the pre-roll
|
||||||
// covers the seconds before). No-op when the recorder is off.
|
// 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>
|
||||||
</div>
|
</div>
|
||||||
@@ -2585,17 +2611,18 @@ export default function App() {
|
|||||||
} else {
|
} else {
|
||||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||||
if (s.band) setBand(s.band);
|
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);
|
onCallsignInput(s.dx_call);
|
||||||
// A POTA spot carries the park ref — pre-fill the POTA
|
// A POTA spot carries the park ref — pre-fill the POTA
|
||||||
// award reference (like the State→RAC auto-match) so it's
|
// award reference (like the State→RAC auto-match) so it's
|
||||||
// logged without re-typing. n-fer refs (comma-separated)
|
// logged without re-typing. n-fer refs (comma-separated)
|
||||||
// become one POTA@ entry each.
|
// become one POTA@ entry each.
|
||||||
applySpotPOTA((s as any).pota_ref);
|
applySpotPOTA((s as any).pota_ref);
|
||||||
// Clicking a spot fills the call programmatically (no blur
|
// (recording (re)starts inside onCallsignInput — the call
|
||||||
// on the call field), so start the QSO recording here too.
|
// changed programmatically with the field unfocused.)
|
||||||
if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -2808,11 +2835,11 @@ export default function App() {
|
|||||||
} else {
|
} else {
|
||||||
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
setFreqMhz((s.freq_hz / 1_000_000).toFixed(5));
|
||||||
if (s.band) setBand(s.band);
|
if (s.band) setBand(s.band);
|
||||||
if (m) setMode(m);
|
|
||||||
}
|
}
|
||||||
|
if (m) applyModeFromSpot(m);
|
||||||
onCallsignInput(s.dx_call);
|
onCallsignInput(s.dx_call);
|
||||||
applySpotPOTA((s as any).pota_ref);
|
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)}
|
onClose={() => setShowBandMap(false)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -105,6 +105,14 @@ function fmtDateTimeUTC(s: any): string {
|
|||||||
|
|
||||||
type ColEntry = ColDef<ClusterSpot> & { group: string; label: string; defaultVisible?: boolean };
|
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[] = [
|
const COL_CATALOG: ColEntry[] = [
|
||||||
{
|
{
|
||||||
group: 'Spot', label: 'Time', colId: 'time',
|
group: 'Spot', label: 'Time', colId: 'time',
|
||||||
@@ -117,28 +125,15 @@ const COL_CATALOG: ColEntry[] = [
|
|||||||
group: 'Spot', label: 'Call', colId: 'call',
|
group: 'Spot', label: 'Call', colId: 'call',
|
||||||
headerName: 'Call', field: 'dx_call' as any, width: 120,
|
headerName: 'Call', field: 'dx_call' as any, width: 120,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
cellRenderer: (p: any) => {
|
cellClass: 'font-mono',
|
||||||
if (!p.value) return '';
|
// New DXCC entity → fill the whole cell (no padded pill, so calls stay
|
||||||
const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[
|
// aligned with non-new rows). Text colour also flags worked-call vs new-call.
|
||||||
spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz)
|
cellStyle: (p: any): any => (statusFor(p)?.status === 'new'
|
||||||
];
|
? { backgroundColor: '#ffe4e6', color: '#be123c', fontWeight: 700 }
|
||||||
const isNew = status?.status === 'new';
|
: { color: statusFor(p)?.worked_call ? '#0369a1' : '#b8410c', fontWeight: 700 }),
|
||||||
const workedCall = !!status?.worked_call;
|
tooltipValueGetter: (p: any) => {
|
||||||
const style: any = {
|
const s = statusFor(p);
|
||||||
fontFamily: 'ui-monospace, monospace', fontWeight: 700, fontSize: 12,
|
return s?.status === 'new' ? `NEW DXCC: ${s?.country ?? ''}` : s?.worked_call ? 'Already worked this call' : undefined;
|
||||||
};
|
|
||||||
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>;
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -159,46 +154,25 @@ const COL_CATALOG: ColEntry[] = [
|
|||||||
group: 'Spot', label: 'Band', colId: 'band',
|
group: 'Spot', label: 'Band', colId: 'band',
|
||||||
headerName: 'Band', field: 'band' as any, width: 75,
|
headerName: 'Band', field: 'band' as any, width: 75,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
cellClass: 'flex items-center',
|
cellClass: 'font-mono',
|
||||||
cellRenderer: (p: any) => {
|
// NEW BAND for this entity → fill the cell (keeps the band text aligned).
|
||||||
const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[
|
cellStyle: (p: any) => (statusFor(p)?.status === 'new-band'
|
||||||
spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz)
|
? { backgroundColor: '#fde68a', color: '#92400e', fontWeight: 700 }
|
||||||
];
|
: undefined),
|
||||||
const newBand = status?.status === 'new-band';
|
tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-band' ? 'NEW BAND for this entity' : undefined),
|
||||||
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>
|
|
||||||
: '';
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Spot', label: 'Mode', colId: 'mode',
|
group: 'Spot', label: 'Mode', colId: 'mode',
|
||||||
headerName: 'Mode', colSpan: undefined, width: 80,
|
headerName: 'Mode', colSpan: undefined, width: 80,
|
||||||
defaultVisible: true,
|
defaultVisible: true,
|
||||||
cellClass: 'flex items-center',
|
cellClass: 'font-mono',
|
||||||
valueGetter: (p: any) => p.data ? inferSpotMode(p.data.comment ?? '', p.data.freq_hz) : '',
|
valueGetter: (p: any) => p.data ? inferSpotMode(p.data.comment ?? '', p.data.freq_hz) : '',
|
||||||
cellRenderer: (p: any) => {
|
// NEW SLOT (mode not yet worked on this band) → fill the cell.
|
||||||
const status: SpotStatusEntry | undefined = p.context?.spotStatus?.[
|
cellStyle: (p: any) => (statusFor(p)?.status === 'new-slot'
|
||||||
spotStatusKey(p.data.dx_call, p.data.band ?? '', p.data.comment ?? '', p.data.freq_hz)
|
? { backgroundColor: '#fef08a', color: '#854d0e', fontWeight: 700 }
|
||||||
];
|
: undefined),
|
||||||
const newSlot = status?.status === 'new-slot';
|
cellRenderer: (p: any) => p.value ? p.value : <span style={{ color: '#a8a29e', fontSize: 10 }}>—</span>,
|
||||||
return p.value
|
tooltipValueGetter: (p: any) => (statusFor(p)?.status === 'new-slot' ? 'NEW SLOT (mode not yet worked on this band)' : undefined),
|
||||||
? <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>;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: 'Spot', label: 'Pfx', colId: 'pfx',
|
group: 'Spot', label: 'Pfx', colId: 'pfx',
|
||||||
|
|||||||
Vendored
+2
@@ -237,6 +237,8 @@ export function QSOAudioBegin():Promise<boolean>;
|
|||||||
|
|
||||||
export function QSOAudioCancel():Promise<void>;
|
export function QSOAudioCancel():Promise<void>;
|
||||||
|
|
||||||
|
export function QSOAudioRestart():Promise<boolean>;
|
||||||
|
|
||||||
export function QuitApp():Promise<void>;
|
export function QuitApp():Promise<void>;
|
||||||
|
|
||||||
export function RefreshCtyDat():Promise<main.CtyDatInfo>;
|
export function RefreshCtyDat():Promise<main.CtyDatInfo>;
|
||||||
|
|||||||
@@ -446,6 +446,10 @@ export function QSOAudioCancel() {
|
|||||||
return window['go']['main']['App']['QSOAudioCancel']();
|
return window['go']['main']['App']['QSOAudioCancel']();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function QSOAudioRestart() {
|
||||||
|
return window['go']['main']['App']['QSOAudioRestart']();
|
||||||
|
}
|
||||||
|
|
||||||
export function QuitApp() {
|
export function QuitApp() {
|
||||||
return window['go']['main']['App']['QuitApp']();
|
return window['go']['main']['App']['QuitApp']();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -206,6 +206,21 @@ func (r *Recorder) BeginQSO() {
|
|||||||
r.active = true
|
r.active = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RestartQSO begins a fresh accumulation even if one is already active —
|
||||||
|
// re-seeding from the pre-roll ring. Used when the target QSO changes (a new
|
||||||
|
// call+freq from a clicked spot or an external app) so the previous take is
|
||||||
|
// dropped and a new one starts from the pre-roll, rather than continuing to
|
||||||
|
// accumulate the old contact.
|
||||||
|
func (r *Recorder) RestartQSO() {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if !r.running {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r.acc = append([]int16(nil), r.ring...)
|
||||||
|
r.active = true
|
||||||
|
}
|
||||||
|
|
||||||
// SaveQSO writes the accumulated recording to path as a WAV and stops
|
// SaveQSO writes the accumulated recording to path as a WAV and stops
|
||||||
// accumulating. Returns an error if no recording was active.
|
// accumulating. Returns an error if no recording was active.
|
||||||
func (r *Recorder) SaveQSO(path string) error {
|
func (r *Recorder) SaveQSO(path string) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user