Files
ledgrab/server/src/wled_controller/templates/modals/css-editor.html
T
alexei.dolgolyov 492bdb95e3 feat: game integration system
Receive real-time events from games (CS2, Dota 2, LoL, etc.) and drive
LED effects through the existing color strip and value source pipelines.

Core:
- GameEventBus (thread-safe pub/sub) with standardized 23-type event vocabulary
- GameAdapter ABC + AdapterRegistry + MappingAdapter (YAML-driven)
- Built-in adapters: CS2 GSI, Dota 2 GSI, LoL Live Client, Generic Webhook
- Community YAML adapters: Minecraft, Valorant, Rocket League
- GameEventColorStripStream with 5 effects (flash/pulse/sweep/color_shift/breathing)
- GameEventValueSource with EMA smoothing and timeout
- 4 built-in effect presets (FPS Combat, MOBA Health, Racing, Generic Alert)
- Auto-setup for Valve GSI games (Steam path detection, cfg file writing)
- Demo capture engine exposed to non-demo mode

Frontend:
- Game tab in Streams tree navigation with integration cards
- Game integration editor modal with adapter picker, config fields, event mappings
- game_event source type in CSS and ValueSource editors
- Setup instructions overlay (markdown rendered)
- Live event monitor and connection test

API:
- Full CRUD for game integrations
- Event ingestion endpoint (adapter-level auth)
- Adapter metadata, presets, auto-setup, status/diagnostics endpoints
2026-03-31 13:17:52 +03:00

778 lines
60 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- Color Strip Source Editor Modal -->
<div id="css-editor-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="css-editor-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="css-editor-title"><svg class="icon" viewBox="0 0 24 24"><rect width="18" height="18" x="3" y="3" rx="2"/><path d="M7 3v18"/><path d="M3 7.5h4"/><path d="M3 12h18"/><path d="M3 16.5h4"/><path d="M17 3v18"/><path d="M17 7.5h4"/><path d="M17 16.5h4"/></svg> <span data-i18n="color_strip.add">Add</span></h2>
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<form id="css-editor-form">
<input type="hidden" id="css-editor-id">
<div class="form-group">
<label for="css-editor-name" data-i18n="color_strip.name">Name:</label>
<input type="text" id="css-editor-name" data-i18n-placeholder="color_strip.name.placeholder" placeholder="Wall Strip" required>
<div id="css-tags-container"></div>
</div>
<div id="css-editor-type-group" class="form-group">
<div class="label-row">
<label for="css-editor-type" data-i18n="color_strip.type">Type:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.type.hint">Picture Source derives colors from a screen capture. Static Color fills all LEDs with one constant color. Gradient distributes a color gradient across all LEDs.</small>
<select id="css-editor-type" onchange="onCSSTypeChange()">
<option value="picture" data-i18n="color_strip.type.picture">Picture Source</option>
<option value="picture_advanced" data-i18n="color_strip.type.picture_advanced">Multi-Monitor</option>
<option value="static" data-i18n="color_strip.type.static">Static Color</option>
<option value="gradient" data-i18n="color_strip.type.gradient">Gradient</option>
<option value="color_cycle" data-i18n="color_strip.type.color_cycle">Color Cycle</option>
<option value="effect" data-i18n="color_strip.type.effect">Procedural Effect</option>
<option value="composite" data-i18n="color_strip.type.composite">Composite</option>
<option value="mapped" data-i18n="color_strip.type.mapped">Mapped</option>
<option value="audio" data-i18n="color_strip.type.audio">Audio Reactive</option>
<option value="api_input" data-i18n="color_strip.type.api_input">API Input</option>
<option value="notification" data-i18n="color_strip.type.notification">Notification</option>
<option value="daylight" data-i18n="color_strip.type.daylight">Daylight Cycle</option>
<option value="candlelight" data-i18n="color_strip.type.candlelight">Candlelight</option>
<option value="weather" data-i18n="color_strip.type.weather">Weather</option>
<option value="processed" data-i18n="color_strip.type.processed">Processed</option>
<option value="key_colors" data-i18n="color_strip.type.key_colors">Key Colors</option>
<option value="game_event" data-i18n="color_strip.type.game_event">Game Event</option>
</select>
</div>
<!-- Picture-source-specific fields -->
<div id="css-editor-picture-section">
<div id="css-editor-picture-source-group" class="form-group">
<div class="label-row">
<label for="css-editor-picture-source" data-i18n="color_strip.picture_source">Picture Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.picture_source.hint">Which screen capture source to use as input for LED color calculation</small>
<select id="css-editor-picture-source"></select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-interpolation" data-i18n="color_strip.interpolation">Color Mode:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.interpolation.hint">How to calculate LED color from sampled border pixels</small>
<select id="css-editor-interpolation">
<option value="average" data-i18n="color_strip.interpolation.average">Average</option>
<option value="median" data-i18n="color_strip.interpolation.median">Median</option>
<option value="dominant" data-i18n="color_strip.interpolation.dominant">Dominant</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.smoothing">Smoothing:</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.smoothing.hint">Temporal blending between frames (0=none, 1=full). Reduces flicker.</small>
<div id="css-editor-smoothing-container"></div>
</div>
</div>
<!-- Static-color-specific fields -->
<div id="css-editor-static-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-color" data-i18n="color_strip.static_color">Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.static_color.hint">The solid color that will be sent to all LEDs on the strip.</small>
<div id="css-editor-color-container"></div>
</div>
</div>
<!-- Color-cycle-specific fields -->
<div id="css-editor-color-cycle-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.color_cycle.colors">Colors:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.color_cycle.colors.hint">Colors to cycle through smoothly. At least 2 required.</small>
<div id="color-cycle-colors-list"></div>
</div>
</div>
<!-- Gradient-specific fields -->
<div id="css-editor-gradient-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-gradient-preset" data-i18n="color_strip.gradient.select">Gradient:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.select.hint">Select a gradient from the library. Create and edit gradients in the Gradients tab.</small>
<select id="css-editor-gradient-preset">
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-gradient-easing" data-i18n="color_strip.gradient.easing">Easing:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.gradient.easing.hint">Controls how colors blend between stops.</small>
<select id="css-editor-gradient-easing">
<option value="linear" data-i18n="color_strip.gradient.easing.linear">Linear</option>
<option value="ease_in_out" data-i18n="color_strip.gradient.easing.ease_in_out">Smooth (S-curve)</option>
<option value="step" data-i18n="color_strip.gradient.easing.step">Step</option>
<option value="cubic" data-i18n="color_strip.gradient.easing.cubic">Cubic</option>
</select>
</div>
</div>
<!-- Procedural effect fields -->
<div id="css-editor-effect-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-effect-type" data-i18n="color_strip.effect.type">Effect:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.type.hint">The procedural effect algorithm to use.</small>
<select id="css-editor-effect-type" onchange="onEffectTypeChange()">
<option value="fire" data-i18n="color_strip.effect.fire">Fire</option>
<option value="meteor" data-i18n="color_strip.effect.meteor">Meteor</option>
<option value="plasma" data-i18n="color_strip.effect.plasma">Plasma</option>
<option value="noise" data-i18n="color_strip.effect.noise">Perlin Noise</option>
<option value="aurora" data-i18n="color_strip.effect.aurora">Aurora</option>
<option value="rain" data-i18n="color_strip.effect.rain">Rain</option>
<option value="comet" data-i18n="color_strip.effect.comet">Comet</option>
<option value="bouncing_ball" data-i18n="color_strip.effect.bouncing_ball">Bouncing Ball</option>
<option value="fireworks" data-i18n="color_strip.effect.fireworks">Fireworks</option>
<option value="sparkle_rain" data-i18n="color_strip.effect.sparkle_rain">Sparkle Rain</option>
<option value="lava_lamp" data-i18n="color_strip.effect.lava_lamp">Lava Lamp</option>
<option value="wave_interference" data-i18n="color_strip.effect.wave_interference">Wave Interference</option>
</select>
<small id="css-editor-effect-type-desc" class="field-desc"></small>
</div>
<div id="css-editor-effect-palette-group" class="form-group">
<div class="label-row">
<label for="css-editor-effect-palette" data-i18n="color_strip.effect.palette">Palette:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.palette.hint">Color palette used by the effect.</small>
<select id="css-editor-effect-palette">
</select>
</div>
<div id="css-editor-effect-color-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-color" data-i18n="color_strip.effect.color">Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.color.hint">Head color for the meteor effect.</small>
<div id="css-editor-effect-color-container"></div>
</div>
<div id="css-editor-effect-intensity-group" class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.effect.intensity">Intensity:</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.intensity.hint">Effect-specific intensity.</small>
<div id="css-editor-effect-intensity-container"></div>
</div>
<div id="css-editor-effect-scale-group" class="form-group" style="display:none">
<div class="label-row">
<label>
<span data-i18n="color_strip.effect.scale">Scale:</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.scale.hint">Spatial zoom level.</small>
<div id="css-editor-effect-scale-container"></div>
</div>
<div id="css-editor-effect-mirror-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-effect-mirror" data-i18n="color_strip.effect.mirror">Mirror:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.effect.mirror.hint">Bounce back and forth instead of wrapping around.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-effect-mirror">
<span class="settings-toggle-slider"></span>
</label>
</div>
</div>
<!-- Composite-specific fields -->
<div id="css-editor-composite-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.composite.layers">Layers:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.composite.layers.hint">Stack multiple color strip sources. First layer is the bottom, last is the top.</small>
<div id="composite-layers-list"></div>
<button type="button" class="btn btn-secondary" onclick="compositeAddLayer()" data-i18n="color_strip.composite.add_layer">+ Add Layer</button>
</div>
</div>
<!-- Mapped-specific fields -->
<div id="css-editor-mapped-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.mapped.zones">Zones:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.mapped.zones.hint">Each zone maps a color strip source to a specific LED range. Zones are placed side-by-side.</small>
<div id="mapped-zones-list"></div>
<button type="button" class="btn btn-secondary" onclick="mappedAddZone()" data-i18n="color_strip.mapped.add_zone">+ Add Zone</button>
</div>
</div>
<!-- Audio-reactive fields -->
<div id="css-editor-audio-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-viz" data-i18n="color_strip.audio.visualization">Visualization:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.visualization.hint">How audio data is rendered to LEDs.</small>
<select id="css-editor-audio-viz" onchange="onAudioVizChange()">
<option value="spectrum" data-i18n="color_strip.audio.viz.spectrum">Spectrum Analyzer</option>
<option value="beat_pulse" data-i18n="color_strip.audio.viz.beat_pulse">Beat Pulse</option>
<option value="vu_meter" data-i18n="color_strip.audio.viz.vu_meter">VU Meter</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-audio-source" data-i18n="color_strip.audio.source">Audio Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.source.hint">Mono audio source that provides audio data for this visualization. Create and manage audio sources in the Sources tab.</small>
<select id="css-editor-audio-source">
<!-- populated dynamically from /api/v1/audio-sources?source_type=mono -->
</select>
</div>
<div class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.audio.sensitivity">Sensitivity:</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.sensitivity.hint">Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.</small>
<div id="css-editor-audio-sensitivity-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.audio.smoothing">Smoothing:</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.smoothing.hint">Temporal smoothing between frames. Higher values produce smoother but slower-reacting visuals.</small>
<div id="css-editor-audio-smoothing-container"></div>
</div>
<div id="css-editor-audio-palette-group" class="form-group">
<div class="label-row">
<label for="css-editor-audio-palette" data-i18n="color_strip.audio.palette">Palette:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.palette.hint">Color palette used for spectrum bars or beat pulse coloring.</small>
<select id="css-editor-audio-palette">
<option value="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</option>
<option value="fire" data-i18n="color_strip.palette.fire">Fire</option>
<option value="ocean" data-i18n="color_strip.palette.ocean">Ocean</option>
<option value="lava" data-i18n="color_strip.palette.lava">Lava</option>
<option value="forest" data-i18n="color_strip.palette.forest">Forest</option>
<option value="aurora" data-i18n="color_strip.palette.aurora">Aurora</option>
<option value="sunset" data-i18n="color_strip.palette.sunset">Sunset</option>
<option value="ice" data-i18n="color_strip.palette.ice">Ice</option>
</select>
</div>
<div id="css-editor-audio-color-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-color" data-i18n="color_strip.audio.color">Base Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.color.hint">Low-level color for VU meter bar.</small>
<div id="css-editor-audio-color-container"></div>
</div>
<div id="css-editor-audio-color-peak-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-color-peak" data-i18n="color_strip.audio.color_peak">Peak Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.color_peak.hint">High-level color at the top of the VU meter bar.</small>
<div id="css-editor-audio-color-peak-container"></div>
</div>
<div id="css-editor-audio-mirror-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-audio-mirror" data-i18n="color_strip.audio.mirror">Mirror:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.audio.mirror.hint">Mirror spectrum from center outward: bass in the middle, treble at the edges.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-audio-mirror">
<span class="settings-toggle-slider"></span>
</label>
</div>
</div>
<!-- API Input fields -->
<div id="css-editor-api-input-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-api-input-fallback-color" data-i18n="color_strip.api_input.fallback_color">Fallback Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.fallback_color.hint">Color to display when no data has been received within the timeout period.</small>
<div id="css-editor-api-input-fallback-color-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.api_input.timeout">Timeout (seconds):</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.timeout.hint">How long to wait for new color data before reverting to the fallback color.</small>
<div id="css-editor-api-input-timeout-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-api-input-interpolation" data-i18n="color_strip.api_input.interpolation">LED Interpolation:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.interpolation.hint">How to resize incoming LED data when its count differs from the device's LED count.</small>
<select id="css-editor-api-input-interpolation">
<option value="linear">Linear</option>
<option value="nearest">Nearest</option>
<option value="none">None</option>
</select>
</div>
<div class="form-group" id="css-editor-api-input-endpoints-group">
<div class="label-row">
<label data-i18n="color_strip.api_input.endpoints">Push Endpoints:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.api_input.endpoints.hint">Use these URLs to push LED color data from your external application.</small>
<div id="css-editor-api-input-endpoints" class="template-config" style="font-family:monospace; font-size:0.85em; word-break:break-all;"></div>
</div>
</div>
<!-- Notification-specific fields -->
<div id="css-editor-notification-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-notification-os-listener" data-i18n="color_strip.notification.os_listener">Listen to OS Notifications:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.os_listener.hint">When enabled, this source automatically fires when a desktop notification appears (Windows toast / Linux D-Bus). Requires the app to have notification access permission.</small>
<label class="toggle-switch">
<input type="checkbox" id="css-editor-notification-os-listener">
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-notification-effect" data-i18n="color_strip.notification.effect">Effect:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.effect.hint">Visual effect when a notification fires.</small>
<select id="css-editor-notification-effect">
<option value="flash" data-i18n="color_strip.notification.effect.flash">Flash</option>
<option value="pulse" data-i18n="color_strip.notification.effect.pulse">Pulse</option>
<option value="sweep" data-i18n="color_strip.notification.effect.sweep">Sweep</option>
<option value="chase" data-i18n="color_strip.notification.effect.chase">Chase</option>
<option value="gradient_flash" data-i18n="color_strip.notification.effect.gradient_flash">Gradient Flash</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.notification.duration">Duration (ms):</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.duration.hint">How long the notification effect plays, in milliseconds.</small>
<div id="css-editor-notification-duration-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-notification-default-color" data-i18n="color_strip.notification.default_color">Default Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.default_color.hint">Color used when the notification has no app-specific color mapping.</small>
<div id="css-editor-notification-default-color-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-notification-filter-mode" data-i18n="color_strip.notification.filter_mode">App Filter:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.filter_mode.hint">Filter notifications by app name. Off = accept all, Whitelist = only listed apps, Blacklist = all except listed apps.</small>
<select id="css-editor-notification-filter-mode" onchange="onNotificationFilterModeChange()">
<option value="off" data-i18n="color_strip.notification.filter_mode.off">Off</option>
<option value="whitelist" data-i18n="color_strip.notification.filter_mode.whitelist">Whitelist</option>
<option value="blacklist" data-i18n="color_strip.notification.filter_mode.blacklist">Blacklist</option>
</select>
</div>
<div id="css-editor-notification-filter-list-group" class="form-group" style="display:none">
<div class="label-row">
<label data-i18n="color_strip.notification.filter_list">App List:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.filter_list.hint">One app name per line. Use Browse to pick from running processes.</small>
<div class="condition-field" id="css-editor-notification-filter-picker-container">
<div class="condition-apps-header">
<button type="button" class="btn btn-icon btn-secondary btn-browse-apps" data-i18n-title="automations.condition.application.browse" title="Browse"><svg class="icon" viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg></button>
</div>
<textarea id="css-editor-notification-filter-list" class="condition-apps" rows="3" data-i18n-placeholder="color_strip.notification.filter_list.placeholder" placeholder="Discord&#10;Slack&#10;Telegram"></textarea>
</div>
</div>
<details class="form-collapse">
<summary data-i18n="color_strip.notification.sound">Sound</summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label for="css-editor-notification-sound" data-i18n="color_strip.notification.sound.asset">Sound Asset:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.sound.asset.hint">Pick a sound asset to play when a notification fires. Leave empty for silent.</small>
<select id="css-editor-notification-sound">
<option value="" data-i18n="color_strip.notification.sound.none">None (silent)</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label>
<span data-i18n="color_strip.notification.sound.volume">Volume:</span>
</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.sound.volume.hint">Global volume for notification sounds (0100%).</small>
<div id="css-editor-notification-volume-container"></div>
</div>
</div>
</details>
<details class="form-collapse">
<summary data-i18n="color_strip.notification.app_overrides">Per-App Overrides</summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.notification.app_overrides.label">App Overrides:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.app_overrides.hint">Per-app overrides for color and sound. Each row can set a custom color, sound asset, and volume for a specific app.</small>
<div id="notification-app-overrides-list"></div>
<button type="button" class="btn btn-secondary" onclick="notificationAddAppOverride()" data-i18n="color_strip.notification.app_overrides.add">+ Add Override</button>
</div>
</div>
</details>
<div class="form-group" id="css-editor-notification-endpoint-group">
<div class="label-row">
<label data-i18n="color_strip.notification.endpoint">Webhook Endpoint:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.notification.endpoint.hint">Use this URL to trigger notifications from external systems.</small>
<div id="css-editor-notification-endpoint" class="template-config" style="font-family:monospace; font-size:0.85em; word-break:break-all;"></div>
</div>
</div>
<!-- Daylight Cycle section -->
<div id="css-editor-daylight-section" style="display:none">
<div id="css-editor-daylight-speed-group" class="form-group">
<div class="label-row">
<label for="css-editor-daylight-speed"><span data-i18n="color_strip.daylight.speed">Speed:</span> <span id="css-editor-daylight-speed-val">1.0</span></label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.daylight.speed.hint">Cycle speed multiplier. 1.0 = full day/night cycle in ~4 minutes. Higher values cycle faster.</small>
<input type="range" id="css-editor-daylight-speed" min="0.1" max="10" step="0.1" value="1.0"
oninput="document.getElementById('css-editor-daylight-speed-val').textContent = parseFloat(this.value).toFixed(1)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-daylight-real-time" data-i18n="color_strip.daylight.use_real_time">Use Real Time:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.daylight.use_real_time.hint">When enabled, LED color matches the actual time of day. Speed is ignored.</small>
<label class="toggle-switch">
<input type="checkbox" id="css-editor-daylight-real-time" onchange="onDaylightRealTimeChange()">
<span class="toggle-slider"></span>
</label>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-daylight-latitude"><span data-i18n="color_strip.daylight.latitude">Latitude:</span> <span id="css-editor-daylight-latitude-val">50</span>&deg;</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.daylight.latitude.hint">Your geographic latitude (-90 to 90). Affects sunrise/sunset timing in real-time mode.</small>
<input type="range" id="css-editor-daylight-latitude" min="-90" max="90" step="1" value="50"
oninput="document.getElementById('css-editor-daylight-latitude-val').textContent = parseInt(this.value)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-daylight-longitude"><span data-i18n="color_strip.daylight.longitude">Longitude:</span> <span id="css-editor-daylight-longitude-val">0</span>&deg;</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.daylight.longitude.hint">Your geographic longitude (-180 to 180). Adjusts solar noon offset for accurate sunrise/sunset timing.</small>
<input type="range" id="css-editor-daylight-longitude" min="-180" max="180" step="1" value="0"
oninput="document.getElementById('css-editor-daylight-longitude-val').textContent = parseInt(this.value)">
</div>
</div>
<!-- Candlelight section -->
<div id="css-editor-candlelight-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-candlelight-color" data-i18n="color_strip.candlelight.color">Base Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.candlelight.color.hint">The warm base color of the candle flame. Default is a natural warm amber.</small>
<div id="css-editor-candlelight-color-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.candlelight.intensity">Flicker Intensity:</span></label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.candlelight.intensity.hint">How much the candles flicker. Low values = gentle glow, high values = windy candle.</small>
<div id="css-editor-candlelight-intensity-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-candlelight-num-candles" data-i18n="color_strip.candlelight.num_candles_label">Number of Candles:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.candlelight.num_candles.hint">How many independent candle sources along the strip. Each flickers with its own pattern. More candles = more variation.</small>
<input type="number" id="css-editor-candlelight-num-candles" min="1" max="20" step="1" value="3">
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.candlelight.speed">Flicker Speed:</span></label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.candlelight.speed.hint">Speed of the flicker animation. Higher values produce faster, more restless flames.</small>
<div id="css-editor-candlelight-speed-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.candlelight.wind">Wind:</span></label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.candlelight.wind.hint">Wind simulation strength. Higher values create correlated gusts across all candles.</small>
<div id="css-editor-candlelight-wind-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-candlelight-type" data-i18n="color_strip.candlelight.type">Candle Type:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.candlelight.type.hint">Preset that adjusts flicker behavior without changing other settings.</small>
<select id="css-editor-candlelight-type">
<option value="default" data-i18n="color_strip.candlelight.type.default">Default</option>
<option value="taper" data-i18n="color_strip.candlelight.type.taper">Taper</option>
<option value="votive" data-i18n="color_strip.candlelight.type.votive">Votive</option>
<option value="bonfire" data-i18n="color_strip.candlelight.type.bonfire">Bonfire</option>
</select>
</div>
</div>
<!-- Processed type fields -->
<!-- Weather-specific fields -->
<div id="css-editor-weather-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-weather-source" data-i18n="color_strip.weather.source">Weather Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.weather.source.hint">The weather data source to use for ambient colors</small>
<select id="css-editor-weather-source">
<option value=""></option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.weather.speed">Animation Speed:</span></label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.weather.speed.hint">Speed of the ambient color drift animation</small>
<div id="css-editor-weather-speed-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.weather.temperature_influence">Temperature Influence:</span></label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.weather.temperature_influence.hint">How much the current temperature shifts the color palette warm/cool. 0 = pure condition colors, 1 = strong shift.</small>
<div id="css-editor-weather-temp-influence-container"></div>
</div>
</div>
<div id="css-editor-processed-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-processed-input" data-i18n="color_strip.processed.input">Source:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.processed.input.hint">The color strip source whose output will be processed</small>
<select id="css-editor-processed-input">
<option value=""></option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-processed-template" data-i18n="color_strip.processed.template">Processing Template:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.processed.template.hint">Filter chain to apply to the input source output</small>
<select id="css-editor-processed-template">
<option value=""></option>
</select>
</div>
</div>
<!-- Key Colors section -->
<div id="css-editor-key-colors-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-kc-picture-source" data-i18n="color_strip.key_colors.picture_source">Picture Source:</label>
</div>
<select id="css-editor-kc-picture-source"></select>
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-kc-interpolation" data-i18n="color_strip.key_colors.interpolation">Color Mode:</label>
</div>
<select id="css-editor-kc-interpolation">
<option value="average">Average</option>
<option value="median">Median</option>
<option value="dominant">Dominant</option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.key_colors.smoothing">Smoothing:</span></label>
</div>
<div id="css-editor-kc-smoothing-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label><span data-i18n="color_strip.key_colors.brightness">Brightness:</span></label>
</div>
<div id="css-editor-kc-brightness-container"></div>
</div>
</div>
<!-- Game Event section -->
<div id="css-editor-game-event-section" style="display:none">
<div class="form-group">
<div class="label-row">
<label for="css-editor-game-integration" data-i18n="color_strip.game_event.integration">Game Integration:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.game_event.integration.hint">Select the game integration that provides events for this source.</small>
<select id="css-editor-game-integration">
<option value=""></option>
</select>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.game_event.idle_color">Idle Color:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.game_event.idle_color.hint">LED color when no game events are active.</small>
<div id="css-editor-game-event-idle-color-container"></div>
</div>
<div class="form-group">
<div class="label-row">
<label data-i18n="color_strip.game_event.event_mappings">Event Mappings:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.game_event.event_mappings.hint">Override or add event-to-effect mappings for this source. These supplement the integration-level mappings.</small>
<div class="gi-mapping-preset-row">
<select id="css-editor-ge-mapping-preset" onchange="onCSSGameMappingPresetChange()">
<option value="" data-i18n="game_integration.preset.select">Load preset...</option>
<option value="fps_combat" data-i18n="game_integration.preset.fps_combat">FPS Combat</option>
<option value="moba_health" data-i18n="game_integration.preset.moba_health">MOBA Health</option>
</select>
</div>
<div id="css-editor-ge-mappings-list" class="gi-mappings-container"></div>
<button type="button" class="btn btn-secondary btn-sm" onclick="addCSSGameMapping()" style="margin-top:6px">
<span data-i18n="game_integration.mapping.add">+ Add Mapping</span>
</button>
</div>
</div>
<!-- Shared LED count field -->
<div id="css-editor-led-count-group" class="form-group">
<div class="label-row">
<label for="css-editor-led-count" data-i18n="color_strip.led_count">LED Count:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.led_count.hint">Total number of LEDs on the strip. Set to 0 to auto-detect from calibration or device.</small>
<input type="number" id="css-editor-led-count" min="0" max="1500" step="1" value="0">
</div>
<!-- Animation — shown for static/gradient, hidden for picture -->
<div id="css-editor-animation-section" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-animation-type" data-i18n="color_strip.animation">Animation:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.animation.type.hint">The animation effect to apply. Available effects depend on source type. Select None to disable animation.</small>
<select id="css-editor-animation-type" onchange="onAnimationTypeChange()">
<!-- populated by onCSSTypeChange() -->
</select>
<small id="css-editor-animation-type-desc" class="field-desc"></small>
</div>
<!-- Sync Clock (shown for animated types: static, gradient, color_cycle, effect) -->
<div id="css-editor-clock-group" class="form-group" style="display:none">
<div class="label-row">
<label for="css-editor-clock" data-i18n="color_strip.clock">Sync Clock:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?" data-i18n-aria-label="aria.hint">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="color_strip.clock.hint">Optionally link to a sync clock to synchronize animation timing and speed with other sources</small>
<select id="css-editor-clock" onchange="onCSSClockChange()">
<option value="" data-i18n="common.none">None</option>
</select>
</div>
<div id="css-editor-error" class="error-message" style="display: none;"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeCSSEditorModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-secondary" onclick="previewCSSFromEditor()" data-i18n-title="color_strip.test.title" title="Test Preview"><svg class="icon" viewBox="0 0 24 24"><path d="M14 2v6a2 2 0 0 0 .245.96l5.51 10.08A2 2 0 0 1 18 22H6a2 2 0 0 1-1.755-2.96l5.51-10.08A2 2 0 0 0 10 8V2"/><path d="M6.453 15h11.094"/><path d="M8.5 2h7"/></svg></button>
<button class="btn btn-icon btn-primary" onclick="saveCSSEditor()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>