feat(dxhunter): the chase gets a page of its own, and US states a switch
The chase switches were born on the DX Cluster page because the cluster was the only thing that drew their badges. They now decide what the FT decode list and Chase new say as well, and settings that govern three screens should not live under the name of one of them. They move to a DXHunter page — named for what it is about, and for what is meant to land beside it as more of DXHunter's ideas are ported over. US states join the switchable markers, gated the same way as POTA, counties, prefixes and squares: badge, filter chip and display marker, all from the one chaseAllows question.
This commit is contained in:
@@ -213,6 +213,7 @@ type SectionId =
|
||||
| 'lists-modes'
|
||||
| 'lists-satellites'
|
||||
| 'cluster'
|
||||
| 'dxhunter'
|
||||
| 'backup'
|
||||
| 'database'
|
||||
| 'autostart'
|
||||
@@ -325,6 +326,7 @@ function buildTree(flexAvailable: boolean, t: (k: string) => string): TreeNode[]
|
||||
{ kind: 'item', label: t('sec.satellites'), id: 'lists-satellites' },
|
||||
]},
|
||||
{ kind: 'item', label: t('sec.cluster'), id: 'cluster' },
|
||||
{ kind: 'item', label: t('sec.dxhunter'), id: 'dxhunter' },
|
||||
{ kind: 'item', label: t('sec.udp'), id: 'udp' },
|
||||
{ kind: 'item', label: t('sec.adifmon'), id: 'adifmon' },
|
||||
{ kind: 'item', label: t('sec.foldersync'), id: 'foldersync' },
|
||||
@@ -2081,6 +2083,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
const [chasePotaOn, setChasePotaOn] = useState(() => localStorage.getItem('opslog.chasePota') !== '0');
|
||||
const [chaseCountyOn, setChaseCountyOn] = useState(() => localStorage.getItem('opslog.chaseCounty') !== '0');
|
||||
const [chasePfxOn, setChasePfxOn] = useState(() => localStorage.getItem('opslog.chasePfx') !== '0');
|
||||
const [chaseStateOn, setChaseStateOn] = useState(() => localStorage.getItem('opslog.chaseState') !== '0');
|
||||
const [chaseSotaOn, setChaseSotaOn] = useState(() => localStorage.getItem('opslog.chaseSota') !== '0');
|
||||
const [chaseNew, setChaseNew] = useState(false);
|
||||
const [spotTTL, setSpotTTL] = useState(0);
|
||||
@@ -5236,6 +5239,131 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
setEditingServer(next);
|
||||
}
|
||||
|
||||
// Chasing is not a cluster feature.
|
||||
//
|
||||
// These switches were born on the DX Cluster page because the cluster was the
|
||||
// only thing that drew their badges. They now decide what the FT decode list
|
||||
// and Chase new say as well, and settings that govern three screens do not
|
||||
// belong under the name of one of them. The page is called DXHunter because
|
||||
// that is what it is about — hunting DX — and because more of DXHunter's
|
||||
// ideas are meant to land beside them.
|
||||
function DXHunterPanel() {
|
||||
return (
|
||||
<>
|
||||
<h3 className="text-sm font-semibold">{t('sec.dxhunter')}</h3>
|
||||
<p className="text-xs text-muted-foreground -mt-1">{t('dxh.intro')}</p>
|
||||
<div className="space-y-3">
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
{/* Chase switches: off, the marker stops being TOLD, everywhere at
|
||||
once — badge, colour, filter chip, reference column. A "new band
|
||||
+ new POTA" spot then reads NEW BAND alone. */}
|
||||
{/* The GLOBAL hunt: every category (DXCC, band, mode, slot, prefix,
|
||||
county, state, grid) judged as new-only or new-plus-unconfirmed,
|
||||
against the confirmation sources the operator trusts. */}
|
||||
<div className="rounded-lg border border-border p-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground w-28 shrink-0">{t('chg.mode')}</span>
|
||||
<Select value={chaseCfg.mode} onValueChange={(v) => { const next = { ...chaseCfg, mode: v }; setChaseCfg(next); void SaveChaseSettings(next as any); }}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new">{t('gsc.huntNew')}</SelectItem>
|
||||
<SelectItem value="new_unconfirmed">{t('gsc.huntUnconf')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{chaseCfg.mode === 'new_unconfirmed' && (
|
||||
<div className="flex items-center gap-4 flex-wrap pl-2">
|
||||
<span className="text-xs text-muted-foreground">{t('chg.sources')}</span>
|
||||
{([['lotw', 'LoTW'], ['card', t('chg.card')], ['eqsl', 'eQSL'], ['qrz', 'QRZ.com']] as const).map(([k, label]) => (
|
||||
<label key={k} className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||
<Checkbox checked={chaseCfg.sources.includes(k)}
|
||||
onCheckedChange={(c) => {
|
||||
const sources = c ? [...chaseCfg.sources, k] : chaseCfg.sources.filter((x) => x !== k);
|
||||
const next = { ...chaseCfg, sources };
|
||||
setChaseCfg(next); void SaveChaseSettings(next as any);
|
||||
}} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chasePotaHint')}>
|
||||
<Checkbox checked={chasePotaOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChasePotaOn(v); writeUiPref('opslog.chasePota', v ? '1' : '0'); }} />
|
||||
{t('clu.chasePota')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chaseSotaHint')}>
|
||||
<Checkbox checked={chaseSotaOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChaseSotaOn(v); writeUiPref('opslog.chaseSota', v ? '1' : '0'); }} />
|
||||
{t('clu.chaseSota')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chaseCountyHint')}>
|
||||
<Checkbox checked={chaseCountyOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChaseCountyOn(v); writeUiPref('opslog.chaseCounty', v ? '1' : '0'); }} />
|
||||
{t('clu.chaseCounty')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chasePfxHint')}>
|
||||
<Checkbox checked={chasePfxOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChasePfxOn(v); writeUiPref('opslog.chasePfx', v ? '1' : '0'); }} />
|
||||
{t('clu.chasePfx')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chaseStateHint')}>
|
||||
<Checkbox checked={chaseStateOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChaseStateOn(v); writeUiPref('opslog.chaseState', v ? '1' : '0'); }} />
|
||||
{t('clu.chaseState')}
|
||||
</label>
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={chaseGrids} className="mt-0.5"
|
||||
onCheckedChange={(c) => { setChaseGrids(!!c); writeUiPref('opslog.chaseGrids', c ? '1' : '0'); SetChaseNewGrids(!!c).catch(() => {}); }} />
|
||||
<span>{t('clu.chaseGrids')} <span className="text-xs text-muted-foreground">{t('clu.chaseGridsHint')}</span></span>
|
||||
</label>
|
||||
{chaseGrids && (
|
||||
<p className="pl-6 text-xs text-muted-foreground">
|
||||
{t('clu.chaseGridsStat', { n: gridStat?.known ?? 0, p: gridStat?.pending ?? 0 })}
|
||||
{pskrStatus?.running ? ` · ${t('bo.feedUp', { n: pskrStatus.received ?? 0 })}` : ''}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* What counts as "I already have this square". There is no single
|
||||
right answer — a VUCC chaser counts a square per band, someone
|
||||
filling a wall map counts it once — so it is a choice, and the
|
||||
same six GridTracker offers, because a square wanted in one and
|
||||
not the other is a bug report every time. */}
|
||||
<div className="pl-6 space-y-1.5 pt-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground w-28 shrink-0">{t('gsc.scope')}</span>
|
||||
<Select value={gridScope.scope} onValueChange={(v) => saveGridScope({ ...gridScope, scope: v })}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(gridScope.scopes ?? []).map((s: any) => (
|
||||
<SelectItem key={s.key} value={s.key}>{t(`gsc.scope_${s.key}`)}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Chase new — its own option, NOT nested under grid chasing. Chasing
|
||||
squares and chasing entities are different wants; they only share
|
||||
the PSK Reporter feed, which either one brings up. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={chaseNew} className="mt-0.5"
|
||||
onCheckedChange={(c) => { setChaseNew(!!c); SetChaseNew(!!c).catch(() => {}); }} />
|
||||
<span>{t('chn.option')} <span className="text-xs text-muted-foreground">{t('chn.optionHelp')}</span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function ClusterPanel() {
|
||||
const sorted = [...clusterServers].sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0));
|
||||
// Written on every keystroke. This panel has no Save button, and a pair of
|
||||
@@ -5438,107 +5566,6 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
{/* Chase switches: off, the marker stops being TOLD, everywhere at
|
||||
once — badge, colour, filter chip, reference column. A "new band
|
||||
+ new POTA" spot then reads NEW BAND alone. */}
|
||||
{/* The GLOBAL hunt: every category (DXCC, band, mode, slot, prefix,
|
||||
county, state, grid) judged as new-only or new-plus-unconfirmed,
|
||||
against the confirmation sources the operator trusts. */}
|
||||
<div className="rounded-lg border border-border p-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground w-28 shrink-0">{t('chg.mode')}</span>
|
||||
<Select value={chaseCfg.mode} onValueChange={(v) => { const next = { ...chaseCfg, mode: v }; setChaseCfg(next); void SaveChaseSettings(next as any); }}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="new">{t('gsc.huntNew')}</SelectItem>
|
||||
<SelectItem value="new_unconfirmed">{t('gsc.huntUnconf')}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{chaseCfg.mode === 'new_unconfirmed' && (
|
||||
<div className="flex items-center gap-4 flex-wrap pl-2">
|
||||
<span className="text-xs text-muted-foreground">{t('chg.sources')}</span>
|
||||
{([['lotw', 'LoTW'], ['card', t('chg.card')], ['eqsl', 'eQSL'], ['qrz', 'QRZ.com']] as const).map(([k, label]) => (
|
||||
<label key={k} className="flex items-center gap-1.5 text-xs cursor-pointer">
|
||||
<Checkbox checked={chaseCfg.sources.includes(k)}
|
||||
onCheckedChange={(c) => {
|
||||
const sources = c ? [...chaseCfg.sources, k] : chaseCfg.sources.filter((x) => x !== k);
|
||||
const next = { ...chaseCfg, sources };
|
||||
setChaseCfg(next); void SaveChaseSettings(next as any);
|
||||
}} />
|
||||
{label}
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chasePotaHint')}>
|
||||
<Checkbox checked={chasePotaOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChasePotaOn(v); writeUiPref('opslog.chasePota', v ? '1' : '0'); }} />
|
||||
{t('clu.chasePota')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chaseSotaHint')}>
|
||||
<Checkbox checked={chaseSotaOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChaseSotaOn(v); writeUiPref('opslog.chaseSota', v ? '1' : '0'); }} />
|
||||
{t('clu.chaseSota')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chaseCountyHint')}>
|
||||
<Checkbox checked={chaseCountyOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChaseCountyOn(v); writeUiPref('opslog.chaseCounty', v ? '1' : '0'); }} />
|
||||
{t('clu.chaseCounty')}
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-sm cursor-pointer" title={t('clu.chasePfxHint')}>
|
||||
<Checkbox checked={chasePfxOn}
|
||||
onCheckedChange={(c) => { const v = !!c; setChasePfxOn(v); writeUiPref('opslog.chasePfx', v ? '1' : '0'); }} />
|
||||
{t('clu.chasePfx')}
|
||||
</label>
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={chaseGrids} className="mt-0.5"
|
||||
onCheckedChange={(c) => { setChaseGrids(!!c); writeUiPref('opslog.chaseGrids', c ? '1' : '0'); SetChaseNewGrids(!!c).catch(() => {}); }} />
|
||||
<span>{t('clu.chaseGrids')} <span className="text-xs text-muted-foreground">{t('clu.chaseGridsHint')}</span></span>
|
||||
</label>
|
||||
{chaseGrids && (
|
||||
<p className="pl-6 text-xs text-muted-foreground">
|
||||
{t('clu.chaseGridsStat', { n: gridStat?.known ?? 0, p: gridStat?.pending ?? 0 })}
|
||||
{pskrStatus?.running ? ` · ${t('bo.feedUp', { n: pskrStatus.received ?? 0 })}` : ''}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* What counts as "I already have this square". There is no single
|
||||
right answer — a VUCC chaser counts a square per band, someone
|
||||
filling a wall map counts it once — so it is a choice, and the
|
||||
same six GridTracker offers, because a square wanted in one and
|
||||
not the other is a bug report every time. */}
|
||||
<div className="pl-6 space-y-1.5 pt-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<span className="text-xs text-muted-foreground w-28 shrink-0">{t('gsc.scope')}</span>
|
||||
<Select value={gridScope.scope} onValueChange={(v) => saveGridScope({ ...gridScope, scope: v })}>
|
||||
<SelectTrigger className="h-7 w-64 text-xs"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
{(gridScope.scopes ?? []).map((s: any) => (
|
||||
<SelectItem key={s.key} value={s.key}>{t(`gsc.scope_${s.key}`)}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
{/* Chase new — its own option, NOT nested under grid chasing. Chasing
|
||||
squares and chasing entities are different wants; they only share
|
||||
the PSK Reporter feed, which either one brings up. */}
|
||||
<div className="border-t border-border/60 pt-3 space-y-2">
|
||||
<label className="flex items-start gap-2 text-sm cursor-pointer">
|
||||
<Checkbox checked={chaseNew} className="mt-0.5"
|
||||
onCheckedChange={(c) => { setChaseNew(!!c); SetChaseNew(!!c).catch(() => {}); }} />
|
||||
<span>{t('chn.option')} <span className="text-xs text-muted-foreground">{t('chn.optionHelp')}</span></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* Band-opening watch. It lives HERE, with the cluster nodes, because
|
||||
switching it on adds two of them — the operator should see that
|
||||
happen where it happens rather than find nodes they did not add. */}
|
||||
@@ -8043,6 +8070,7 @@ function SettingsModalImpl({ onClose, onSaved, initialSection, onMainPaneChanged
|
||||
'lists-modes': ModesPanel,
|
||||
'lists-satellites': SatellitesPanel,
|
||||
cluster: ClusterPanel,
|
||||
dxhunter: DXHunterPanel,
|
||||
udp: UDPIntegrationsPanelWrapper,
|
||||
// Module-scope components, wrapped so their props can be passed. The nested
|
||||
// panels below go through PanelHost instead — which is what now lets either
|
||||
|
||||
Reference in New Issue
Block a user