chore: release v0.26.2
This commit is contained in:
+74
-50
@@ -1,7 +1,7 @@
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import {
|
||||
Activity, AlertCircle, Antenna, Bell, Check, CheckCircle2, ChevronDown, Clock, CloudOff, Compass, Database, Ear, Eraser, Flame, Gauge, Hash, Loader2, Lock,
|
||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radar, Radio, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||
Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radar, Radio, RadioTower, RefreshCw, Satellite, Send, SatelliteDish, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -20,7 +20,6 @@ import {
|
||||
RotatorGoToPath,
|
||||
GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, EntryBandChanged, FlexApplyBandAntenna, FlexApplyBandPower,
|
||||
GetSecretStatus, UnlockSecrets,
|
||||
RefreshCtyDat, DownloadAllReferenceLists,
|
||||
RotatorGoTo, RotatorStop, SetActiveRotor,
|
||||
GetDBConnectionInfo, GetLogbookRevision,
|
||||
GetUltrabeamStatus, SetUltrabeamDirection, UILog,
|
||||
@@ -56,7 +55,7 @@ import {
|
||||
QSLViaRepairStatus, RepairQSLVia, DismissQSLViaRepair,
|
||||
} from '../wailsjs/go/main/App';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList , withIOTARef } from '@/lib/awardRefs';
|
||||
import { applyAwardRefs, parseAwardRefs as parseManualRefs, spotRefList , withIOTARef, withRDARef } from '@/lib/awardRefs';
|
||||
import { EventsOn, BrowserOpenURL, WindowMinimise, WindowToggleMaximise, WindowIsMaximised, Quit } from '../wailsjs/runtime/runtime';
|
||||
import type { adif as adifModels, lookup as lookupModels, cat as catModels } from '../wailsjs/go/models';
|
||||
import type { QSOForm, WorkedBeforeView, StationSettingsForm, ListsSettingsForm, ModePresetForm } from '@/types';
|
||||
@@ -80,6 +79,7 @@ import { FlexPanel } from '@/components/FlexPanel';
|
||||
import { IcomPanel } from '@/components/IcomPanel';
|
||||
import { YaesuPanel } from '@/components/YaesuPanel';
|
||||
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
||||
import { MotorAntennaWidget, type AntStatus } from '@/components/MotorAntennaWidget';
|
||||
import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel';
|
||||
import { AmpWidget } from '@/components/AmpWidget';
|
||||
import { ScpPanel, type ScpResult } from '@/components/ScpPanel';
|
||||
@@ -598,7 +598,14 @@ export default function App() {
|
||||
return () => { window.clearTimeout(t); off(); };
|
||||
}, [callsign]);
|
||||
const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 });
|
||||
const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false });
|
||||
// The FULL antenna status, not the four fields the beam overlay needed: the
|
||||
// docked widget shows frequency, bands and tracking too, and polling the same
|
||||
// controller twice for two shapes of the same answer is how the two views end
|
||||
// up disagreeing. One poll, one object.
|
||||
const [ubStatus, setUbStatus] = useState<AntStatus>({
|
||||
enabled: false, type: '', connected: false, direction: 0, frequency: 0, moving: false, elements: [],
|
||||
});
|
||||
const [showMotorAnt, setShowMotorAnt] = useState(() => localStorage.getItem('opslog.showMotorAnt') === '1');
|
||||
const [agStatus, setAgStatus] = useState<AGStatus>({ connected: false, port_a: 0, port_b: 0, antennas: [] });
|
||||
const [agEnabled, setAgEnabled] = useState(false);
|
||||
const [tgStatus, setTgStatus] = useState<TGStatus>({ connected: false });
|
||||
@@ -2129,8 +2136,6 @@ export default function App() {
|
||||
}
|
||||
}, [updateInfo]);
|
||||
const [deletingAll, setDeletingAll] = useState(false);
|
||||
const [ctyRefreshing, setCtyRefreshing] = useState(false);
|
||||
const [refsDownloading, setRefsDownloading] = useState(false);
|
||||
|
||||
// === ADIF ===
|
||||
const [importing, setImporting] = useState(false);
|
||||
@@ -2759,7 +2764,14 @@ export default function App() {
|
||||
// open, and every poll opens and closes the port.
|
||||
useEffect(() => subscribeRotorHeading((h) => setRotatorHeading(h as any)), []);
|
||||
|
||||
// Poll the Ultrabeam antenna for its connection + pattern direction.
|
||||
// Poll the motorised antenna: connection, pattern, frequency, tracking.
|
||||
//
|
||||
// pokeUbStatus re-reads it NOW rather than waiting for the next tick, so a
|
||||
// button in the docked widget shows its effect immediately instead of three
|
||||
// seconds later — long enough to press it a second time.
|
||||
const pokeUbStatus = useCallback(async () => {
|
||||
try { const s: any = await GetUltrabeamStatus(); if (s) setUbStatus(s); } catch { /* transient */ }
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = async () => {
|
||||
@@ -4405,7 +4417,13 @@ export default function App() {
|
||||
// previous contact with the call, so the island appeared for stations
|
||||
// already in the log and never for the new one on the island — which is
|
||||
// the entire point of the feature.
|
||||
award_refs: withIOTARef(d.award_refs ?? '', String((r as any)?.iota ?? '')),
|
||||
// The island, then the Russian district — both from the callbook, both
|
||||
// only ever applied to the contact being typed. See withRDARef for why
|
||||
// that restriction is the whole design.
|
||||
award_refs: withRDARef(
|
||||
withIOTARef(d.award_refs ?? '', String((r as any)?.iota ?? '')),
|
||||
String((r as any)?.rda ?? ''),
|
||||
),
|
||||
}));
|
||||
// Backfill anything the provider didn't supply from the last time we worked
|
||||
// this call (call not found on QRZ/HamQTH, or lookup off → cty.dat only).
|
||||
@@ -4481,11 +4499,21 @@ export default function App() {
|
||||
// the first keystroke, so we only abort once.)
|
||||
if (v.trim() !== '') {
|
||||
// Someone answered: on the FIRST character of a new call (the field was
|
||||
// empty), abort whatever CW is being sent — a single macro OR an auto-call
|
||||
// empty), abort whatever is being sent — a single macro OR an auto-call
|
||||
// CQ loop — routed to the ACTIVE engine (was WinKeyer-only, so a Flex CWX /
|
||||
// Icom macro kept keying over the answering station).
|
||||
if (callsignValRef.current.trim() === '') stopKeyerTx();
|
||||
//
|
||||
// The VOICE keyer gets the same treatment, and did not: typing a callsign
|
||||
// stopped the CW auto-call and left the DVK cheerfully calling CQ over the
|
||||
// station that had just answered. Nothing about the reason is specific to
|
||||
// Morse — it is "somebody is there, stop calling" — so both engines stop
|
||||
// on the same event.
|
||||
if (callsignValRef.current.trim() === '') {
|
||||
stopKeyerTx();
|
||||
DVKStop().catch(() => { /* nothing playing */ });
|
||||
}
|
||||
stopAutoCall();
|
||||
stopDvkAutoCq();
|
||||
}
|
||||
// No-op guard: external apps (MSHV/WSJT-X) re-broadcast the same DX call
|
||||
// on every status packet. If it matches what's already in the entry,
|
||||
@@ -4629,11 +4657,10 @@ export default function App() {
|
||||
{ type: 'item', label: t('tools.alerts'), action: 'tools.alerts' },
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('tools.duplicates'), action: 'tools.duplicates', disabled: total === 0 },
|
||||
{ type: 'separator' },
|
||||
// Maintenance — bumped here while we only have one entry. Will move
|
||||
// to a Tools → Maintenance submenu once Clublog + LoTW refresh land.
|
||||
{ type: 'item', label: ctyRefreshing ? 'Refreshing cty.dat…' : 'Refresh cty.dat', action: 'tools.refreshCty', disabled: ctyRefreshing },
|
||||
{ type: 'item', label: refsDownloading ? 'Downloading reference lists…' : 'Download reference lists (IOTA/POTA/WWFF/SOTA)', action: 'tools.downloadRefs', disabled: refsDownloading },
|
||||
// The two maintenance entries that used to sit here — refresh cty.dat and
|
||||
// download the reference lists — moved to Settings → Maintenance →
|
||||
// Databases, where every other database already lives. One place to look
|
||||
// beats two, and the Tools menu is for what you DO with the log.
|
||||
]},
|
||||
{ name: 'help', label: t('menu.help'), items: [
|
||||
{ type: 'item', label: t('whatsnew.title'), action: 'help.whatsnew' },
|
||||
@@ -4649,7 +4676,7 @@ export default function App() {
|
||||
{ type: 'separator' },
|
||||
{ type: 'item', label: t('help.about'), action: 'help.about' },
|
||||
]},
|
||||
], [total, selectedId, selectedIds, ctyRefreshing, refsDownloading, exporting, wkEnabled, dvkEnabled, cwEnabled, netEnabled, contestTabEnabled, smtpConfigured, sendingLog, t]);
|
||||
], [total, selectedId, selectedIds, exporting, wkEnabled, dvkEnabled, cwEnabled, netEnabled, contestTabEnabled, smtpConfigured, sendingLog, t]);
|
||||
|
||||
function handleMenu(action: string) {
|
||||
switch (action) {
|
||||
@@ -4677,8 +4704,6 @@ export default function App() {
|
||||
case 'tools.contest': setContestTabEnabled((v) => { const nv = !v; if (nv) setActiveTab('contest'); else setActiveTab((tb) => (tb === 'contest' ? 'recent' : tb)); return nv; }); break;
|
||||
case 'tools.alerts': setAlertsOpen(true); break;
|
||||
case 'tools.duplicates': setShowDuplicates(true); break;
|
||||
case 'tools.refreshCty': refreshCtyDat(); break;
|
||||
case 'tools.downloadRefs': downloadRefs(); break;
|
||||
case 'help.about': setShowAbout(true); checkUpdateNow(); break;
|
||||
case 'help.log': setShowLogViewer(true); break;
|
||||
case 'help.whatsnew': GetChangelog().then((e: any) => { if (Array.isArray(e) && e.length) setWhatsNew(e); else showToast(t('whatsnew.none')); }).catch(() => {}); break;
|
||||
@@ -4703,37 +4728,6 @@ export default function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadRefs() {
|
||||
if (refsDownloading) return;
|
||||
setRefsDownloading(true);
|
||||
setError('');
|
||||
try {
|
||||
const summary = await DownloadAllReferenceLists();
|
||||
showToast(`Reference lists updated — ${summary}`);
|
||||
} catch (e: any) {
|
||||
setError(`Reference download failed: ${String(e?.message ?? e)}`);
|
||||
} finally {
|
||||
setRefsDownloading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function refreshCtyDat() {
|
||||
if (ctyRefreshing) return;
|
||||
setCtyRefreshing(true);
|
||||
setError('');
|
||||
try {
|
||||
const info = await RefreshCtyDat();
|
||||
// Use the regular error banner area for a brief success note — keeps
|
||||
// us from pulling in a toast system just for one maintenance action.
|
||||
setError(`cty.dat refreshed — ${info.entities} entities loaded`);
|
||||
setTimeout(() => setError((e) => e.startsWith('cty.dat refreshed') ? '' : e), 4000);
|
||||
} catch (e: any) {
|
||||
setError(`cty.dat refresh failed: ${String(e?.message ?? e)}`);
|
||||
} finally {
|
||||
setCtyRefreshing(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
function onKey(e: KeyboardEvent) {
|
||||
const tag = (e.target as HTMLElement)?.tagName;
|
||||
@@ -6157,6 +6151,25 @@ export default function App() {
|
||||
>
|
||||
<Compass className="size-4" />
|
||||
</button>
|
||||
{ubStatus.enabled && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => { const v = !showMotorAnt; setShowMotorAnt(v); writeUiPref('opslog.showMotorAnt', v ? '1' : '0'); }}
|
||||
title={showMotorAnt ? t('station.motorWidgetHide') : t('station.motorWidgetShow')}
|
||||
className={cn(
|
||||
'relative inline-flex items-center justify-center size-7 rounded-md border transition-colors',
|
||||
showMotorAnt ? 'border-success-border bg-success-muted text-success-muted-foreground hover:bg-success-muted'
|
||||
: 'border-border text-muted-foreground hover:bg-muted',
|
||||
)}
|
||||
>
|
||||
<SatelliteDish className="size-4" />
|
||||
{/* Amber while the elements travel: on a SteppIR that is also
|
||||
when transmitting is a bad idea, so it is worth seeing from
|
||||
the icon without opening the widget. */}
|
||||
{ubStatus.moving && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-warning animate-pulse" />}
|
||||
{!ubStatus.moving && showMotorAnt && ubStatus.connected && <span className="absolute -top-0.5 -right-0.5 size-2 rounded-full bg-success" />}
|
||||
</button>
|
||||
)}
|
||||
{agEnabled && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -6788,7 +6801,7 @@ export default function App() {
|
||||
{/* Reserved free space to the right. The WinKeyer CW keyer and/or the
|
||||
Digital Voice Keyer take this slot when enabled (Log4OM-style);
|
||||
otherwise it shows the QRZ profile photo. */}
|
||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showAmpWidget && ampSts.length > 0) || (showScp && scpEnabled) || (chaseNewOn && showChaseNew) || (showLiveStations && dbConn?.backend === 'mysql')) && (
|
||||
{!compact && (chatShown || wkEnabled || dvkEnabled || lookupResult?.image_url || (showRotor && (rotatorHeading.enabled || dxPath)) || (showMotorAnt && ubStatus.enabled) || (showAntGenius && agEnabled) || (showTuner && tgEnabled) || (showAmpWidget && ampSts.length > 0) || (showScp && scpEnabled) || (chaseNewOn && showChaseNew) || (showLiveStations && dbConn?.backend === 'mysql')) && (
|
||||
// relative + absolute inner (like the F1-F5 panel): a taller widget (e.g.
|
||||
// the DVK with Auto CQ) can't grow the row — the row height stays set by
|
||||
// the entry strip and each widget fills that height, scrolling inside.
|
||||
@@ -6872,6 +6885,17 @@ export default function App() {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showMotorAnt && ubStatus.enabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<MotorAntennaWidget
|
||||
ant={ubStatus}
|
||||
refetch={pokeUbStatus}
|
||||
t={t}
|
||||
essentialsOnly
|
||||
onClose={() => { setShowMotorAnt(false); writeUiPref('opslog.showMotorAnt', '0'); }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showAntGenius && agEnabled && (
|
||||
<div className="w-[230px] shrink-0 min-h-0">
|
||||
<AntGeniusPanel
|
||||
|
||||
Reference in New Issue
Block a user