feat(ui): the FT decodes panel, two DXHunter themes, and the maps
Decodes: - one click SELECTS, two transmit. A single click handed the decode straight to WSJT-X as a reply, so brushing a row while reading the band started calling a station. - the list empties for a receiver that changes band, and a receiver column appears when more than one is feeding one merged list. - the period clock turns red while transmitting: it is the one thing on the screen that moves, so it is where the eye already is. - a WL badge, after the LoTW "L" — one letter, always in the same place, so the column does not shift from row to row. - the auto-call switch, its target and its count, and the chase list: naming the station you are waiting for is done while watching the band, not in a settings tree. Themes: DXHunter's slate with its own blue, and the same slate with OpsLog's orange. Counted across its sources rather than guessed from one panel — blue is 132 uses to violet's 25, and the violet is the PSK Reporter panel alone. Watchlist: drawn as DXHunter draws it — the callsign in the interface font rather than monospaced, which is the difference that shows with the two windows side by side. FT Map: arcs no longer run off the side of the map. The map shows one world, and a path crossing the antimeridian was drawn past 180° into the blank space beside it — from VK that is most of them. Cluster: "superfox", "fox/hound" and "F/H" in a comment are read as FT8. They are WSJT-X's DXpedition transmit modes, and the comment fell through to the band plan and came out DATA — which then decided the band+mode verdict. A decoder is named by what it IS: Nexus sends its packets as "Tempo", the engine inside it, and OpsLog showed a program nobody has heard of.
This commit is contained in:
@@ -15,7 +15,7 @@ import { useI18n } from '@/lib/i18n';
|
||||
import { chaseAllows } from '@/lib/spotDisplay';
|
||||
import { markerColour } from '@/lib/spotMarkers';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { GetChaseNewSpots } from '../../wailsjs/go/main/App';
|
||||
import { GetChaseNewSpots, GetPSKReporterStatus } from '../../wailsjs/go/main/App';
|
||||
|
||||
export interface ChaseNewSpot {
|
||||
call: string;
|
||||
@@ -80,15 +80,25 @@ function categoryOf(s: ChaseNewSpot): Category | null {
|
||||
|
||||
const FILTER_KEY = 'opslog.chaseNewFilters';
|
||||
|
||||
function allowedCategories(): Category[] {
|
||||
return CATEGORIES.filter((c) => chaseAllows(c.key)).map((c) => c.key);
|
||||
}
|
||||
|
||||
function loadFilters(): Set<Category> {
|
||||
const allowed = allowedCategories();
|
||||
try {
|
||||
const raw = localStorage.getItem(FILTER_KEY);
|
||||
if (raw) {
|
||||
const list = JSON.parse(raw) as Category[];
|
||||
if (Array.isArray(list)) return new Set(list);
|
||||
// A stored set holding NONE of the categories on offer hides the whole
|
||||
// panel, for ever, with nothing to say why — and that is exactly what a
|
||||
// preference written by an older build does once a category is renamed.
|
||||
// Treated as "no preference": a panel that shows nothing at every launch
|
||||
// is never what was meant, and the chips are one click away.
|
||||
if (Array.isArray(list) && list.some((k) => allowed.includes(k))) return new Set(list);
|
||||
}
|
||||
} catch { /* a corrupt preference is not worth a broken panel */ }
|
||||
return new Set(CATEGORIES.filter((c) => chaseAllows(c.key)).map((c) => c.key));
|
||||
return new Set(allowed);
|
||||
}
|
||||
|
||||
export function ChaseNewPanel({ onPick, onClose }: Props) {
|
||||
@@ -96,6 +106,10 @@ export function ChaseNewPanel({ onPick, onClose }: Props) {
|
||||
const [spots, setSpots] = useState<ChaseNewSpot[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const [on, setOn] = useState<Set<Category>>(loadFilters);
|
||||
// The FEED, not the list: connected or not, how many reports it has taken,
|
||||
// and what it is filtered on. Without it an empty panel says nothing about
|
||||
// whether anything is arriving at all — which is the first question.
|
||||
const [feed, setFeed] = useState<any>(null);
|
||||
|
||||
// Polled rather than pushed: the feed can deliver several a second under an
|
||||
// opening, and an event per row would be a redraw per row for a list nobody
|
||||
@@ -106,6 +120,8 @@ export function ChaseNewPanel({ onPick, onClose }: Props) {
|
||||
try {
|
||||
const r = ((await GetChaseNewSpots()) ?? []) as ChaseNewSpot[];
|
||||
if (alive) { setSpots(r); setLoaded(true); }
|
||||
const st = await GetPSKReporterStatus();
|
||||
if (alive) setFeed(st);
|
||||
} catch { /* the feed may not be up yet */ }
|
||||
};
|
||||
tick();
|
||||
@@ -154,8 +170,13 @@ export function ChaseNewPanel({ onPick, onClose }: Props) {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Both numbers: what is on screen, and what was heard. They differ
|
||||
exactly when a category is switched off, which is the one case an
|
||||
operator reads this panel as broken. */}
|
||||
<span className="shrink-0 text-[10px] text-muted-foreground">
|
||||
{loaded ? t('chn.count', { n: shown.length }) : ''}
|
||||
{loaded ? (shown.length === spots.length
|
||||
? t('chn.count', { n: spots.length })
|
||||
: t('chn.countOf', { n: shown.length, total: spots.length })) : ''}
|
||||
</span>
|
||||
{onClose && (
|
||||
<button
|
||||
@@ -218,7 +239,16 @@ export function ChaseNewPanel({ onPick, onClose }: Props) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<p className="border-t border-border px-2 py-1 text-[10px] text-muted-foreground">{t('chn.digitalOnly')}</p>
|
||||
{/* The radius comes from the FEED, not from a sentence: it was written
|
||||
into this line as "~300 km" and stayed 300 while the setting said
|
||||
1000, which is the panel telling the operator their change did not
|
||||
take when it had. */}
|
||||
<p className="border-t border-border px-2 py-1 text-[10px] text-muted-foreground">
|
||||
{t('chn.heardWithin', { km: feed?.near_km || 300 })}
|
||||
{feed && (feed.running
|
||||
? <span className="text-success"> · {t('chn.feedOn', { n: feed.received ?? 0, sq: feed.squares ?? 0 })}</span>
|
||||
: <span className="text-warning"> · {feed.last_err ? t('chn.feedErr', { e: feed.last_err }) : t('chn.feedOff')}</span>)}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user