// 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 += `
${tabName}
`;
}
}
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 = `
`;
// 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
};