Chase POTA already withdrew its badge, filter chip and column when unchecked; US counties and new prefixes now have the same switch, and unchecking Chase new grids finally withdraws the NEW GRID badge too (the backend stopped learning squares but the display kept shouting the ones it knew — the setting is mirrored to localStorage so the display layer can gate synchronously). All withdrawal happens at the display layer: the facts keep being computed, so re-ticking a box needs no rescan.
141 lines
6.4 KiB
TypeScript
141 lines
6.4 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;
|
|
};
|
|
|
|
// 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; }
|
|
}
|
|
// 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; }
|
|
}
|
|
|
|
// 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() };
|
|
}
|
|
try {
|
|
return {
|
|
muteWorked: localStorage.getItem('opslog.clusterMuteWorked') === '1',
|
|
slotHighlight: localStorage.getItem('opslog.clusterSlotHighlight') === '1',
|
|
chasePota: chasePota(), chaseSota: chaseSota(),
|
|
chaseCounty: chaseCounty(), chasePfx: chasePfx(), chaseGrid: chaseGrid(),
|
|
};
|
|
} catch {
|
|
return { muteWorked: false, slotHighlight: false, chasePota: true, chaseSota: true, chaseCounty: true, chasePfx: true, chaseGrid: true };
|
|
}
|
|
}
|
|
|
|
type Entry = {
|
|
status?: string;
|
|
worked_call?: boolean;
|
|
worked_slot?: boolean;
|
|
new_county?: boolean;
|
|
new_pota?: boolean;
|
|
new_pfx?: boolean;
|
|
new_grid?: 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>;
|
|
}
|
|
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';
|
|
}
|