This improves navigation and profile workflows on smaller screens. Co-authored-by: Cursor <cursoragent@cursor.com>
803 lines
29 KiB
JavaScript
803 lines
29 KiB
JavaScript
// 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 = '<div class="error">Failed to load tabs</div>';
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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 = '<div class="muted-text">No tabs available</div>';
|
|
return;
|
|
}
|
|
|
|
let html = '<div class="tabs-list">';
|
|
for (const tabId of tabOrder) {
|
|
const tab = tabs[tabId];
|
|
if (tab) {
|
|
const activeClass = tabId === currentTabId ? 'active' : '';
|
|
const tabName = tab.name || `Tab ${tabId}`;
|
|
html += `
|
|
<button class="tab-button ${activeClass}"
|
|
data-tab-id="${tabId}"
|
|
title="Click to select, right-click to edit"
|
|
onclick="selectTab('${tabId}')">
|
|
${tabName}
|
|
</button>
|
|
`;
|
|
}
|
|
}
|
|
html += '</div>';
|
|
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 = `<div class="error">${tab.error}</div>`;
|
|
return;
|
|
}
|
|
|
|
// Render tab content (presets section)
|
|
const tabName = tab.name || `Tab ${tabId}`;
|
|
const deviceNames = Array.isArray(tab.names) ? tab.names.join(',') : '';
|
|
container.innerHTML = `
|
|
<div class="presets-section" data-tab-id="${tabId}" data-device-names="${deviceNames}">
|
|
<h3>Presets for ${tabName}</h3>
|
|
<div class="profiles-actions presets-toolbar" style="margin-bottom: 1rem;">
|
|
<div class="tab-brightness-group">
|
|
<label for="tab-brightness-slider">Brightness</label>
|
|
<input type="range" id="tab-brightness-slider" min="0" max="255" value="255">
|
|
</div>
|
|
</div>
|
|
<div id="presets-list-tab" class="presets-list">
|
|
<!-- Presets will be loaded here by presets.js -->
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// 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 = '<div class="error">Failed to load tab content</div>';
|
|
}
|
|
}
|
|
|
|
// 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 = '<span class="muted-text">Loading…</span>';
|
|
try {
|
|
const tabRes = await fetch(`/tabs/${tabId}`, { headers: { Accept: 'application/json' } });
|
|
if (!tabRes.ok) {
|
|
listEl.innerHTML = '<span class="muted-text">Failed to load presets.</span>';
|
|
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 = '<span class="muted-text">No presets to add. All presets are already in this tab.</span>';
|
|
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 = '<span class="muted-text">Failed to load presets.</span>';
|
|
}
|
|
}
|
|
|
|
// 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
|
|
};
|