From 39b31aec34024dcd0657544fbedbc586e39a6df6 Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sat, 28 Feb 2026 18:16:21 +0300 Subject: [PATCH] Move Scenes into Automations tab, smaller Capture button, scene crosslinks - Merge Scenes tab into Automations tab as a second CardSection below automations - Make dashboard Capture button match Stop All sizing - Dashboard scene cards navigate to automations tab on click (crosslink) - Add scene steps to automations tutorial - Fix tour.tgt.devices to say "LED controllers" instead of "WLED controllers" - Update command palette and navigation for new scene location Co-Authored-By: Claude Opus 4.6 --- server/src/wled_controller/static/js/app.js | 8 +-- .../static/js/core/command-palette.js | 2 +- .../static/js/core/navigation.js | 1 - .../static/js/features/automations.js | 18 +++-- .../static/js/features/scene-presets.js | 66 +++---------------- .../static/js/features/tabs.js | 4 -- .../static/js/features/tutorials.js | 5 +- .../wled_controller/static/locales/en.json | 5 +- .../wled_controller/static/locales/ru.json | 5 +- .../wled_controller/static/locales/zh.json | 5 +- .../src/wled_controller/templates/index.html | 6 -- 11 files changed, 39 insertions(+), 86 deletions(-) diff --git a/server/src/wled_controller/static/js/app.js b/server/src/wled_controller/static/js/app.js index 16c3891..558497f 100644 --- a/server/src/wled_controller/static/js/app.js +++ b/server/src/wled_controller/static/js/app.js @@ -83,7 +83,6 @@ import { expandAllAutomationSections, collapseAllAutomationSections, } from './features/automations.js'; import { - loadScenes, expandAllSceneSections, collapseAllSceneSections, openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor, activateScenePreset, recaptureScenePreset, deleteScenePreset, } from './features/scene-presets.js'; @@ -312,9 +311,6 @@ Object.assign(window, { collapseAllAutomationSections, // scene presets - loadScenes, - expandAllSceneSections, - collapseAllSceneSections, openScenePresetCapture, editScenePreset, saveScenePreset, @@ -438,9 +434,9 @@ document.addEventListener('keydown', (e) => { return; } - // Tab shortcuts: Ctrl+1..5 (skip when typing in inputs) + // Tab shortcuts: Ctrl+1..4 (skip when typing in inputs) if (!inInput && e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) { - const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams', '5': 'scenes' }; + const tabMap = { '1': 'dashboard', '2': 'automations', '3': 'targets', '4': 'streams' }; const tab = tabMap[e.key]; if (tab) { e.preventDefault(); diff --git a/server/src/wled_controller/static/js/core/command-palette.js b/server/src/wled_controller/static/js/core/command-palette.js index c706263..69782a9 100644 --- a/server/src/wled_controller/static/js/core/command-palette.js +++ b/server/src/wled_controller/static/js/core/command-palette.js @@ -101,7 +101,7 @@ function _buildItems(results) { _mapEntities(scenePresets, sp => items.push({ name: sp.name, detail: sp.description || '', group: 'scenes', icon: ICON_SCENE, - nav: ['scenes', null, 'scenes', 'data-scene-id', sp.id], + nav: ['automations', null, 'scenes', 'data-scene-id', sp.id], })); return items; diff --git a/server/src/wled_controller/static/js/core/navigation.js b/server/src/wled_controller/static/js/core/navigation.js index 1ddadf0..d67bdde 100644 --- a/server/src/wled_controller/static/js/core/navigation.js +++ b/server/src/wled_controller/static/js/core/navigation.js @@ -90,7 +90,6 @@ function _triggerTabLoad(tab) { else if (tab === 'automations' && typeof window.loadAutomations === 'function') window.loadAutomations(); else if (tab === 'streams' && typeof window.loadPictureSources === 'function') window.loadPictureSources(); else if (tab === 'targets' && typeof window.loadTargetsTab === 'function') window.loadTargetsTab(); - else if (tab === 'scenes' && typeof window.loadScenes === 'function') window.loadScenes(); } function _showDimOverlay(duration) { diff --git a/server/src/wled_controller/static/js/features/automations.js b/server/src/wled_controller/static/js/features/automations.js index 87bf0f3..e72fb8a 100644 --- a/server/src/wled_controller/static/js/features/automations.js +++ b/server/src/wled_controller/static/js/features/automations.js @@ -10,6 +10,7 @@ import { Modal } from '../core/modal.js'; import { CardSection } from '../core/card-sections.js'; import { updateTabBadge } from './tabs.js'; import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js'; +import { csScenes, createSceneCard } from './scene-presets.js'; // ===== Scene presets cache (shared by both selectors) ===== let _scenesCache = []; @@ -80,22 +81,25 @@ export async function loadAutomations() { } export function expandAllAutomationSections() { - CardSection.expandAll([csAutomations]); + CardSection.expandAll([csAutomations, csScenes]); } export function collapseAllAutomationSections() { - CardSection.collapseAll([csAutomations]); + CardSection.collapseAll([csAutomations, csScenes]); } function renderAutomations(automations, sceneMap) { const container = document.getElementById('automations-content'); - const items = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) }))); - const toolbar = `
`; - container.innerHTML = toolbar + csAutomations.render(items); - csAutomations.bind(); + const autoItems = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) }))); + const sceneItems = csScenes.applySortOrder(_scenesCache.map(s => ({ key: s.id, html: createSceneCard(s) }))); - // Localize data-i18n elements within the automations container only + const toolbar = `
`; + container.innerHTML = toolbar + csAutomations.render(autoItems) + csScenes.render(sceneItems); + csAutomations.bind(); + csScenes.bind(); + + // Localize data-i18n elements within the container container.querySelectorAll('[data-i18n]').forEach(el => { el.textContent = t(el.getAttribute('data-i18n')); }); diff --git a/server/src/wled_controller/static/js/features/scene-presets.js b/server/src/wled_controller/static/js/features/scene-presets.js index 51a3015..6b00a62 100644 --- a/server/src/wled_controller/static/js/features/scene-presets.js +++ b/server/src/wled_controller/static/js/features/scene-presets.js @@ -1,22 +1,19 @@ /** * Scene Presets — capture, activate, edit, delete system state snapshots. - * Renders as a dedicated tab and also provides dashboard section rendering. + * Rendered as a CardSection inside the Automations tab, plus dashboard compact cards. */ -import { apiKey } from '../core/state.js'; import { fetchWithAuth, escapeHtml } from '../core/api.js'; import { t } from '../core/i18n.js'; import { showToast, showConfirm } from '../core/ui.js'; import { Modal } from '../core/modal.js'; import { CardSection } from '../core/card-sections.js'; -import { updateTabBadge } from './tabs.js'; import { ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET, ICON_SETTINGS, } from '../core/icons.js'; let _presetsCache = []; let _editingId = null; -let _scenesLoading = false; class ScenePresetEditorModal extends Modal { constructor() { super('scene-preset-editor-modal'); } @@ -30,59 +27,14 @@ class ScenePresetEditorModal extends Modal { } const scenePresetModal = new ScenePresetEditorModal(); -const csScenes = new CardSection('scenes', { +export const csScenes = new CardSection('scenes', { titleKey: 'scenes.title', gridClass: 'devices-grid', addCardOnclick: "openScenePresetCapture()", keyAttr: 'data-scene-id', }); -// Re-render scenes when language changes (only if tab is active) -document.addEventListener('languageChanged', () => { - if (apiKey && (localStorage.getItem('activeTab') || 'dashboard') === 'scenes') loadScenes(); -}); - -// ===== Tab rendering ===== - -export async function loadScenes() { - if (_scenesLoading) return; - _scenesLoading = true; - - try { - const resp = await fetchWithAuth('/scene-presets'); - if (!resp.ok) { _scenesLoading = false; return; } - const data = await resp.json(); - _presetsCache = data.presets || []; - } catch { - _scenesLoading = false; - return; - } - - const container = document.getElementById('scenes-content'); - const items = csScenes.applySortOrder(_presetsCache.map(p => ({ key: p.id, html: _createSceneCard(p) }))); - - updateTabBadge('scenes', _presetsCache.length); - - if (csScenes.isMounted()) { - csScenes.reconcile(items); - } else { - const toolbar = `
`; - container.innerHTML = toolbar + csScenes.render(items); - csScenes.bind(); - } - - _scenesLoading = false; -} - -export function expandAllSceneSections() { - CardSection.expandAll([csScenes]); -} - -export function collapseAllSceneSections() { - CardSection.collapseAll([csScenes]); -} - -function _createSceneCard(preset) { +export function createSceneCard(preset) { const targetCount = (preset.targets || []).length; const deviceCount = (preset.devices || []).length; const automationCount = (preset.automations || []).length; @@ -133,7 +85,7 @@ export async function loadScenePresets() { export function renderScenePresetsSection(presets) { if (!presets || presets.length === 0) return ''; - const captureBtn = ``; + const captureBtn = ``; const cards = presets.map(p => _renderDashboardPresetCard(p)).join(''); return { headerExtra: captureBtn, content: `
${cards}
` }; @@ -151,8 +103,8 @@ function _renderDashboardPresetCard(preset) { automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null, ].filter(Boolean).join(' \u00b7 '); - return `
-
+ return `