|
|
|
|
@@ -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),
|
|
|
|
|
|