feat(watchlist): dockable in the Main tab; sticky filters; Needed-only hides worked lines
The watchlist joins the Main-tab pane list (Settings → Main view), rendered in a card like the other docked panels. The three filters persist across tab switches — the component unmounts on every switch, and filters that reset each time are filters nobody trusts. Needed-only now hides the worked SPOT LINES as well as the all-worked cards, which is DXHunter's own Not-Worked behaviour and the reported complaint: a filter that says needed and still lists five green Worked rows answers a different question than the one asked. The add-row checkbox is relabelled 'as contest' — it marks the entry being ADDED, and read as a filter beside the All/DX/Contest buttons it looked redundant.
This commit is contained in:
@@ -1086,7 +1086,7 @@ function RelayAutoPanel() {
|
||||
// panes. The first two are the view; the third and fourth can be left empty, and
|
||||
// an empty one is not drawn at all. Per-profile (stored via SetUIPref, which is
|
||||
// profile-prefixed). Self-contained so it owns its async-loaded state.
|
||||
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol', 'decodes'];
|
||||
const MAIN_PANE_VALUES = ['map1', 'map2', 'cluster', 'worked', 'recent', 'netcontrol', 'decodes', 'watchlist'];
|
||||
const PANE_NONE = 'none';
|
||||
function MainViewPanes({ onChanged, flexAvailable, icomAvailable, yaesuAvailable, elecraftAvailable, tciAvailable }: { onChanged?: (side: 'left' | 'right' | 'p3' | 'p4' | 'layout', value: string) => void; flexAvailable?: boolean; icomAvailable?: boolean; yaesuAvailable?: boolean; elecraftAvailable?: boolean; tciAvailable?: boolean }) {
|
||||
const { t } = useI18n();
|
||||
|
||||
@@ -52,9 +52,17 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
const [addCall, setAddCall] = useState('');
|
||||
const [addContest, setAddContest] = useState(false);
|
||||
const [search, setSearch] = useState('');
|
||||
const [neededOnly, setNeededOnly] = useState(false);
|
||||
const [activeOnly, setActiveOnly] = useState(false);
|
||||
const [family, setFamily] = useState<'all' | 'normal' | 'contest'>('all');
|
||||
// The filters survive leaving the tab: the component unmounts on every tab
|
||||
// switch, and filters that reset each time are filters nobody trusts.
|
||||
const [neededOnly, setNeededOnlyRaw] = useState(() => localStorage.getItem('opslog.wlNeededOnly') === '1');
|
||||
const [activeOnly, setActiveOnlyRaw] = useState(() => localStorage.getItem('opslog.wlActiveOnly') === '1');
|
||||
const [family, setFamilyRaw] = useState<'all' | 'normal' | 'contest'>(() => {
|
||||
const v = localStorage.getItem('opslog.wlFamily');
|
||||
return v === 'normal' || v === 'contest' ? v : 'all';
|
||||
});
|
||||
const setNeededOnly = (f: (v: boolean) => boolean) => setNeededOnlyRaw((v) => { const nv = f(v); try { localStorage.setItem('opslog.wlNeededOnly', nv ? '1' : '0'); } catch {} return nv; });
|
||||
const setActiveOnly = (f: (v: boolean) => boolean) => setActiveOnlyRaw((v) => { const nv = f(v); try { localStorage.setItem('opslog.wlActiveOnly', nv ? '1' : '0'); } catch {} return nv; });
|
||||
const setFamily = (v: 'all' | 'normal' | 'contest') => { setFamilyRaw(v); try { localStorage.setItem('opslog.wlFamily', v); } catch {} };
|
||||
const [error, setError] = useState('');
|
||||
const [removeArm, setRemoveArm] = useState('');
|
||||
// worked answer per "call|band|modeclass|contest" key.
|
||||
@@ -178,7 +186,7 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
onKeyDown={(e) => { if (e.key === 'Enter') void add(); }} />
|
||||
<label className="flex items-center gap-1.5 text-xs cursor-pointer" title={t('wl.contestHint')}>
|
||||
<Checkbox checked={addContest} onCheckedChange={(c) => setAddContest(!!c)} />
|
||||
<Trophy className="size-3.5 text-warning" /> {t('wl.contest')}
|
||||
<Trophy className="size-3.5 text-warning" /> {t('wl.addAsContest')}
|
||||
</label>
|
||||
<Button size="sm" className="h-8" onClick={() => void add()} disabled={!addCall.trim()}>
|
||||
<Plus className="size-3.5" /> {t('wl.add')}
|
||||
@@ -218,8 +226,12 @@ export function WatchlistTab({ spots, spotStatus, onSpotSelect, onSpotClick }: P
|
||||
{entries.length === 0 ? t('wl.empty') : t('wl.noneMatch')}
|
||||
</div>
|
||||
) : shown.map((e) => {
|
||||
const list = spotsFor.get(e.callsign) ?? [];
|
||||
const needed = list.filter((s) => !workedFor(e, s)).length;
|
||||
const all = spotsFor.get(e.callsign) ?? [];
|
||||
const needed = all.filter((s) => !workedFor(e, s)).length;
|
||||
// Needed-only hides the worked LINES as well as the all-worked cards:
|
||||
// a filter that says needed and still lists five green Worked rows is
|
||||
// answering a different question than the one asked.
|
||||
const list = neededOnly ? all.filter((s) => !workedFor(e, s)) : all;
|
||||
return (
|
||||
<div key={e.callsign}
|
||||
className={cn('rounded-lg border bg-card p-3',
|
||||
|
||||
Reference in New Issue
Block a user