/** * Tab switching — switchTab, initTabs, startAutoRefresh, hash routing. */ import { apiKey, refreshInterval, setRefreshInterval, dashboardPollInterval } from '../core/state.js'; /** Parse location.hash into {tab, subTab}. */ export function parseHash() { const hash = location.hash.replace(/^#/, ''); if (!hash) return {}; const [tab, subTab] = hash.split('/'); return { tab, subTab }; } /** Update the URL hash without triggering popstate. */ function _setHash(tab, subTab) { const hash = '#' + (subTab ? `${tab}/${subTab}` : tab); history.replaceState(null, '', hash); } let _suppressHashUpdate = false; let _activeTab = null; const _tabScrollPositions = {}; export function switchTab(name, { updateHash = true, skipLoad = false } = {}) { if (_activeTab === name) return; // Save scroll position of the tab we're leaving if (_activeTab) _tabScrollPositions[_activeTab] = window.scrollY; _activeTab = name; document.querySelectorAll('.tab-btn').forEach(btn => { const isActive = btn.dataset.tab === name; btn.classList.toggle('active', isActive); btn.setAttribute('aria-selected', String(isActive)); }); document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`)); localStorage.setItem('activeTab', name); // Restore scroll position for this tab requestAnimationFrame(() => window.scrollTo(0, _tabScrollPositions[name] || 0)); if (updateHash && !_suppressHashUpdate) { const subTab = name === 'targets' ? (localStorage.getItem('activeTargetSubTab') || 'led') : name === 'streams' ? (localStorage.getItem('activeStreamTab') || 'raw') : null; _setHash(name, subTab); } if (name === 'dashboard') { // Use window.* to avoid circular imports with feature modules if (!skipLoad && apiKey && typeof window.loadDashboard === 'function') window.loadDashboard(); } else { if (typeof window.stopPerfPolling === 'function') window.stopPerfPolling(); if (typeof window.stopUptimeTimer === 'function') window.stopUptimeTimer(); // Clean up WebSockets when leaving targets tab if (name !== 'targets') { if (typeof window.disconnectAllKCWebSockets === 'function') window.disconnectAllKCWebSockets(); if (typeof window.disconnectAllLedPreviewWS === 'function') window.disconnectAllLedPreviewWS(); } if (!apiKey || skipLoad) return; if (name === 'streams') { if (typeof window.loadPictureSources === 'function') window.loadPictureSources(); } else if (name === 'targets') { if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); } else if (name === 'automations') { if (typeof window.loadAutomations === 'function') window.loadAutomations(); } } } export function initTabs() { // Hash takes priority over localStorage const hashRoute = parseHash(); let saved; if (hashRoute.tab && document.getElementById(`tab-${hashRoute.tab}`)) { saved = hashRoute.tab; // Pre-set sub-tab so the sub-tab switch functions pick it up if (hashRoute.subTab) { if (saved === 'targets') localStorage.setItem('activeTargetSubTab', hashRoute.subTab); if (saved === 'streams') localStorage.setItem('activeStreamTab', hashRoute.subTab); } } else { saved = localStorage.getItem('activeTab'); } if (!saved || !document.getElementById(`tab-${saved}`)) saved = 'dashboard'; switchTab(saved); } /** Update hash when sub-tab changes. Called from targets.js / streams.js. */ export function updateSubTabHash(tab, subTab) { _setHash(tab, subTab); } /** Update the count badge on a main tab button. Hidden when count is 0. */ export function updateTabBadge(tabName, count) { const badge = document.getElementById(`tab-badge-${tabName}`); if (!badge) return; if (count > 0) { badge.textContent = count; badge.style.display = ''; } else { badge.style.display = 'none'; } } export function startAutoRefresh() { if (refreshInterval) { clearInterval(refreshInterval); } setRefreshInterval(setInterval(() => { if (!apiKey || document.hidden) return; const activeTab = localStorage.getItem('activeTab') || 'dashboard'; if (activeTab === 'targets') { // Skip refresh while user interacts with a picker or slider const panel = document.getElementById('targets-panel-content'); if (panel && panel.contains(document.activeElement) && document.activeElement.matches('input')) return; if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); } else if (activeTab === 'dashboard') { if (typeof window.loadDashboard === 'function') window.loadDashboard(); } }, dashboardPollInterval)); } /** * Handle browser back/forward via popstate. * Called from app.js. */ export function handlePopState() { const hashRoute = parseHash(); if (!hashRoute.tab) return; const currentTab = localStorage.getItem('activeTab'); _suppressHashUpdate = true; if (hashRoute.tab !== currentTab) { switchTab(hashRoute.tab, { updateHash: false }); } if (hashRoute.subTab) { if (hashRoute.tab === 'targets') { const currentSub = localStorage.getItem('activeTargetSubTab'); if (hashRoute.subTab !== currentSub && typeof window.switchTargetSubTab === 'function') { window.switchTargetSubTab(hashRoute.subTab); } } else if (hashRoute.tab === 'streams') { const currentSub = localStorage.getItem('activeStreamTab'); if (hashRoute.subTab !== currentSub && typeof window.switchStreamTab === 'function') { window.switchStreamTab(hashRoute.subTab); } } } _suppressHashUpdate = false; }