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:
@@ -4,6 +4,7 @@ import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { spotStatusKey, inferSpotMode, spotModeCategory } from '@/lib/spot';
|
||||
import { SPOT_MARKERS, activeMarkers } from '@/lib/spotMarkers';
|
||||
import { applySpotDisplay, readSpotDisplayOptions } from '@/lib/spotDisplay';
|
||||
|
||||
// BandMap — vertical spectrum panel inspired by Log4OM.
|
||||
// - Full band is always visible; zoom changes pixels-per-kHz, scroll
|
||||
@@ -32,6 +33,8 @@ type SpotStatusEntry = {
|
||||
status?: string;
|
||||
country?: string;
|
||||
worked_call?: boolean;
|
||||
// worked_slot: this exact callsign already worked on THIS band and mode.
|
||||
worked_slot?: boolean;
|
||||
new_county?: boolean;
|
||||
new_pota?: boolean;
|
||||
new_pfx?: boolean;
|
||||
@@ -213,8 +216,21 @@ const BOT_PAD = 14; // the top-most freq label isn't clipped at y=0
|
||||
// last; ties broken by closeness to the rig freq).
|
||||
const MAX_VISIBLE_SPOTS = 30;
|
||||
|
||||
export function BandMap({ band, spots, spotStatus, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false, keyNav = false }: Props) {
|
||||
export function BandMap({ band, spots, spotStatus: spotStatusRaw, currentFreqHz, onSpotClick, onClose, side = 'right', onToggleSide, hideDigital = false, fitToBand = false, keyNav = false }: Props) {
|
||||
const { t } = useI18n();
|
||||
|
||||
// The two display options are applied ONCE here, on the whole map, so the
|
||||
// leader lines, the dots, the pills and the badges can never disagree — and so
|
||||
// the map and the cluster list share the single rule set in lib/spotDisplay.
|
||||
// Re-derived whenever the poll delivers a new status map, which is also when a
|
||||
// just-changed option takes effect.
|
||||
const spotStatus = useMemo(() => {
|
||||
const o = readSpotDisplayOptions();
|
||||
if (!o.muteWorked && !o.slotHighlight) return spotStatusRaw;
|
||||
const out: Record<string, SpotStatusEntry> = {};
|
||||
for (const k of Object.keys(spotStatusRaw)) out[k] = applySpotDisplay(spotStatusRaw[k], o) as SpotStatusEntry;
|
||||
return out;
|
||||
}, [spotStatusRaw]);
|
||||
const range = BAND_RANGES[band];
|
||||
const segments = SEGMENT_COLORS[band] ?? [];
|
||||
const [zoomIdx, setZoomIdx] = useState(2); // default 32 px/kHz
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Button } from '@/components/ui/button';
|
||||
import { Checkbox } from '@/components/ui/checkbox';
|
||||
import { cleanSpotter, inferSpotMode, spotStatusKey } from '@/lib/spot';
|
||||
import { markerColour } from '@/lib/spotMarkers';
|
||||
import { applySpotDisplay, readSpotDisplayOptions } from '@/lib/spotDisplay';
|
||||
import { loadLocal, loadRemote, saveState, seedLocal, whenGridPrefsReady } from '@/lib/gridPrefs';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
@@ -52,6 +53,9 @@ export type SpotStatusEntry = {
|
||||
country?: string;
|
||||
continent?: string;
|
||||
worked_call?: boolean;
|
||||
// worked_slot: this exact callsign already worked on THIS band and mode.
|
||||
// Always slot-scoped, unlike worked_call which follows the "same slot" option.
|
||||
worked_slot?: boolean;
|
||||
new_county?: boolean;
|
||||
new_pota?: boolean;
|
||||
new_pfx?: boolean;
|
||||
@@ -97,9 +101,13 @@ type ColEntry = ColDef<ClusterSpot> & { group: string; label: string; defaultVis
|
||||
// statusFor resolves the precomputed spot status (new / new-band / new-slot /
|
||||
// worked-call) for an ag-Grid cell's row.
|
||||
function statusFor(p: any): SpotStatusEntry | undefined {
|
||||
return p?.context?.spotStatus?.[
|
||||
const s = p?.context?.spotStatus?.[
|
||||
spotStatusKey(p.data?.dx_call, p.data?.band ?? '', p.data?.comment ?? '', p.data?.freq_hz)
|
||||
];
|
||||
// One chokepoint: the colour, the badges and the Status text all read through
|
||||
// here, so the two display options are applied once rather than in each
|
||||
// renderer — and the band map applies the very same function.
|
||||
return applySpotDisplay(s, readSpotDisplayOptions());
|
||||
}
|
||||
|
||||
// Spot status is shown by COLOURING THE TEXT, never by a pill or a badge.
|
||||
|
||||
@@ -1347,6 +1347,8 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
const [lookupOnBlur, setLookupOnBlur] = useState(() => localStorage.getItem('opslog.lookupOnBlur') === '1');
|
||||
const [groupDigital, setGroupDigital] = useState(() => localStorage.getItem('opslog.groupDigitalSlots') === '1');
|
||||
const [clusterWorkedSameSlot, setClusterWorkedSameSlot] = useState(() => localStorage.getItem('opslog.clusterWorkedSameSlot') === '1');
|
||||
const [clusterMuteWorked, setClusterMuteWorked] = useState(() => localStorage.getItem('opslog.clusterMuteWorked') === '1');
|
||||
const [clusterSlotHighlight, setClusterSlotHighlight] = useState(() => localStorage.getItem('opslog.clusterSlotHighlight') === '1');
|
||||
const [showQsoRate, setShowQsoRate] = useState(() => localStorage.getItem('opslog.showQsoRate') === '1');
|
||||
const [catModeBeforeFreq, setCatModeBeforeFreq] = useState(() => localStorage.getItem('opslog.catModeBeforeFreq') === '1');
|
||||
// Password-encryption (secret vault) state.
|
||||
@@ -4137,6 +4139,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterWorkedSameSlot(v); writeUiPref('opslog.clusterWorkedSameSlot', v ? '1' : '0'); }} />
|
||||
<span>{t('clu.workedSameSlot')} <span className="text-xs text-muted-foreground">{t('clu.workedSameSlotHint')}</span></span>
|
||||
</label>
|
||||
{/* Two ways to cut through a busy cluster, and they compose: mute what
|
||||
is done, light up what is not. */}
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={clusterMuteWorked} className="mt-0.5"
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterMuteWorked(v); writeUiPref('opslog.clusterMuteWorked', v ? '1' : '0'); }} />
|
||||
<span>{t('clu.muteWorked')} <span className="text-xs text-muted-foreground">{t('clu.muteWorkedHint')}</span></span>
|
||||
</label>
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={clusterSlotHighlight} className="mt-0.5"
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterSlotHighlight(v); writeUiPref('opslog.clusterSlotHighlight', v ? '1' : '0'); }} />
|
||||
<span>{t('clu.slotHighlight')} <span className="text-xs text-muted-foreground">{t('clu.slotHighlightHint')}</span></span>
|
||||
</label>
|
||||
|
||||
{/* Self-spot. The interval only shows once it's on — an interval for
|
||||
something switched off is just a question the operator can't act on. */}
|
||||
|
||||
Reference in New Issue
Block a user