- Settings modal split into 3 tabs: General, Backup, MQTT - Log viewer moved to full-screen overlay with compact toolbar - External URL setting: API endpoints + UI for configuring server domain used in webhook/WS URLs instead of auto-detected local IP - Sources tab tree restructured: Picture Source (Screen Capture/Static/ Processed sub-groups), Color Strip, Audio, Utility - TreeNav extended to support nested groups (3-level tree) - Audio tab split into Sources and Templates sub-tabs - Fix audio template test: device picker now filters by engine type (was showing WASAPI indices for sounddevice templates) - Audio template test device picker disabled during active test - Rename "Input Source" to "Source" in CSS test preview (en/ru/zh) - Fix i18n: log filter/level items deferred to avoid stale t() calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
635 lines
51 KiB
HTML
635 lines
51 KiB
HTML
<!-- 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 Color Strip Source</span></h2>
|
|
<button class="modal-close-btn" onclick="closeCSSEditorModal()" title="Close" data-i18n-aria-label="aria.close">✕</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="processed" data-i18n="color_strip.type.processed">Processed</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 for="css-editor-smoothing">
|
|
<span data-i18n="color_strip.smoothing">Smoothing:</span>
|
|
<span id="css-editor-smoothing-value">0.30</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>
|
|
<input type="range" id="css-editor-smoothing" min="0.0" max="1.0" step="0.05" value="0.3" oninput="document.getElementById('css-editor-smoothing-value').textContent = parseFloat(this.value).toFixed(2)">
|
|
</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>
|
|
<input type="color" id="css-editor-color" value="#ffffff">
|
|
</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.preset">Preset:</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.preset.hint">Load a predefined gradient palette. Selecting a preset replaces the current stops.</small>
|
|
<select id="css-editor-gradient-preset" onchange="onGradientPresetChange(this.value)">
|
|
<option value="" data-i18n="color_strip.gradient.preset.custom">— Custom —</option>
|
|
<option value="rainbow" data-i18n="color_strip.gradient.preset.rainbow">Rainbow</option>
|
|
<option value="sunset" data-i18n="color_strip.gradient.preset.sunset">Sunset</option>
|
|
<option value="ocean" data-i18n="color_strip.gradient.preset.ocean">Ocean</option>
|
|
<option value="forest" data-i18n="color_strip.gradient.preset.forest">Forest</option>
|
|
<option value="fire" data-i18n="color_strip.gradient.preset.fire">Fire</option>
|
|
<option value="lava" data-i18n="color_strip.gradient.preset.lava">Lava</option>
|
|
<option value="aurora" data-i18n="color_strip.gradient.preset.aurora">Aurora</option>
|
|
<option value="ice" data-i18n="color_strip.gradient.preset.ice">Ice</option>
|
|
<option value="warm" data-i18n="color_strip.gradient.preset.warm">Warm</option>
|
|
<option value="cool" data-i18n="color_strip.gradient.preset.cool">Cool</option>
|
|
<option value="neon" data-i18n="color_strip.gradient.preset.neon">Neon</option>
|
|
<option value="pastel" data-i18n="color_strip.gradient.preset.pastel">Pastel</option>
|
|
</select>
|
|
<div style="margin-top:6px;">
|
|
<button type="button" class="btn btn-secondary btn-sm"
|
|
onclick="promptAndSaveGradientPreset()"
|
|
data-i18n="color_strip.gradient.preset.save_button">Save as preset…</button>
|
|
</div>
|
|
<div id="css-editor-custom-presets-list" class="custom-presets-list"></div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label data-i18n="color_strip.gradient.preview">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.preview.hint">Visual preview. Click the marker track below to add a stop. Drag markers to reposition.</small>
|
|
<div class="gradient-editor">
|
|
<canvas id="gradient-canvas" height="44"></canvas>
|
|
<div id="gradient-markers-track" class="gradient-markers-track"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label data-i18n="color_strip.gradient.stops">Color Stops:</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.stops.hint">Each stop defines a color at a relative position (0.0 = start, 1.0 = end). The ↔ button adds a right-side color to create a hard edge at that stop.</small>
|
|
<div id="gradient-stops-list"></div>
|
|
</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>
|
|
</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">
|
|
<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="rainbow" data-i18n="color_strip.palette.rainbow">Rainbow</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-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>
|
|
<input type="color" id="css-editor-effect-color" value="#ff5000" oninput="updateEffectPreview()">
|
|
</div>
|
|
|
|
<div id="css-editor-effect-intensity-group" class="form-group">
|
|
<div class="label-row">
|
|
<label for="css-editor-effect-intensity">
|
|
<span data-i18n="color_strip.effect.intensity">Intensity:</span>
|
|
<span id="css-editor-effect-intensity-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.effect.intensity.hint">Effect-specific intensity.</small>
|
|
<input type="range" id="css-editor-effect-intensity" min="0.1" max="2.0" step="0.1" value="1.0"
|
|
oninput="document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(this.value).toFixed(1)">
|
|
</div>
|
|
|
|
<div id="css-editor-effect-scale-group" class="form-group" style="display:none">
|
|
<div class="label-row">
|
|
<label for="css-editor-effect-scale">
|
|
<span data-i18n="color_strip.effect.scale">Scale:</span>
|
|
<span id="css-editor-effect-scale-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.effect.scale.hint">Spatial zoom level.</small>
|
|
<input type="range" id="css-editor-effect-scale" min="0.5" max="5.0" step="0.1" value="1.0"
|
|
oninput="document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(this.value).toFixed(1)">
|
|
</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 for="css-editor-audio-sensitivity">
|
|
<span data-i18n="color_strip.audio.sensitivity">Sensitivity:</span>
|
|
<span id="css-editor-audio-sensitivity-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.audio.sensitivity.hint">Gain multiplier for audio levels. Higher values make LEDs react to quieter sounds.</small>
|
|
<input type="range" id="css-editor-audio-sensitivity" min="0.1" max="5.0" step="0.1" value="1.0"
|
|
oninput="document.getElementById('css-editor-audio-sensitivity-val').textContent = parseFloat(this.value).toFixed(1)">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="css-editor-audio-smoothing">
|
|
<span data-i18n="color_strip.audio.smoothing">Smoothing:</span>
|
|
<span id="css-editor-audio-smoothing-val">0.30</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>
|
|
<input type="range" id="css-editor-audio-smoothing" min="0.0" max="1.0" step="0.05" value="0.3"
|
|
oninput="document.getElementById('css-editor-audio-smoothing-val').textContent = parseFloat(this.value).toFixed(2)">
|
|
</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>
|
|
<input type="color" id="css-editor-audio-color" value="#00ff00">
|
|
</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>
|
|
<input type="color" id="css-editor-audio-color-peak" value="#ff0000">
|
|
</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>
|
|
<input type="color" id="css-editor-api-input-fallback-color" value="#000000">
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="css-editor-api-input-timeout">
|
|
<span data-i18n="color_strip.api_input.timeout">Timeout (seconds):</span>
|
|
<span id="css-editor-api-input-timeout-val">5.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.api_input.timeout.hint">How long to wait for new color data before reverting to the fallback color.</small>
|
|
<input type="range" id="css-editor-api-input-timeout" min="0" max="60" step="0.5" value="5.0"
|
|
oninput="document.getElementById('css-editor-api-input-timeout-val').textContent = parseFloat(this.value).toFixed(1)">
|
|
</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-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>
|
|
</select>
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="css-editor-notification-duration">
|
|
<span data-i18n="color_strip.notification.duration">Duration (ms):</span>
|
|
<span id="css-editor-notification-duration-val">1500</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>
|
|
<input type="range" id="css-editor-notification-duration" min="100" max="10000" step="100" value="1500"
|
|
oninput="document.getElementById('css-editor-notification-duration-val').textContent = this.value">
|
|
</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>
|
|
<input type="color" id="css-editor-notification-default-color" value="#ffffff">
|
|
</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-browse-apps" data-i18n="automations.condition.application.browse">Browse</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 Slack Telegram"></textarea>
|
|
<div class="process-picker" style="display:none">
|
|
<input type="text" class="process-picker-search" data-i18n-placeholder="automations.condition.application.search" placeholder="Search..." autocomplete="off">
|
|
<div class="process-picker-list"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<details class="form-collapse">
|
|
<summary data-i18n="color_strip.notification.app_colors">App Colors</summary>
|
|
<div class="form-collapse-body">
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label data-i18n="color_strip.notification.app_colors.label">Color 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.notification.app_colors.hint">Per-app color overrides. Each row maps an app name to a specific color.</small>
|
|
<div id="notification-app-colors-list"></div>
|
|
<button type="button" class="btn btn-secondary" onclick="notificationAddAppColor()" data-i18n="color_strip.notification.app_colors.add">+ Add Mapping</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>°</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>
|
|
|
|
<!-- 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>
|
|
<input type="color" id="css-editor-candlelight-color" value="#ff9329">
|
|
</div>
|
|
<div class="form-group">
|
|
<div class="label-row">
|
|
<label for="css-editor-candlelight-intensity"><span data-i18n="color_strip.candlelight.intensity">Flicker Intensity:</span> <span id="css-editor-candlelight-intensity-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.candlelight.intensity.hint">How much the candles flicker. Low values = gentle glow, high values = windy candle.</small>
|
|
<input type="range" id="css-editor-candlelight-intensity" min="0.1" max="2.0" step="0.1" value="1.0"
|
|
oninput="document.getElementById('css-editor-candlelight-intensity-val').textContent = parseFloat(this.value).toFixed(1)">
|
|
</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 for="css-editor-candlelight-speed"><span data-i18n="color_strip.candlelight.speed">Flicker Speed:</span> <span id="css-editor-candlelight-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.candlelight.speed.hint">Speed of the flicker animation. Higher values produce faster, more restless flames.</small>
|
|
<input type="range" id="css-editor-candlelight-speed" min="0.1" max="5.0" step="0.1" value="1.0"
|
|
oninput="document.getElementById('css-editor-candlelight-speed-val').textContent = parseFloat(this.value).toFixed(1)">
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Processed type fields -->
|
|
<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>
|
|
|
|
<!-- 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">✕</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">✓</button>
|
|
</div>
|
|
</div>
|
|
</div>
|