feat: Added persistence on cluster filters and last tab selected

This commit is contained in:
2026-07-15 22:35:05 +02:00
parent 33a6342a07
commit f650183936
2 changed files with 46 additions and 10 deletions
+41 -10
View File
@@ -608,7 +608,16 @@ export default function App() {
const [filterOpen, setFilterOpen] = useState(false); const [filterOpen, setFilterOpen] = useState(false);
const [activeFilter, setActiveFilter] = useState<QueryFilter>({ conditions: [], match: 'AND' }); const [activeFilter, setActiveFilter] = useState<QueryFilter>({ conditions: [], match: 'AND' });
const [matchCount, setMatchCount] = useState<number | null>(null); 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. // QSL Manager is a closable tab opened on demand from Tools → QSL Manager.
const [qslTabOpen, setQslTabOpen] = useState(false); const [qslTabOpen, setQslTabOpen] = useState(false);
const [qslDesignerOpen, setQslDesignerOpen] = 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. // Ring buffer — only keep the last N spots; cluster firehose can be heavy.
const [spots, setSpots] = useState<ClusterSpot[]>([]); const [spots, setSpots] = useState<ClusterSpot[]>([]);
const SPOTS_CAP = 1000; const SPOTS_CAP = 1000;
const [clusterFilterSource, setClusterFilterSource] = useState<number | ''>(''); // Cluster filter selections persist across restarts (writeUiPref → localStorage
const [clusterGroup, setClusterGroup] = useState(true); // + 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(''); const [clusterCmd, setClusterCmd] = useState('');
// Cluster console: the raw traffic. Spots are parsed out of the stream into the // 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, // 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; if (atBottom) el.scrollTop = el.scrollHeight;
}, [clusterLines]); }, [clusterLines]);
// Multi-band filter: empty set = all bands. The user toggles chips. // 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 // Lock-to-entry: when on, the band filter follows the entry's current
// band and the mode filter follows the entry's current mode. // band and the mode filter follows the entry's current mode.
const [clusterLockBand, setClusterLockBand] = useState(false); const [clusterLockBand, setClusterLockBand] = useState(() => lsBool('opslog.clusterLockBand', false));
const [clusterLockMode, setClusterLockMode] = useState(false); const [clusterLockMode, setClusterLockMode] = useState(() => lsBool('opslog.clusterLockMode', false));
// Status filter chips. Empty set = show every status (including // Status filter chips. Empty set = show every status (including
// already-worked). Otherwise only matching spots pass. // already-worked). Otherwise only matching spots pass.
type SpotStatusKey = 'new' | 'new-band' | 'new-mode' | 'new-slot' | 'worked'; 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 // Mode filter chips. Empty set = show every mode. Categories map the
// inferred per-spot mode onto SSB (phone) / CW / DATA (digital). // inferred per-spot mode onto SSB (phone) / CW / DATA (digital).
type SpotModeCat = 'SSB' | 'CW' | 'DATA'; type SpotModeCat = 'SSB' | 'CW' | 'DATA';
const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(new Set()); const [clusterModeFilter, setClusterModeFilter] = useState<Set<SpotModeCat>>(() => lsSet<SpotModeCat>('opslog.clusterModeFilter'));
const [clusterSearch, setClusterSearch] = useState(''); const [clusterSearch, setClusterSearch] = useState(() => localStorage.getItem('opslog.clusterSearch') || '');
// Hide spots already worked (exact call worked, or this band+mode slot done). // 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). // Bands shown side-by-side in the Band Map tab (portable).
const [bandMapBands, setBandMapBands] = useState<string[]>(() => { const [bandMapBands, setBandMapBands] = useState<string[]>(() => {
try { const v = JSON.parse(localStorage.getItem('opslog.bandMapBands') || '[]'); return Array.isArray(v) ? v : []; } try { const v = JSON.parse(localStorage.getItem('opslog.bandMapBands') || '[]'); return Array.isArray(v) ? v : []; }
+5
View File
@@ -26,6 +26,11 @@ const PORTABLE_KEYS = [
'opslog.lookupOnBlur', // run the callsign lookup on blur instead of while typing 'opslog.lookupOnBlur', // run the callsign lookup on blur instead of while typing
'opslog.clusterShowFilters', // cluster filter sidebar shown (tab + Main pane) 'opslog.clusterShowFilters', // cluster filter sidebar shown (tab + Main pane)
'opslog.mapBasemap', // world map basemap (light / street / satellite) '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: // syncPortablePrefs reconciles the DB with the local cache at startup: