// 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 }; // 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 { if (!SPOT_DISPLAY_OPTIONS_EXPOSED) return { muteWorked: false, slotHighlight: false }; 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; 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(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; } // 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; } 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'; }