feat: Added persistence on cluster filters and last tab selected
This commit is contained in:
+41
-10
@@ -608,7 +608,16 @@ export default function App() {
|
||||
const [filterOpen, setFilterOpen] = useState(false);
|
||||
const [activeFilter, setActiveFilter] = useState<QueryFilter>({ conditions: [], match: 'AND' });
|
||||
const [matchCount, setMatchCount] = useState<number | null>(null);
|
||||
const [activeTab, setActiveTab] = useState('recent');
|
||||
// The selected tab is remembered across restarts. Only the always-present tabs
|
||||
// are restored: the conditional ones (flex/icom/contest/net/stats/qsl) depend on
|
||||
// a feature or CAT backend that isn't known this early, and restoring one that
|
||||
// isn't there would show a blank pane.
|
||||
const ALWAYS_TABS = ['main', 'recent', 'cluster', 'worked', 'awards', 'bandmap'];
|
||||
const [activeTab, setActiveTab] = useState(() => {
|
||||
const saved = localStorage.getItem('opslog.activeTab') || '';
|
||||
return ALWAYS_TABS.includes(saved) ? saved : 'recent';
|
||||
});
|
||||
useEffect(() => { writeUiPref('opslog.activeTab', activeTab); }, [activeTab]);
|
||||
// QSL Manager is a closable tab opened on demand from Tools → QSL Manager.
|
||||
const [qslTabOpen, setQslTabOpen] = useState(false);
|
||||
const [qslDesignerOpen, setQslDesignerOpen] = useState(false);
|
||||
@@ -909,8 +918,15 @@ export default function App() {
|
||||
// Ring buffer — only keep the last N spots; cluster firehose can be heavy.
|
||||
const [spots, setSpots] = useState<ClusterSpot[]>([]);
|
||||
const SPOTS_CAP = 1000;
|
||||
const [clusterFilterSource, setClusterFilterSource] = useState<number | ''>('');
|
||||
const [clusterGroup, setClusterGroup] = useState(true);
|
||||
// Cluster filter selections persist across restarts (writeUiPref → localStorage
|
||||
// + DB, so they also travel with a copied data/ folder). Loaders read the cache
|
||||
// synchronously at first render; a single effect below writes them back.
|
||||
const lsBool = (k: string, d: boolean) => { const v = localStorage.getItem(k); return v === null ? d : v === '1'; };
|
||||
const lsSet = <T,>(k: string): Set<T> => { try { const a = JSON.parse(localStorage.getItem(k) || '[]'); return new Set(Array.isArray(a) ? a : []); } catch { return new Set<T>(); } };
|
||||
const [clusterFilterSource, setClusterFilterSource] = useState<number | ''>(() => {
|
||||
const v = localStorage.getItem('opslog.clusterFilterSource'); const n = v ? parseInt(v, 10) : NaN; return Number.isFinite(n) ? n : '';
|
||||
});
|
||||
const [clusterGroup, setClusterGroup] = useState(() => lsBool('opslog.clusterGroup', true));
|
||||
const [clusterCmd, setClusterCmd] = useState('');
|
||||
// Cluster console: the raw traffic. Spots are parsed out of the stream into the
|
||||
// grid; everything else (SH/DX output, WHO, MOTD, errors) used to be discarded,
|
||||
@@ -938,22 +954,37 @@ export default function App() {
|
||||
if (atBottom) el.scrollTop = el.scrollHeight;
|
||||
}, [clusterLines]);
|
||||
// Multi-band filter: empty set = all bands. The user toggles chips.
|
||||
const [clusterBands, setClusterBands] = useState<Set<string>>(new Set());
|
||||
const [clusterBands, setClusterBands] = useState<Set<string>>(() => lsSet<string>('opslog.clusterBands'));
|
||||
// Lock-to-entry: when on, the band filter follows the entry's current
|
||||
// band and the mode filter follows the entry's current mode.
|
||||
const [clusterLockBand, setClusterLockBand] = useState(false);
|
||||
const [clusterLockMode, setClusterLockMode] = useState(false);
|
||||
const [clusterLockBand, setClusterLockBand] = useState(() => lsBool('opslog.clusterLockBand', false));
|
||||
const [clusterLockMode, setClusterLockMode] = useState(() => lsBool('opslog.clusterLockMode', false));
|
||||
// Status filter chips. Empty set = show every status (including
|
||||
// already-worked). Otherwise only matching spots pass.
|
||||
type SpotStatusKey = 'new' | 'new-band' | 'new-mode' | 'new-slot' | 'worked';
|
||||
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotStatusKey>>(new Set());
|
||||
const [clusterStatusFilter, setClusterStatusFilter] = useState<Set<SpotStatusKey>>(() => lsSet<SpotStatusKey>('opslog.clusterStatusFilter'));
|
||||
// Mode filter chips. Empty set = show every mode. Categories map the
|
||||
// inferred per-spot mode onto SSB (phone) / CW / DATA (digital).
|
||||
type SpotModeCat = 'SSB' | 'CW' | 'DATA';
|
||||
const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(new Set());
|
||||
const [clusterSearch, setClusterSearch] = useState('');
|
||||
const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(() => lsSet<SpotModeCat>('opslog.clusterModeFilter'));
|
||||
const [clusterSearch, setClusterSearch] = useState(() => localStorage.getItem('opslog.clusterSearch') || '');
|
||||
// Hide spots already worked (exact call worked, or this band+mode slot done).
|
||||
const [clusterHideWorked, setClusterHideWorked] = useState(false);
|
||||
const [clusterHideWorked, setClusterHideWorked] = useState(() => lsBool('opslog.clusterHideWorked', false));
|
||||
|
||||
// Persist every cluster filter selection whenever it changes, so it is still
|
||||
// set after a close/reopen.
|
||||
useEffect(() => {
|
||||
writeUiPref('opslog.clusterFilterSource', clusterFilterSource === '' ? '' : String(clusterFilterSource));
|
||||
writeUiPref('opslog.clusterGroup', clusterGroup ? '1' : '0');
|
||||
writeUiPref('opslog.clusterBands', JSON.stringify([...clusterBands]));
|
||||
writeUiPref('opslog.clusterLockBand', clusterLockBand ? '1' : '0');
|
||||
writeUiPref('opslog.clusterLockMode', clusterLockMode ? '1' : '0');
|
||||
writeUiPref('opslog.clusterStatusFilter', JSON.stringify([...clusterStatusFilter]));
|
||||
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]);
|
||||
// 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 : []; }
|
||||
|
||||
@@ -26,6 +26,11 @@ const PORTABLE_KEYS = [
|
||||
'opslog.lookupOnBlur', // run the callsign lookup on blur instead of while typing
|
||||
'opslog.clusterShowFilters', // cluster filter sidebar shown (tab + Main pane)
|
||||
'opslog.mapBasemap', // world map basemap (light / street / satellite)
|
||||
// Cluster filter selections — restored on reopen.
|
||||
'opslog.clusterFilterSource', 'opslog.clusterGroup', 'opslog.clusterBands',
|
||||
'opslog.clusterLockBand', 'opslog.clusterLockMode', 'opslog.clusterStatusFilter',
|
||||
'opslog.clusterModeFilter', 'opslog.clusterSearch', 'opslog.clusterHideWorked',
|
||||
'opslog.activeTab', // last selected tab
|
||||
];
|
||||
|
||||
// syncPortablePrefs reconciles the DB with the local cache at startup:
|
||||
|
||||
Reference in New Issue
Block a user