// Tab management JavaScript let currentTabId = null; // Get current tab from cookie function getCurrentTabFromCookie() { const cookies = document.cookie.split(';'); for (let cookie of cookies) { const [name, value] = cookie.trim().split('='); if (name === 'current_tab') { return value; } } return null; } // Load tabs list async function loadTabs() { try { const response = await fetch('/tabs'); const data = await response.json(); // Get current tab from cookie first, then from server response const cookieTabId = getCurrentTabFromCookie(); const serverCurrent = data.current_tab_id; const tabs = data.tabs || {}; const tabIds = Object.keys(tabs); let candidateId = cookieTabId || serverCurrent || null; // If the candidate doesn't exist anymore (e.g. after DB reset), fall back to first tab. if (candidateId && !tabIds.includes(String(candidateId))) { candidateId = tabIds.length > 0 ? tabIds[0] : null; // Clear stale cookie document.cookie = 'current_tab=; path=/; max-age=0'; } currentTabId = candidateId; renderTabsList(data.tabs, data.tab_order, currentTabId); // Load current tab content if available if (currentTabId) { loadTabContent(currentTabId); } else if (data.tab_order && data.tab_order.length > 0) { // Set first tab as current if none is set await setCurrentTab(data.tab_order[0]); } } catch (error) { console.error('Failed to load tabs:', error); const container = document.getElementById('tabs-list'); if (container) { container.innerHTML = '
Failed to load tabs
'; } } } // Render tabs list in the main UI function renderTabsList(tabs, tabOrder, currentTabId) { const container = document.getElementById('tabs-list'); if (!container) return; if (!tabOrder || tabOrder.length === 0) { container.innerHTML = '
No tabs available
'; return; } let html = '
'; for (const tabId of tabOrder) { const tab = tabs[tabId]; if (tab) { const activeClass = tabId === currentTabId ? 'active' : ''; const tabName = tab.name || `Tab ${tabId}`; html += ` `; } } html += '
'; container.innerHTML = html; } // Render tabs list in modal (like profiles) function renderTabsListModal(tabs, tabOrder, currentTabId) { const container = document.getElementById('tabs-list-modal'); if (!container) return; container.innerHTML = ""; let entries = []; if (Array.isArray(tabOrder)) { entries = tabOrder.map((tabId) => [tabId, tabs[tabId] || {}]); } else if (tabs && typeof tabs === "object") { entries = Object.entries(tabs).filter(([key]) => { return key !== 'current_tab_id' && key !== 'tabs' && key !== 'tab_order'; }); } if (entries.length === 0) { const empty = document.createElement("p"); empty.className = "muted-text"; empty.textContent = "No tabs found."; container.appendChild(empty); return; } entries.forEach(([tabId, tab]) => { const row = document.createElement("div"); row.className = "profiles-row"; const label = document.createElement("span"); label.textContent = (tab && tab.name) || tabId; if (String(tabId) === String(currentTabId)) { label.textContent = `✓ ${label.textContent}`; label.style.fontWeight = "bold"; label.style.color = "#FFD700"; } const applyButton = document.createElement("button"); applyButton.className = "btn btn-secondary btn-small"; applyButton.textContent = "Select"; applyButton.addEventListener("click", async () => { await selectTab(tabId); document.getElementById('tabs-modal').classList.remove('active'); }); const editButton = document.createElement("button"); editButton.className = "btn btn-secondary btn-small"; editButton.textContent = "Edit"; editButton.addEventListener("click", () => { openEditTabModal(tabId, tab); }); const sendPresetsButton = document.createElement("button"); sendPresetsButton.className = "btn btn-secondary btn-small"; sendPresetsButton.textContent = "Send Presets"; sendPresetsButton.addEventListener("click", async () => { await sendTabPresets(tabId); }); const cloneButton = document.createElement("button"); cloneButton.className = "btn btn-secondary btn-small"; cloneButton.textContent = "Clone"; cloneButton.addEventListener("click", async () => { const baseName = (tab && tab.name) || tabId; const suggested = `${baseName} Copy`; const name = prompt("New tab name:", suggested); if (name === null) { return; } const trimmed = String(name).trim(); if (!trimmed) { alert("Tab name cannot be empty."); return; } try { const response = await fetch(`/tabs/${tabId}/clone`, { method: "POST", headers: { "Content-Type": "application/json", "Accept": "application/json", }, body: JSON.stringify({ name: trimmed }), }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: "Failed to clone tab" })); throw new Error(errorData.error || "Failed to clone tab"); } const data = await response.json().catch(() => null); let newTabId = null; if (data && typeof data === "object") { if (data.id) { newTabId = String(data.id); } else { const ids = Object.keys(data); if (ids.length > 0) { newTabId = String(ids[0]); } } } await loadTabsModal(); if (newTabId) { await selectTab(newTabId); } else { await loadTabs(); } } catch (error) { console.error("Clone tab failed:", error); alert("Failed to clone tab: " + error.message); } }); const deleteButton = document.createElement("button"); deleteButton.className = "btn btn-danger btn-small"; deleteButton.textContent = "Delete"; deleteButton.addEventListener("click", async () => { const confirmed = confirm(`Delete tab "${label.textContent}"?`); if (!confirmed) { return; } try { const response = await fetch(`/tabs/${tabId}`, { method: "DELETE", headers: { Accept: "application/json" }, }); if (!response.ok) { const errorData = await response.json().catch(() => ({ error: "Failed to delete tab" })); throw new Error(errorData.error || "Failed to delete tab"); } // Clear cookie if deleted tab was current if (tabId === currentTabId) { document.cookie = 'current_tab=; path=/; max-age=0'; currentTabId = null; } await loadTabsModal(); await loadTabs(); // Reload main tabs list } catch (error) { console.error("Delete tab failed:", error); alert("Failed to delete tab: " + error.message); } }); row.appendChild(label); row.appendChild(applyButton); row.appendChild(editButton); row.appendChild(sendPresetsButton); row.appendChild(cloneButton); row.appendChild(deleteButton); container.appendChild(row); }); } // Load tabs in modal async function loadTabsModal() { const container = document.getElementById('tabs-list-modal'); if (!container) return; container.innerHTML = ""; const loading = document.createElement("p"); loading.className = "muted-text"; loading.textContent = "Loading tabs..."; container.appendChild(loading); try { const response = await fetch("/tabs", { headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error("Failed to load tabs"); } const data = await response.json(); const tabs = data.tabs || data; const currentTabId = getCurrentTabFromCookie() || data.current_tab_id || null; renderTabsListModal(tabs, data.tab_order || [], currentTabId); } catch (error) { console.error("Load tabs failed:", error); container.innerHTML = ""; const errorMessage = document.createElement("p"); errorMessage.className = "muted-text"; errorMessage.textContent = "Failed to load tabs."; container.appendChild(errorMessage); } } // Select a tab async function selectTab(tabId) { // Update active state document.querySelectorAll('.tab-button').forEach(btn => { btn.classList.remove('active'); }); const btn = document.querySelector(`[data-tab-id="${tabId}"]`); if (btn) { btn.classList.add('active'); } // Set as current tab await setCurrentTab(tabId); // Load tab content loadTabContent(tabId); } // Set current tab in cookie async function setCurrentTab(tabId) { try { const response = await fetch(`/tabs/${tabId}/set-current`, { method: 'POST' }); const data = await response.json(); if (response.ok) { currentTabId = tabId; // Also set cookie on client side document.cookie = `current_tab=${tabId}; path=/; max-age=31536000`; } else { console.error('Failed to set current tab:', data.error); } } catch (error) { console.error('Error setting current tab:', error); } } // Load tab content async function loadTabContent(tabId) { const container = document.getElementById('tab-content'); if (!container) return; try { const response = await fetch(`/tabs/${tabId}`); const tab = await response.json(); if (tab.error) { container.innerHTML = `
${tab.error}
`; return; } // Render tab content (presets section) const tabName = tab.name || `Tab ${tabId}`; const deviceNames = Array.isArray(tab.names) ? tab.names.join(',') : ''; container.innerHTML = `

Presets for ${tabName}

`; // Wire up per-tab brightness slider to send global brightness via ESPNow. const brightnessSlider = container.querySelector('#tab-brightness-slider'); let brightnessSendTimeout = null; if (brightnessSlider) { brightnessSlider.addEventListener('input', (e) => { const val = parseInt(e.target.value, 10) || 0; if (brightnessSendTimeout) { clearTimeout(brightnessSendTimeout); } brightnessSendTimeout = setTimeout(() => { if (typeof window.sendEspnowRaw === 'function') { try { window.sendEspnowRaw({ v: '1', b: val }); } catch (err) { console.error('Failed to send brightness via ESPNow:', err); } } }, 150); }); } // Trigger presets loading if the function exists if (typeof renderTabPresets === 'function') { renderTabPresets(tabId); } } catch (error) { console.error('Failed to load tab content:', error); container.innerHTML = '
Failed to load tab content
'; } } // Send all presets used by a tab via the /presets/send HTTP endpoint. async function sendTabPresets(tabId) { try { // Load tab data to determine which presets are used const tabResponse = await fetch(`/tabs/${tabId}`, { headers: { Accept: 'application/json' }, }); if (!tabResponse.ok) { alert('Failed to load tab to send presets.'); return; } const tabData = await tabResponse.json(); // Extract preset IDs from tab (supports grid, flat, and legacy formats) let presetIds = []; if (Array.isArray(tabData.presets_flat)) { presetIds = tabData.presets_flat; } else if (Array.isArray(tabData.presets)) { if (tabData.presets.length && typeof tabData.presets[0] === 'string') { // Flat array of IDs presetIds = tabData.presets; } else if (tabData.presets.length && Array.isArray(tabData.presets[0])) { // 2D grid presetIds = tabData.presets.flat(); } } presetIds = (presetIds || []).filter(Boolean); if (!presetIds.length) { alert('This tab has no presets to send.'); return; } // Call server-side ESPNow sender with just the IDs; it handles chunking. const response = await fetch('/presets/send', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ preset_ids: presetIds }), }); const data = await response.json().catch(() => ({})); if (!response.ok) { const msg = (data && data.error) || 'Failed to send presets.'; alert(msg); return; } const sent = typeof data.presets_sent === 'number' ? data.presets_sent : presetIds.length; const messages = typeof data.messages_sent === 'number' ? data.messages_sent : '?'; alert(`Sent ${sent} preset(s) in ${messages} ESPNow message(s).`); } catch (error) { console.error('Failed to send tab presets:', error); alert('Failed to send tab presets.'); } } // Send all presets used by all tabs in the current profile via /presets/send. async function sendProfilePresets() { try { // Load current profile to get its tabs const profileRes = await fetch('/profiles/current', { headers: { Accept: 'application/json' }, }); if (!profileRes.ok) { alert('Failed to load current profile.'); return; } const profileData = await profileRes.json(); const profile = profileData.profile || {}; let tabList = null; if (Array.isArray(profile.tabs)) { tabList = profile.tabs; } else if (profile.tabs) { tabList = [profile.tabs]; } if (!tabList || tabList.length === 0) { if (Array.isArray(profile.tab_order)) { tabList = profile.tab_order; } else if (profile.tab_order) { tabList = [profile.tab_order]; } else { tabList = []; } } if (!tabList || tabList.length === 0) { console.warn('sendProfilePresets: no tabs found', { profileData, profile, }); } if (!tabList.length) { alert('Current profile has no tabs to send presets for.'); return; } const allPresetIdsSet = new Set(); // Collect all preset IDs used in all tabs of this profile for (const tabId of tabList) { try { const tabResp = await fetch(`/tabs/${tabId}`, { headers: { Accept: 'application/json' }, }); if (!tabResp.ok) { continue; } const tabData = await tabResp.json(); let presetIds = []; if (Array.isArray(tabData.presets_flat)) { presetIds = tabData.presets_flat; } else if (Array.isArray(tabData.presets)) { if (tabData.presets.length && typeof tabData.presets[0] === 'string') { presetIds = tabData.presets; } else if (tabData.presets.length && Array.isArray(tabData.presets[0])) { presetIds = tabData.presets.flat(); } } (presetIds || []).forEach((id) => { if (id) allPresetIdsSet.add(id); }); } catch (e) { console.error('Failed to load tab for profile presets:', e); } } const allPresetIds = Array.from(allPresetIdsSet); if (!allPresetIds.length) { alert('No presets to send for the current profile.'); return; } // Call server-side ESPNow sender with all unique preset IDs; it handles chunking and save flag. const response = await fetch('/presets/send', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Accept': 'application/json', }, body: JSON.stringify({ preset_ids: allPresetIds }), }); const data = await response.json().catch(() => ({})); if (!response.ok) { const msg = (data && data.error) || 'Failed to send presets for profile.'; alert(msg); return; } const sent = typeof data.presets_sent === 'number' ? data.presets_sent : allPresetIds.length; const messages = typeof data.messages_sent === 'number' ? data.messages_sent : '?'; alert(`Sent ${sent} preset(s) for the current profile in ${messages} ESPNow message(s).`); } catch (error) { console.error('Failed to send profile presets:', error); alert('Failed to send profile presets.'); } } // Populate the "Add presets to this tab" list: only presets NOT already in the tab, each with a Select button. async function populateEditTabPresetsList(tabId) { const listEl = document.getElementById('edit-tab-presets-list'); if (!listEl) return; listEl.innerHTML = 'Loading…'; try { const tabRes = await fetch(`/tabs/${tabId}`, { headers: { Accept: 'application/json' } }); if (!tabRes.ok) { listEl.innerHTML = 'Failed to load presets.'; return; } const tabData = await tabRes.json(); let inTabIds = []; if (Array.isArray(tabData.presets_flat)) { inTabIds = tabData.presets_flat; } else if (Array.isArray(tabData.presets)) { if (tabData.presets.length && typeof tabData.presets[0] === 'string') { inTabIds = tabData.presets; } else if (tabData.presets.length && Array.isArray(tabData.presets[0])) { inTabIds = tabData.presets.flat(); } } const presetsRes = await fetch('/presets', { headers: { Accept: 'application/json' } }); const allPresets = presetsRes.ok ? await presetsRes.json() : {}; const allIds = Object.keys(allPresets); const availableToAdd = allIds.filter(id => !inTabIds.includes(id)); listEl.innerHTML = ''; if (availableToAdd.length === 0) { listEl.innerHTML = 'No presets to add. All presets are already in this tab.'; return; } for (const presetId of availableToAdd) { const preset = allPresets[presetId] || {}; const name = preset.name || presetId; const row = document.createElement('div'); row.className = 'profiles-row'; row.style.display = 'flex'; row.style.alignItems = 'center'; row.style.justifyContent = 'space-between'; row.style.gap = '0.5rem'; const label = document.createElement('span'); label.textContent = name; const selectBtn = document.createElement('button'); selectBtn.type = 'button'; selectBtn.className = 'btn btn-primary btn-small'; selectBtn.textContent = 'Select'; selectBtn.addEventListener('click', async () => { if (typeof window.addPresetToTab === 'function') { await window.addPresetToTab(presetId, tabId); await populateEditTabPresetsList(tabId); } }); row.appendChild(label); row.appendChild(selectBtn); listEl.appendChild(row); } } catch (e) { console.error('populateEditTabPresetsList:', e); listEl.innerHTML = 'Failed to load presets.'; } } // Open edit tab modal function openEditTabModal(tabId, tab) { const modal = document.getElementById('edit-tab-modal'); const idInput = document.getElementById('edit-tab-id'); const nameInput = document.getElementById('edit-tab-name'); const idsInput = document.getElementById('edit-tab-ids'); if (idInput) idInput.value = tabId; if (nameInput) nameInput.value = tab ? (tab.name || '') : ''; if (idsInput) idsInput.value = tab && tab.names ? tab.names.join(', ') : '1'; if (modal) modal.classList.add('active'); populateEditTabPresetsList(tabId); } // Update an existing tab async function updateTab(tabId, name, ids) { try { const names = ids ? ids.split(',').map(id => id.trim()) : ['1']; const response = await fetch(`/tabs/${tabId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, names: names }) }); const data = await response.json(); if (response.ok) { // Reload tabs list await loadTabsModal(); await loadTabs(); // Close modal document.getElementById('edit-tab-modal').classList.remove('active'); return true; } else { alert(`Error: ${data.error || 'Failed to update tab'}`); return false; } } catch (error) { console.error('Failed to update tab:', error); alert('Failed to update tab'); return false; } } // Create a new tab async function createTab(name, ids) { try { const names = ids ? ids.split(',').map(id => id.trim()) : ['1']; const response = await fetch('/tabs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: name, names: names }) }); const data = await response.json(); if (response.ok) { // Reload tabs list await loadTabsModal(); await loadTabs(); // Select the new tab if (data && Object.keys(data).length > 0) { const newTabId = Object.keys(data)[0]; await selectTab(newTabId); } return true; } else { alert(`Error: ${data.error || 'Failed to create tab'}`); return false; } } catch (error) { console.error('Failed to create tab:', error); alert('Failed to create tab'); return false; } } // Initialize on page load document.addEventListener('DOMContentLoaded', () => { loadTabs(); // Set up tabs modal const tabsButton = document.getElementById('tabs-btn'); const tabsModal = document.getElementById('tabs-modal'); const tabsCloseButton = document.getElementById('tabs-close-btn'); const newTabNameInput = document.getElementById('new-tab-name'); const newTabIdsInput = document.getElementById('new-tab-ids'); const createTabButton = document.getElementById('create-tab-btn'); if (tabsButton && tabsModal) { tabsButton.addEventListener('click', () => { tabsModal.classList.add('active'); loadTabsModal(); }); } if (tabsCloseButton) { tabsCloseButton.addEventListener('click', () => { tabsModal.classList.remove('active'); }); } if (tabsModal) { tabsModal.addEventListener('click', (event) => { if (event.target === tabsModal) { tabsModal.classList.remove('active'); } }); } // Right-click on a tab button in the main header bar to edit that tab document.addEventListener('contextmenu', async (event) => { const btn = event.target.closest('.tab-button'); if (!btn || !btn.dataset.tabId) { return; } event.preventDefault(); const tabId = btn.dataset.tabId; try { const response = await fetch(`/tabs/${tabId}`); if (response.ok) { const tab = await response.json(); openEditTabModal(tabId, tab); } else { alert('Failed to load tab for editing'); } } catch (error) { console.error('Failed to load tab:', error); alert('Failed to load tab for editing'); } }); // Set up create tab const createTabHandler = async () => { if (!newTabNameInput) return; const name = newTabNameInput.value.trim(); const ids = (newTabIdsInput && newTabIdsInput.value.trim()) || '1'; if (name) { await createTab(name, ids); if (newTabNameInput) newTabNameInput.value = ''; if (newTabIdsInput) newTabIdsInput.value = '1'; } }; if (createTabButton) { createTabButton.addEventListener('click', createTabHandler); } if (newTabNameInput) { newTabNameInput.addEventListener('keypress', (event) => { if (event.key === 'Enter') { createTabHandler(); } }); } // Set up edit tab form const editTabForm = document.getElementById('edit-tab-form'); if (editTabForm) { editTabForm.addEventListener('submit', async (e) => { e.preventDefault(); const idInput = document.getElementById('edit-tab-id'); const nameInput = document.getElementById('edit-tab-name'); const idsInput = document.getElementById('edit-tab-ids'); const tabId = idInput ? idInput.value : null; const name = nameInput ? nameInput.value.trim() : ''; const ids = idsInput ? idsInput.value.trim() : '1'; if (tabId && name) { await updateTab(tabId, name, ids); editTabForm.reset(); } }); } // Close edit modal when clicking outside const editTabModal = document.getElementById('edit-tab-modal'); if (editTabModal) { editTabModal.addEventListener('click', (event) => { if (event.target === editTabModal) { editTabModal.classList.remove('active'); } }); } // Profile-wide "Send Presets" button in header const sendProfilePresetsBtn = document.getElementById('send-profile-presets-btn'); if (sendProfilePresetsBtn) { sendProfilePresetsBtn.addEventListener('click', async () => { await sendProfilePresets(); }); } }); // Export for use in other scripts window.tabsManager = { loadTabs, selectTab, createTab, updateTab, openEditTabModal, getCurrentTabId: () => currentTabId };