|
|
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
|
|
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION,
|
|
|
|
|
ICON_LED, ICON_PALETTE, ICON_FPS, ICON_MAP_PIN, ICON_MUSIC,
|
|
|
|
|
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
|
|
|
|
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK,
|
|
|
|
|
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK, ICON_BELL,
|
|
|
|
|
} from '../core/icons.js';
|
|
|
|
|
import { wrapCard } from '../core/card-colors.js';
|
|
|
|
|
|
|
|
|
|
@@ -56,6 +56,12 @@ class CSSEditorModal extends Modal {
|
|
|
|
|
audio_mirror: document.getElementById('css-editor-audio-mirror').checked,
|
|
|
|
|
api_input_fallback_color: document.getElementById('css-editor-api-input-fallback-color').value,
|
|
|
|
|
api_input_timeout: document.getElementById('css-editor-api-input-timeout').value,
|
|
|
|
|
notification_effect: document.getElementById('css-editor-notification-effect').value,
|
|
|
|
|
notification_duration: document.getElementById('css-editor-notification-duration').value,
|
|
|
|
|
notification_default_color: document.getElementById('css-editor-notification-default-color').value,
|
|
|
|
|
notification_filter_mode: document.getElementById('css-editor-notification-filter-mode').value,
|
|
|
|
|
notification_filter_list: document.getElementById('css-editor-notification-filter-list').value,
|
|
|
|
|
notification_app_colors: JSON.stringify(_notificationAppColors),
|
|
|
|
|
clock_id: document.getElementById('css-editor-clock').value,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
@@ -76,6 +82,7 @@ export function onCSSTypeChange() {
|
|
|
|
|
document.getElementById('css-editor-mapped-section').style.display = type === 'mapped' ? '' : 'none';
|
|
|
|
|
document.getElementById('css-editor-audio-section').style.display = type === 'audio' ? '' : 'none';
|
|
|
|
|
document.getElementById('css-editor-api-input-section').style.display = type === 'api_input' ? '' : 'none';
|
|
|
|
|
document.getElementById('css-editor-notification-section').style.display = type === 'notification' ? '' : 'none';
|
|
|
|
|
|
|
|
|
|
if (type === 'effect') onEffectTypeChange();
|
|
|
|
|
if (type === 'audio') onAudioVizChange();
|
|
|
|
|
@@ -576,6 +583,108 @@ function _resetAudioState() {
|
|
|
|
|
document.getElementById('css-editor-audio-mirror').checked = false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Notification helpers ────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
let _notificationAppColors = []; // [{app: '', color: '#...'}]
|
|
|
|
|
|
|
|
|
|
export function onNotificationFilterModeChange() {
|
|
|
|
|
const mode = document.getElementById('css-editor-notification-filter-mode').value;
|
|
|
|
|
document.getElementById('css-editor-notification-filter-list-group').style.display = mode === 'off' ? 'none' : '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _notificationAppColorsRenderList() {
|
|
|
|
|
const list = document.getElementById('notification-app-colors-list');
|
|
|
|
|
if (!list) return;
|
|
|
|
|
list.innerHTML = _notificationAppColors.map((entry, i) => `
|
|
|
|
|
<div class="color-cycle-item">
|
|
|
|
|
<input type="text" class="notif-app-name" data-idx="${i}" value="${escapeHtml(entry.app)}" placeholder="App name" style="flex:1">
|
|
|
|
|
<input type="color" class="notif-app-color" data-idx="${i}" value="${entry.color}">
|
|
|
|
|
<button type="button" class="btn btn-secondary color-cycle-remove-btn"
|
|
|
|
|
onclick="notificationRemoveAppColor(${i})">✕</button>
|
|
|
|
|
</div>
|
|
|
|
|
`).join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function notificationAddAppColor() {
|
|
|
|
|
_notificationAppColorsSyncFromDom();
|
|
|
|
|
_notificationAppColors.push({ app: '', color: '#ffffff' });
|
|
|
|
|
_notificationAppColorsRenderList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function notificationRemoveAppColor(i) {
|
|
|
|
|
_notificationAppColorsSyncFromDom();
|
|
|
|
|
_notificationAppColors.splice(i, 1);
|
|
|
|
|
_notificationAppColorsRenderList();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _notificationAppColorsSyncFromDom() {
|
|
|
|
|
const list = document.getElementById('notification-app-colors-list');
|
|
|
|
|
if (!list) return;
|
|
|
|
|
const names = list.querySelectorAll('.notif-app-name');
|
|
|
|
|
const colors = list.querySelectorAll('.notif-app-color');
|
|
|
|
|
if (names.length === _notificationAppColors.length) {
|
|
|
|
|
for (let i = 0; i < names.length; i++) {
|
|
|
|
|
_notificationAppColors[i].app = names[i].value;
|
|
|
|
|
_notificationAppColors[i].color = colors[i].value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _notificationGetAppColorsDict() {
|
|
|
|
|
_notificationAppColorsSyncFromDom();
|
|
|
|
|
const dict = {};
|
|
|
|
|
for (const entry of _notificationAppColors) {
|
|
|
|
|
if (entry.app.trim()) dict[entry.app.trim()] = entry.color;
|
|
|
|
|
}
|
|
|
|
|
return dict;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _loadNotificationState(css) {
|
|
|
|
|
document.getElementById('css-editor-notification-effect').value = css.notification_effect || 'flash';
|
|
|
|
|
const dur = css.duration_ms ?? 1500;
|
|
|
|
|
document.getElementById('css-editor-notification-duration').value = dur;
|
|
|
|
|
document.getElementById('css-editor-notification-duration-val').textContent = dur;
|
|
|
|
|
document.getElementById('css-editor-notification-default-color').value = css.default_color || '#ffffff';
|
|
|
|
|
document.getElementById('css-editor-notification-filter-mode').value = css.app_filter_mode || 'off';
|
|
|
|
|
document.getElementById('css-editor-notification-filter-list').value = (css.app_filter_list || []).join(', ');
|
|
|
|
|
onNotificationFilterModeChange();
|
|
|
|
|
|
|
|
|
|
// App colors dict → list
|
|
|
|
|
const ac = css.app_colors || {};
|
|
|
|
|
_notificationAppColors = Object.entries(ac).map(([app, color]) => ({ app, color }));
|
|
|
|
|
_notificationAppColorsRenderList();
|
|
|
|
|
|
|
|
|
|
_showNotificationEndpoint(css.id);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _resetNotificationState() {
|
|
|
|
|
document.getElementById('css-editor-notification-effect').value = 'flash';
|
|
|
|
|
document.getElementById('css-editor-notification-duration').value = 1500;
|
|
|
|
|
document.getElementById('css-editor-notification-duration-val').textContent = '1500';
|
|
|
|
|
document.getElementById('css-editor-notification-default-color').value = '#ffffff';
|
|
|
|
|
document.getElementById('css-editor-notification-filter-mode').value = 'off';
|
|
|
|
|
document.getElementById('css-editor-notification-filter-list').value = '';
|
|
|
|
|
onNotificationFilterModeChange();
|
|
|
|
|
_notificationAppColors = [];
|
|
|
|
|
_notificationAppColorsRenderList();
|
|
|
|
|
_showNotificationEndpoint(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function _showNotificationEndpoint(cssId) {
|
|
|
|
|
const el = document.getElementById('css-editor-notification-endpoint');
|
|
|
|
|
if (!el) return;
|
|
|
|
|
if (!cssId) {
|
|
|
|
|
el.innerHTML = `<em data-i18n="color_strip.notification.save_first">${t('color_strip.notification.save_first')}</em>`;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const base = `${window.location.origin}/api/v1`;
|
|
|
|
|
const url = `${base}/color-strip-sources/${cssId}/notify`;
|
|
|
|
|
el.innerHTML = `
|
|
|
|
|
<small class="endpoint-label">POST</small>
|
|
|
|
|
<div class="ws-url-row"><input type="text" value="${url}" readonly style="font-size:0.85em"><button type="button" class="btn btn-sm btn-secondary" onclick="copyEndpointUrl(this)" title="Copy">📋</button></div>
|
|
|
|
|
`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── Card ─────────────────────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|
|
|
|
@@ -587,6 +696,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|
|
|
|
const isMapped = source.source_type === 'mapped';
|
|
|
|
|
const isAudio = source.source_type === 'audio';
|
|
|
|
|
const isApiInput = source.source_type === 'api_input';
|
|
|
|
|
const isNotification = source.source_type === 'notification';
|
|
|
|
|
|
|
|
|
|
// Clock crosslink badge (replaces speed badge when clock is assigned)
|
|
|
|
|
const clockObj = source.clock_id ? _cachedSyncClocks.find(c => c.id === source.clock_id) : null;
|
|
|
|
|
@@ -692,6 +802,20 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|
|
|
|
</span>
|
|
|
|
|
<span class="stream-card-prop" title="${t('color_strip.api_input.timeout')}">${ICON_TIMER} ${timeoutVal}s</span>
|
|
|
|
|
`;
|
|
|
|
|
} else if (isNotification) {
|
|
|
|
|
const effectLabel = t('color_strip.notification.effect.' + (source.notification_effect || 'flash')) || source.notification_effect || 'flash';
|
|
|
|
|
const durationVal = source.duration_ms || 1500;
|
|
|
|
|
const defColor = source.default_color || '#FFFFFF';
|
|
|
|
|
const appCount = source.app_colors ? Object.keys(source.app_colors).length : 0;
|
|
|
|
|
propsHtml = `
|
|
|
|
|
<span class="stream-card-prop" title="${t('color_strip.notification.effect')}">${ICON_BELL} ${escapeHtml(effectLabel)}</span>
|
|
|
|
|
<span class="stream-card-prop" title="${t('color_strip.notification.duration')}">${ICON_TIMER} ${durationVal}ms</span>
|
|
|
|
|
<span class="stream-card-prop" title="${t('color_strip.notification.default_color')}">
|
|
|
|
|
<span style="display:inline-block;width:14px;height:14px;background:${defColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${defColor.toUpperCase()}
|
|
|
|
|
</span>
|
|
|
|
|
${appCount > 0 ? `<span class="stream-card-prop">${ICON_PALETTE} ${appCount} ${t('color_strip.notification.app_count')}</span>` : ''}
|
|
|
|
|
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
|
|
|
|
`;
|
|
|
|
|
} else {
|
|
|
|
|
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id];
|
|
|
|
|
const srcName = ps ? ps.name : source.picture_source_id || '—';
|
|
|
|
|
@@ -710,7 +834,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const icon = getColorStripIcon(source.source_type);
|
|
|
|
|
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput)
|
|
|
|
|
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput && !isNotification)
|
|
|
|
|
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
|
|
|
|
: '';
|
|
|
|
|
|
|
|
|
|
@@ -809,6 +933,8 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|
|
|
|
document.getElementById('css-editor-api-input-timeout-val').textContent =
|
|
|
|
|
parseFloat(css.timeout ?? 5.0).toFixed(1);
|
|
|
|
|
_showApiInputEndpoints(css.id);
|
|
|
|
|
} else if (sourceType === 'notification') {
|
|
|
|
|
_loadNotificationState(css);
|
|
|
|
|
} else {
|
|
|
|
|
sourceSelect.value = css.picture_source_id || '';
|
|
|
|
|
|
|
|
|
|
@@ -899,6 +1025,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|
|
|
|
document.getElementById('css-editor-api-input-timeout').value = 5.0;
|
|
|
|
|
document.getElementById('css-editor-api-input-timeout-val').textContent = '5.0';
|
|
|
|
|
_showApiInputEndpoints(null);
|
|
|
|
|
_resetNotificationState();
|
|
|
|
|
document.getElementById('css-editor-title').innerHTML = `${ICON_FILM} ${t('color_strip.add')}`;
|
|
|
|
|
document.getElementById('css-editor-gradient-preset').value = '';
|
|
|
|
|
gradientInit([
|
|
|
|
|
@@ -1030,6 +1157,20 @@ export async function saveCSSEditor() {
|
|
|
|
|
timeout: parseFloat(document.getElementById('css-editor-api-input-timeout').value),
|
|
|
|
|
};
|
|
|
|
|
if (!cssId) payload.source_type = 'api_input';
|
|
|
|
|
} else if (sourceType === 'notification') {
|
|
|
|
|
const filterList = document.getElementById('css-editor-notification-filter-list').value
|
|
|
|
|
.split(',').map(s => s.trim()).filter(Boolean);
|
|
|
|
|
payload = {
|
|
|
|
|
name,
|
|
|
|
|
notification_effect: document.getElementById('css-editor-notification-effect').value,
|
|
|
|
|
duration_ms: parseInt(document.getElementById('css-editor-notification-duration').value) || 1500,
|
|
|
|
|
default_color: document.getElementById('css-editor-notification-default-color').value,
|
|
|
|
|
app_filter_mode: document.getElementById('css-editor-notification-filter-mode').value,
|
|
|
|
|
app_filter_list: filterList,
|
|
|
|
|
app_colors: _notificationGetAppColorsDict(),
|
|
|
|
|
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
|
|
|
|
|
};
|
|
|
|
|
if (!cssId) payload.source_type = 'notification';
|
|
|
|
|
} else {
|
|
|
|
|
payload = {
|
|
|
|
|
name,
|
|
|
|
|
|