refactor(cluster): one rule for the filter panel
The panel had grown two shapes for the same kind of choice. Some filters were
checkboxes, some were chips, and New counties only was a checkbox duplicating a
chip - which I added, and which was the worst of it: a control that exists twice
is not more discoverable, it is one control the operator has to recognise twice.
The rule now:
SWITCH a behaviour that is on or off, and narrows nothing by a property of
the station: hide worked, group duplicates, the two display options,
LoTW users only.
CHIPS pick any number from a set; none picked means all. Status, mode,
spotter continent. Selected is solid, unselected is the same chip
faded, so the palette keeps teaching the colour code while switched
off.
Nothing appears in both shapes. The duplicate county checkbox is gone.
Spotter continent became a chip row: seven two-letter codes fit on two lines,
they read as a set the way Status and Mode do, and several can be picked at
once - which "EU or NA" needs and a dropdown cannot express.
Both Lock buttons now sit in the heading of the section they lock, instead of
floating between sections, and every multi-select section clears the same way
through the same heading slot. One section helper and one chip helper, so the
next filter cannot drift.
The panel was also entirely hardcoded English - Search call, Hide worked, Bands,
Status - against the project's own bilingual rule. All of it goes through t()
now, both locales.
This commit is contained in:
+117
-127
@@ -1495,7 +1495,7 @@ export default function App() {
|
||||
// of the station rather than by what the spot is worth, which is why they sit
|
||||
// beside Hide worked and not among the status chips.
|
||||
const [clusterLotwOnly, setClusterLotwOnly] = useState(() => localStorage.getItem('opslog.clusterLotwOnly') === '1');
|
||||
const [clusterSpotterCont, setClusterSpotterCont] = useState(() => localStorage.getItem('opslog.clusterSpotterCont') ?? '');
|
||||
const [clusterSpotterConts, setClusterSpotterConts] = useState<Set<string>>(() => lsSet<string>('opslog.clusterSpotterCont'));
|
||||
const [clusterMuteWorked, setClusterMuteWorked] = useState(() => localStorage.getItem('opslog.clusterMuteWorked') === '1');
|
||||
const [clusterSlotHighlight, setClusterSlotHighlight] = useState(() => localStorage.getItem('opslog.clusterSlotHighlight') === '1');
|
||||
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotFilterKey>>(() => lsSet<SpotFilterKey>('opslog.clusterStatusFilter'));
|
||||
@@ -1516,11 +1516,13 @@ export default function App() {
|
||||
writeUiPref('opslog.clusterLockBand', clusterLockBand ? '1' : '0');
|
||||
writeUiPref('opslog.clusterLockMode', clusterLockMode ? '1' : '0');
|
||||
writeUiPref('opslog.clusterStatusFilter', JSON.stringify([...clusterStatusFilter]));
|
||||
writeUiPref('opslog.clusterSpotterCont', JSON.stringify([...clusterSpotterConts]));
|
||||
writeUiPref('opslog.clusterModeFilter', JSON.stringify([...clusterModeFilter]));
|
||||
writeUiPref('opslog.clusterSearch', clusterSearch);
|
||||
writeUiPref('opslog.clusterHideWorked', clusterHideWorked ? '1' : '0');
|
||||
}, [clusterFilterSource, clusterGroup, clusterBands, clusterLockBand, clusterLockMode,
|
||||
clusterStatusFilter, clusterModeFilter, clusterSearch, clusterHideWorked]);
|
||||
clusterStatusFilter, clusterModeFilter, clusterSearch, clusterHideWorked,
|
||||
clusterLotwOnly, clusterSpotterConts]);
|
||||
// Bands shown side-by-side in the Band Map tab (portable).
|
||||
const [bandMapBands, setBandMapBands] = useState<string[]>(() => {
|
||||
try { const v = JSON.parse(localStorage.getItem('opslog.bandMapBands') || '[]'); return Array.isArray(v) ? v : []; }
|
||||
@@ -4717,14 +4719,14 @@ export default function App() {
|
||||
// LoTW only, and the spotter's continent. Both are properties of the
|
||||
// station rather than judgements about the spot, so they AND with the
|
||||
// status chips instead of joining that OR: "a new band, and from Europe".
|
||||
if (clusterLotwOnly || clusterSpotterCont) {
|
||||
if (clusterLotwOnly || clusterSpotterConts.size > 0) {
|
||||
const k = spotStatusKey(s.dx_call, s.band ?? '', s.comment ?? '', s.freq_hz);
|
||||
const e = spotStatus[k];
|
||||
// An unresolved spot is not filtered out. The status arrives a moment
|
||||
// after the row does, and dropping it meanwhile made the list flicker.
|
||||
if (e) {
|
||||
if (clusterLotwOnly && !e.lotw) return false;
|
||||
if (clusterSpotterCont && e.spotter_continent && e.spotter_continent !== clusterSpotterCont) return false;
|
||||
if (clusterSpotterConts.size > 0 && e.spotter_continent && !clusterSpotterConts.has(e.spotter_continent)) return false;
|
||||
}
|
||||
}
|
||||
if (clusterHideWorked) {
|
||||
@@ -4753,11 +4755,46 @@ export default function App() {
|
||||
// The Log4OM-style cluster filter sidebar (callsign search, hide-worked,
|
||||
// group, band/mode/status/source). Rendered both in the Cluster tab and the
|
||||
// Main-view cluster pane; toggled by clusterShowFilters.
|
||||
// One rule for the whole panel, because two shapes for the same kind of choice
|
||||
// is what made it unreadable:
|
||||
//
|
||||
// SWITCH — a behaviour that is on or off (hide worked, group duplicates).
|
||||
// CHIPS — pick any number from a set; none picked means all (status, mode,
|
||||
// continent). Selected is solid, unselected is the same chip faded,
|
||||
// so the palette teaches the colour code even while switched off.
|
||||
//
|
||||
// Nothing appears in both shapes. A control that exists twice is not more
|
||||
// discoverable, it is one control the operator has to recognise twice.
|
||||
const fSection = (label: string, children: any, right?: any) => (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1 min-h-[16px]">
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">{label}</span>
|
||||
{right}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
const fSwitch = (label: string, on: boolean, set: (v: boolean) => void) => (
|
||||
<label className="flex items-center gap-2 cursor-pointer rounded px-1 py-1 -mx-1 hover:bg-accent/40">
|
||||
<Checkbox checked={on} onCheckedChange={(c) => set(!!c)} />
|
||||
<span className="leading-none">{label}</span>
|
||||
</label>
|
||||
);
|
||||
|
||||
const F_CHIP = 'px-1.5 py-[3px] rounded-md border text-[10px] font-bold tracking-wider transition-opacity';
|
||||
const fChip = (key: string, label: string, cls: string, on: boolean, toggle: () => void) => (
|
||||
<button key={key} type="button" onClick={toggle}
|
||||
className={cn(F_CHIP, on ? cls : `${cls} opacity-40 hover:opacity-80`)}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
||||
const renderClusterFilters = () => (
|
||||
<div className="w-56 shrink-0 border-l border-border/60 flex flex-col min-h-0 bg-muted/10">
|
||||
<div className="px-2.5 py-2 border-b border-border/60 flex items-center justify-between">
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">Filters</span>
|
||||
<button type="button" onClick={toggleClusterFilters} title="Hide filters"
|
||||
<span className="text-[11px] font-semibold uppercase tracking-wider text-muted-foreground">{t('clu.filters')}</span>
|
||||
<button type="button" onClick={toggleClusterFilters} title={t('clu.hideFilters')}
|
||||
className="text-muted-foreground hover:text-foreground">
|
||||
<X className="size-3.5" />
|
||||
</button>
|
||||
@@ -4766,89 +4803,48 @@ export default function App() {
|
||||
{/* Callsign search */}
|
||||
<Input
|
||||
className="h-7 text-xs font-mono uppercase"
|
||||
placeholder="Search call…"
|
||||
placeholder={t('clu.searchCall')}
|
||||
value={clusterSearch}
|
||||
onChange={(e) => setClusterSearch(e.target.value.toUpperCase())}
|
||||
/>
|
||||
|
||||
{/* Toggles */}
|
||||
<div className="space-y-1.5">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={clusterHideWorked} onCheckedChange={(c) => setClusterHideWorked(!!c)} />
|
||||
Hide worked
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={clusterGroup} onCheckedChange={(c) => setClusterGroup(!!c)} />
|
||||
Group duplicates
|
||||
</label>
|
||||
{/* Two ways to cut through a busy cluster, and they compose: mute what
|
||||
is done, light up what is not. They sit HERE, with the other things
|
||||
an operator changes mid-run, and not in the preferences dialog.
|
||||
Both drive the band map as well, through lib/spotDisplay. */}
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={clusterMuteWorked}
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterMuteWorked(v); writeUiPref('opslog.clusterMuteWorked', v ? '1' : '0'); }} />
|
||||
{t('clu.muteWorkedShort')}
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={clusterSlotHighlight}
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterSlotHighlight(v); writeUiPref('opslog.clusterSlotHighlight', v ? '1' : '0'); }} />
|
||||
{t('clu.slotHighlightShort')}
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={clusterLotwOnly}
|
||||
onCheckedChange={(c) => { const v = !!c; setClusterLotwOnly(v); writeUiPref('opslog.clusterLotwOnly', v ? '1' : '0'); }} />
|
||||
{t('clu.lotwOnly')}
|
||||
</label>
|
||||
{/* The same state as the NEW COUNTY status chip, reached a second way.
|
||||
Not a second filter: county chasing is a mode you switch into, and
|
||||
hunting for one chip among eight is not how you switch into it. */}
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<Checkbox checked={clusterStatusFilter.has('new-county')}
|
||||
onCheckedChange={(c) => setClusterStatusFilter((cur) => {
|
||||
const n = new Set(cur);
|
||||
if (c) n.add('new-county'); else n.delete('new-county');
|
||||
return n;
|
||||
})} />
|
||||
{t('clu.newCountyOnly')}
|
||||
</label>
|
||||
{/* Behaviours: each is on or off, and none of them narrows by a property
|
||||
of the station. The last two also drive the band map, via
|
||||
lib/spotDisplay — they live here rather than in Preferences because
|
||||
they are changed while working a run. */}
|
||||
<div className="space-y-0.5">
|
||||
{fSwitch(t('clu.hideWorked'), clusterHideWorked, setClusterHideWorked)}
|
||||
{fSwitch(t('clu.groupDup'), clusterGroup, setClusterGroup)}
|
||||
{fSwitch(t('clu.muteWorkedShort'), clusterMuteWorked, (v) => { setClusterMuteWorked(v); writeUiPref('opslog.clusterMuteWorked', v ? '1' : '0'); })}
|
||||
{fSwitch(t('clu.slotHighlightShort'), clusterSlotHighlight, (v) => { setClusterSlotHighlight(v); writeUiPref('opslog.clusterSlotHighlight', v ? '1' : '0'); })}
|
||||
{fSwitch(t('clu.lotwOnly'), clusterLotwOnly, (v) => { setClusterLotwOnly(v); writeUiPref('opslog.clusterLotwOnly', v ? '1' : '0'); })}
|
||||
</div>
|
||||
|
||||
{/* Spotter's continent. Not the DX's: this asks whether anyone near you
|
||||
is hearing the band at all, which is a different question. */}
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">{t('clu.spotterCont')}</div>
|
||||
<select
|
||||
className="w-full h-7 rounded border border-border bg-background px-1 text-xs"
|
||||
value={clusterSpotterCont}
|
||||
onChange={(e) => { const v = e.target.value; setClusterSpotterCont(v); writeUiPref('opslog.clusterSpotterCont', v); }}
|
||||
>
|
||||
<option value="">{t('clu.allContinents')}</option>
|
||||
{['AF', 'AN', 'AS', 'EU', 'NA', 'OC', 'SA'].map((c) => (
|
||||
<option key={c} value={c}>{c}</option>
|
||||
{/* The SPOTTER's continent, not the DX's: this asks whether anyone near
|
||||
you is hearing the band at all. Chips rather than a dropdown — seven
|
||||
two-letter codes fit on two lines, they read as a set the way Status
|
||||
and Mode do, and several can be picked at once, which "EU or NA" needs
|
||||
and a dropdown cannot express. */}
|
||||
{fSection(t('clu.spotterCont'),
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{['AF', 'AN', 'AS', 'EU', 'NA', 'OC', 'SA'].map((c) => fChip(
|
||||
c, c, 'bg-muted text-foreground border-border',
|
||||
clusterSpotterConts.has(c),
|
||||
() => setClusterSpotterConts((cur) => {
|
||||
const n = new Set(cur);
|
||||
if (n.has(c)) n.delete(c); else n.add(c);
|
||||
return n;
|
||||
}),
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>,
|
||||
clusterSpotterConts.size > 0 ? (
|
||||
<button type="button" onClick={() => setClusterSpotterConts(new Set())}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground underline">{t('clu.clear')}</button>
|
||||
) : undefined,
|
||||
)}
|
||||
|
||||
{/* Band filter — multi-select listbox */}
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">Bands</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClusterLockBand((v) => !v)}
|
||||
className={cn('inline-flex items-center gap-0.5 text-[10px] px-1 py-0.5 rounded border',
|
||||
clusterLockBand ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
title="Lock to the entry strip's current band"
|
||||
>
|
||||
{clusterLockBand ? <Lock className="size-2.5" /> : <Unlock className="size-2.5" />} {band}
|
||||
</button>
|
||||
{clusterBands.size > 0 && (
|
||||
<button type="button" onClick={() => setClusterBands(new Set())} className="text-[10px] text-muted-foreground hover:text-foreground underline">clear</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{fSection(t('clu.bands'),
|
||||
<div className={cn('rounded border border-border max-h-36 overflow-auto bg-background', clusterLockBand && 'opacity-40 pointer-events-none')}>
|
||||
{bands.map((b) => {
|
||||
const on = clusterBands.has(b);
|
||||
@@ -4864,23 +4860,25 @@ export default function App() {
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mode lock */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClusterLockMode((v) => !v)}
|
||||
className={cn('inline-flex items-center gap-1 px-1.5 py-0.5 rounded border text-[10px]',
|
||||
clusterLockMode ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
title="Only show spots whose mode matches the entry strip"
|
||||
>
|
||||
{clusterLockMode ? <Lock className="size-2.5" /> : <Unlock className="size-2.5" />} Lock mode ({mode})
|
||||
</button>
|
||||
</div>,
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setClusterLockBand((v) => !v)}
|
||||
className={cn('inline-flex items-center gap-0.5 text-[10px] px-1 py-0.5 rounded border',
|
||||
clusterLockBand ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
title={t('clu.lockBandTitle')}
|
||||
>
|
||||
{clusterLockBand ? <Lock className="size-2.5" /> : <Unlock className="size-2.5" />} {band}
|
||||
</button>
|
||||
{clusterBands.size > 0 && (
|
||||
<button type="button" onClick={() => setClusterBands(new Set())} className="text-[10px] text-muted-foreground hover:text-foreground underline">{t('clu.clear')}</button>
|
||||
)}
|
||||
</div>,
|
||||
)}
|
||||
|
||||
{/* Status filter */}
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Status</div>
|
||||
{fSection(t('clu.status'),
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{([
|
||||
{ k: 'new' as SpotFilterKey, label: 'NEW', cls: 'bg-danger-muted text-danger-muted-foreground border-danger-border' },
|
||||
@@ -4899,51 +4897,43 @@ export default function App() {
|
||||
// worked spots; the separate "Hide worked" checkbox drops them — they
|
||||
// are opposite controls, so don't use both at once.
|
||||
{ k: 'worked' as SpotFilterKey, label: 'WORKED', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||
]).map((s) => {
|
||||
const on = clusterStatusFilter.has(s.k);
|
||||
return (
|
||||
<button key={s.k} type="button"
|
||||
onClick={() => setClusterStatusFilter((cur) => { const n = new Set(cur); if (n.has(s.k)) n.delete(s.k); else n.add(s.k); return n; })}
|
||||
className={cn('px-1.5 py-0.5 rounded border text-[10px] font-bold tracking-wider transition-opacity', on ? s.cls : `${s.cls} opacity-40 hover:opacity-100`)}>
|
||||
{s.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
]).map((s) => fChip(s.k, s.label, s.cls, clusterStatusFilter.has(s.k),
|
||||
() => setClusterStatusFilter((cur) => { const n = new Set(cur); if (n.has(s.k)) n.delete(s.k); else n.add(s.k); return n; })))}
|
||||
</div>,
|
||||
clusterStatusFilter.size > 0 ? (
|
||||
<button type="button" onClick={() => setClusterStatusFilter(new Set())}
|
||||
className="text-[10px] text-muted-foreground hover:text-foreground underline">{t('clu.clear')}</button>
|
||||
) : undefined,
|
||||
)}
|
||||
|
||||
{/* Mode filter */}
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Mode</div>
|
||||
{fSection(t('clu.mode'),
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{([
|
||||
{ k: 'SSB' as SpotModeCat, label: 'SSB', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||
{ k: 'CW' as SpotModeCat, label: 'CW', cls: 'bg-info-muted text-info-muted-foreground border-info-border' },
|
||||
{ k: 'DATA' as SpotModeCat, label: 'DATA', cls: 'bg-success-muted text-success-muted-foreground border-success-border' },
|
||||
]).map((s) => {
|
||||
const on = clusterModeFilter.has(s.k);
|
||||
return (
|
||||
<button key={s.k} type="button"
|
||||
onClick={() => setClusterModeFilter((cur) => { const n = new Set(cur); if (n.has(s.k)) n.delete(s.k); else n.add(s.k); return n; })}
|
||||
className={cn('px-1.5 py-0.5 rounded border text-[10px] font-bold tracking-wider transition-opacity', on ? s.cls : `${s.cls} opacity-40 hover:opacity-100`)}>
|
||||
{s.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
]).map((s) => fChip(s.k, s.label, s.cls, clusterModeFilter.has(s.k),
|
||||
() => setClusterModeFilter((cur) => { const n = new Set(cur); if (n.has(s.k)) n.delete(s.k); else n.add(s.k); return n; })))}
|
||||
</div>,
|
||||
<button type="button" onClick={() => setClusterLockMode((v) => !v)}
|
||||
className={cn('inline-flex items-center gap-0.5 text-[10px] px-1 py-0.5 rounded border',
|
||||
clusterLockMode ? 'bg-warning-muted text-warning-muted-foreground border-warning-border' : 'text-muted-foreground border-border hover:bg-muted')}
|
||||
title={t('clu.lockModeTitle')}>
|
||||
{clusterLockMode ? <Lock className="size-2.5" /> : <Unlock className="size-2.5" />} {mode}
|
||||
</button>,
|
||||
)}
|
||||
|
||||
{/* Source */}
|
||||
<div>
|
||||
<div className="text-[10px] uppercase tracking-wider text-muted-foreground mb-1">Source</div>
|
||||
{fSection(t('clu.source'),
|
||||
<Select value={String(clusterFilterSource || '_')} onValueChange={(v) => setClusterFilterSource(v === '_' ? '' : parseInt(v, 10))}>
|
||||
<SelectTrigger className="w-full h-7 text-xs"><SelectValue placeholder="All sources" /></SelectTrigger>
|
||||
<SelectTrigger className="w-full h-7 text-xs"><SelectValue placeholder={t('clu.allSources')} /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="_">All sources</SelectItem>
|
||||
<SelectItem value="_">{t('clu.allSources')}</SelectItem>
|
||||
{clusterServers.map((s) => <SelectItem key={s.id} value={String(s.id)}>{s.name}</SelectItem>)}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Select>,
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -266,7 +266,7 @@ const en: Dict = {
|
||||
'clu.selfSpot': 'Self-spot while I log', 'clu.selfSpotEvery': 'at most every', 'clu.selfSpotMinutes': 'min',
|
||||
'clu.selfSpotHint': 'Announces YOU on the master cluster when you log a QSO — the spot carries your station callsign and the frequency you just worked on, so callers find you without waiting for someone else to spot you. Sent on the first QSO of a frequency, then no more often than the gap below. Five minutes is the floor: a self-spot is traffic every user of the node sees.',
|
||||
'clu.freeNodes': 'Free public nodes:',
|
||||
'clu.lotwOnly': 'LoTW users only', 'clu.newCountyOnly': 'New counties only', 'clu.spotterCont': 'Spotter continent', 'clu.allContinents': 'All continents', 'clu.lotwBadge': 'Uploads to LoTW', 'clu.muteWorkedShort': 'No colour on worked', 'clu.slotHighlightShort': 'Colour unworked here', 'clu.muteWorked': 'No colour or badge on stations already worked',
|
||||
'clu.filters': 'Filters', 'clu.hideFilters': 'Hide filters', 'clu.searchCall': 'Search call…', 'clu.hideWorked': 'Hide worked', 'clu.groupDup': 'Group duplicates', 'clu.mode': 'Mode', 'clu.bands': 'Bands', 'clu.source': 'Source', 'clu.clear': 'clear', 'clu.allSources': 'All sources', 'clu.lockModeTitle': 'Only show spots whose mode matches the entry strip', 'clu.lockBandTitle': "Lock to the entry strip's current band", 'clu.lotwOnly': 'LoTW users only', 'clu.spotterCont': 'Spotter continent', 'clu.lotwBadge': 'Uploads to LoTW', 'clu.muteWorkedShort': 'No colour on worked', 'clu.slotHighlightShort': 'Colour unworked here', 'clu.muteWorked': 'No colour or badge on stations already worked',
|
||||
'clu.muteWorkedHint': '(they stay in the list, just quiet — leaves the colour for what is left to do)',
|
||||
'clu.slotHighlight': 'Colour the stations not worked on this band and mode',
|
||||
'clu.slotHighlightHint': '(by callsign, whatever the entity status says)',
|
||||
@@ -686,7 +686,7 @@ const fr: Dict = {
|
||||
'clu.selfSpot': "M'auto-spotter quand j'enregistre", 'clu.selfSpotEvery': 'au plus toutes les', 'clu.selfSpotMinutes': 'min',
|
||||
'clu.selfSpotHint': "Annonce TON indicatif sur le cluster maître quand tu enregistres un QSO — le spot porte l'indicatif de station et la fréquence que tu viens de travailler, pour qu'on te trouve sans attendre que quelqu'un te spotte. Envoyé au premier QSO d'une fréquence, puis pas plus souvent que l'intervalle ci-dessous. Cinq minutes est le plancher : un auto-spot est du trafic que voient tous les utilisateurs du nœud.",
|
||||
'clu.freeNodes': 'Nœuds publics gratuits :',
|
||||
'clu.lotwOnly': 'Utilisateurs LoTW seulement', 'clu.newCountyOnly': 'Nouveaux comtés seulement', 'clu.spotterCont': 'Continent du spotter', 'clu.allContinents': 'Tous les continents', 'clu.lotwBadge': 'Utilise LoTW', 'clu.muteWorkedShort': 'Pas de couleur sur les faits', 'clu.slotHighlightShort': 'Colorer les non faits ici', 'clu.muteWorked': 'Aucune couleur ni badge sur les stations déjà contactées',
|
||||
'clu.filters': 'Filtres', 'clu.hideFilters': 'Masquer les filtres', 'clu.searchCall': 'Chercher un indicatif…', 'clu.hideWorked': 'Masquer les contactés', 'clu.groupDup': 'Grouper les doublons', 'clu.mode': 'Mode', 'clu.bands': 'Bandes', 'clu.source': 'Source', 'clu.clear': 'effacer', 'clu.allSources': 'Toutes les sources', 'clu.lockModeTitle': 'N afficher que les spots du mode de la barre de saisie', 'clu.lockBandTitle': 'Verrouiller sur la bande de la barre de saisie', 'clu.lotwOnly': 'Utilisateurs LoTW seulement', 'clu.spotterCont': 'Continent du spotter', 'clu.lotwBadge': 'Utilise LoTW', 'clu.muteWorkedShort': 'Pas de couleur sur les faits', 'clu.slotHighlightShort': 'Colorer les non faits ici', 'clu.muteWorked': 'Aucune couleur ni badge sur les stations déjà contactées',
|
||||
'clu.muteWorkedHint': '(elles restent dans la liste, simplement discrètes — la couleur reste pour ce qui est à faire)',
|
||||
'clu.slotHighlight': 'Colorer les stations non contactées sur cette bande et ce mode',
|
||||
'clu.slotHighlightHint': "(par indicatif, quel que soit le statut de l'entité)",
|
||||
|
||||
Reference in New Issue
Block a user