The chase switches were born on the DX Cluster page because the cluster was the only thing that drew their badges. They now decide what the FT decode list and Chase new say as well, and settings that govern three screens should not live under the name of one of them. They move to a DXHunter page — named for what it is about, and for what is meant to land beside it as more of DXHunter's ideas are ported over. US states join the switchable markers, gated the same way as POTA, counties, prefixes and squares: badge, filter chip and display marker, all from the one chaseAllows question.
171 lines
7.8 KiB
TypeScript
171 lines
7.8 KiB
TypeScript
// Two operator options that change how a spot LOOKS, shared by the DX-cluster
|
|
// list and the band map so the two panels can never disagree about the same
|
|
// spot — the lesson already learnt with the marker colours.
|
|
//
|
|
// muteWorked — a station already worked gets no colour and no badge. It
|
|
// stays in the list, it simply stops competing for attention.
|
|
// slotHighlight — a callsign not yet worked on THIS band and mode is coloured,
|
|
// whatever the entity says. For an operator filling slots, a
|
|
// common entity on a new band+mode is the whole point, and the
|
|
// entity-level status calls it "worked".
|
|
//
|
|
// They compose deliberately: mute what is done, light up what is not.
|
|
|
|
export type SpotDisplayOptions = {
|
|
muteWorked: boolean; slotHighlight: boolean;
|
|
// Chase switches (Settings → DX Cluster), ON by default. An operator who does
|
|
// not chase parks does not want NEW POTA shouting from every activator spot:
|
|
// off, the marker is withdrawn at the display layer — a "new band + new POTA"
|
|
// spot simply reads NEW BAND — and the reference column goes quiet. The facts
|
|
// keep being computed; only the telling stops, so ticking the box back on
|
|
// needs no rescan.
|
|
chasePota: boolean; chaseSota: boolean;
|
|
chaseCounty: boolean; chasePfx: boolean; chaseGrid: boolean; chaseState: boolean;
|
|
};
|
|
|
|
// chasePota/chaseSota read the switches directly — for the places that show a
|
|
// REFERENCE rather than a status (the POTA and SOTA columns).
|
|
export function chasePota(): boolean {
|
|
try { return localStorage.getItem('opslog.chasePota') !== '0'; } catch { return true; }
|
|
}
|
|
export function chaseSota(): boolean {
|
|
try { return localStorage.getItem('opslog.chaseSota') !== '0'; } catch { return true; }
|
|
}
|
|
export function chaseCounty(): boolean {
|
|
try { return localStorage.getItem('opslog.chaseCounty') !== '0'; } catch { return true; }
|
|
}
|
|
export function chasePfx(): boolean {
|
|
try { return localStorage.getItem('opslog.chasePfx') !== '0'; } catch { return true; }
|
|
}
|
|
export function chaseState(): boolean {
|
|
try { return localStorage.getItem('opslog.chaseState') !== '0'; } catch { return true; }
|
|
}
|
|
// chaseGrid mirrors the backend "chase new grids" setting (Settings writes the
|
|
// mirror on load and on toggle) so the display layer can gate NEW GRID without
|
|
// an async round-trip per row.
|
|
export function chaseGrid(): boolean {
|
|
try { return localStorage.getItem('opslog.chaseGrids') !== '0'; } catch { return true; }
|
|
}
|
|
|
|
// chaseAllows answers "does this operator chase this kind of thing at all?"
|
|
// for the ORTHOGONAL markers — park, square, prefix, county.
|
|
//
|
|
// The switches were written for the cluster and stayed there, so an operator
|
|
// who does not chase parks still met NEW POTA in the decode list and in Chase
|
|
// new: the same badge, withdrawn on one screen and shouting on the next. The
|
|
// setting is about what the operator hunts, not about which panel is open.
|
|
//
|
|
// The keys are both the status-entry field names (new_pota…) and the shorter
|
|
// category names the panels filter with (pota…), so one call serves both.
|
|
// Anything without a switch of its own — a new US state — is always allowed.
|
|
export function chaseAllows(key: string): boolean {
|
|
switch (key) {
|
|
case 'new_pota': case 'pota': return chasePota();
|
|
case 'new_grid': case 'grid': return chaseGrid();
|
|
case 'new_pfx': case 'pfx': return chasePfx();
|
|
case 'new_county': case 'cty': return chaseCounty();
|
|
case 'new_state': case 'state': return chaseState();
|
|
default: return true;
|
|
}
|
|
}
|
|
|
|
// Both options are withdrawn from the filter panel for now. The machinery below
|
|
// is deliberately kept whole — it is correct and hard-won — so putting the two
|
|
// switches back is this one flag and the block they came from in App.tsx.
|
|
//
|
|
// The saved preferences are left untouched in localStorage rather than cleared:
|
|
// an operator who had either turned on gets them back exactly as they were the
|
|
// day the options return, instead of silently starting from off.
|
|
export const SPOT_DISPLAY_OPTIONS_EXPOSED = false;
|
|
|
|
export function readSpotDisplayOptions(): SpotDisplayOptions {
|
|
// The EXPOSED flag only withdraws the two original switches; the chase
|
|
// switches are live regardless.
|
|
if (!SPOT_DISPLAY_OPTIONS_EXPOSED) {
|
|
return { muteWorked: false, slotHighlight: false, chasePota: chasePota(), chaseSota: chaseSota(), chaseCounty: chaseCounty(), chasePfx: chasePfx(), chaseGrid: chaseGrid(), chaseState: chaseState() };
|
|
}
|
|
try {
|
|
return {
|
|
muteWorked: localStorage.getItem('opslog.clusterMuteWorked') === '1',
|
|
slotHighlight: localStorage.getItem('opslog.clusterSlotHighlight') === '1',
|
|
chasePota: chasePota(), chaseSota: chaseSota(),
|
|
chaseCounty: chaseCounty(), chasePfx: chasePfx(), chaseGrid: chaseGrid(),
|
|
chaseState: chaseState(),
|
|
};
|
|
} catch {
|
|
return { muteWorked: false, slotHighlight: false, chasePota: true, chaseSota: true, chaseCounty: true, chasePfx: true, chaseGrid: true, chaseState: true };
|
|
}
|
|
}
|
|
|
|
type Entry = {
|
|
status?: string;
|
|
worked_call?: boolean;
|
|
worked_slot?: boolean;
|
|
new_county?: boolean;
|
|
new_pota?: boolean;
|
|
new_pfx?: boolean;
|
|
new_grid?: boolean;
|
|
new_state?: boolean;
|
|
} | undefined;
|
|
|
|
// applySpotDisplay rewrites a status entry per the options, so every consumer —
|
|
// colour, badge, status text — follows from one decision instead of each panel
|
|
// re-deriving it.
|
|
export function applySpotDisplay<T extends Entry>(s: T, o: SpotDisplayOptions): T {
|
|
if (!s) return s;
|
|
let e = s;
|
|
|
|
// Slot promotion runs first: a callsign not yet worked on this band and mode
|
|
// is not done, whatever the entity says, so it earns a status before the mute
|
|
// below can take its colour away.
|
|
if (o.slotHighlight && e.worked_slot === false && (!e.status || e.status === 'worked')) {
|
|
e = { ...e, status: 'new-call' } as NonNullable<T>;
|
|
}
|
|
|
|
// Mute drops the blue already-worked-callsign mark, and NOTHING else.
|
|
//
|
|
// It used to blank the status as well, on the theory that a spot bringing no
|
|
// novelty should stop painting entirely. That was wrong twice over. The status
|
|
// is what the cluster list reads to DIM a row, so blanking it turned every
|
|
// quiet grey row bright white — the option made the list louder, not quieter.
|
|
// And an empty status means "entity not resolved" everywhere else, so muted
|
|
// spots had to carry a flag saying they did not really mean that.
|
|
//
|
|
// Leaving the status alone costs nothing: a worked entity already renders with
|
|
// no colour and gets dimmed, so removing the blue is the entire job.
|
|
if (o.muteWorked) {
|
|
e = { ...e, worked_call: false } as NonNullable<T>;
|
|
}
|
|
if (!o.chasePota && e.new_pota) {
|
|
e = { ...e, new_pota: false } as NonNullable<T>;
|
|
}
|
|
// Same withdrawal for the other extra markers: the facts keep being
|
|
// computed, only the telling stops — re-ticking a box needs no rescan.
|
|
if (!o.chaseCounty && e.new_county) {
|
|
e = { ...e, new_county: false } as NonNullable<T>;
|
|
}
|
|
if (!o.chasePfx && e.new_pfx) {
|
|
e = { ...e, new_pfx: false } as NonNullable<T>;
|
|
}
|
|
if (!o.chaseGrid && e.new_grid) {
|
|
e = { ...e, new_grid: false } as NonNullable<T>;
|
|
}
|
|
if (!o.chaseState && e.new_state) {
|
|
e = { ...e, new_state: false } as NonNullable<T>;
|
|
}
|
|
return e;
|
|
}
|
|
|
|
// spotIsWorked answers "does this spot bring me nothing?", which is what the
|
|
// "hide worked" filter is really asking.
|
|
//
|
|
// The extra markers are ORTHOGONAL to the entity status — a spot can be a worked
|
|
// entity AND a new park, prefix, county or grid — so they win. Without that, a
|
|
// new prefix in an entity worked years ago vanished the moment the filter went
|
|
// on, which is the opposite of what an operator hunting prefixes wants.
|
|
export function spotIsWorked(e: Entry): boolean {
|
|
if (!e) return false;
|
|
if (e.new_pota || e.new_county || e.new_pfx || e.new_grid) return false;
|
|
return !!e.worked_call || e.status === 'worked';
|
|
}
|