feat(zones): rename tabs to zones across api, ui, and storage
Made-with: Cursor
This commit is contained in:
@@ -5,7 +5,7 @@ class LightingController {
|
||||
this.state = {
|
||||
lights: {},
|
||||
patterns: {},
|
||||
tab_order: [],
|
||||
zone_order: [],
|
||||
presets: {}
|
||||
};
|
||||
this.selectedColorIndex = 0;
|
||||
@@ -19,8 +19,8 @@ class LightingController {
|
||||
await this.loadState();
|
||||
this.setupEventListeners();
|
||||
this.renderTabs();
|
||||
if (this.state.tab_order.length > 0) {
|
||||
this.selectTab(this.state.tab_order[0]);
|
||||
if (this.state.zone_order.length > 0) {
|
||||
this.selectTab(this.state.zone_order[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,19 +62,19 @@ class LightingController {
|
||||
}
|
||||
|
||||
setupEventListeners() {
|
||||
// Tab management
|
||||
document.getElementById('add-tab-btn').addEventListener('click', () => this.showAddTabModal());
|
||||
document.getElementById('edit-tab-btn').addEventListener('click', () => this.showEditTabModal());
|
||||
document.getElementById('delete-tab-btn').addEventListener('click', () => this.deleteCurrentTab());
|
||||
// Zone management
|
||||
document.getElementById('add-zone-btn').addEventListener('click', () => this.showAddTabModal());
|
||||
document.getElementById('edit-zone-btn').addEventListener('click', () => this.showEditTabModal());
|
||||
document.getElementById('delete-zone-btn').addEventListener('click', () => this.deleteCurrentTab());
|
||||
document.getElementById('color-palette-btn').addEventListener('click', () => this.showColorPalette());
|
||||
document.getElementById('presets-btn').addEventListener('click', () => this.showPresets());
|
||||
document.getElementById('profiles-btn').addEventListener('click', () => this.showProfiles());
|
||||
|
||||
// Modal actions
|
||||
document.getElementById('add-tab-confirm').addEventListener('click', () => this.createTab());
|
||||
document.getElementById('add-tab-cancel').addEventListener('click', () => this.hideModal('add-tab-modal'));
|
||||
document.getElementById('edit-tab-confirm').addEventListener('click', () => this.updateTab());
|
||||
document.getElementById('edit-tab-cancel').addEventListener('click', () => this.hideModal('edit-tab-modal'));
|
||||
document.getElementById('add-zone-confirm').addEventListener('click', () => this.createTab());
|
||||
document.getElementById('add-zone-cancel').addEventListener('click', () => this.hideModal('add-zone-modal'));
|
||||
document.getElementById('edit-zone-confirm').addEventListener('click', () => this.updateTab());
|
||||
document.getElementById('edit-zone-cancel').addEventListener('click', () => this.hideModal('edit-zone-modal'));
|
||||
document.getElementById('profiles-close-btn').addEventListener('click', () => this.hideModal('profiles-modal'));
|
||||
document.getElementById('color-palette-close-btn').addEventListener('click', () => this.hideModal('color-palette-modal'));
|
||||
document.getElementById('presets-close-btn').addEventListener('click', () => this.hideModal('presets-modal'));
|
||||
@@ -125,12 +125,12 @@ class LightingController {
|
||||
}
|
||||
|
||||
renderTabs() {
|
||||
const tabsList = document.getElementById('tabs-list');
|
||||
const tabsList = document.getElementById('zones-list');
|
||||
tabsList.innerHTML = '';
|
||||
|
||||
this.state.tab_order.forEach(tabName => {
|
||||
this.state.zone_order.forEach(tabName => {
|
||||
const tabButton = document.createElement('button');
|
||||
tabButton.className = 'tab-button';
|
||||
tabButton.className = 'zone-button';
|
||||
tabButton.textContent = tabName;
|
||||
tabButton.addEventListener('click', () => this.selectTab(tabName));
|
||||
if (tabName === this.currentTab) {
|
||||
@@ -217,13 +217,13 @@ class LightingController {
|
||||
}
|
||||
|
||||
renderPresets(tabName) {
|
||||
const presetsList = document.getElementById('presets-list-tab');
|
||||
const presetsList = document.getElementById('presets-list-zone');
|
||||
presetsList.innerHTML = '';
|
||||
|
||||
const presets = this.state.presets || {};
|
||||
const presetNames = Object.keys(presets);
|
||||
|
||||
// Get current tab's settings for comparison
|
||||
// Get current zone's settings for comparison
|
||||
const currentSettings = this.getCurrentTabSettings(tabName);
|
||||
|
||||
// Always include "on" and "off" presets
|
||||
@@ -267,7 +267,7 @@ class LightingController {
|
||||
const presetButton = document.createElement('button');
|
||||
presetButton.className = 'pattern-button';
|
||||
|
||||
// Check if this preset matches the current tab's settings
|
||||
// Check if this preset matches the current zone's settings
|
||||
const isActive = this.presetMatchesSettings(preset, currentSettings);
|
||||
if (isActive) {
|
||||
presetButton.classList.add('active');
|
||||
@@ -344,7 +344,7 @@ class LightingController {
|
||||
})
|
||||
});
|
||||
|
||||
// Reload state and tab content
|
||||
// Reload state and zone content
|
||||
await this.loadState();
|
||||
await this.loadTabContent(tabName);
|
||||
} else {
|
||||
@@ -591,7 +591,7 @@ class LightingController {
|
||||
}
|
||||
// Reload state from server to ensure consistency
|
||||
await this.loadState();
|
||||
// Reload tab content to update UI
|
||||
// Reload zone content to update UI
|
||||
await this.loadTabContent(tabName);
|
||||
} else {
|
||||
const errorText = await response.text();
|
||||
@@ -769,23 +769,23 @@ class LightingController {
|
||||
}
|
||||
|
||||
showAddTabModal() {
|
||||
document.getElementById('new-tab-name').value = '';
|
||||
document.getElementById('new-tab-ids').value = '1';
|
||||
document.getElementById('add-tab-modal').classList.add('active');
|
||||
document.getElementById('new-zone-name').value = '';
|
||||
document.getElementById('new-zone-ids').value = '1';
|
||||
document.getElementById('add-zone-modal').classList.add('active');
|
||||
}
|
||||
|
||||
async createTab() {
|
||||
const name = document.getElementById('new-tab-name').value.trim();
|
||||
const idsStr = document.getElementById('new-tab-ids').value.trim();
|
||||
const name = document.getElementById('new-zone-name').value.trim();
|
||||
const idsStr = document.getElementById('new-zone-ids').value.trim();
|
||||
const ids = idsStr.split(',').map(id => id.trim()).filter(id => id);
|
||||
|
||||
if (!name) {
|
||||
alert('Tab name cannot be empty');
|
||||
alert('Zone name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/tabs', {
|
||||
const response = await fetch('/zones', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, ids })
|
||||
@@ -795,41 +795,41 @@ class LightingController {
|
||||
await this.loadState();
|
||||
this.renderTabs();
|
||||
this.selectTab(name);
|
||||
this.hideModal('add-tab-modal');
|
||||
this.hideModal('add-zone-modal');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to create tab');
|
||||
alert(error.error || 'Failed to create zone');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to create tab:', error);
|
||||
alert('Failed to create tab');
|
||||
console.error('Failed to create zone:', error);
|
||||
alert('Failed to create zone');
|
||||
}
|
||||
}
|
||||
|
||||
showEditTabModal() {
|
||||
if (!this.currentTab) {
|
||||
alert('Please select a tab first');
|
||||
alert('Please select a zone first');
|
||||
return;
|
||||
}
|
||||
|
||||
const light = this.state.lights[this.currentTab];
|
||||
document.getElementById('edit-tab-name').value = this.currentTab;
|
||||
document.getElementById('edit-tab-ids').value = light.names.join(', ');
|
||||
document.getElementById('edit-tab-modal').classList.add('active');
|
||||
document.getElementById('edit-zone-name').value = this.currentTab;
|
||||
document.getElementById('edit-zone-ids').value = light.names.join(', ');
|
||||
document.getElementById('edit-zone-modal').classList.add('active');
|
||||
}
|
||||
|
||||
async updateTab() {
|
||||
const newName = document.getElementById('edit-tab-name').value.trim();
|
||||
const idsStr = document.getElementById('edit-tab-ids').value.trim();
|
||||
const newName = document.getElementById('edit-zone-name').value.trim();
|
||||
const idsStr = document.getElementById('edit-zone-ids').value.trim();
|
||||
const ids = idsStr.split(',').map(id => id.trim()).filter(id => id);
|
||||
|
||||
if (!newName) {
|
||||
alert('Tab name cannot be empty');
|
||||
alert('Zone name cannot be empty');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/tabs/${this.currentTab}`, {
|
||||
const response = await fetch(`/zones/${this.currentTab}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name: newName, ids })
|
||||
@@ -839,45 +839,45 @@ class LightingController {
|
||||
await this.loadState();
|
||||
this.renderTabs();
|
||||
this.selectTab(newName);
|
||||
this.hideModal('edit-tab-modal');
|
||||
this.hideModal('edit-zone-modal');
|
||||
} else {
|
||||
const error = await response.json();
|
||||
alert(error.error || 'Failed to update tab');
|
||||
alert(error.error || 'Failed to update zone');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to update tab:', error);
|
||||
alert('Failed to update tab');
|
||||
console.error('Failed to update zone:', error);
|
||||
alert('Failed to update zone');
|
||||
}
|
||||
}
|
||||
|
||||
async deleteCurrentTab() {
|
||||
if (!this.currentTab) {
|
||||
alert('Please select a tab first');
|
||||
alert('Please select a zone first');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirm(`Are you sure you want to delete the tab '${this.currentTab}'?`)) {
|
||||
if (!confirm(`Are you sure you want to delete the zone '${this.currentTab}'?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/tabs/${this.currentTab}`, {
|
||||
const response = await fetch(`/zones/${this.currentTab}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
await this.loadState();
|
||||
this.renderTabs();
|
||||
if (this.state.tab_order.length > 0) {
|
||||
this.selectTab(this.state.tab_order[0]);
|
||||
if (this.state.zone_order.length > 0) {
|
||||
this.selectTab(this.state.zone_order[0]);
|
||||
} else {
|
||||
this.currentTab = null;
|
||||
document.getElementById('tab-content').innerHTML = '<p>No tabs available. Create a new tab to get started.</p>';
|
||||
document.getElementById('zone-content').innerHTML = '<p>No tabs available. Create a new zone to get started.</p>';
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to delete tab:', error);
|
||||
alert('Failed to delete tab');
|
||||
console.error('Failed to delete zone:', error);
|
||||
alert('Failed to delete zone');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1008,9 +1008,9 @@ class LightingController {
|
||||
if (this.state.current_profile === profileName) {
|
||||
this.state.current_profile = '';
|
||||
this.state.lights = {};
|
||||
this.state.tab_order = [];
|
||||
this.state.zone_order = [];
|
||||
this.renderTabs();
|
||||
document.getElementById('tab-content').innerHTML = '<p>No tabs available. Create a new tab to get started.</p>';
|
||||
document.getElementById('zone-content').innerHTML = '<p>No tabs available. Create a new zone to get started.</p>';
|
||||
this.updateCurrentProfileDisplay();
|
||||
}
|
||||
} else {
|
||||
@@ -1032,8 +1032,8 @@ class LightingController {
|
||||
if (response.ok) {
|
||||
await this.loadState();
|
||||
this.renderTabs();
|
||||
if (this.state.tab_order.length > 0) {
|
||||
this.selectTab(this.state.tab_order[0]);
|
||||
if (this.state.zone_order.length > 0) {
|
||||
this.selectTab(this.state.zone_order[0]);
|
||||
} else {
|
||||
this.currentTab = null;
|
||||
}
|
||||
@@ -1129,7 +1129,7 @@ class LightingController {
|
||||
swatch.style.cssText = 'width: 40px; height: 40px; background-color: ' + color + '; border: 2px solid #4a4a4a; border-radius: 4px; cursor: pointer; position: relative;';
|
||||
swatch.title = `Click to apply ${color} to selected color`;
|
||||
|
||||
// Click to apply color to currently selected color in active tab
|
||||
// Click to apply color to currently selected color in active zone
|
||||
swatch.addEventListener('click', (e) => {
|
||||
// Only apply if not clicking the remove button
|
||||
if (e.target === swatch || !e.target.closest('button')) {
|
||||
@@ -1151,7 +1151,7 @@ class LightingController {
|
||||
|
||||
applyPaletteColorToSelected(paletteColor) {
|
||||
if (!this.currentTab) {
|
||||
alert('No tab selected. Please select a tab first.');
|
||||
alert('No zone selected. Please select a zone first.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1439,7 +1439,7 @@ class LightingController {
|
||||
|
||||
async applyPreset(presetName) {
|
||||
if (!this.currentTab) {
|
||||
alert('Please select a tab first');
|
||||
alert('Please select a zone first');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1621,7 +1621,7 @@ class LightingController {
|
||||
|
||||
loadCurrentTabToPresetEditor() {
|
||||
if (!this.currentTab || !this.state.lights[this.currentTab]) {
|
||||
alert('Please select a tab first');
|
||||
alert('Please select a zone first');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,34 +19,34 @@ const numTabs = 3;
|
||||
|
||||
// Select the container for tabs and content
|
||||
const tabsContainer = document.querySelector(".tabs");
|
||||
const tabContentContainer = document.querySelector(".tab-content");
|
||||
const tabContentContainer = document.querySelector(".zone-content");
|
||||
|
||||
// Create tabs dynamically
|
||||
for (let i = 1; i <= numTabs; i++) {
|
||||
// Create the tab button
|
||||
// Create the zone button
|
||||
const tabButton = document.createElement("button");
|
||||
tabButton.classList.add("tab");
|
||||
tabButton.id = `tab${i}`;
|
||||
tabButton.textContent = `Tab ${i}`;
|
||||
tabButton.classList.add("zone");
|
||||
tabButton.id = `zone${i}`;
|
||||
tabButton.textContent = `Zone ${i}`;
|
||||
|
||||
// Add the tab button to the container
|
||||
// Add the zone button to the container
|
||||
tabsContainer.appendChild(tabButton);
|
||||
|
||||
// Create the corresponding tab content (RGB slider)
|
||||
// Create the corresponding zone content (RGB slider)
|
||||
const tabContent = document.createElement("div");
|
||||
tabContent.classList.add("tab-pane");
|
||||
tabContent.classList.add("zone-pane");
|
||||
tabContent.id = `content${i}`;
|
||||
const slider = document.createElement("rgb-slider");
|
||||
slider.id = i;
|
||||
tabContent.appendChild(slider);
|
||||
|
||||
// Add the tab content to the container
|
||||
// Add the zone content to the container
|
||||
tabContentContainer.appendChild(tabContent);
|
||||
|
||||
// Listen for color change on each RGB slider
|
||||
slider.addEventListener("color-change", (e) => {
|
||||
const { r, g, b } = e.detail;
|
||||
console.log(`Color changed in tab ${i}:`, e.detail);
|
||||
console.log(`Color changed in zone ${i}:`, e.detail);
|
||||
// Send RGB data to WebSocket server
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
const colorData = { r, g, b };
|
||||
@@ -56,26 +56,26 @@ for (let i = 1; i <= numTabs; i++) {
|
||||
}
|
||||
|
||||
// Function to switch tabs
|
||||
function switchTab(tabId) {
|
||||
const tabs = document.querySelectorAll(".tab");
|
||||
const tabContents = document.querySelectorAll(".tab-pane");
|
||||
function switchTab(zoneId) {
|
||||
const tabs = document.querySelectorAll(".zone");
|
||||
const tabContents = document.querySelectorAll(".zone-pane");
|
||||
|
||||
tabs.forEach((tab) => tab.classList.remove("active"));
|
||||
zones.forEach((zone) => zone.classList.remove("active"));
|
||||
tabContents.forEach((content) => content.classList.remove("active"));
|
||||
|
||||
// Activate the clicked tab and corresponding content
|
||||
document.getElementById(tabId).classList.add("active");
|
||||
// Activate the clicked zone and corresponding content
|
||||
document.getElementById(zoneId).classList.add("active");
|
||||
document
|
||||
.getElementById("content" + tabId.replace("tab", ""))
|
||||
.getElementById("content" + zoneId.replace("zone", ""))
|
||||
.classList.add("active");
|
||||
}
|
||||
|
||||
// Add event listeners to tabs
|
||||
tabsContainer.addEventListener("click", (e) => {
|
||||
if (e.target.classList.contains("tab")) {
|
||||
if (e.target.classList.contains("zone")) {
|
||||
switchTab(e.target.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Initially set the first tab as active
|
||||
// Initially set the first zone as active
|
||||
switchTab("tab1");
|
||||
|
||||
@@ -175,9 +175,9 @@ async function postDriverSequence(sequence, targetMacs, delayS) {
|
||||
return res.json().catch(() => ({}));
|
||||
}
|
||||
|
||||
// Send a select message for a preset to all devices on the current tab (ESP-NOW or Wi-Fi).
|
||||
// Send a select message for a preset to all devices on the current zone (ESP-NOW or Wi-Fi).
|
||||
const sendSelectForCurrentTabDevices = async (presetId, sectionEl) => {
|
||||
const section = sectionEl || document.querySelector('.presets-section[data-tab-id]');
|
||||
const section = sectionEl || document.querySelector('.presets-section[data-zone-id]');
|
||||
if (!section || !presetId) {
|
||||
return;
|
||||
}
|
||||
@@ -223,7 +223,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const presetBrightnessInput = document.getElementById('preset-brightness-input');
|
||||
const presetDelayInput = document.getElementById('preset-delay-input');
|
||||
const presetDefaultButton = document.getElementById('preset-default-btn');
|
||||
const presetRemoveFromTabButton = document.getElementById('preset-remove-from-tab-btn');
|
||||
const presetRemoveFromTabButton = document.getElementById('preset-remove-from-zone-btn');
|
||||
const presetSaveButton = document.getElementById('preset-save-btn');
|
||||
const presetAddFromPaletteButton = document.getElementById('preset-add-from-palette-btn');
|
||||
|
||||
@@ -623,8 +623,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (currentEditTabId) {
|
||||
return currentEditTabId;
|
||||
}
|
||||
const section = document.querySelector('.presets-section[data-tab-id]');
|
||||
return section ? section.dataset.tabId : null;
|
||||
const section = document.querySelector('.presets-section[data-zone-id]');
|
||||
return section ? section.dataset.zoneId : null;
|
||||
};
|
||||
|
||||
const updatePresetEditorTabActionsVisibility = () => {
|
||||
@@ -634,12 +634,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
const updateTabDefaultPreset = async (presetId) => {
|
||||
const tabId = getActiveTabId();
|
||||
if (!tabId) {
|
||||
const zoneId = getActiveTabId();
|
||||
if (!zoneId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const tabResponse = await fetch(`/tabs/${tabId}`, {
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!tabResponse.ok) {
|
||||
@@ -647,13 +647,13 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
tabData.default_preset = presetId;
|
||||
await fetch(`/tabs/${tabId}`, {
|
||||
await fetch(`/zones/${zoneId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(tabData),
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to save tab default preset:', error);
|
||||
console.warn('Failed to save zone default preset:', error);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -950,22 +950,22 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
const showAddPresetToTabModal = async (optionalTabId) => {
|
||||
let tabId = optionalTabId;
|
||||
if (!tabId) {
|
||||
// Get current tab ID from the presets section
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
tabId = leftPanel ? leftPanel.dataset.tabId : null;
|
||||
let zoneId = optionalTabId;
|
||||
if (!zoneId) {
|
||||
// Get current zone ID from the presets section
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
|
||||
}
|
||||
if (!tabId) {
|
||||
if (!zoneId) {
|
||||
// Fallback: try to get from URL
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const tabIndex = pathParts.indexOf('tabs');
|
||||
const tabIndex = pathParts.indexOf('zones');
|
||||
if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
|
||||
tabId = pathParts[tabIndex + 1];
|
||||
zoneId = pathParts[tabIndex + 1];
|
||||
}
|
||||
}
|
||||
if (!tabId) {
|
||||
alert('Could not determine current tab.');
|
||||
if (!zoneId) {
|
||||
alert('Could not determine current zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -980,10 +980,10 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const allPresetsRaw = await response.json();
|
||||
const allPresets = await filterPresetsForCurrentProfile(allPresetsRaw);
|
||||
|
||||
// Load only the current tab's presets so we can avoid duplicates within this tab.
|
||||
// Load only the current zone's presets so we can avoid duplicates within this zone.
|
||||
let currentTabPresets = [];
|
||||
try {
|
||||
const tabResponse = await fetch(`/tabs/${tabId}`, {
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (tabResponse.ok) {
|
||||
@@ -999,19 +999,19 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Could not load current tab presets:', e);
|
||||
console.warn('Could not load current zone presets:', e);
|
||||
}
|
||||
|
||||
// Create modal
|
||||
const modal = document.createElement('div');
|
||||
modal.className = 'modal active';
|
||||
modal.id = 'add-preset-to-tab-modal';
|
||||
modal.id = 'add-preset-to-zone-modal';
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content">
|
||||
<h2>Add Preset to Tab</h2>
|
||||
<h2>Add Preset to Zone</h2>
|
||||
<div id="add-preset-list" class="profiles-list" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
<div class="modal-actions">
|
||||
<button class="btn btn-secondary" id="add-preset-to-tab-close-btn">Close</button>
|
||||
<button class="btn btn-secondary" id="add-preset-to-zone-close-btn">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1023,7 +1023,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const availableToAdd = presetNames.filter(presetId => !currentTabPresets.includes(presetId));
|
||||
if (availableToAdd.length === 0) {
|
||||
listContainer.innerHTML = '<p class="muted-text">No presets to add. All presets are already in this tab, or create a preset first.</p>';
|
||||
listContainer.innerHTML = '<p class="muted-text">No presets to add. All presets are already in this zone, or create a preset first.</p>';
|
||||
} else {
|
||||
availableToAdd.forEach(presetId => {
|
||||
const preset = allPresets[presetId];
|
||||
@@ -1042,7 +1042,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
addButton.className = 'btn btn-primary btn-small';
|
||||
addButton.textContent = 'Add';
|
||||
addButton.addEventListener('click', async () => {
|
||||
await addPresetToTab(presetId, tabId);
|
||||
await addPresetToTab(presetId, zoneId);
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
@@ -1054,7 +1054,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
// Close button handler
|
||||
document.getElementById('add-preset-to-tab-close-btn').addEventListener('click', () => {
|
||||
document.getElementById('add-preset-to-zone-close-btn').addEventListener('click', () => {
|
||||
modal.remove();
|
||||
});
|
||||
|
||||
@@ -1067,34 +1067,34 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.showAddPresetToTabModal = showAddPresetToTabModal;
|
||||
} catch (e) {}
|
||||
|
||||
const addPresetToTab = async (presetId, tabId) => {
|
||||
if (!tabId) {
|
||||
// Try to get tab ID from the left-panel
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
tabId = leftPanel ? leftPanel.dataset.tabId : null;
|
||||
const addPresetToTab = async (presetId, zoneId) => {
|
||||
if (!zoneId) {
|
||||
// Try to get zone ID from the left-panel
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
|
||||
|
||||
if (!tabId) {
|
||||
if (!zoneId) {
|
||||
// Fallback: try to get from URL
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const tabIndex = pathParts.indexOf('tabs');
|
||||
const tabIndex = pathParts.indexOf('zones');
|
||||
if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
|
||||
tabId = pathParts[tabIndex + 1];
|
||||
zoneId = pathParts[tabIndex + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!tabId) {
|
||||
alert('Could not determine current tab.');
|
||||
if (!zoneId) {
|
||||
alert('Could not determine current zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current tab data
|
||||
const tabResponse = await fetch(`/tabs/${tabId}`, {
|
||||
// Get current zone data
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!tabResponse.ok) {
|
||||
throw new Error('Failed to load tab');
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
|
||||
@@ -1111,7 +1111,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
|
||||
if (flat.includes(presetId)) {
|
||||
alert('Preset is already added to this tab.');
|
||||
alert('Preset is already added to this zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1120,23 +1120,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
tabData.presets = newGrid;
|
||||
tabData.presets_flat = flat;
|
||||
|
||||
// Update tab
|
||||
const updateResponse = await fetch(`/tabs/${tabId}`, {
|
||||
// Update zone
|
||||
const updateResponse = await fetch(`/zones/${zoneId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(tabData),
|
||||
});
|
||||
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error('Failed to update tab');
|
||||
throw new Error('Failed to update zone');
|
||||
}
|
||||
|
||||
// Reload the tab content to show the new preset
|
||||
// Reload the zone content to show the new preset
|
||||
if (typeof renderTabPresets === 'function') {
|
||||
await renderTabPresets(tabId);
|
||||
await renderTabPresets(zoneId);
|
||||
} else if (window.htmx) {
|
||||
htmx.ajax('GET', `/tabs/${tabId}/content-fragment`, {
|
||||
target: '#tab-content',
|
||||
htmx.ajax('GET', `/zones/${zoneId}/content-fragment`, {
|
||||
target: '#zone-content',
|
||||
swap: 'innerHTML'
|
||||
});
|
||||
} else {
|
||||
@@ -1144,8 +1144,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
window.location.reload();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add preset to tab:', error);
|
||||
alert('Failed to add preset to tab.');
|
||||
console.error('Failed to add preset to zone:', error);
|
||||
alert('Failed to add preset to zone.');
|
||||
}
|
||||
};
|
||||
try {
|
||||
@@ -1269,8 +1269,8 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
alert('Preset name is required to send.');
|
||||
return;
|
||||
}
|
||||
// Send current editor values and then select on all devices in the current tab (if any)
|
||||
const section = document.querySelector('.presets-section[data-tab-id]');
|
||||
// Send current editor values and then select on all devices in the current zone (if any)
|
||||
const section = document.querySelector('.presets-section[data-zone-id]');
|
||||
const deviceNames = tabDeviceNamesFromSection(section);
|
||||
// Work out the preset ID: for existing presets use currentEditId, otherwise fall back to name
|
||||
const presetId = currentEditId || payload.name;
|
||||
@@ -1286,7 +1286,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
alert('Preset name is required.');
|
||||
return;
|
||||
}
|
||||
const section = document.querySelector('.presets-section[data-tab-id]');
|
||||
const section = document.querySelector('.presets-section[data-zone-id]');
|
||||
const deviceNames = tabDeviceNamesFromSection(section);
|
||||
const presetId = currentEditId || payload.name;
|
||||
await updateTabDefaultPreset(presetId);
|
||||
@@ -1297,7 +1297,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (presetRemoveFromTabButton) {
|
||||
presetRemoveFromTabButton.addEventListener('click', async () => {
|
||||
if (!currentEditTabId || !currentEditId) return;
|
||||
if (!window.confirm('Remove this preset from this tab?')) return;
|
||||
if (!window.confirm('Remove this preset from this zone?')) return;
|
||||
await removePresetFromTab(currentEditTabId, currentEditId);
|
||||
clearForm();
|
||||
closeEditor();
|
||||
@@ -1348,12 +1348,12 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
clearForm();
|
||||
closeEditor();
|
||||
|
||||
// Reload tab presets if we're in a tab view
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
// Reload zone presets if we're in a zone view
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
if (leftPanel) {
|
||||
const tabId = leftPanel.dataset.tabId;
|
||||
if (tabId && typeof renderTabPresets !== 'undefined') {
|
||||
renderTabPresets(tabId);
|
||||
const zoneId = leftPanel.dataset.zoneId;
|
||||
if (zoneId && typeof renderTabPresets !== 'undefined') {
|
||||
renderTabPresets(zoneId);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -1362,11 +1362,11 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// Listen for edit preset events from tab preset buttons
|
||||
// Listen for edit preset events from zone preset buttons
|
||||
document.addEventListener('editPreset', async (event) => {
|
||||
const { presetId, preset, tabId } = event.detail;
|
||||
const { presetId, preset, zoneId } = event.detail;
|
||||
currentEditId = presetId;
|
||||
currentEditTabId = tabId || null;
|
||||
currentEditTabId = zoneId || null;
|
||||
await loadPatterns();
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
setFormValues({
|
||||
@@ -1478,11 +1478,11 @@ const sendDefaultPreset = async (presetId, deviceNames) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Expose for other scripts (tabs.js) so they can reuse the shared WebSocket.
|
||||
// Expose for other scripts (zones.js) so they can reuse the shared WebSocket.
|
||||
try {
|
||||
window.sendPresetViaEspNow = sendPresetViaEspNow;
|
||||
window.postDriverSequence = postDriverSequence;
|
||||
// Expose a generic ESPNow sender so other scripts (tabs.js) can send
|
||||
// Expose a generic ESPNow sender so other scripts (zones.js) can send
|
||||
// non-preset messages such as global brightness.
|
||||
window.sendEspnowRaw = sendEspnowMessage;
|
||||
window.getEspnowSocket = getEspnowSocket;
|
||||
@@ -1490,9 +1490,9 @@ try {
|
||||
// window may not exist in some environments; ignore.
|
||||
}
|
||||
|
||||
// Store selected preset per tab
|
||||
// Store selected preset per zone
|
||||
const selectedPresets = {};
|
||||
// Run vs Edit for tab preset strip (in-memory only — each full page load starts in run mode)
|
||||
// Run vs Edit for zone preset strip (in-memory only — each full page load starts in run mode)
|
||||
let presetUiMode = 'run';
|
||||
|
||||
const getPresetUiMode = () => (presetUiMode === 'edit' ? 'edit' : 'run');
|
||||
@@ -1559,15 +1559,15 @@ const arrayToGrid = (presetIds, columns = 3) => {
|
||||
return grid;
|
||||
};
|
||||
|
||||
// Function to save preset grid for a tab
|
||||
const savePresetGrid = async (tabId, presetGrid) => {
|
||||
// Function to save preset grid for a zone
|
||||
const savePresetGrid = async (zoneId, presetGrid) => {
|
||||
try {
|
||||
// Get current tab data
|
||||
const tabResponse = await fetch(`/tabs/${tabId}`, {
|
||||
// Get current zone data
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!tabResponse.ok) {
|
||||
throw new Error('Failed to load tab');
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
|
||||
@@ -1576,8 +1576,8 @@ const savePresetGrid = async (tabId, presetGrid) => {
|
||||
// Also store as flat array for backward compatibility
|
||||
tabData.presets_flat = presetGrid.flat();
|
||||
|
||||
// Save updated tab
|
||||
const updateResponse = await fetch(`/tabs/${tabId}`, {
|
||||
// Save updated zone
|
||||
const updateResponse = await fetch(`/zones/${zoneId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(tabData),
|
||||
@@ -1631,18 +1631,18 @@ const insertDraggingOntoTarget = (presetsList, dragging, dropTarget) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Function to render presets for a specific tab in 2D grid
|
||||
const renderTabPresets = async (tabId) => {
|
||||
const presetsList = document.getElementById('presets-list-tab');
|
||||
// Function to render presets for a specific zone in 2D grid
|
||||
const renderTabPresets = async (zoneId) => {
|
||||
const presetsList = document.getElementById('presets-list-zone');
|
||||
if (!presetsList) return;
|
||||
|
||||
try {
|
||||
// Get tab data to see which presets are associated
|
||||
const tabResponse = await fetch(`/tabs/${tabId}`, {
|
||||
// Get zone data to see which presets are associated
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!tabResponse.ok) {
|
||||
throw new Error('Failed to load tab');
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
|
||||
@@ -1669,7 +1669,7 @@ const renderTabPresets = async (tabId) => {
|
||||
const paletteColors = await getCurrentProfilePaletteColors();
|
||||
|
||||
presetsList.innerHTML = '';
|
||||
presetsList.dataset.reorderTabId = tabId;
|
||||
presetsList.dataset.reorderTabId = zoneId;
|
||||
|
||||
// Drag-and-drop on the list (wire once — re-render would duplicate listeners otherwise)
|
||||
if (!presetsList.dataset.dragWired) {
|
||||
@@ -1719,7 +1719,7 @@ const renderTabPresets = async (tabId) => {
|
||||
|
||||
try {
|
||||
if (!saveId) {
|
||||
console.warn('No tab id for preset reorder save');
|
||||
console.warn('No zone id for preset reorder save');
|
||||
return;
|
||||
}
|
||||
await savePresetGrid(saveId, newGrid);
|
||||
@@ -1733,19 +1733,19 @@ const renderTabPresets = async (tabId) => {
|
||||
});
|
||||
}
|
||||
|
||||
// Get the currently selected preset for this tab
|
||||
const selectedPresetId = selectedPresets[tabId];
|
||||
// Get the currently selected preset for this zone
|
||||
const selectedPresetId = selectedPresets[zoneId];
|
||||
|
||||
// Render presets in grid layout
|
||||
// Flatten the grid and render all presets (grid CSS will handle layout)
|
||||
const flatPresets = presetGrid.flat().filter(id => id);
|
||||
|
||||
if (flatPresets.length === 0) {
|
||||
// Show empty message if this tab has no presets
|
||||
// Show empty message if this zone has no presets
|
||||
const empty = document.createElement('p');
|
||||
empty.className = 'muted-text';
|
||||
empty.style.gridColumn = '1 / -1'; // Span all columns
|
||||
empty.textContent = 'No presets added to this tab. Open the tab\'s Edit menu and click "Add Preset" to add one.';
|
||||
empty.textContent = 'No presets added to this zone. Open the zone\'s Edit menu and click "Add Preset" to add one.';
|
||||
presetsList.appendChild(empty);
|
||||
} else {
|
||||
flatPresets.forEach((presetId) => {
|
||||
@@ -1756,18 +1756,18 @@ const renderTabPresets = async (tabId) => {
|
||||
...preset,
|
||||
colors: resolveColorsWithPaletteRefs(preset.colors, preset.palette_refs, paletteColors),
|
||||
};
|
||||
const wrapper = createPresetButton(presetId, displayPreset, tabId, isSelected);
|
||||
const wrapper = createPresetButton(presetId, displayPreset, zoneId, isSelected);
|
||||
presetsList.appendChild(wrapper);
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to render tab presets:', error);
|
||||
console.error('Failed to render zone presets:', error);
|
||||
presetsList.innerHTML = '<p class="muted-text">Failed to load presets.</p>';
|
||||
}
|
||||
};
|
||||
|
||||
const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
const createPresetButton = (presetId, preset, zoneId, isSelected = false) => {
|
||||
const uiMode = getPresetUiMode();
|
||||
|
||||
const row = document.createElement('div');
|
||||
@@ -1806,12 +1806,12 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
|
||||
button.addEventListener('click', () => {
|
||||
if (isDraggingPreset) return;
|
||||
const presetsListEl = document.getElementById('presets-list-tab');
|
||||
const presetsListEl = document.getElementById('presets-list-zone');
|
||||
if (presetsListEl) {
|
||||
presetsListEl.querySelectorAll('.pattern-button').forEach((btn) => btn.classList.remove('active'));
|
||||
}
|
||||
button.classList.add('active');
|
||||
selectedPresets[tabId] = presetId;
|
||||
selectedPresets[zoneId] = presetId;
|
||||
const section = row.closest('.presets-section');
|
||||
sendSelectForCurrentTabDevices(presetId, section).catch((err) => {
|
||||
console.error(err);
|
||||
@@ -1828,7 +1828,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
|
||||
row.addEventListener('dragend', () => {
|
||||
row.classList.remove('dragging');
|
||||
const presetsListEl = document.getElementById('presets-list-tab');
|
||||
const presetsListEl = document.getElementById('presets-list-zone');
|
||||
if (presetsListEl) {
|
||||
delete presetsListEl.dataset.dropTargetId;
|
||||
}
|
||||
@@ -1854,7 +1854,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (isDraggingPreset) return;
|
||||
editPresetFromTab(presetId, tabId, preset);
|
||||
editPresetFromTab(presetId, zoneId, preset);
|
||||
});
|
||||
|
||||
actions.appendChild(editBtn);
|
||||
@@ -1864,7 +1864,7 @@ const createPresetButton = (presetId, preset, tabId, isSelected = false) => {
|
||||
return row;
|
||||
};
|
||||
|
||||
const editPresetFromTab = async (presetId, tabId, existingPreset) => {
|
||||
const editPresetFromTab = async (presetId, zoneId, existingPreset) => {
|
||||
try {
|
||||
let preset = existingPreset;
|
||||
if (!preset) {
|
||||
@@ -1880,7 +1880,7 @@ const editPresetFromTab = async (presetId, tabId, existingPreset) => {
|
||||
|
||||
// Dispatch a custom event to trigger the edit in the DOMContentLoaded scope
|
||||
const editEvent = new CustomEvent('editPreset', {
|
||||
detail: { presetId, preset, tabId }
|
||||
detail: { presetId, preset, zoneId }
|
||||
});
|
||||
document.dispatchEvent(editEvent);
|
||||
} catch (error) {
|
||||
@@ -1889,36 +1889,36 @@ const editPresetFromTab = async (presetId, tabId, existingPreset) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Remove a preset from a specific tab (does not delete the preset itself)
|
||||
// Expected call style: removePresetFromTab(tabId, presetId)
|
||||
const removePresetFromTab = async (tabId, presetId) => {
|
||||
if (!tabId) {
|
||||
// Try to get tab ID from the left-panel
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
tabId = leftPanel ? leftPanel.dataset.tabId : null;
|
||||
// Remove a preset from a specific zone (does not delete the preset itself)
|
||||
// Expected call style: removePresetFromTab(zoneId, presetId)
|
||||
const removePresetFromTab = async (zoneId, presetId) => {
|
||||
if (!zoneId) {
|
||||
// Try to get zone ID from the left-panel
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
zoneId = leftPanel ? leftPanel.dataset.zoneId : null;
|
||||
|
||||
if (!tabId) {
|
||||
if (!zoneId) {
|
||||
// Fallback: try to get from URL
|
||||
const pathParts = window.location.pathname.split('/');
|
||||
const tabIndex = pathParts.indexOf('tabs');
|
||||
const tabIndex = pathParts.indexOf('zones');
|
||||
if (tabIndex !== -1 && tabIndex + 1 < pathParts.length) {
|
||||
tabId = pathParts[tabIndex + 1];
|
||||
zoneId = pathParts[tabIndex + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!tabId) {
|
||||
alert('Could not determine current tab.');
|
||||
if (!zoneId) {
|
||||
alert('Could not determine current zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Get current tab data
|
||||
const tabResponse = await fetch(`/tabs/${tabId}`, {
|
||||
// Get current zone data
|
||||
const tabResponse = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!tabResponse.ok) {
|
||||
throw new Error('Failed to load tab');
|
||||
throw new Error('Failed to load zone');
|
||||
}
|
||||
const tabData = await tabResponse.json();
|
||||
|
||||
@@ -1937,7 +1937,7 @@ const removePresetFromTab = async (tabId, presetId) => {
|
||||
const beforeLen = flat.length;
|
||||
flat = flat.filter(id => String(id) !== String(presetId));
|
||||
if (flat.length === beforeLen) {
|
||||
alert('Preset is not in this tab.');
|
||||
alert('Preset is not in this zone.');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1945,19 +1945,19 @@ const removePresetFromTab = async (tabId, presetId) => {
|
||||
tabData.presets = newGrid;
|
||||
tabData.presets_flat = flat;
|
||||
|
||||
const updateResponse = await fetch(`/tabs/${tabId}`, {
|
||||
const updateResponse = await fetch(`/zones/${zoneId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(tabData),
|
||||
});
|
||||
if (!updateResponse.ok) {
|
||||
throw new Error('Failed to update tab presets');
|
||||
throw new Error('Failed to update zone presets');
|
||||
}
|
||||
|
||||
await renderTabPresets(tabId);
|
||||
await renderTabPresets(zoneId);
|
||||
} catch (error) {
|
||||
console.error('Failed to remove preset from tab:', error);
|
||||
alert('Failed to remove preset from tab.');
|
||||
console.error('Failed to remove preset from zone:', error);
|
||||
alert('Failed to remove preset from zone.');
|
||||
}
|
||||
};
|
||||
try {
|
||||
@@ -1966,13 +1966,13 @@ try {
|
||||
|
||||
// Listen for HTMX swaps to render presets
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
if (event.target && event.target.id === 'tab-content') {
|
||||
// Get tab ID from the left-panel
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
if (event.target && event.target.id === 'zone-content') {
|
||||
// Get zone ID from the left-panel
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
if (leftPanel) {
|
||||
const tabId = leftPanel.dataset.tabId;
|
||||
if (tabId) {
|
||||
renderTabPresets(tabId);
|
||||
const zoneId = leftPanel.dataset.zoneId;
|
||||
if (zoneId) {
|
||||
renderTabPresets(zoneId);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1993,9 +1993,9 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
const mainMenu = document.getElementById('main-menu-dropdown');
|
||||
if (mainMenu) mainMenu.classList.remove('open');
|
||||
const leftPanel = document.querySelector('.presets-section[data-tab-id]');
|
||||
const leftPanel = document.querySelector('.presets-section[data-zone-id]');
|
||||
if (leftPanel) {
|
||||
renderTabPresets(leftPanel.dataset.tabId);
|
||||
renderTabPresets(leftPanel.dataset.zoneId);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,8 +35,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
};
|
||||
|
||||
const refreshTabsForActiveProfile = async () => {
|
||||
// Clear stale current tab so tab controller falls back to first tab of applied profile.
|
||||
document.cookie = "current_tab=; path=/; max-age=0";
|
||||
// Clear stale current zone so zone controller falls back to first zone of applied profile.
|
||||
document.cookie = "current_zone=; path=/; max-age=0";
|
||||
|
||||
if (window.tabsManager && typeof window.tabsManager.loadTabs === "function") {
|
||||
await window.tabsManager.loadTabs();
|
||||
@@ -231,7 +231,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
name,
|
||||
seed_dj_tab: !!(newProfileSeedDjInput && newProfileSeedDjInput.checked),
|
||||
seed_dj_zone: !!(newProfileSeedDjInput && newProfileSeedDjInput.checked),
|
||||
}),
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -203,7 +203,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
.zones-container {
|
||||
background-color: transparent;
|
||||
padding: 0.5rem 0;
|
||||
flex: 1;
|
||||
@@ -213,7 +213,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs-list {
|
||||
.zones-list {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
overflow-x: auto;
|
||||
@@ -222,7 +222,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
.zone-button {
|
||||
padding: 0.5rem 1rem;
|
||||
background-color: #3a3a3a;
|
||||
color: white;
|
||||
@@ -234,16 +234,16 @@ body.preset-ui-run .edit-mode-only {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
.zone-button:hover {
|
||||
background-color: #4a4a4a;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
.zone-button.active {
|
||||
background-color: #6a5acd;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.zone-content {
|
||||
flex: 1;
|
||||
display: block;
|
||||
overflow-y: auto;
|
||||
@@ -255,7 +255,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-brightness-group {
|
||||
.zone-brightness-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
@@ -263,7 +263,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.tab-brightness-group label {
|
||||
.zone-brightness-group label {
|
||||
white-space: nowrap;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
@@ -509,8 +509,8 @@ body.preset-ui-run .edit-mode-only {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Tab preset selecting area: 3 columns, vertical scroll only */
|
||||
#presets-list-tab {
|
||||
/* Zone preset selecting area: 3 columns, vertical scroll only */
|
||||
#presets-list-zone {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
@@ -750,8 +750,8 @@ body.preset-ui-run .edit-mode-only {
|
||||
background-color: #5a4f9f;
|
||||
}
|
||||
|
||||
/* Preset select buttons inside the tab grid */
|
||||
#presets-list-tab .pattern-button {
|
||||
/* Preset select buttons inside the zone grid */
|
||||
#presets-list-zone .pattern-button {
|
||||
display: flex;
|
||||
}
|
||||
.pattern-button .pattern-button-label {
|
||||
@@ -966,12 +966,12 @@ body.preset-ui-run .edit-mode-only {
|
||||
padding: 0.4rem 0.7rem;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
.zones-container {
|
||||
padding: 0.5rem 0;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.zone-content {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
@@ -1064,24 +1064,24 @@ body.preset-ui-run .edit-mode-only {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.tab-modal-create-row {
|
||||
.zone-modal-create-row {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tab-modal-create-row input[type="text"] {
|
||||
.zone-modal-create-row input[type="text"] {
|
||||
flex: 1;
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
.tab-devices-label {
|
||||
.zone-devices-label {
|
||||
display: block;
|
||||
margin-top: 0.75rem;
|
||||
margin-bottom: 0.35rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.tab-devices-editor {
|
||||
.zone-devices-editor {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
@@ -1090,12 +1090,12 @@ body.preset-ui-run .edit-mode-only {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.tab-device-row-label {
|
||||
.zone-device-row-label {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.tab-device-add-select {
|
||||
.zone-device-add-select {
|
||||
flex: 1;
|
||||
min-width: 10rem;
|
||||
padding: 0.5rem;
|
||||
@@ -1105,19 +1105,19 @@ body.preset-ui-run .edit-mode-only {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-devices-add {
|
||||
.zone-devices-add {
|
||||
margin-top: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-presets-section-label {
|
||||
.zone-presets-section-label {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 0.35rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.edit-tab-presets-scroll {
|
||||
.edit-zone-presets-scroll {
|
||||
max-height: 200px;
|
||||
overflow-y: auto;
|
||||
margin-bottom: 1rem;
|
||||
@@ -1195,7 +1195,7 @@ body.preset-ui-run .edit-mode-only {
|
||||
}
|
||||
/* Presets list: 3 columns and vertical scroll (defined above); mobile same */
|
||||
@media (max-width: 800px) {
|
||||
#presets-list-tab {
|
||||
#presets-list-zone {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
@@ -1234,8 +1234,8 @@ body.preset-ui-run .edit-mode-only {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
/* Tab content placeholder (no tab selected) */
|
||||
.tab-content-placeholder {
|
||||
/* Zone content placeholder (no zone selected) */
|
||||
.zone-content-placeholder {
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
color: #aaa;
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
/* General tab styles */
|
||||
/* General zone styles */
|
||||
.tabs {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.tab {
|
||||
.zone {
|
||||
padding: 10px 20px;
|
||||
margin: 0 10px;
|
||||
cursor: pointer;
|
||||
@@ -15,23 +15,23 @@
|
||||
transition: background-color 0.3s ease;
|
||||
}
|
||||
|
||||
.tab:hover {
|
||||
.zone:hover {
|
||||
background-color: #ddd;
|
||||
}
|
||||
|
||||
.tab.active {
|
||||
.zone.active {
|
||||
background-color: #ccc;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
.zone-content {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.tab-pane {
|
||||
.zone-pane {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tab-pane.active {
|
||||
.zone-pane.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
let selectedIndex = null;
|
||||
|
||||
const getTab = async (tabId) => {
|
||||
const response = await fetch(`/tabs/${tabId}`, {
|
||||
const getTab = async (zoneId) => {
|
||||
const response = await fetch(`/zones/${zoneId}`, {
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('No tab found');
|
||||
throw new Error('No zone found');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
const saveTabColors = async (tabId, colors) => {
|
||||
const response = await fetch(`/tabs/${tabId}`, {
|
||||
const saveTabColors = async (zoneId, colors) => {
|
||||
const response = await fetch(`/zones/${zoneId}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ colors }),
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to save tab colors');
|
||||
throw new Error('Failed to save zone colors');
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
@@ -101,23 +101,23 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
|
||||
const initTabPalette = async () => {
|
||||
const paletteContainer = document.getElementById('color-palette');
|
||||
const addButton = document.getElementById('tab-color-add-btn');
|
||||
const addFromPaletteButton = document.getElementById('tab-color-add-from-palette-btn');
|
||||
const colorInput = document.getElementById('tab-color-input');
|
||||
const addButton = document.getElementById('zone-color-add-btn');
|
||||
const addFromPaletteButton = document.getElementById('zone-color-add-from-palette-btn');
|
||||
const colorInput = document.getElementById('zone-color-input');
|
||||
|
||||
if (!paletteContainer || !addButton || !colorInput) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tabId = paletteContainer.dataset.tabId;
|
||||
if (!tabId) {
|
||||
const zoneId = paletteContainer.dataset.zoneId;
|
||||
if (!zoneId) {
|
||||
renderPalette(paletteContainer, []);
|
||||
return;
|
||||
}
|
||||
|
||||
let tabData;
|
||||
try {
|
||||
tabData = await getTab(tabId);
|
||||
tabData = await getTab(zoneId);
|
||||
} catch (error) {
|
||||
renderPalette(paletteContainer, []);
|
||||
return;
|
||||
@@ -134,7 +134,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
try {
|
||||
const updated = colors.filter((_, i) => i !== index);
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
const saved = await saveTabColors(zoneId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = null;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
@@ -152,7 +152,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
const updated = [...colors];
|
||||
const [moved] = updated.splice(fromIndex, 1);
|
||||
updated.splice(toIndex, 0, moved);
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
const saved = await saveTabColors(zoneId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = toIndex;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
@@ -169,7 +169,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
const updated = [...colors];
|
||||
updated[index] = newColor;
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
const saved = await saveTabColors(zoneId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = index;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
@@ -192,7 +192,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
}
|
||||
try {
|
||||
const updated = [...colors, newColor];
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
const saved = await saveTabColors(zoneId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = colors.length - 1;
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
@@ -229,7 +229,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
try {
|
||||
if (!colors.includes(picked)) {
|
||||
const updated = [...colors, picked];
|
||||
const saved = await saveTabColors(tabId, updated);
|
||||
const saved = await saveTabColors(zoneId, updated);
|
||||
colors = saved.colors || updated;
|
||||
selectedIndex = colors.indexOf(picked);
|
||||
renderPalette(paletteContainer, colors, onColorChange, onRemoveColor, onReorder);
|
||||
@@ -252,7 +252,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
};
|
||||
|
||||
document.body.addEventListener('htmx:afterSwap', (event) => {
|
||||
if (event.target && event.target.id === 'tab-content') {
|
||||
if (event.target && event.target.id === 'zone-content') {
|
||||
selectedIndex = null;
|
||||
initTabPalette();
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user