Files
wled-screen-controller-mixed/server/src/wled_controller/static/js/app.js
alexei.dolgolyov ff7b595032 Add cursor-tracking card glare and accent-linked background blobs
- Subtle radial glare follows cursor over card/template-card elements
  using a single document-level mousemove listener (event delegation)
- Ambient background blob colors now derive from the selected accent
  color with hue-shifted variants
- Glare intensity kept very subtle (3.5% dark / 12% light theme)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-12 11:13:55 +03:00

591 lines
18 KiB
JavaScript

/**
* Entry point — imports all modules, registers globals, initializes app.
*/
// Layer 0: state
import { apiKey, setApiKey, refreshInterval } from './core/state.js';
import { Modal } from './core/modal.js';
// Layer 1: api, i18n
import { loadServerInfo, loadDisplays, configureApiKey, startConnectionMonitor, stopConnectionMonitor } from './core/api.js';
import { t, initLocale, changeLocale } from './core/i18n.js';
// Layer 1.5: visual effects
import { initCardGlare } from './core/card-glare.js';
// Layer 2: ui
import {
toggleHint, lockBody, unlockBody, closeLightbox,
showToast, showConfirm, closeConfirmModal,
openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner,
} from './core/ui.js';
// Layer 3: displays, tutorials
import {
openDisplayPicker, closeDisplayPicker, selectDisplay, formatDisplayLabel,
} from './features/displays.js';
import {
startCalibrationTutorial, startDeviceTutorial, startGettingStartedTutorial,
startDashboardTutorial, startTargetsTutorial, startSourcesTutorial, startAutomationsTutorial,
closeTutorial, tutorialNext, tutorialPrev,
} from './features/tutorials.js';
// Layer 4: devices, dashboard, streams, kc-targets, pattern-templates, automations
import {
showSettings, closeDeviceSettingsModal, forceCloseDeviceSettingsModal,
saveDeviceSettings, updateBrightnessLabel, saveCardBrightness,
turnOffDevice, pingDevice, removeDevice, loadDevices,
updateSettingsBaudFpsHint, copyWsUrl,
} from './features/devices.js';
import {
loadDashboard, stopUptimeTimer,
dashboardToggleAutomation, dashboardStartTarget, dashboardStopTarget, dashboardStopAll,
dashboardPauseClock, dashboardResumeClock, dashboardResetClock,
toggleDashboardSection, changeDashboardPollInterval,
} from './features/dashboard.js';
import { startEventsWS, stopEventsWS } from './core/events-ws.js';
import { startEntityEventListeners } from './core/entity-events.js';
import {
startPerfPolling, stopPerfPolling,
} from './features/perf-charts.js';
import {
loadPictureSources, switchStreamTab,
showAddTemplateModal, editTemplate, closeTemplateModal, saveTemplate, deleteTemplate,
showTestTemplateModal, closeTestTemplateModal, onEngineChange, runTemplateTest,
showAddAudioTemplateModal, editAudioTemplate, closeAudioTemplateModal, saveAudioTemplate, deleteAudioTemplate,
cloneAudioTemplate, onAudioEngineChange,
showTestAudioTemplateModal, closeTestAudioTemplateModal, startAudioTemplateTest,
showAddStreamModal, editStream, closeStreamModal, saveStream, deleteStream,
onStreamTypeChange, onStreamDisplaySelected, onTestDisplaySelected,
showTestStreamModal, closeTestStreamModal, updateStreamTestDuration, runStreamTest,
showTestPPTemplateModal, closeTestPPTemplateModal, updatePPTestDuration, runPPTemplateTest,
showAddPPTemplateModal, editPPTemplate, closePPTemplateModal, savePPTemplate, deletePPTemplate,
addFilterFromSelect, toggleFilterExpand, removeFilter, moveFilter, updateFilterOption,
renderModalFilterList, updateCaptureDuration,
cloneStream, cloneCaptureTemplate, clonePPTemplate,
expandAllStreamSections, collapseAllStreamSections,
} from './features/streams.js';
import {
createKCTargetCard, testKCTarget, toggleKCTestAutoRefresh,
showKCEditor, closeKCEditorModal, forceCloseKCEditorModal, saveKCEditor,
deleteKCTarget, disconnectAllKCWebSockets,
updateKCBrightnessLabel, saveKCBrightness,
cloneKCTarget,
} from './features/kc-targets.js';
import {
createPatternTemplateCard,
showPatternTemplateEditor, closePatternTemplateModal, forceClosePatternTemplateModal,
savePatternTemplate, deletePatternTemplate,
renderPatternRectList, selectPatternRect, updatePatternRect,
addPatternRect, deleteSelectedPatternRect, removePatternRect,
capturePatternBackground,
clonePatternTemplate,
} from './features/pattern-templates.js';
import {
loadAutomations, openAutomationEditor, closeAutomationEditorModal,
saveAutomationEditor, addAutomationCondition,
toggleAutomationEnabled, cloneAutomation, deleteAutomation, copyWebhookUrl,
expandAllAutomationSections, collapseAllAutomationSections,
} from './features/automations.js';
import {
openScenePresetCapture, editScenePreset, saveScenePreset, closeScenePresetEditor,
activateScenePreset, recaptureScenePreset, cloneScenePreset, deleteScenePreset,
addSceneTarget, removeSceneTarget,
} from './features/scene-presets.js';
// Layer 5: device-discovery, targets
import {
onDeviceTypeChanged, updateBaudFpsHint, onSerialPortFocus,
showAddDevice, closeAddDeviceModal, scanForDevices, handleAddDevice,
} from './features/device-discovery.js';
import {
loadTargetsTab, switchTargetSubTab,
showTargetEditor, closeTargetEditorModal, forceCloseTargetEditorModal, saveTargetEditor,
startTargetProcessing, stopTargetProcessing,
stopAllLedTargets, stopAllKCTargets,
startTargetOverlay, stopTargetOverlay, deleteTarget,
cloneTarget, toggleLedPreview,
expandAllTargetSections, collapseAllTargetSections,
disconnectAllLedPreviewWS,
} from './features/targets.js';
// Layer 5: color-strip sources
import {
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
onCSSTypeChange, onEffectTypeChange, onAnimationTypeChange, onCSSClockChange, onDaylightRealTimeChange,
colorCycleAddColor, colorCycleRemoveColor,
compositeAddLayer, compositeRemoveLayer,
mappedAddZone, mappedRemoveZone,
onAudioVizChange,
applyGradientPreset,
cloneColorStrip,
copyEndpointUrl,
onNotificationFilterModeChange,
notificationAddAppColor, notificationRemoveAppColor,
testNotification,
} from './features/color-strips.js';
// Layer 5: audio sources
import {
showAudioSourceModal, closeAudioSourceModal, saveAudioSource,
editAudioSource, cloneAudioSource, deleteAudioSource,
testAudioSource, closeTestAudioSourceModal,
} from './features/audio-sources.js';
// Layer 5: value sources
import {
showValueSourceModal, closeValueSourceModal, saveValueSource,
editValueSource, cloneValueSource, deleteValueSource, onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource, closeTestValueSourceModal,
} from './features/value-sources.js';
// Layer 5: calibration
import {
showCalibration, closeCalibrationModal, forceCloseCalibrationModal, saveCalibration,
updateOffsetSkipLock, updateCalibrationPreview,
setStartPosition, toggleEdgeInputs, toggleDirection, toggleTestEdge,
showCSSCalibration, toggleCalibrationOverlay,
} from './features/calibration.js';
import {
showAdvancedCalibration, closeAdvancedCalibration, saveAdvancedCalibration,
addCalibrationLine, removeCalibrationLine, selectCalibrationLine, moveCalibrationLine,
updateCalibrationLine, resetCalibrationView,
} from './features/advanced-calibration.js';
// Layer 6: tabs, navigation, command palette, settings
import { switchTab, initTabs, startAutoRefresh, handlePopState } from './features/tabs.js';
import { navigateToCard } from './core/navigation.js';
import { openCommandPalette, closeCommandPalette, initCommandPalette } from './core/command-palette.js';
import {
openSettingsModal, closeSettingsModal, downloadBackup, handleRestoreFileSelected,
saveAutoBackupSettings, restoreSavedBackup, downloadSavedBackup, deleteSavedBackup,
} from './features/settings.js';
// ─── Register all HTML onclick / onchange / onfocus globals ───
Object.assign(window, {
// core / state (for inline script)
setApiKey,
// core / ui
toggleHint,
lockBody,
unlockBody,
closeLightbox,
showToast,
showConfirm,
closeConfirmModal,
openFullImageLightbox,
showOverlaySpinner,
hideOverlaySpinner,
// core / api + i18n
t,
configureApiKey,
loadServerInfo,
loadDisplays,
changeLocale,
// displays
openDisplayPicker,
closeDisplayPicker,
selectDisplay,
formatDisplayLabel,
// tutorials
startCalibrationTutorial,
startDeviceTutorial,
startGettingStartedTutorial,
startDashboardTutorial,
startTargetsTutorial,
startSourcesTutorial,
startAutomationsTutorial,
closeTutorial,
tutorialNext,
tutorialPrev,
// devices
showSettings,
closeDeviceSettingsModal,
forceCloseDeviceSettingsModal,
saveDeviceSettings,
updateBrightnessLabel,
saveCardBrightness,
turnOffDevice,
pingDevice,
removeDevice,
loadDevices,
updateSettingsBaudFpsHint,
copyWsUrl,
// dashboard
loadDashboard,
dashboardToggleAutomation,
dashboardStartTarget,
dashboardStopTarget,
dashboardStopAll,
dashboardPauseClock,
dashboardResumeClock,
dashboardResetClock,
toggleDashboardSection,
changeDashboardPollInterval,
stopUptimeTimer,
startPerfPolling,
stopPerfPolling,
// streams / capture templates / PP templates
loadPictureSources,
switchStreamTab,
expandAllStreamSections, collapseAllStreamSections,
showAddTemplateModal,
editTemplate,
closeTemplateModal,
saveTemplate,
deleteTemplate,
showTestTemplateModal,
closeTestTemplateModal,
onEngineChange,
runTemplateTest,
updateCaptureDuration,
showAddStreamModal,
editStream,
closeStreamModal,
saveStream,
deleteStream,
onStreamTypeChange,
onStreamDisplaySelected,
onTestDisplaySelected,
showTestStreamModal,
closeTestStreamModal,
updateStreamTestDuration,
runStreamTest,
showTestPPTemplateModal,
closeTestPPTemplateModal,
updatePPTestDuration,
runPPTemplateTest,
showAddPPTemplateModal,
editPPTemplate,
closePPTemplateModal,
savePPTemplate,
deletePPTemplate,
addFilterFromSelect,
toggleFilterExpand,
removeFilter,
moveFilter,
updateFilterOption,
renderModalFilterList,
cloneStream,
cloneCaptureTemplate,
clonePPTemplate,
showAddAudioTemplateModal,
editAudioTemplate,
closeAudioTemplateModal,
saveAudioTemplate,
deleteAudioTemplate,
cloneAudioTemplate,
onAudioEngineChange,
showTestAudioTemplateModal,
closeTestAudioTemplateModal,
startAudioTemplateTest,
// kc-targets
createKCTargetCard,
testKCTarget,
toggleKCTestAutoRefresh,
showKCEditor,
closeKCEditorModal,
forceCloseKCEditorModal,
saveKCEditor,
deleteKCTarget,
disconnectAllKCWebSockets,
updateKCBrightnessLabel,
saveKCBrightness,
cloneKCTarget,
// pattern-templates
createPatternTemplateCard,
showPatternTemplateEditor,
closePatternTemplateModal,
forceClosePatternTemplateModal,
savePatternTemplate,
deletePatternTemplate,
renderPatternRectList,
selectPatternRect,
updatePatternRect,
addPatternRect,
deleteSelectedPatternRect,
removePatternRect,
capturePatternBackground,
clonePatternTemplate,
// automations
loadAutomations,
openAutomationEditor,
closeAutomationEditorModal,
saveAutomationEditor,
addAutomationCondition,
toggleAutomationEnabled,
cloneAutomation,
deleteAutomation,
copyWebhookUrl,
expandAllAutomationSections,
collapseAllAutomationSections,
// scene presets
openScenePresetCapture,
editScenePreset,
saveScenePreset,
closeScenePresetEditor,
activateScenePreset,
recaptureScenePreset,
cloneScenePreset,
deleteScenePreset,
addSceneTarget,
removeSceneTarget,
// device-discovery
onDeviceTypeChanged,
updateBaudFpsHint,
onSerialPortFocus,
showAddDevice,
closeAddDeviceModal,
scanForDevices,
handleAddDevice,
// targets
loadTargetsTab,
switchTargetSubTab,
expandAllTargetSections, collapseAllTargetSections,
showTargetEditor,
closeTargetEditorModal,
forceCloseTargetEditorModal,
saveTargetEditor,
startTargetProcessing,
stopTargetProcessing,
stopAllLedTargets,
stopAllKCTargets,
startTargetOverlay,
stopTargetOverlay,
deleteTarget,
cloneTarget,
toggleLedPreview,
disconnectAllLedPreviewWS,
// color-strip sources
showCSSEditor,
closeCSSEditorModal,
forceCSSEditorClose,
saveCSSEditor,
deleteColorStrip,
onCSSTypeChange,
onEffectTypeChange,
onCSSClockChange,
onAnimationTypeChange,
onDaylightRealTimeChange,
colorCycleAddColor,
colorCycleRemoveColor,
compositeAddLayer,
compositeRemoveLayer,
mappedAddZone,
mappedRemoveZone,
onAudioVizChange,
applyGradientPreset,
cloneColorStrip,
copyEndpointUrl,
onNotificationFilterModeChange,
notificationAddAppColor, notificationRemoveAppColor,
testNotification,
// audio sources
showAudioSourceModal,
closeAudioSourceModal,
saveAudioSource,
editAudioSource,
cloneAudioSource,
deleteAudioSource,
testAudioSource,
closeTestAudioSourceModal,
// value sources
showValueSourceModal,
closeValueSourceModal,
saveValueSource,
editValueSource,
cloneValueSource,
deleteValueSource,
onValueSourceTypeChange,
onDaylightVSRealTimeChange,
addSchedulePoint,
testValueSource,
closeTestValueSourceModal,
// calibration
showCalibration,
closeCalibrationModal,
forceCloseCalibrationModal,
saveCalibration,
updateOffsetSkipLock,
updateCalibrationPreview,
setStartPosition,
toggleEdgeInputs,
toggleDirection,
toggleTestEdge,
showCSSCalibration,
toggleCalibrationOverlay,
// advanced calibration
showAdvancedCalibration,
closeAdvancedCalibration,
saveAdvancedCalibration,
addCalibrationLine,
removeCalibrationLine,
selectCalibrationLine,
moveCalibrationLine,
updateCalibrationLine,
resetCalibrationView,
// tabs / navigation / command palette
switchTab,
startAutoRefresh,
navigateToCard,
openCommandPalette,
closeCommandPalette,
// settings (backup / restore / auto-backup)
openSettingsModal,
closeSettingsModal,
downloadBackup,
handleRestoreFileSelected,
saveAutoBackupSettings,
restoreSavedBackup,
downloadSavedBackup,
deleteSavedBackup,
});
// ─── Global keyboard shortcuts ───
document.addEventListener('keydown', (e) => {
const tag = document.activeElement?.tagName;
const inInput = tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT';
// Command palette: Ctrl+K / Cmd+K (works even in inputs)
if ((e.ctrlKey || e.metaKey) && e.key === 'k' && !e.altKey && !e.shiftKey) {
e.preventDefault();
openCommandPalette();
return;
}
// 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' };
const tab = tabMap[e.key];
if (tab) {
e.preventDefault();
switchTab(tab);
return;
}
}
if (e.key === 'Escape') {
// Close in order: overlay lightboxes first, then modals via stack
if (document.getElementById('display-picker-lightbox').classList.contains('active')) {
closeDisplayPicker();
} else if (document.getElementById('image-lightbox').classList.contains('active')) {
closeLightbox();
} else {
Modal.closeTopmost();
}
}
});
// ─── Browser back/forward via hash routing ───
window.addEventListener('popstate', handlePopState);
// ─── Cleanup on page unload ───
window.addEventListener('beforeunload', () => {
if (refreshInterval) {
clearInterval(refreshInterval);
}
stopConnectionMonitor();
stopEventsWS();
disconnectAllKCWebSockets();
disconnectAllLedPreviewWS();
});
// ─── Initialization ───
document.addEventListener('DOMContentLoaded', async () => {
// Load API key from localStorage before anything that triggers API calls
setApiKey(localStorage.getItem('wled_api_key'));
// Initialize locale (dispatches languageChanged which may trigger API calls)
await initLocale();
// Restore active tab before showing content to avoid visible jump
initTabs();
// Show content now that translations are loaded and tabs are set
document.body.style.visibility = 'visible';
// Initialize card glare effect
initCardGlare();
// Set CSS variable for sticky header height (header now includes tab bar)
const headerEl = document.querySelector('header');
if (headerEl) {
const updateHeaderHeight = () => {
const hh = headerEl.offsetHeight;
document.documentElement.style.setProperty('--header-height', hh + 'px');
document.documentElement.style.setProperty('--sticky-top', hh + 'px');
};
updateHeaderHeight();
window.addEventListener('resize', updateHeaderHeight);
}
// Scroll-to-top button visibility
const scrollBtn = document.getElementById('scroll-to-top');
if (scrollBtn) {
window.addEventListener('scroll', () => {
scrollBtn.classList.toggle('visible', window.scrollY > 300);
}, { passive: true });
}
// Initialize command palette
initCommandPalette();
// Setup form handler
document.getElementById('add-device-form').addEventListener('submit', handleAddDevice);
// Always monitor server connection (even before login)
loadServerInfo();
startConnectionMonitor();
// Show modal if no API key is stored
if (!apiKey) {
setTimeout(() => {
if (typeof window.showApiKeyModal === 'function') {
window.showApiKeyModal(null, true);
}
}, 100);
return;
}
// User is logged in, load data
loadDisplays();
loadTargetsTab();
// Start global events WebSocket and auto-refresh
startEventsWS();
startEntityEventListeners();
startAutoRefresh();
// Show getting-started tutorial on first visit
if (!localStorage.getItem('tour_completed')) {
setTimeout(() => startGettingStartedTutorial(), 600);
}
});