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 <noreply@anthropic.com>
This commit is contained in:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
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 = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
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'));
|
||||
});
|
||||
|
||||
@@ -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 = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllSceneSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllSceneSections()" title="${t('section.collapse_all')}">⊟</button></span></div>`;
|
||||
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 = `<button class="btn btn-sm btn-primary" onclick="event.stopPropagation(); openScenePresetCapture()" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
|
||||
const captureBtn = `<button class="btn btn-sm btn-primary dashboard-stop-all" onclick="event.stopPropagation(); openScenePresetCapture()" title="${t('scenes.capture')}">${ICON_CAPTURE} ${t('scenes.capture')}</button>`;
|
||||
const cards = presets.map(p => _renderDashboardPresetCard(p)).join('');
|
||||
|
||||
return { headerExtra: captureBtn, content: `<div class="dashboard-autostart-grid">${cards}</div>` };
|
||||
@@ -151,8 +103,8 @@ function _renderDashboardPresetCard(preset) {
|
||||
automationCount > 0 ? `${automationCount} ${t('scenes.automations_count')}` : null,
|
||||
].filter(Boolean).join(' \u00b7 ');
|
||||
|
||||
return `<div class="dashboard-target dashboard-scene-preset" data-scene-id="${preset.id}" style="${borderStyle}">
|
||||
<div class="dashboard-target-info" onclick="activateScenePreset('${preset.id}')">
|
||||
return `<div class="dashboard-target dashboard-scene-preset dashboard-card-link" data-scene-id="${preset.id}" style="${borderStyle}" onclick="if(!event.target.closest('button')){navigateToCard('automations',null,'scenes','data-scene-id','${preset.id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_SCENE}</span>
|
||||
<div>
|
||||
<div class="dashboard-target-name">${escapeHtml(preset.name)}</div>
|
||||
@@ -327,9 +279,9 @@ export async function deleteScenePreset(presetId) {
|
||||
// ===== Helpers =====
|
||||
|
||||
function _reloadScenesTab() {
|
||||
// Reload the scenes tab if it's active
|
||||
if ((localStorage.getItem('activeTab') || 'dashboard') === 'scenes') {
|
||||
loadScenes();
|
||||
// Reload automations tab (which includes scenes section)
|
||||
if ((localStorage.getItem('activeTab') || 'dashboard') === 'automations') {
|
||||
if (typeof window.loadAutomations === 'function') window.loadAutomations();
|
||||
}
|
||||
// Also refresh dashboard (scene presets section)
|
||||
if (typeof window.loadDashboard === 'function') window.loadDashboard(true);
|
||||
|
||||
@@ -60,8 +60,6 @@ export function switchTab(name, { updateHash = true, skipLoad = false } = {}) {
|
||||
if (typeof window.loadTargetsTab === 'function') window.loadTargetsTab();
|
||||
} else if (name === 'automations') {
|
||||
if (typeof window.loadAutomations === 'function') window.loadAutomations();
|
||||
} else if (name === 'scenes') {
|
||||
if (typeof window.loadScenes === 'function') window.loadScenes();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -82,8 +80,6 @@ export function initTabs() {
|
||||
saved = localStorage.getItem('activeTab');
|
||||
}
|
||||
|
||||
// Migrate legacy 'devices' tab to 'targets'
|
||||
if (saved === 'devices') saved = 'targets';
|
||||
if (!saved || !document.getElementById(`tab-${saved}`)) saved = 'dashboard';
|
||||
switchTab(saved);
|
||||
}
|
||||
|
||||
@@ -62,7 +62,10 @@ const sourcesTourSteps = [
|
||||
const automationsTutorialSteps = [
|
||||
{ selector: '[data-card-section="automations"]', textKey: 'tour.auto.list', position: 'bottom' },
|
||||
{ selector: '[data-cs-add="automations"]', textKey: 'tour.auto.add', position: 'bottom' },
|
||||
{ selector: '.card[data-automation-id]', textKey: 'tour.auto.card', position: 'bottom' }
|
||||
{ selector: '.card[data-automation-id]', textKey: 'tour.auto.card', position: 'bottom' },
|
||||
{ selector: '[data-card-section="scenes"]', textKey: 'tour.auto.scenes_list', position: 'bottom' },
|
||||
{ selector: '[data-cs-add="scenes"]', textKey: 'tour.auto.scenes_add', position: 'bottom' },
|
||||
{ selector: '.card[data-scene-id]', textKey: 'tour.auto.scenes_card', position: 'bottom' },
|
||||
];
|
||||
|
||||
const _fixedResolve = (step) => {
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
"tour.dash.stopped": "Stopped targets — ready to start with one click.",
|
||||
"tour.dash.automations": "Automations — active automation status and quick enable/disable toggle.",
|
||||
"tour.tgt.led_tab": "LED tab — standard LED strip targets with device and color strip configuration.",
|
||||
"tour.tgt.devices": "Devices — your WLED controllers discovered on the network.",
|
||||
"tour.tgt.devices": "Devices — your LED controllers discovered on the network.",
|
||||
"tour.tgt.css": "Color Strips — define how screen regions map to LED segments.",
|
||||
"tour.tgt.targets": "LED Targets — combine a device, color strip, and capture source for streaming.",
|
||||
"tour.tgt.kc_tab": "Key Colors — alternative target type using color-matching instead of pixel mapping.",
|
||||
@@ -249,6 +249,9 @@
|
||||
"tour.auto.list": "Automations — automate scene activation based on time, audio, or value conditions.",
|
||||
"tour.auto.add": "Click + to create a new automation with conditions and a scene to activate.",
|
||||
"tour.auto.card": "Each card shows automation status, conditions, and quick controls to edit or toggle.",
|
||||
"tour.auto.scenes_list": "Scenes — saved system states that automations can activate or you can apply manually.",
|
||||
"tour.auto.scenes_add": "Click + to capture the current system state as a new scene preset.",
|
||||
"tour.auto.scenes_card": "Each scene card shows target/device counts. Click to edit, recapture, or activate.",
|
||||
"calibration.tutorial.start": "Start tutorial",
|
||||
"calibration.overlay_toggle": "Overlay",
|
||||
"calibration.start_position": "Starting Position:",
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
"tour.dash.stopped": "Остановленные цели — готовы к запуску одним нажатием.",
|
||||
"tour.dash.automations": "Автоматизации — статус активных автоматизаций и быстрое включение/выключение.",
|
||||
"tour.tgt.led_tab": "LED — стандартные LED-цели с настройкой устройств и цветовых полос.",
|
||||
"tour.tgt.devices": "Устройства — ваши WLED-контроллеры, найденные в сети.",
|
||||
"tour.tgt.devices": "Устройства — ваши LED-контроллеры, найденные в сети.",
|
||||
"tour.tgt.css": "Цветовые полосы — определите, как области экрана соответствуют сегментам LED.",
|
||||
"tour.tgt.targets": "LED-цели — объедините устройство, цветовую полосу и источник захвата для стриминга.",
|
||||
"tour.tgt.kc_tab": "Key Colors — альтернативный тип цели с подбором цветов вместо пиксельного маппинга.",
|
||||
@@ -249,6 +249,9 @@
|
||||
"tour.auto.list": "Автоматизации — автоматизируйте активацию сцен по времени, звуку или значениям.",
|
||||
"tour.auto.add": "Нажмите + для создания новой автоматизации с условиями и сценой для активации.",
|
||||
"tour.auto.card": "Каждая карточка показывает статус автоматизации, условия и кнопки управления.",
|
||||
"tour.auto.scenes_list": "Сцены — сохранённые состояния системы, которые автоматизации могут активировать или вы можете применить вручную.",
|
||||
"tour.auto.scenes_add": "Нажмите + для захвата текущего состояния системы как нового пресета сцены.",
|
||||
"tour.auto.scenes_card": "Каждая карточка сцены показывает количество целей/устройств. Нажмите для редактирования, перезахвата или активации.",
|
||||
"calibration.tutorial.start": "Начать обучение",
|
||||
"calibration.overlay_toggle": "Оверлей",
|
||||
"calibration.start_position": "Начальная Позиция:",
|
||||
|
||||
@@ -236,7 +236,7 @@
|
||||
"tour.dash.stopped": "已停止的目标 — 一键启动。",
|
||||
"tour.dash.automations": "自动化 — 活动自动化状态和快速启用/禁用切换。",
|
||||
"tour.tgt.led_tab": "LED 标签 — 标准 LED 灯带目标,包含设备和色带配置。",
|
||||
"tour.tgt.devices": "设备 — 在网络中发现的 WLED 控制器。",
|
||||
"tour.tgt.devices": "设备 — 在网络中发现的 LED 控制器。",
|
||||
"tour.tgt.css": "色带 — 定义屏幕区域如何映射到 LED 段。",
|
||||
"tour.tgt.targets": "LED 目标 — 将设备、色带和捕获源组合进行流式传输。",
|
||||
"tour.tgt.kc_tab": "Key Colors — 使用颜色匹配代替像素映射的替代目标类型。",
|
||||
@@ -249,6 +249,9 @@
|
||||
"tour.auto.list": "自动化 — 基于时间、音频或数值条件自动激活场景。",
|
||||
"tour.auto.add": "点击 + 创建包含条件和要激活场景的新自动化。",
|
||||
"tour.auto.card": "每张卡片显示自动化状态、条件和快速编辑/切换控制。",
|
||||
"tour.auto.scenes_list": "场景 — 保存的系统状态,自动化可以激活或您可以手动应用。",
|
||||
"tour.auto.scenes_add": "点击 + 将当前系统状态捕获为新的场景预设。",
|
||||
"tour.auto.scenes_card": "每个场景卡片显示目标/设备数量。点击编辑、重新捕获或激活。",
|
||||
"calibration.tutorial.start": "开始教程",
|
||||
"calibration.overlay_toggle": "叠加层",
|
||||
"calibration.start_position": "起始位置:",
|
||||
|
||||
@@ -85,7 +85,6 @@
|
||||
<button class="tab-btn" data-tab="automations" onclick="switchTab('automations')" role="tab" aria-selected="false" aria-controls="tab-automations" id="tab-btn-automations" title="Ctrl+2"><svg class="icon" viewBox="0 0 24 24"><rect width="8" height="4" x="8" y="2" rx="1" ry="1"/><path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/><path d="M12 11h4"/><path d="M12 16h4"/><path d="M8 11h.01"/><path d="M8 16h.01"/></svg> <span data-i18n="automations.title">Automations</span><span class="tab-badge" id="tab-badge-automations" style="display:none"></span></button>
|
||||
<button class="tab-btn" data-tab="targets" onclick="switchTab('targets')" role="tab" aria-selected="false" aria-controls="tab-targets" id="tab-btn-targets" title="Ctrl+3"><svg class="icon" viewBox="0 0 24 24"><path d="M4 14a1 1 0 0 1-.78-1.63l9.9-10.2a.5.5 0 0 1 .86.46l-1.92 6.02A1 1 0 0 0 13 10h7a1 1 0 0 1 .78 1.63l-9.9 10.2a.5.5 0 0 1-.86-.46l1.92-6.02A1 1 0 0 0 11 14z"/></svg> <span data-i18n="targets.title">Targets</span><span class="tab-badge" id="tab-badge-targets" style="display:none"></span></button>
|
||||
<button class="tab-btn" data-tab="streams" onclick="switchTab('streams')" role="tab" aria-selected="false" aria-controls="tab-streams" id="tab-btn-streams" title="Ctrl+4"><svg class="icon" viewBox="0 0 24 24"><path d="m17 2-5 5-5-5"/><rect width="20" height="15" x="2" y="7" rx="2"/></svg> <span data-i18n="streams.title">Sources</span></button>
|
||||
<button class="tab-btn" data-tab="scenes" onclick="switchTab('scenes')" role="tab" aria-selected="false" aria-controls="tab-scenes" id="tab-btn-scenes" title="Ctrl+5"><svg class="icon" viewBox="0 0 24 24"><path d="M9.937 15.5A2 2 0 0 0 8.5 14.063l-6.135-1.582a.5.5 0 0 1 0-.962L8.5 9.936A2 2 0 0 0 9.937 8.5l1.582-6.135a.5.5 0 0 1 .963 0L14.063 8.5A2 2 0 0 0 15.5 9.937l6.135 1.581a.5.5 0 0 1 0 .964L15.5 14.063a2 2 0 0 0-1.437 1.437l-1.582 6.135a.5.5 0 0 1-.963 0z"/><path d="M20 3v4"/><path d="M22 5h-4"/><path d="M4 17v2"/><path d="M5 18H3"/></svg> <span data-i18n="scenes.title">Scenes</span><span class="tab-badge" id="tab-badge-scenes" style="display:none"></span></button>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-dashboard" role="tabpanel" aria-labelledby="tab-btn-dashboard">
|
||||
@@ -112,11 +111,6 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="tab-panel" id="tab-scenes" role="tabpanel" aria-labelledby="tab-btn-scenes">
|
||||
<div id="scenes-content">
|
||||
<div class="loading-spinner"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Apply saved tab immediately during parse to prevent visible jump
|
||||
|
||||
Reference in New Issue
Block a user