- Add loading guard to loadPictureSources to prevent concurrent fetches - Pause perf chart polling and uptime timer when browser tab is hidden - Disconnect KC and LED preview WebSockets when leaving targets tab - Add error toasts to loadCaptureTemplates and saveKCBrightness - Skip auto-refresh polling when document is hidden - Widen auto-start dashboard cards for better text display Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
152 lines
5.7 KiB
JavaScript
152 lines
5.7 KiB
JavaScript
/**
|
|
* 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;
|
|
|
|
export function switchTab(name, { updateHash = true, skipLoad = false } = {}) {
|
|
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);
|
|
|
|
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 === 'profiles') {
|
|
if (typeof window.loadProfiles === 'function') window.loadProfiles();
|
|
}
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
|
|
// Migrate legacy 'devices' tab to 'targets'
|
|
if (saved === 'devices') saved = 'targets';
|
|
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;
|
|
}
|