feat(cluster): quiet the worked spots, light up the empty slots

Two options that change what the eye is pulled towards, both applying to the
cluster list AND the band map.

Mute worked before: a spot that brings nothing new loses its colour and its
badges. It stays in the list - the operator asked for less noise, not less
information. "Brings nothing new" reuses the dimming rule the cluster list
already had rather than inventing a second notion of done, so it keeps obeying
the same-slot option and the digital-mode grouping for free. A new-band or
new-slot status is NOT muted: having worked that callsign once on another band
says nothing about the band in front of you.

Highlight unworked in this slot: colours any callsign not yet worked on this
band and this mode, whatever the entity says. For an operator filling slots a
common entity on a fresh band+mode is the whole point, and the entity-level
status flatly calls it worked. It reuses the existing new-slot status, so no new
colour, no new legend, no new badge - both panels already knew how to draw it.

WorkedSlot is computed independently of the same-slot preference: it is what
this option reads, and it must not change meaning because a different option was
toggled. The slot index is now built when either option needs it, and the status
cache is keyed on both so a toggle invalidates it.

The rules live in one module used by both panels. Marker colours already taught
us what happens when the two derive the same thing separately.
This commit is contained in:
2026-08-10 11:44:21 +02:00
parent f6f5235a8b
commit a3815c24a1
9 changed files with 169 additions and 16 deletions
+65
View File
@@ -0,0 +1,65 @@
// 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 };
export function readSpotDisplayOptions(): SpotDisplayOptions {
try {
return {
muteWorked: localStorage.getItem('opslog.clusterMuteWorked') === '1',
slotHighlight: localStorage.getItem('opslog.clusterSlotHighlight') === '1',
};
} catch {
return { muteWorked: false, slotHighlight: false };
}
}
type Entry = {
status?: string;
worked_call?: boolean;
worked_slot?: boolean;
new_county?: boolean;
new_pota?: boolean;
new_pfx?: boolean;
} | undefined;
// bringsNothingNew: the entity is resolved and worked, and no other dimension
// (county, park, prefix) is new. Same test the cluster list already used to dim
// a row — muting reuses it rather than inventing a second notion of "done".
//
// Note what is NOT muted: a status of new-band / new-slot survives, because
// having worked that callsign once on another band says nothing about the band
// in front of you.
function bringsNothingNew(s: Entry): boolean {
if (!s || !s.status) return false; // unresolved — never hide, it would flicker
if (s.status === 'new' || s.status === 'new-band' || s.status === 'new-mode' || s.status === 'new-slot') return false;
return !(s.new_pota || s.new_county || s.new_pfx);
}
// 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;
if (o.muteWorked && bringsNothingNew(s)) {
// Strip everything that paints: the row keeps its data, loses its emphasis.
return { ...s, status: '', worked_call: false } as T;
}
if (o.slotHighlight && s.worked_slot === false) {
// Not worked on this band+mode. If the entity check found nothing new, say
// so with the slot status rather than leaving the spot colourless — that is
// exactly the row this option exists to surface.
if (!s.status || s.status === 'worked') return { ...s, status: 'new-slot' } as T;
}
return s;
}