diff --git a/OpsLog-res.syso b/OpsLog-res.syso new file mode 100644 index 0000000..79a0989 Binary files /dev/null and b/OpsLog-res.syso differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index d7a1565..20d26d2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,7 +1,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { AlertCircle, Antenna, CheckCircle2, Clock, Compass, Eraser, Hash, Loader2, Lock, - Maximize2, Minimize2, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Trash2, Unlock, X, + Maximize2, Minimize2, Mic, Pencil, RadioTower, RefreshCw, Satellite, Send, Settings, SlidersHorizontal, Square, Trash2, Unlock, X, Zap, } from 'lucide-react'; import { @@ -68,7 +68,7 @@ import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { cn } from '@/lib/utils'; -import { pathBetween, pathBetweenLatLon, gridToLatLon } from '@/lib/maidenhead'; +import { pathBetween, pathBetweenLatLon, gridToLatLon, latLonToGrid } from '@/lib/maidenhead'; import { flagURL } from '@/lib/flags'; type QSO = QSOForm; @@ -1316,7 +1316,14 @@ export default function App() { const ue = userEditedRef.current; if (!ue.has('name') && (r.name ?? '') !== '') setName(r.name ?? ''); if (!ue.has('qth') && (r.qth ?? '') !== '') setQth(r.qth ?? ''); - if (!ue.has('grid') && (r.grid ?? '') !== '') setGrid(r.grid ?? ''); + if (!ue.has('grid')) { + if ((r.grid ?? '') !== '') setGrid(r.grid ?? ''); + // No provider grid (cty.dat-only / portable): derive a 4-char grid from + // the entity centroid (e.g. Svalbard → JQ88) so the field — and the + // bearing/map — aren't empty. 4 chars signals it's entity-level, not a + // precise QTH (matches how Log4OM shows it). + else if (r.lat || r.lon) setGrid(latLonToGrid(r.lat || 0, r.lon || 0, 4)); + } // Country/zones are exactly what cty.dat IS authoritative for — set them // (only skipped if empty, so we never blank a known country). if (!ue.has('country') && (r.country ?? '') !== '') setCountry(r.country ?? ''); @@ -1353,6 +1360,14 @@ export default function App() { lookupTimerRef.current = window.setTimeout(() => runLookup(call), 400); wbTimerRef.current = window.setTimeout(() => runWorkedBefore(call), 150); } + // applySpotPOTA sets the QSO's POTA award reference(s) from a clicked spot's + // park ref ("US-4164" or n-fer "US-1,US-2"). Empty ref clears it (fresh + // target). Routed to the pota_ref column at save via applyAwardRefs. + function applySpotPOTA(potaRef?: string) { + const refs = String(potaRef || '') + .split(/[,;]/).map((x) => x.trim().toUpperCase()).filter(Boolean); + setDetails((d) => ({ ...d, award_refs: refs.map((r) => `POTA@${r}`).join(';') })); + } function onCallsignInput(v: string) { // 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, @@ -1576,7 +1591,7 @@ export default function App() { // them as shared consts avoids duplicating the (large) per-field JSX + // handlers across the two layouts. const callsignBlock = ( -
+
); })()} + + {/* Voice keyer (DVK) + CW keyer (WinKeyer) quick status/access. */} +
+ +
@@ -2542,6 +2588,11 @@ export default function App() { if (m) setMode(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(() => {}); @@ -2760,6 +2811,7 @@ export default function App() { if (m) setMode(m); } onCallsignInput(s.dx_call); + applySpotPOTA((s as any).pota_ref); if (s.dx_call.trim()) QSOAudioBegin().then(setRecording).catch(() => {}); }} onClose={() => setShowBandMap(false)} diff --git a/frontend/src/components/AwardRefSelector.tsx b/frontend/src/components/AwardRefSelector.tsx index 42d5393..18f05e5 100644 --- a/frontend/src/components/AwardRefSelector.tsx +++ b/frontend/src/components/AwardRefSelector.tsx @@ -280,6 +280,19 @@ export function AwardRefSelector({ dxcc, value, onChange, fieldValues }: Props) value={q} onChange={(e) => setQ(e.target.value)} /> + {/* Add an UNLISTED reference: type a new code (e.g. a brand-new POTA park + not yet in the list) and add it directly. */} + {q.trim().length >= 2 && !results.some((r) => r.code.toUpperCase() === q.trim().toUpperCase()) && ( + + )}
{busy && (
diff --git a/frontend/src/lib/maidenhead.ts b/frontend/src/lib/maidenhead.ts index f1c0538..cdbee60 100644 --- a/frontend/src/lib/maidenhead.ts +++ b/frontend/src/lib/maidenhead.ts @@ -49,6 +49,31 @@ export function gridToLatLon(grid: string): { lat: number; lon: number } | null return { lat, lon }; } +// latLonToGrid encodes a lat/lon to a Maidenhead locator (default 6 chars). +// Inverse of gridToLatLon. Used to derive a default grid from a cty.dat entity +// centroid when no provider grid is available (e.g. a portable/rare DX call) — +// a centre-of-entity locator is more useful than an empty field. +export function latLonToGrid(lat: number, lon: number, precision = 6): string { + let adjLon = Math.min(359.9999, Math.max(0, lon + 180)); + let adjLat = Math.min(179.9999, Math.max(0, lat + 90)); + const A = 'A'.charCodeAt(0); + const a = 'a'.charCodeAt(0); + const fLon = Math.floor(adjLon / 20); + const fLat = Math.floor(adjLat / 10); + adjLon -= fLon * 20; + adjLat -= fLat * 10; + const sLon = Math.floor(adjLon / 2); + const sLat = Math.floor(adjLat); + let grid = String.fromCharCode(A + fLon) + String.fromCharCode(A + fLat) + sLon + sLat; + if (precision >= 6) { + adjLon -= sLon * 2; + adjLat -= sLat; + grid += String.fromCharCode(a + Math.min(23, Math.floor(adjLon * 12))) + + String.fromCharCode(a + Math.min(23, Math.floor(adjLat * 24))); + } + return grid; +} + // gridSquareBounds returns the SW/NE corners of a Maidenhead square so a map // can draw its outline. Half-extents shrink with locator precision. export function gridSquareBounds(grid: string):