refactor: replace type-dispatch if/elif chains with registry patterns and handler maps
Some checks failed
Lint & Test / test (push) Failing after 30s
Some checks failed
Lint & Test / test (push) Failing after 30s
Backend: add registry dicts (_CONDITION_MAP, _VALUE_SOURCE_MAP, _PICTURE_SOURCE_MAP) and per-subclass from_dict() methods to eliminate ~300 lines of if/elif in factory functions. Convert automation engine dispatch (condition eval, match_mode, match_type, deactivation_mode) to dict-based lookup. Frontend: extract CSS_CARD_RENDERERS, CSS_SECTION_MAP, CSS_TYPE_SETUP, CONDITION_PILL_RENDERERS, and PICTURE_SOURCE_CARD_RENDERERS handler maps to replace scattered type-check chains in color-strips.ts, automations.ts, and streams.ts.
This commit is contained in:
@@ -206,6 +206,29 @@ function renderAutomations(automations: any, sceneMap: any) {
|
||||
}
|
||||
}
|
||||
|
||||
type ConditionPillRenderer = (c: any) => string;
|
||||
|
||||
const CONDITION_PILL_RENDERERS: Record<string, ConditionPillRenderer> = {
|
||||
always: (c) => `<span class="stream-card-prop">${ICON_OK} ${t('automations.condition.always')}</span>`,
|
||||
startup: (c) => `<span class="stream-card-prop">${ICON_START} ${t('automations.condition.startup')}</span>`,
|
||||
application: (c) => {
|
||||
const apps = (c.apps || []).join(', ');
|
||||
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
|
||||
return `<span class="stream-card-prop stream-card-prop-full">${t('automations.condition.application')}: ${apps} (${matchLabel})</span>`;
|
||||
},
|
||||
time_of_day: (c) => `<span class="stream-card-prop">${ICON_CLOCK} ${t('automations.condition.time_of_day')}: ${c.start_time || '00:00'} – ${c.end_time || '23:59'}</span>`,
|
||||
system_idle: (c) => {
|
||||
const mode = c.when_idle !== false ? t('automations.condition.system_idle.when_idle') : t('automations.condition.system_idle.when_active');
|
||||
return `<span class="stream-card-prop">${ICON_TIMER} ${c.idle_minutes || 5}m (${mode})</span>`;
|
||||
},
|
||||
display_state: (c) => {
|
||||
const stateLabel = t('automations.condition.display_state.' + (c.state || 'on'));
|
||||
return `<span class="stream-card-prop">${ICON_MONITOR} ${t('automations.condition.display_state')}: ${stateLabel}</span>`;
|
||||
},
|
||||
mqtt: (c) => `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`,
|
||||
webhook: (c) => `<span class="stream-card-prop">🔗 ${t('automations.condition.webhook')}</span>`,
|
||||
};
|
||||
|
||||
function createAutomationCard(automation: Automation, sceneMap = new Map()) {
|
||||
const statusClass = !automation.enabled ? 'disabled' : automation.is_active ? 'active' : 'inactive';
|
||||
const statusText = !automation.enabled ? t('automations.status.disabled') : automation.is_active ? t('automations.status.active') : t('automations.status.inactive');
|
||||
@@ -215,35 +238,8 @@ function createAutomationCard(automation: Automation, sceneMap = new Map()) {
|
||||
condPills = `<span class="stream-card-prop">${t('automations.conditions.empty')}</span>`;
|
||||
} else {
|
||||
const parts = automation.conditions.map(c => {
|
||||
if (c.condition_type === 'always') {
|
||||
return `<span class="stream-card-prop">${ICON_OK} ${t('automations.condition.always')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'startup') {
|
||||
return `<span class="stream-card-prop">${ICON_START} ${t('automations.condition.startup')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'application') {
|
||||
const apps = (c.apps || []).join(', ');
|
||||
const matchLabel = t('automations.condition.application.match_type.' + (c.match_type || 'running'));
|
||||
return `<span class="stream-card-prop stream-card-prop-full">${t('automations.condition.application')}: ${apps} (${matchLabel})</span>`;
|
||||
}
|
||||
if (c.condition_type === 'time_of_day') {
|
||||
return `<span class="stream-card-prop">${ICON_CLOCK} ${t('automations.condition.time_of_day')}: ${c.start_time || '00:00'} – ${c.end_time || '23:59'}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'system_idle') {
|
||||
const mode = c.when_idle !== false ? t('automations.condition.system_idle.when_idle') : t('automations.condition.system_idle.when_active');
|
||||
return `<span class="stream-card-prop">${ICON_TIMER} ${c.idle_minutes || 5}m (${mode})</span>`;
|
||||
}
|
||||
if (c.condition_type === 'display_state') {
|
||||
const stateLabel = t('automations.condition.display_state.' + (c.state || 'on'));
|
||||
return `<span class="stream-card-prop">${ICON_MONITOR} ${t('automations.condition.display_state')}: ${stateLabel}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'mqtt') {
|
||||
return `<span class="stream-card-prop stream-card-prop-full">${ICON_RADIO} ${t('automations.condition.mqtt')}: ${escapeHtml(c.topic || '')} = ${escapeHtml(c.payload || '*')}</span>`;
|
||||
}
|
||||
if (c.condition_type === 'webhook') {
|
||||
return `<span class="stream-card-prop">🔗 ${t('automations.condition.webhook')}</span>`;
|
||||
}
|
||||
return `<span class="stream-card-prop">${c.condition_type}</span>`;
|
||||
const renderer = CONDITION_PILL_RENDERERS[c.condition_type];
|
||||
return renderer ? renderer(c) : `<span class="stream-card-prop">${c.condition_type}</span>`;
|
||||
});
|
||||
const logicLabel = automation.condition_logic === 'and' ? t('automations.logic.and') : t('automations.logic.or');
|
||||
condPills = parts.join(`<span class="automation-logic-label">${logicLabel}</span>`);
|
||||
|
||||
@@ -158,46 +158,62 @@ function _ensureCSSTypeIconSelect() {
|
||||
|
||||
/* ── Type-switch helper ───────────────────────────────────────── */
|
||||
|
||||
const CSS_SECTION_MAP: Record<string, string> = {
|
||||
'picture': 'css-editor-picture-section',
|
||||
'picture_advanced': 'css-editor-picture-section',
|
||||
'static': 'css-editor-static-section',
|
||||
'color_cycle': 'css-editor-color-cycle-section',
|
||||
'gradient': 'css-editor-gradient-section',
|
||||
'effect': 'css-editor-effect-section',
|
||||
'composite': 'css-editor-composite-section',
|
||||
'mapped': 'css-editor-mapped-section',
|
||||
'audio': 'css-editor-audio-section',
|
||||
'api_input': 'css-editor-api-input-section',
|
||||
'notification': 'css-editor-notification-section',
|
||||
'daylight': 'css-editor-daylight-section',
|
||||
'candlelight': 'css-editor-candlelight-section',
|
||||
'processed': 'css-editor-processed-section',
|
||||
};
|
||||
|
||||
const CSS_ALL_SECTION_IDS = [...new Set(Object.values(CSS_SECTION_MAP))];
|
||||
|
||||
const CSS_TYPE_SETUP: Record<string, () => void> = {
|
||||
processed: () => _populateProcessedSelectors(),
|
||||
effect: () => { _ensureEffectTypeIconSelect(); _ensureEffectPaletteIconSelect(); onEffectTypeChange(); },
|
||||
audio: () => { _ensureAudioVizIconSelect(); _ensureAudioPaletteIconSelect(); onAudioVizChange(); _loadAudioSources(); },
|
||||
gradient: () => { _ensureGradientPresetIconSelect(); _ensureGradientEasingIconSelect(); requestAnimationFrame(() => gradientRenderAll()); },
|
||||
notification: () => { ensureNotificationEffectIconSelect(); ensureNotificationFilterModeIconSelect(); },
|
||||
candlelight: () => _ensureCandleTypeIconSelect(),
|
||||
composite: () => compositeRenderList(),
|
||||
mapped: () => _mappedRenderList(),
|
||||
};
|
||||
|
||||
export function onCSSTypeChange() {
|
||||
const type = (document.getElementById('css-editor-type') as HTMLInputElement).value;
|
||||
// Sync icon-select trigger display
|
||||
if (_cssTypeIconSelect) _cssTypeIconSelect.setValue(type);
|
||||
const isPictureType = type === 'picture' || type === 'picture_advanced';
|
||||
(document.getElementById('css-editor-picture-section') as HTMLElement).style.display = isPictureType ? '' : 'none';
|
||||
|
||||
// Hide all type-specific sections, then show the active one
|
||||
CSS_ALL_SECTION_IDS.forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.style.display = 'none';
|
||||
});
|
||||
const activeSection = CSS_SECTION_MAP[type];
|
||||
if (activeSection) {
|
||||
const el = document.getElementById(activeSection);
|
||||
if (el) el.style.display = '';
|
||||
}
|
||||
|
||||
// Hide picture source dropdown for advanced (sources are per-line in calibration)
|
||||
const psGroup = document.getElementById('css-editor-picture-source-group') as HTMLElement | null;
|
||||
if (psGroup) psGroup.style.display = (type === 'picture') ? '' : 'none';
|
||||
(document.getElementById('css-editor-static-section') as HTMLElement).style.display = type === 'static' ? '' : 'none';
|
||||
(document.getElementById('css-editor-color-cycle-section') as HTMLElement).style.display = type === 'color_cycle' ? '' : 'none';
|
||||
(document.getElementById('css-editor-gradient-section') as HTMLElement).style.display = type === 'gradient' ? '' : 'none';
|
||||
(document.getElementById('css-editor-effect-section') as HTMLElement).style.display = type === 'effect' ? '' : 'none';
|
||||
(document.getElementById('css-editor-composite-section') as HTMLElement).style.display = type === 'composite' ? '' : 'none';
|
||||
(document.getElementById('css-editor-mapped-section') as HTMLElement).style.display = type === 'mapped' ? '' : 'none';
|
||||
(document.getElementById('css-editor-audio-section') as HTMLElement).style.display = type === 'audio' ? '' : 'none';
|
||||
(document.getElementById('css-editor-api-input-section') as HTMLElement).style.display = type === 'api_input' ? '' : 'none';
|
||||
(document.getElementById('css-editor-notification-section') as HTMLElement).style.display = type === 'notification' ? '' : 'none';
|
||||
(document.getElementById('css-editor-daylight-section') as HTMLElement).style.display = type === 'daylight' ? '' : 'none';
|
||||
(document.getElementById('css-editor-candlelight-section') as HTMLElement).style.display = type === 'candlelight' ? '' : 'none';
|
||||
(document.getElementById('css-editor-processed-section') as HTMLElement).style.display = type === 'processed' ? '' : 'none';
|
||||
|
||||
const isPictureType = type === 'picture' || type === 'picture_advanced';
|
||||
if (isPictureType) _ensureInterpolationIconSelect();
|
||||
if (type === 'processed') _populateProcessedSelectors();
|
||||
if (type === 'effect') {
|
||||
_ensureEffectTypeIconSelect();
|
||||
_ensureEffectPaletteIconSelect();
|
||||
onEffectTypeChange();
|
||||
}
|
||||
if (type === 'audio') {
|
||||
_ensureAudioVizIconSelect();
|
||||
_ensureAudioPaletteIconSelect();
|
||||
onAudioVizChange();
|
||||
}
|
||||
if (type === 'gradient') { _ensureGradientPresetIconSelect(); _ensureGradientEasingIconSelect(); }
|
||||
if (type === 'notification') {
|
||||
ensureNotificationEffectIconSelect();
|
||||
ensureNotificationFilterModeIconSelect();
|
||||
}
|
||||
if (type === 'candlelight') _ensureCandleTypeIconSelect();
|
||||
|
||||
// Run type-specific setup
|
||||
const setupFn = CSS_TYPE_SETUP[type];
|
||||
if (setupFn) setupFn();
|
||||
|
||||
// Animation section — shown for static/gradient only
|
||||
const animSection = document.getElementById('css-editor-animation-section') as HTMLElement;
|
||||
@@ -226,16 +242,6 @@ export function onCSSTypeChange() {
|
||||
(document.getElementById('css-editor-clock-group') as HTMLElement).style.display = clockTypes.includes(type) ? '' : 'none';
|
||||
if (clockTypes.includes(type)) _populateClockDropdown();
|
||||
|
||||
if (type === 'audio') {
|
||||
_loadAudioSources();
|
||||
} else if (type === 'composite') {
|
||||
compositeRenderList();
|
||||
} else if (type === 'mapped') {
|
||||
_mappedRenderList();
|
||||
} else if (type === 'gradient') {
|
||||
requestAnimationFrame(() => gradientRenderAll());
|
||||
}
|
||||
|
||||
_autoGenerateCSSName();
|
||||
}
|
||||
|
||||
@@ -912,56 +918,44 @@ function _resetAudioState() {
|
||||
|
||||
/* ── Card ─────────────────────────────────────────────────────── */
|
||||
|
||||
export function createColorStripCard(source: ColorStripSource, pictureSourceMap: Record<string, any>, audioSourceMap: Record<string, any>) {
|
||||
const isStatic = source.source_type === 'static';
|
||||
const isGradient = source.source_type === 'gradient';
|
||||
const isColorCycle = source.source_type === 'color_cycle';
|
||||
const isEffect = source.source_type === 'effect';
|
||||
const isComposite = source.source_type === 'composite';
|
||||
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';
|
||||
const isPictureAdvanced = source.source_type === 'picture_advanced';
|
||||
type CardPropsRenderer = (source: ColorStripSource, opts: {
|
||||
clockBadge: string;
|
||||
animBadge: string;
|
||||
audioSourceMap: Record<string, any>;
|
||||
pictureSourceMap: Record<string, any>;
|
||||
}) => string;
|
||||
|
||||
// Clock crosslink badge (replaces speed badge when clock is assigned)
|
||||
const clockObj = source.clock_id ? _cachedSyncClocks.find(c => c.id === source.clock_id) : null;
|
||||
const clockBadge = clockObj
|
||||
? `<span class="stream-card-prop stream-card-link" title="${t('color_strip.clock')}" onclick="event.stopPropagation(); navigateToCard('streams','sync','sync-clocks','data-id','${source.clock_id}')">${ICON_CLOCK} ${escapeHtml(clockObj.name)}</span>`
|
||||
: source.clock_id ? `<span class="stream-card-prop">${ICON_CLOCK} ${source.clock_id}</span>` : '';
|
||||
const NON_PICTURE_TYPES = new Set([
|
||||
'static', 'gradient', 'color_cycle', 'effect', 'composite', 'mapped',
|
||||
'audio', 'api_input', 'notification', 'daylight', 'candlelight', 'processed',
|
||||
]);
|
||||
|
||||
const anim = (isStatic || isGradient) && source.animation && source.animation.enabled ? source.animation : null;
|
||||
const animBadge = anim
|
||||
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>`
|
||||
: '';
|
||||
|
||||
let propsHtml;
|
||||
if (isStatic) {
|
||||
const CSS_CARD_RENDERERS: Record<string, CardPropsRenderer> = {
|
||||
static: (source, { clockBadge, animBadge }) => {
|
||||
const hexColor = rgbArrayToHex(source.color!);
|
||||
propsHtml = `
|
||||
return `
|
||||
<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>
|
||||
${animBadge}
|
||||
${clockBadge}
|
||||
`;
|
||||
} else if (isColorCycle) {
|
||||
},
|
||||
color_cycle: (source, { clockBadge }) => {
|
||||
const colors = source.colors || [];
|
||||
const swatches = colors.slice(0, 8).map((c: any) =>
|
||||
`<span style="display:inline-block;width:12px;height:12px;background:${rgbArrayToHex(c)};border:1px solid #888;border-radius:2px;margin-right:2px"></span>`
|
||||
).join('');
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop">${swatches}</span>
|
||||
${clockBadge}
|
||||
`;
|
||||
} else if (isGradient) {
|
||||
},
|
||||
gradient: (source, { clockBadge, animBadge }) => {
|
||||
const stops = source.stops || [];
|
||||
const sortedStops = [...stops].sort((a, b) => a.position - b.position);
|
||||
let cssGradient = '';
|
||||
if (sortedStops.length >= 2) {
|
||||
// Build CSS stops that mirror the interpolation algorithm:
|
||||
// for each stop emit its primary color, then immediately emit color_right
|
||||
// at the same position to produce a hard edge (bidirectional stop).
|
||||
const parts: string[] = [];
|
||||
sortedStops.forEach(s => {
|
||||
const pct = Math.round(s.position * 100);
|
||||
@@ -970,39 +964,43 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap:
|
||||
});
|
||||
cssGradient = `linear-gradient(to right, ${parts.join(', ')})`;
|
||||
}
|
||||
propsHtml = `
|
||||
return `
|
||||
${cssGradient ? `<span style="flex:1 1 100%;height:12px;background:${cssGradient};border-radius:3px;border:1px solid rgba(128,128,128,0.3)"></span>` : ''}
|
||||
<span class="stream-card-prop">${ICON_PALETTE} ${stops.length} ${t('color_strip.gradient.stops_count')}</span>
|
||||
${animBadge}
|
||||
${clockBadge}
|
||||
`;
|
||||
} else if (isEffect) {
|
||||
},
|
||||
effect: (source, { clockBadge }) => {
|
||||
const effectLabel = t('color_strip.effect.' + (source.effect_type || 'fire')) || source.effect_type || 'fire';
|
||||
const paletteLabel = source.palette ? (t('color_strip.palette.' + source.palette) || source.palette) : '';
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop">${ICON_FPS} ${escapeHtml(effectLabel)}</span>
|
||||
${paletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.effect.palette')}">${ICON_PALETTE} ${escapeHtml(paletteLabel)}</span>` : ''}
|
||||
${clockBadge}
|
||||
`;
|
||||
} else if (isComposite) {
|
||||
},
|
||||
composite: (source) => {
|
||||
const layerCount = (source.layers || []).length;
|
||||
const enabledCount = (source.layers || []).filter((l: any) => l.enabled !== false).length;
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop">${ICON_LINK} ${enabledCount}/${layerCount} ${t('color_strip.composite.layers_count')}</span>
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
`;
|
||||
} else if (isMapped) {
|
||||
},
|
||||
mapped: (source) => {
|
||||
const zoneCount = (source.zones || []).length;
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop">${ICON_MAP_PIN} ${zoneCount} ${t('color_strip.mapped.zones_count')}</span>
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
`;
|
||||
} else if (isAudio) {
|
||||
},
|
||||
audio: (source, { audioSourceMap }) => {
|
||||
const vizLabel = t('color_strip.audio.viz.' + (source.visualization_mode || 'spectrum')) || source.visualization_mode || 'spectrum';
|
||||
const vizMode = source.visualization_mode || 'spectrum';
|
||||
const showPalette = (vizMode === 'spectrum' || vizMode === 'beat_pulse') && source.palette;
|
||||
const audioPaletteLabel = showPalette ? (t('color_strip.palette.' + source.palette) || source.palette) : '';
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop">${ICON_MUSIC} ${escapeHtml(vizLabel)}</span>
|
||||
${audioPaletteLabel ? `<span class="stream-card-prop" title="${t('color_strip.audio.palette')}">${ICON_PALETTE} ${escapeHtml(audioPaletteLabel)}</span>` : ''}
|
||||
${source.audio_source_id ? (() => {
|
||||
@@ -1013,21 +1011,23 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap:
|
||||
})() : ''}
|
||||
${source.mirror ? `<span class="stream-card-prop">🪞</span>` : ''}
|
||||
`;
|
||||
} else if (isApiInput) {
|
||||
},
|
||||
api_input: (source) => {
|
||||
const fbColor = rgbArrayToHex(source.fallback_color || [0, 0, 0]);
|
||||
const timeoutVal = (source.timeout ?? 5.0).toFixed(1);
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop" title="${t('color_strip.api_input.fallback_color')}">
|
||||
<span style="display:inline-block;width:14px;height:14px;background:${fbColor};border:1px solid #888;border-radius:2px;vertical-align:middle;margin-right:4px"></span>${fbColor.toUpperCase()}
|
||||
</span>
|
||||
<span class="stream-card-prop" title="${t('color_strip.api_input.timeout')}">${ICON_TIMER} ${timeoutVal}s</span>
|
||||
`;
|
||||
} else if (isNotification) {
|
||||
},
|
||||
notification: (source) => {
|
||||
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 = `
|
||||
return `
|
||||
<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')}">
|
||||
@@ -1035,73 +1035,96 @@ export function createColorStripCard(source: ColorStripSource, pictureSourceMap:
|
||||
</span>
|
||||
${appCount > 0 ? `<span class="stream-card-prop">${ICON_PALETTE} ${appCount} ${t('color_strip.notification.app_count')}</span>` : ''}
|
||||
`;
|
||||
} else if (source.source_type === 'daylight') {
|
||||
},
|
||||
daylight: (source, { clockBadge }) => {
|
||||
const useRealTime = source.use_real_time;
|
||||
const speedVal = (source.speed ?? 1.0).toFixed(1);
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop">${useRealTime ? '🕐 ' + t('color_strip.daylight.real_time') : '⏩ ' + speedVal + 'x'}</span>
|
||||
${clockBadge}
|
||||
`;
|
||||
} else if (source.source_type === 'candlelight') {
|
||||
},
|
||||
candlelight: (source, { clockBadge }) => {
|
||||
const hexColor = rgbArrayToHex(source.color || [255, 147, 41]);
|
||||
const numCandles = source.num_candles ?? 3;
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop" title="${t('color_strip.candlelight.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>
|
||||
<span class="stream-card-prop">${numCandles} ${t('color_strip.candlelight.num_candles')}</span>
|
||||
${clockBadge}
|
||||
`;
|
||||
} else if (source.source_type === 'processed') {
|
||||
},
|
||||
processed: (source) => {
|
||||
const inputSrc = ((colorStripSourcesCache.data || []) as any[]).find(s => s.id === source.input_source_id);
|
||||
const inputName = inputSrc?.name || source.input_source_id || '—';
|
||||
const tplName = source.processing_template_id
|
||||
? (_cachedCSPTemplates.find(t => t.id === source.processing_template_id)?.name || source.processing_template_id)
|
||||
: '—';
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop" title="${t('color_strip.processed.input')}">${ICON_LINK_SOURCE} ${escapeHtml(inputName)}</span>
|
||||
<span class="stream-card-prop" title="${t('color_strip.processed.template')}">${ICON_SPARKLES} ${escapeHtml(tplName)}</span>
|
||||
`;
|
||||
} else if (isPictureAdvanced) {
|
||||
},
|
||||
picture_advanced: (source, { pictureSourceMap }) => {
|
||||
const cal = source.calibration ?? {} as Partial<import('../types.ts').Calibration>;
|
||||
const lines = cal.lines || [];
|
||||
const totalLeds = lines.reduce((s: any, l: any) => s + (l.led_count || 0), 0);
|
||||
const ledCount = (source.led_count > 0) ? source.led_count : totalLeds;
|
||||
// Collect unique picture source names
|
||||
const psIds: any[] = [...new Set(lines.map((l: any) => l.picture_source_id).filter(Boolean))];
|
||||
const psNames = psIds.map((id: any) => {
|
||||
const ps = pictureSourceMap && pictureSourceMap[id];
|
||||
return ps ? ps.name : id;
|
||||
});
|
||||
propsHtml = `
|
||||
return `
|
||||
<span class="stream-card-prop" title="${t('calibration.advanced.lines_title')}">${ICON_MAP_PIN} ${lines.length} ${t('calibration.advanced.lines_title').toLowerCase()}</span>
|
||||
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${ledCount}</span>` : ''}
|
||||
${psNames.length ? `<span class="stream-card-prop stream-card-prop-full" title="${t('color_strip.picture_source')}">${ICON_LINK_SOURCE} ${escapeHtml(psNames.join(', '))}</span>` : ''}
|
||||
`;
|
||||
} else {
|
||||
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id!];
|
||||
const srcName = ps ? ps.name : source.picture_source_id || '—';
|
||||
const cal = source.calibration ?? {} as Partial<import('../types.ts').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;
|
||||
let psSubTab = 'raw', psSection = 'raw-streams';
|
||||
if (ps) {
|
||||
if (ps.stream_type === 'static_image') { psSubTab = 'static_image'; psSection = 'static-streams'; }
|
||||
else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; }
|
||||
}
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop stream-card-prop-full${ps ? ' stream-card-link' : ''}" title="${t('color_strip.picture_source')}"${ps ? ` onclick="event.stopPropagation(); navigateToCard('streams','${psSubTab}','${psSection}','data-stream-id','${source.picture_source_id}')"` : ''}>${ICON_LINK_SOURCE} ${escapeHtml(srcName)}</span>
|
||||
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${ledCount}</span>` : ''}
|
||||
`;
|
||||
},
|
||||
};
|
||||
|
||||
/** Fallback renderer for picture-type sources (plain picture). */
|
||||
function _renderPictureCardProps(source: ColorStripSource, pictureSourceMap: Record<string, any>): string {
|
||||
const ps = pictureSourceMap && pictureSourceMap[source.picture_source_id!];
|
||||
const srcName = ps ? ps.name : source.picture_source_id || '—';
|
||||
const cal = source.calibration ?? {} as Partial<import('../types.ts').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;
|
||||
let psSubTab = 'raw', psSection = 'raw-streams';
|
||||
if (ps) {
|
||||
if (ps.stream_type === 'static_image') { psSubTab = 'static_image'; psSection = 'static-streams'; }
|
||||
else if (ps.stream_type === 'processed') { psSubTab = 'processed'; psSection = 'proc-streams'; }
|
||||
}
|
||||
return `
|
||||
<span class="stream-card-prop stream-card-prop-full${ps ? ' stream-card-link' : ''}" title="${t('color_strip.picture_source')}"${ps ? ` onclick="event.stopPropagation(); navigateToCard('streams','${psSubTab}','${psSection}','data-stream-id','${source.picture_source_id}')"` : ''}>${ICON_LINK_SOURCE} ${escapeHtml(srcName)}</span>
|
||||
${ledCount ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${ledCount}</span>` : ''}
|
||||
`;
|
||||
}
|
||||
|
||||
export function createColorStripCard(source: ColorStripSource, pictureSourceMap: Record<string, any>, audioSourceMap: Record<string, any>) {
|
||||
// Clock crosslink badge (replaces speed badge when clock is assigned)
|
||||
const clockObj = source.clock_id ? _cachedSyncClocks.find(c => c.id === source.clock_id) : null;
|
||||
const clockBadge = clockObj
|
||||
? `<span class="stream-card-prop stream-card-link" title="${t('color_strip.clock')}" onclick="event.stopPropagation(); navigateToCard('streams','sync','sync-clocks','data-id','${source.clock_id}')">${ICON_CLOCK} ${escapeHtml(clockObj.name)}</span>`
|
||||
: source.clock_id ? `<span class="stream-card-prop">${ICON_CLOCK} ${source.clock_id}</span>` : '';
|
||||
|
||||
const isAnimatable = source.source_type === 'static' || source.source_type === 'gradient';
|
||||
const anim = isAnimatable && source.animation && source.animation.enabled ? source.animation : null;
|
||||
const animBadge = anim
|
||||
? `<span class="stream-card-prop" title="${t('color_strip.animation')}">${ICON_SPARKLES} ${t('color_strip.animation.type.' + anim.type) || anim.type}</span>`
|
||||
: '';
|
||||
|
||||
const renderer = CSS_CARD_RENDERERS[source.source_type];
|
||||
const propsHtml = renderer
|
||||
? renderer(source, { clockBadge, animBadge, audioSourceMap, pictureSourceMap })
|
||||
: _renderPictureCardProps(source, pictureSourceMap);
|
||||
|
||||
const icon = getColorStripIcon(source.source_type);
|
||||
const isDaylight = source.source_type === 'daylight';
|
||||
const isCandlelight = source.source_type === 'candlelight';
|
||||
const isProcessed = source.source_type === 'processed';
|
||||
const isPictureKind = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput && !isNotification && !isDaylight && !isCandlelight && !isProcessed);
|
||||
const isNotification = source.source_type === 'notification';
|
||||
const isPictureKind = !NON_PICTURE_TYPES.has(source.source_type);
|
||||
const calibrationBtn = isPictureKind
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="${isPictureAdvanced ? `showAdvancedCalibration('${source.id}')` : `showCSSCalibration('${source.id}')`}" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="${source.source_type === 'picture_advanced' ? `showAdvancedCalibration('${source.id}')` : `showCSSCalibration('${source.id}')`}" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
||||
: '';
|
||||
const overlayBtn = isPictureKind
|
||||
? `<button class="btn btn-icon btn-secondary" onclick="event.stopPropagation(); toggleCSSOverlay('${source.id}')" title="${t('overlay.toggle')}">${ICON_OVERLAY}</button>`
|
||||
|
||||
@@ -276,6 +276,54 @@ const _streamSectionMap = {
|
||||
sync: [csSyncClocks],
|
||||
};
|
||||
|
||||
type StreamCardRenderer = (stream: any) => string;
|
||||
|
||||
const PICTURE_SOURCE_CARD_RENDERERS: Record<string, StreamCardRenderer> = {
|
||||
raw: (stream) => {
|
||||
let capTmplName = '';
|
||||
if (stream.capture_template_id) {
|
||||
const capTmpl = _cachedCaptureTemplates.find(t => t.id === stream.capture_template_id);
|
||||
if (capTmpl) capTmplName = escapeHtml(capTmpl.name);
|
||||
}
|
||||
return `<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('streams.display')}">${ICON_MONITOR} ${stream.display_index ?? 0}</span>
|
||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
|
||||
${capTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.capture_template')}" onclick="event.stopPropagation(); navigateToCard('streams','raw_templates','raw-templates','data-template-id','${stream.capture_template_id}')">${ICON_TEMPLATE} ${capTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
},
|
||||
processed: (stream) => {
|
||||
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
||||
const sourceName = sourceStream ? escapeHtml(sourceStream.name) : (stream.source_stream_id || '-');
|
||||
const sourceSubTab = sourceStream ? (sourceStream.stream_type === 'static_image' ? 'static_image' : 'raw') : 'raw';
|
||||
const sourceSection = sourceStream ? (sourceStream.stream_type === 'static_image' ? 'static-streams' : 'raw-streams') : 'raw-streams';
|
||||
let ppTmplName = '';
|
||||
if (stream.postprocessing_template_id) {
|
||||
const ppTmpl = _cachedPPTemplates.find(p => p.id === stream.postprocessing_template_id);
|
||||
if (ppTmpl) ppTmplName = escapeHtml(ppTmpl.name);
|
||||
}
|
||||
return `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-link" title="${t('streams.source')}" onclick="event.stopPropagation(); navigateToCard('streams','${sourceSubTab}','${sourceSection}','data-stream-id','${stream.source_stream_id}')">${ICON_LINK_SOURCE} ${sourceName}</span>
|
||||
${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','proc_templates','proc-templates','data-pp-template-id','${stream.postprocessing_template_id}')">${ICON_TEMPLATE} ${ppTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
},
|
||||
static_image: (stream) => {
|
||||
const src = stream.image_source || '';
|
||||
return `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(src)}">${ICON_WEB} ${escapeHtml(src)}</span>
|
||||
</div>`;
|
||||
},
|
||||
video: (stream) => {
|
||||
const url = stream.url || '';
|
||||
const shortUrl = url.length > 40 ? url.slice(0, 37) + '...' : url;
|
||||
return `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(url)}">${ICON_WEB} ${escapeHtml(shortUrl)}</span>
|
||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
|
||||
${stream.loop !== false ? `<span class="stream-card-prop">↻</span>` : ''}
|
||||
${stream.playback_speed && stream.playback_speed !== 1.0 ? `<span class="stream-card-prop">${stream.playback_speed}×</span>` : ''}
|
||||
</div>`;
|
||||
},
|
||||
};
|
||||
|
||||
function renderPictureSourcesList(streams: any) {
|
||||
const container = document.getElementById('streams-list')!;
|
||||
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
||||
@@ -283,47 +331,8 @@ function renderPictureSourcesList(streams: any) {
|
||||
const renderStreamCard = (stream: any) => {
|
||||
const typeIcon = getPictureSourceIcon(stream.stream_type);
|
||||
|
||||
let detailsHtml = '';
|
||||
if (stream.stream_type === 'raw') {
|
||||
let capTmplName = '';
|
||||
if (stream.capture_template_id) {
|
||||
const capTmpl = _cachedCaptureTemplates.find(t => t.id === stream.capture_template_id);
|
||||
if (capTmpl) capTmplName = escapeHtml(capTmpl.name);
|
||||
}
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('streams.display')}">${ICON_MONITOR} ${stream.display_index ?? 0}</span>
|
||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
|
||||
${capTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.capture_template')}" onclick="event.stopPropagation(); navigateToCard('streams','raw_templates','raw-templates','data-template-id','${stream.capture_template_id}')">${ICON_TEMPLATE} ${capTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
} else if (stream.stream_type === 'processed') {
|
||||
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
||||
const sourceName = sourceStream ? escapeHtml(sourceStream.name) : (stream.source_stream_id || '-');
|
||||
const sourceSubTab = sourceStream ? (sourceStream.stream_type === 'static_image' ? 'static_image' : 'raw') : 'raw';
|
||||
const sourceSection = sourceStream ? (sourceStream.stream_type === 'static_image' ? 'static-streams' : 'raw-streams') : 'raw-streams';
|
||||
let ppTmplName = '';
|
||||
if (stream.postprocessing_template_id) {
|
||||
const ppTmpl = _cachedPPTemplates.find(p => p.id === stream.postprocessing_template_id);
|
||||
if (ppTmpl) ppTmplName = escapeHtml(ppTmpl.name);
|
||||
}
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-link" title="${t('streams.source')}" onclick="event.stopPropagation(); navigateToCard('streams','${sourceSubTab}','${sourceSection}','data-stream-id','${stream.source_stream_id}')">${ICON_LINK_SOURCE} ${sourceName}</span>
|
||||
${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','proc_templates','proc-templates','data-pp-template-id','${stream.postprocessing_template_id}')">${ICON_TEMPLATE} ${ppTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
} else if (stream.stream_type === 'static_image') {
|
||||
const src = stream.image_source || '';
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(src)}">${ICON_WEB} ${escapeHtml(src)}</span>
|
||||
</div>`;
|
||||
} else if (stream.stream_type === 'video') {
|
||||
const url = stream.url || '';
|
||||
const shortUrl = url.length > 40 ? url.slice(0, 37) + '...' : url;
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(url)}">${ICON_WEB} ${escapeHtml(shortUrl)}</span>
|
||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
|
||||
${stream.loop !== false ? `<span class="stream-card-prop">↻</span>` : ''}
|
||||
${stream.playback_speed && stream.playback_speed !== 1.0 ? `<span class="stream-card-prop">${stream.playback_speed}×</span>` : ''}
|
||||
</div>`;
|
||||
}
|
||||
const renderer = PICTURE_SOURCE_CARD_RENDERERS[stream.stream_type];
|
||||
const detailsHtml = renderer ? renderer(stream) : '';
|
||||
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
|
||||
Reference in New Issue
Block a user