Update tab UI, presets interactions, and help

Refine tab presets selection and editing, add per-tab removal, improve layout, and provide an in-app help modal.
This commit is contained in:
2026-01-28 04:44:30 +13:00
parent 8503315bef
commit 1576383d09
5 changed files with 569 additions and 127 deletions

View File

@@ -58,6 +58,7 @@ function renderTabsList(tabs, tabOrder, currentTabId) {
html += `
<button class="tab-button ${activeClass}"
data-tab-id="${tabId}"
title="Click to select, right-click to edit"
onclick="selectTab('${tabId}')">
${tabName}
</button>
@@ -241,11 +242,13 @@ async function loadTabContent(tabId) {
// 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}">
<h3>Presets</h3>
<div class="presets-section" data-tab-id="${tabId}" data-device-names="${deviceNames}">
<h3>Presets for ${tabName}</h3>
<div class="profiles-actions" style="margin-bottom: 1rem;">
<button class="btn btn-primary" id="preset-add-btn-tab">Add Preset</button>
<button class="btn btn-secondary" id="send-tab-presets-btn">Send Presets</button>
</div>
<div id="presets-list-tab" class="presets-list">
<!-- Presets will be loaded here by presets.js -->
@@ -253,6 +256,14 @@ async function loadTabContent(tabId) {
</div>
`;
// Wire up "Send Presets" button for this tab
const sendBtn = container.querySelector('#send-tab-presets-btn');
if (sendBtn) {
sendBtn.addEventListener('click', () => {
sendTabPresets(tabId);
});
}
// Trigger presets loading if the function exists
if (typeof renderTabPresets === 'function') {
renderTabPresets(tabId);
@@ -263,6 +274,65 @@ async function loadTabContent(tabId) {
}
}
// 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.');
}
}
// Open edit tab modal
function openEditTabModal(tabId, tab) {
const modal = document.getElementById('edit-tab-modal');
@@ -360,29 +430,6 @@ document.addEventListener('DOMContentLoaded', () => {
const newTabIdsInput = document.getElementById('new-tab-ids');
const createTabButton = document.getElementById('create-tab-btn');
// Set up edit tab button in header
const editTabBtn = document.getElementById('edit-tab-btn');
if (editTabBtn) {
editTabBtn.addEventListener('click', async () => {
if (!currentTabId) {
alert('No tab selected. Please select a tab first.');
return;
}
try {
const response = await fetch(`/tabs/${currentTabId}`);
if (response.ok) {
const tab = await response.json();
openEditTabModal(currentTabId, 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');
}
});
}
if (tabsButton && tabsModal) {
tabsButton.addEventListener('click', () => {
tabsModal.classList.add('active');
@@ -403,6 +450,28 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
}
// 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 () => {