Add CSPT entity, processed CSS source type, reverse filter, and UI improvements

- Add Color Strip Processing Template (CSPT) entity: reusable filter chains
  for 1D LED strip postprocessing (backend, storage, API, frontend CRUD)
- Add "processed" color strip source type that wraps another CSS source and
  applies a CSPT filter chain (dataclass, stream, schema, modal, cards)
- Add Reverse filter for strip LED order reversal
- Add CSPT and processed CSS nodes/edges to visual graph editor
- Add CSPT test preview WS endpoint with input source selection
- Add device settings CSPT template selector (add + edit modals with hints)
- Use icon grids for palette quantization preset selector in filter lists
- Use EntitySelect for template references and test modal source selectors
- Fix filters.css_filter_template.desc missing localization
- Fix icon grid cell height inequality (grid-auto-rows: 1fr)
- Rename "Processed" subtab to "Processing Templates"
- Localize all new strings (en/ru/zh)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 02:16:59 +03:00
parent 7e78323c9c
commit 294d704eb0
72 changed files with 2992 additions and 1416 deletions

View File

@@ -178,6 +178,7 @@
{% include 'modals/test-pp-template.html' %}
{% include 'modals/stream.html' %}
{% include 'modals/pp-template.html' %}
{% include 'modals/cspt-modal.html' %}
{% include 'modals/automation-editor.html' %}
{% include 'modals/scene-preset-editor.html' %}
{% include 'modals/audio-source-editor.html' %}

View File

@@ -256,6 +256,16 @@
<option value="indicator">Indicator</option>
</select>
</div>
<div class="form-group" id="device-cspt-group">
<div class="label-row">
<label for="device-css-processing-template" data-i18n="device.css_processing_template">Strip Processing Template:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.css_processing_template.hint">Default processing template applied to all color strip outputs on this device</small>
<select id="device-css-processing-template">
<option value=""></option>
</select>
</div>
<div id="add-device-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -0,0 +1,40 @@
<!-- Color Strip Processing Template Modal -->
<div id="cspt-modal" class="modal" role="dialog" aria-modal="true" aria-labelledby="cspt-modal-title">
<div class="modal-content">
<div class="modal-header">
<h2 id="cspt-modal-title" data-i18n="css_processing.add">Add Strip Processing Template</h2>
<button class="modal-close-btn" onclick="closeCSPTModal()" title="Close" data-i18n-aria-label="aria.close">&#x2715;</button>
</div>
<div class="modal-body">
<input type="hidden" id="cspt-id">
<form id="cspt-form">
<div class="form-group">
<label for="cspt-name" data-i18n="css_processing.name">Template Name:</label>
<input type="text" id="cspt-name" data-i18n-placeholder="css_processing.name_placeholder" placeholder="My Strip Processing Template" required>
<div id="cspt-tags-container"></div>
</div>
<!-- Dynamic filter list -->
<div id="cspt-filter-list" class="pp-filter-list"></div>
<!-- Add filter control -->
<div class="pp-add-filter-row">
<select id="cspt-add-filter-select" class="pp-add-filter-select">
<option value="" data-i18n="filters.select_type">Select filter type...</option>
</select>
</div>
<div class="form-group">
<label for="cspt-description" data-i18n="css_processing.description_label">Description (optional):</label>
<input type="text" id="cspt-description" data-i18n-placeholder="css_processing.description_placeholder" placeholder="Describe this template...">
</div>
<div id="cspt-error" class="error-message" style="display: none;"></div>
</form>
</div>
<div class="modal-footer">
<button class="btn btn-icon btn-secondary" onclick="closeCSPTModal()" title="Cancel" data-i18n-aria-label="aria.cancel">&#x2715;</button>
<button class="btn btn-icon btn-primary" onclick="saveCSPT()" title="Save" data-i18n-aria-label="aria.save">&#x2713;</button>
</div>
</div>
</div>

View File

@@ -35,6 +35,7 @@
<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>
@@ -74,58 +75,6 @@
<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 class="form-group">
<div class="label-row">
<label for="css-editor-frame-interpolation" data-i18n="color_strip.frame_interpolation">Frame 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.frame_interpolation.hint">Blends between consecutive captured frames to produce output at the full target FPS even when capture rate is lower. Reduces visible stepping on slow ambient transitions.</small>
<label class="settings-toggle">
<input type="checkbox" id="css-editor-frame-interpolation">
<span class="settings-toggle-slider"></span>
</label>
</div>
<details class="form-collapse">
<summary data-i18n="color_strip.color_corrections">Color Corrections</summary>
<div class="form-collapse-body">
<div class="form-group">
<div class="label-row">
<label for="css-editor-brightness">
<span data-i18n="color_strip.brightness">Brightness:</span>
<span id="css-editor-brightness-value">1.00</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.brightness.hint">Output brightness multiplier (0=off, 1=unchanged, 2=double). Applied after color extraction.</small>
<input type="range" id="css-editor-brightness" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-brightness-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-saturation">
<span data-i18n="color_strip.saturation">Saturation:</span>
<span id="css-editor-saturation-value">1.00</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.saturation.hint">Color saturation (0=grayscale, 1=unchanged, 2=double saturation)</small>
<input type="range" id="css-editor-saturation" min="0.0" max="2.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-saturation-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
<div class="form-group">
<div class="label-row">
<label for="css-editor-gamma">
<span data-i18n="color_strip.gamma">Gamma:</span>
<span id="css-editor-gamma-value">1.00</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.gamma.hint">Gamma correction (1=none, &lt;1=brighter midtones, &gt;1=darker midtones)</small>
<input type="range" id="css-editor-gamma" min="0.1" max="3.0" step="0.05" value="1.0" oninput="document.getElementById('css-editor-gamma-value').textContent = parseFloat(this.value).toFixed(2)">
</div>
</div>
</details>
</div>
<!-- Static-color-specific fields -->
@@ -608,6 +557,30 @@
</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">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">

View File

@@ -165,6 +165,17 @@
</div>
</div>
<div class="form-group" id="settings-cspt-group">
<div class="label-row">
<label for="settings-css-processing-template" data-i18n="device.css_processing_template">Strip Processing Template:</label>
<button type="button" class="hint-toggle" onclick="toggleHint(this)" title="?">?</button>
</div>
<small class="input-hint" style="display:none" data-i18n="device.css_processing_template.hint">Default processing template applied to all color strip outputs on this device</small>
<select id="settings-css-processing-template">
<option value=""></option>
</select>
</div>
<div id="device-settings-error" class="error-message" style="display: none;"></div>
</form>
</div>

View File

@@ -44,6 +44,12 @@
<canvas id="css-test-layers-axis" class="css-test-strip-axis"></canvas>
</div>
<!-- CSPT test: input source selector (hidden by default) -->
<div id="css-test-cspt-input-group" style="display:none" class="css-test-led-control">
<label for="css-test-cspt-input-select" data-i18n="color_strip.processed.input">Input Source:</label>
<select id="css-test-cspt-input-select" class="css-test-cspt-select" onchange="applyCssTestSettings()"></select>
</div>
<!-- LED count & FPS controls -->
<div class="css-test-led-control">
<span id="css-test-led-group">