feat(dxped): the announced DX, judged against your own log
A DXpeditions tab holding the two feeds the DX world announces itself on: NG3K's ADXO (structured — dates, entity, calls, bands, modes, QSL route) and DX-World's headlines. What a logger can say that a news reader cannot is whether the operation is worth chasing, so every announcement is put through the SAME verdict the cluster paints on a spot — one badge, strongest wins, dimmed when the need is only a missing QSL — with an 'only what I need' filter. One click watches every callsign of an operation; the news headlines are mined for callsigns so they can be watched the same way. Parsers ported from DXHunter's and pinned with table tests against real feed text: the 'as PJ2/W2APF' mining, the DXCC-prefix-first normalisation without which no spot ever matches, both ADXO date forms, and the '160-6m' span whose unit is written once (DXHunter read that as 6m alone and lost the low end). Watchlist membership now announces itself app-wide, on the same card as a new version: the bindings emit watchlist:changed, so a call added from the cluster or this tab is confirmed where the operator is actually looking — the panel's own inline message never was.
This commit is contained in:
+71
-1
@@ -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,
|
||||
ChevronUp, Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radar, Radio, RadioTower, RefreshCw, Satellite, Send, SatelliteDish, Settings, SlidersHorizontal, SpellCheck, Square, Terminal, Trash2, Unlock, X, Zap,
|
||||
ChevronUp, Maximize2, Minimize2, Mic, MessageSquare, Pencil, Radar, Radio, RadioTower, RefreshCw, Satellite, Send, SatelliteDish, Settings, SlidersHorizontal, SpellCheck, Square, Star, Terminal, Trash2, Unlock, X, Zap,
|
||||
} from 'lucide-react';
|
||||
|
||||
import {
|
||||
@@ -76,6 +76,7 @@ import { AutoEQSL } from '@/components/qsl/AutoEQSL';
|
||||
import { ConfirmDialog } from '@/components/ConfirmDialog';
|
||||
import { SettingsModal } from '@/components/SettingsModal';
|
||||
import { FTMapPanel } from '@/components/FTMapPanel';
|
||||
import { DXpeditionsPanel } from '@/components/DXpeditionsPanel';
|
||||
import { FirstRunModal } from '@/components/FirstRunModal';
|
||||
import { QSOEditModal } from '@/components/QSOEditModal';
|
||||
import { BandMap } from '@/components/BandMap';
|
||||
@@ -1315,6 +1316,17 @@ export default function App() {
|
||||
setActiveTab((t) => (t === 'grids' ? 'recent' : t));
|
||||
}
|
||||
const [ftmapTabOpen, setFtmapTabOpen] = useState(() => localStorage.getItem('opslog.ftmapTab') === '1');
|
||||
const [dxpedTabOpen, setDxpedTabOpen] = useState(() => localStorage.getItem('opslog.dxpedTab') === '1');
|
||||
function openDxpedTab() {
|
||||
setDxpedTabOpen(true);
|
||||
writeUiPref('opslog.dxpedTab', '1');
|
||||
setActiveTab('dxped');
|
||||
}
|
||||
function closeDxpedTab() {
|
||||
setDxpedTabOpen(false);
|
||||
writeUiPref('opslog.dxpedTab', '0');
|
||||
setActiveTab((t) => (t === 'dxped' ? 'recent' : t));
|
||||
}
|
||||
function openFtmapTab() {
|
||||
setFtmapTabOpen(true);
|
||||
writeUiPref('opslog.ftmapTab', '1');
|
||||
@@ -2346,6 +2358,18 @@ export default function App() {
|
||||
}, [showSettings]);
|
||||
const [showDuplicates, setShowDuplicates] = useState(false);
|
||||
const [updateInfo, setUpdateInfo] = useState<{ latest: string; url: string; downloadUrl: string } | null>(null);
|
||||
// Watchlist membership changes announce themselves here rather than inside
|
||||
// the watchlist panel: a call can be added from the cluster or the
|
||||
// DXpeditions tab, and the old inline message lived on a page nobody was
|
||||
// looking at.
|
||||
const [wlNotice, setWlNotice] = useState<{ call: string; added: boolean } | null>(null);
|
||||
const wlNoticeTimer = useRef<number | undefined>(undefined);
|
||||
useEffect(() => EventsOn('watchlist:changed', (e: any) => {
|
||||
if (!e?.call) return;
|
||||
setWlNotice({ call: String(e.call), added: !!e.added });
|
||||
if (wlNoticeTimer.current) window.clearTimeout(wlNoticeTimer.current);
|
||||
wlNoticeTimer.current = window.setTimeout(() => setWlNotice(null), 4000);
|
||||
}), []);
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false);
|
||||
// Fresh update check on demand (opening About), so it never shows a stale
|
||||
// "you're up to date". Clears updateInfo when the latest check finds nothing.
|
||||
@@ -5126,6 +5150,7 @@ export default function App() {
|
||||
]},
|
||||
{ name: 'tools', label: t('menu.tools'), items: [
|
||||
{ type: 'item', label: t('tools.qslManager'), action: 'tools.qslmanager' },
|
||||
{ type: 'item', label: t('dxp.tab'), action: 'tools.dxped' },
|
||||
{ type: 'item', label: t('stats.tab'), action: 'tools.stats' },
|
||||
{ type: 'item', label: t('station.title'), action: 'tools.station' },
|
||||
{ type: 'item', label: t('tools.qslDesigner'), action: 'tools.qsldesigner' },
|
||||
@@ -5180,6 +5205,7 @@ export default function App() {
|
||||
case 'tools.qslmanager': setQslTabOpen(true); setActiveTab('qsl'); break;
|
||||
case 'tools.stats': setStatsTabOpen(true); setActiveTab('stats'); break;
|
||||
case 'tools.station': setStationTabOpen(true); setActiveTab('station'); break;
|
||||
case 'tools.dxped': openDxpedTab(); break;
|
||||
case 'tools.decodes': openDecodesTab(); break;
|
||||
case 'tools.ftmap': openFtmapTab(); break;
|
||||
case 'tools.grids': openGridsTab(); break;
|
||||
@@ -7076,6 +7102,26 @@ export default function App() {
|
||||
<FirstRunModal onDone={() => { setShowFirstRun(false); loadStation(); refresh(); }} />
|
||||
)}
|
||||
|
||||
{wlNotice && (
|
||||
<div className={cn('fixed right-4 z-[150] w-72 rounded-lg border bg-card shadow-xl p-3 animate-in slide-in-from-bottom-2 fade-in',
|
||||
wlNotice.added ? 'border-warning/40' : 'border-border',
|
||||
// Stacked above the update card when both are up.
|
||||
updateInfo ? 'bottom-44' : 'bottom-4')}>
|
||||
<div className="flex items-start gap-2">
|
||||
<Star className={cn('size-4 mt-0.5 shrink-0', wlNotice.added ? 'fill-current text-warning' : 'text-muted-foreground')} />
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-semibold">
|
||||
{t(wlNotice.added ? 'wlnote.added' : 'wlnote.removed', { call: wlNotice.call })}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">{t('wlnote.hint')}</p>
|
||||
</div>
|
||||
<button onClick={() => setWlNotice(null)}
|
||||
className="shrink-0 text-muted-foreground hover:text-foreground" aria-label="Close">
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{updateInfo && (
|
||||
<div className="fixed bottom-4 right-4 z-[150] w-80 rounded-lg border border-primary/40 bg-card shadow-xl p-3 animate-in slide-in-from-bottom-2 fade-in">
|
||||
<div className="flex items-start gap-2">
|
||||
@@ -7816,6 +7862,21 @@ export default function App() {
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{dxpedTabOpen && (
|
||||
<TabsTrigger value="dxped" className="gap-1.5">
|
||||
{t('dxp.tab')}
|
||||
<span
|
||||
role="button"
|
||||
aria-label="Close DXpeditions"
|
||||
title="Close"
|
||||
className="inline-flex items-center justify-center size-4 rounded hover:bg-foreground/10 text-muted-foreground hover:text-foreground"
|
||||
onPointerDown={(e) => { e.stopPropagation(); }}
|
||||
onClick={(e) => { e.stopPropagation(); closeDxpedTab(); }}
|
||||
>
|
||||
<X className="size-3" />
|
||||
</span>
|
||||
</TabsTrigger>
|
||||
)}
|
||||
{ftmapTabOpen && (
|
||||
<TabsTrigger value="ftmap" className="gap-1.5">
|
||||
{t('ftmap.tab')}
|
||||
@@ -8429,6 +8490,15 @@ export default function App() {
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{dxpedTabOpen && (
|
||||
<TabsContent value="dxped" className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
||||
{activeTab === 'dxped' && (
|
||||
<div className="h-full w-full min-h-0 p-1">
|
||||
<DXpeditionsPanel />
|
||||
</div>
|
||||
)}
|
||||
</TabsContent>
|
||||
)}
|
||||
{ftmapTabOpen && (
|
||||
<TabsContent value="ftmap" className="mt-0 flex flex-col min-h-0 flex-1 data-[state=inactive]:hidden">
|
||||
{activeTab === 'ftmap' && (
|
||||
|
||||
Reference in New Issue
Block a user