CSS: add StaticColorStripSource type with auto-sized LED count
Introduces a new 'static' source type that fills all device LEDs with a single constant RGB color — no screen capture or processing required. - StaticColorStripSource storage model (color + led_count=0 auto-size) - StaticColorStripStream: no background thread, configure() sizes to device LED count at processor start; hot-updates preserve runtime size - ColorStripStreamManager dispatches static sources (no LiveStream needed) - WledTargetProcessor calls stream.configure(device_led_count) on start - API schemas/routes: source_type Literal["picture","static"]; color field; overlay/calibration-test endpoints return 400 for static - Frontend: type selector modal, color picker, type-aware card rendering (🎨 icon + color swatch), LED count field hidden for static type - Locale keys: color_strip.type, color_strip.static_color (en + ru) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -90,6 +90,7 @@ import {
|
||||
// Layer 5: color-strip sources
|
||||
import {
|
||||
showCSSEditor, closeCSSEditorModal, forceCSSEditorClose, saveCSSEditor, deleteColorStrip,
|
||||
onCSSTypeChange,
|
||||
} from './features/color-strips.js';
|
||||
|
||||
// Layer 5: calibration
|
||||
@@ -274,6 +275,7 @@ Object.assign(window, {
|
||||
forceCSSEditorClose,
|
||||
saveCSSEditor,
|
||||
deleteColorStrip,
|
||||
onCSSTypeChange,
|
||||
|
||||
// calibration
|
||||
showCalibration,
|
||||
|
||||
@@ -13,8 +13,10 @@ class CSSEditorModal extends Modal {
|
||||
}
|
||||
|
||||
snapshotValues() {
|
||||
const type = document.getElementById('css-editor-type').value;
|
||||
return {
|
||||
name: document.getElementById('css-editor-name').value,
|
||||
type,
|
||||
picture_source: document.getElementById('css-editor-picture-source').value,
|
||||
fps: document.getElementById('css-editor-fps').value,
|
||||
interpolation: document.getElementById('css-editor-interpolation').value,
|
||||
@@ -22,39 +24,81 @@ class CSSEditorModal extends Modal {
|
||||
brightness: document.getElementById('css-editor-brightness').value,
|
||||
saturation: document.getElementById('css-editor-saturation').value,
|
||||
gamma: document.getElementById('css-editor-gamma').value,
|
||||
led_count: document.getElementById('css-editor-led-count').value,
|
||||
color: document.getElementById('css-editor-color').value,
|
||||
led_count: type === 'static' ? '0' : document.getElementById('css-editor-led-count').value,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
const cssEditorModal = new CSSEditorModal();
|
||||
|
||||
/* ── Type-switch helper ───────────────────────────────────────── */
|
||||
|
||||
export function onCSSTypeChange() {
|
||||
const type = document.getElementById('css-editor-type').value;
|
||||
document.getElementById('css-editor-picture-section').style.display = type === 'picture' ? '' : 'none';
|
||||
document.getElementById('css-editor-static-section').style.display = type === 'static' ? '' : 'none';
|
||||
// LED count is only meaningful for picture sources; static uses device LED count automatically
|
||||
document.getElementById('css-editor-led-count-group').style.display = type === 'static' ? 'none' : '';
|
||||
}
|
||||
|
||||
/** Convert an [R, G, B] array to a CSS hex color string like "#rrggbb". */
|
||||
function rgbArrayToHex(rgb) {
|
||||
if (!Array.isArray(rgb) || rgb.length !== 3) return '#ffffff';
|
||||
return '#' + rgb.map(v => Math.max(0, Math.min(255, v)).toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/** Convert a CSS hex string like "#rrggbb" to an [R, G, B] array. */
|
||||
function hexToRgbArray(hex) {
|
||||
const m = /^#?([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i.exec(hex);
|
||||
return m ? [parseInt(m[1], 16), parseInt(m[2], 16), parseInt(m[3], 16)] : [255, 255, 255];
|
||||
}
|
||||
|
||||
/* ── Card ─────────────────────────────────────────────────────── */
|
||||
|
||||
export function createColorStripCard(source, pictureSourceMap) {
|
||||
const srcName = (pictureSourceMap && pictureSourceMap[source.picture_source_id])
|
||||
? pictureSourceMap[source.picture_source_id].name
|
||||
: source.picture_source_id || '—';
|
||||
const cal = source.calibration || {};
|
||||
const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
|
||||
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
|
||||
const isStatic = source.source_type === 'static';
|
||||
|
||||
let propsHtml;
|
||||
if (isStatic) {
|
||||
const hexColor = rgbArrayToHex(source.color);
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop" title="${t('color_strip.static_color')}">
|
||||
<span style="display:inline-block;width:14px;height:14px;background:${hexColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${hexColor.toUpperCase()}
|
||||
</span>
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${source.led_count}</span>` : ''}
|
||||
`;
|
||||
} else {
|
||||
const srcName = (pictureSourceMap && pictureSourceMap[source.picture_source_id])
|
||||
? pictureSourceMap[source.picture_source_id].name
|
||||
: source.picture_source_id || '—';
|
||||
const cal = source.calibration || {};
|
||||
const calLeds = (cal.leds_top || 0) + (cal.leds_right || 0) + (cal.leds_bottom || 0) + (cal.leds_left || 0);
|
||||
const ledCount = (source.led_count > 0) ? source.led_count : calLeds;
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop" title="${t('color_strip.fps')}">⚡ ${source.fps || 30} fps</span>
|
||||
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${ledCount}</span>` : ''}
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${t('color_strip.picture_source')}">📺 ${escapeHtml(srcName)}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
const icon = isStatic ? '🎨' : '🎞️';
|
||||
const calibrationBtn = isStatic ? '' : `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">📐</button>`;
|
||||
|
||||
return `
|
||||
<div class="card" data-css-id="${source.id}">
|
||||
<button class="card-remove-btn" onclick="deleteColorStrip('${source.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="card-header">
|
||||
<div class="card-title">
|
||||
🎞️ ${escapeHtml(source.name)}
|
||||
${icon} ${escapeHtml(source.name)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('color_strip.fps')}">⚡ ${source.fps || 30} fps</span>
|
||||
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">💡 ${ledCount}</span>` : ''}
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${t('color_strip.picture_source')}">📺 ${escapeHtml(srcName)}</span>
|
||||
${propsHtml}
|
||||
</div>
|
||||
<div class="card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="showCSSEditor('${source.id}')" title="${t('common.edit')}">✏️</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">📐</button>
|
||||
${calibrationBtn}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -85,36 +129,46 @@ export async function showCSSEditor(cssId = null) {
|
||||
|
||||
document.getElementById('css-editor-id').value = css.id;
|
||||
document.getElementById('css-editor-name').value = css.name;
|
||||
sourceSelect.value = css.picture_source_id || '';
|
||||
|
||||
const fps = css.fps ?? 30;
|
||||
document.getElementById('css-editor-fps').value = fps;
|
||||
document.getElementById('css-editor-fps-value').textContent = fps;
|
||||
const sourceType = css.source_type || 'picture';
|
||||
document.getElementById('css-editor-type').value = sourceType;
|
||||
onCSSTypeChange();
|
||||
|
||||
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
|
||||
if (sourceType === 'static') {
|
||||
document.getElementById('css-editor-color').value = rgbArrayToHex(css.color);
|
||||
} else {
|
||||
sourceSelect.value = css.picture_source_id || '';
|
||||
|
||||
const smoothing = css.smoothing ?? 0.3;
|
||||
document.getElementById('css-editor-smoothing').value = smoothing;
|
||||
document.getElementById('css-editor-smoothing-value').textContent = parseFloat(smoothing).toFixed(2);
|
||||
const fps = css.fps ?? 30;
|
||||
document.getElementById('css-editor-fps').value = fps;
|
||||
document.getElementById('css-editor-fps-value').textContent = fps;
|
||||
|
||||
const brightness = css.brightness ?? 1.0;
|
||||
document.getElementById('css-editor-brightness').value = brightness;
|
||||
document.getElementById('css-editor-brightness-value').textContent = parseFloat(brightness).toFixed(2);
|
||||
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
|
||||
|
||||
const saturation = css.saturation ?? 1.0;
|
||||
document.getElementById('css-editor-saturation').value = saturation;
|
||||
document.getElementById('css-editor-saturation-value').textContent = parseFloat(saturation).toFixed(2);
|
||||
const smoothing = css.smoothing ?? 0.3;
|
||||
document.getElementById('css-editor-smoothing').value = smoothing;
|
||||
document.getElementById('css-editor-smoothing-value').textContent = parseFloat(smoothing).toFixed(2);
|
||||
|
||||
const gamma = css.gamma ?? 1.0;
|
||||
document.getElementById('css-editor-gamma').value = gamma;
|
||||
document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2);
|
||||
const brightness = css.brightness ?? 1.0;
|
||||
document.getElementById('css-editor-brightness').value = brightness;
|
||||
document.getElementById('css-editor-brightness-value').textContent = parseFloat(brightness).toFixed(2);
|
||||
|
||||
const saturation = css.saturation ?? 1.0;
|
||||
document.getElementById('css-editor-saturation').value = saturation;
|
||||
document.getElementById('css-editor-saturation-value').textContent = parseFloat(saturation).toFixed(2);
|
||||
|
||||
const gamma = css.gamma ?? 1.0;
|
||||
document.getElementById('css-editor-gamma').value = gamma;
|
||||
document.getElementById('css-editor-gamma-value').textContent = parseFloat(gamma).toFixed(2);
|
||||
}
|
||||
|
||||
document.getElementById('css-editor-led-count').value = css.led_count ?? 0;
|
||||
|
||||
document.getElementById('css-editor-title').textContent = t('color_strip.edit');
|
||||
} else {
|
||||
document.getElementById('css-editor-id').value = '';
|
||||
document.getElementById('css-editor-name').value = '';
|
||||
document.getElementById('css-editor-type').value = 'picture';
|
||||
onCSSTypeChange();
|
||||
document.getElementById('css-editor-fps').value = 30;
|
||||
document.getElementById('css-editor-fps-value').textContent = '30';
|
||||
document.getElementById('css-editor-interpolation').value = 'average';
|
||||
@@ -126,6 +180,7 @@ export async function showCSSEditor(cssId = null) {
|
||||
document.getElementById('css-editor-saturation-value').textContent = '1.00';
|
||||
document.getElementById('css-editor-gamma').value = 1.0;
|
||||
document.getElementById('css-editor-gamma-value').textContent = '1.00';
|
||||
document.getElementById('css-editor-color').value = '#ffffff';
|
||||
document.getElementById('css-editor-led-count').value = 0;
|
||||
document.getElementById('css-editor-title').textContent = t('color_strip.add');
|
||||
}
|
||||
@@ -150,23 +205,38 @@ export function isCSSEditorDirty() { return cssEditorModal.isDirty(); }
|
||||
export async function saveCSSEditor() {
|
||||
const cssId = document.getElementById('css-editor-id').value;
|
||||
const name = document.getElementById('css-editor-name').value.trim();
|
||||
const sourceType = document.getElementById('css-editor-type').value;
|
||||
|
||||
if (!name) {
|
||||
cssEditorModal.showError(t('color_strip.error.name_required'));
|
||||
return;
|
||||
}
|
||||
|
||||
const payload = {
|
||||
name,
|
||||
picture_source_id: document.getElementById('css-editor-picture-source').value,
|
||||
fps: parseInt(document.getElementById('css-editor-fps').value) || 30,
|
||||
interpolation_mode: document.getElementById('css-editor-interpolation').value,
|
||||
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
|
||||
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
||||
saturation: parseFloat(document.getElementById('css-editor-saturation').value),
|
||||
gamma: parseFloat(document.getElementById('css-editor-gamma').value),
|
||||
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
||||
};
|
||||
let payload;
|
||||
if (sourceType === 'static') {
|
||||
payload = {
|
||||
name,
|
||||
color: hexToRgbArray(document.getElementById('css-editor-color').value),
|
||||
};
|
||||
if (!cssId) {
|
||||
payload.source_type = 'static';
|
||||
}
|
||||
} else {
|
||||
payload = {
|
||||
name,
|
||||
picture_source_id: document.getElementById('css-editor-picture-source').value,
|
||||
fps: parseInt(document.getElementById('css-editor-fps').value) || 30,
|
||||
interpolation_mode: document.getElementById('css-editor-interpolation').value,
|
||||
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
|
||||
brightness: parseFloat(document.getElementById('css-editor-brightness').value),
|
||||
saturation: parseFloat(document.getElementById('css-editor-saturation').value),
|
||||
gamma: parseFloat(document.getElementById('css-editor-gamma').value),
|
||||
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
||||
};
|
||||
if (!cssId) {
|
||||
payload.source_type = 'picture';
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
let response;
|
||||
@@ -176,7 +246,6 @@ export async function saveCSSEditor() {
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
} else {
|
||||
payload.source_type = 'picture';
|
||||
response = await fetchWithAuth('/color-strip-sources', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(payload),
|
||||
|
||||
Reference in New Issue
Block a user