Frontend performance and code quality improvements

Performance: cache getBoundingClientRect in card-glare and drag-drop,
build adjacency Maps for O(1) graph BFS, batch WebGL uniform uploads,
cache matchMedia/search text in card-sections, use Map in graph-layout.

Code quality: extract shared FPS chart factory (chart-utils.js) and
FilterListManager (filter-list.js), replace 14-way CSS editor dispatch
with type handler registry, move state to state.js, fix layer violation
in api.js, add i18n for hardcoded strings, sync 53 missing locale keys,
add HTTP error logging in DataCache.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-15 18:14:26 +03:00
parent 014b4175b9
commit 50c40ed13f
20 changed files with 1070 additions and 716 deletions

View File

@@ -1406,6 +1406,360 @@ function _autoGenerateCSSName() {
document.getElementById('css-editor-name').value = detail ? `${typeLabel} · ${detail}` : typeLabel;
}
/* ── Per-type handler registry ─────────────────────────────────
* Each handler has:
* load(css) — populate editor fields from a saved/cloned source object
* reset() — set editor fields to default values for "new" mode
* getPayload(name) — read editor fields and return the API payload (or null to signal validation error)
* The handlers delegate to existing _loadXState / _resetXState helpers where available.
*/
const _typeHandlers = {
static: {
load(css) {
document.getElementById('css-editor-color').value = rgbArrayToHex(css.color);
_loadAnimationState(css.animation);
},
reset() {
document.getElementById('css-editor-color').value = '#ffffff';
_loadAnimationState(null);
},
getPayload(name) {
return {
name,
color: hexToRgbArray(document.getElementById('css-editor-color').value),
animation: _getAnimationPayload(),
};
},
},
color_cycle: {
load(css) {
_loadColorCycleState(css);
},
reset() {
_loadColorCycleState(null);
},
getPayload(name) {
const cycleColors = _colorCycleGetColors();
if (cycleColors.length < 2) {
cssEditorModal.showError(t('color_strip.color_cycle.min_colors'));
return null;
}
return { name, colors: cycleColors };
},
},
gradient: {
load(css) {
document.getElementById('css-editor-gradient-preset').value = '';
if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue('');
gradientInit(css.stops || [
{ position: 0.0, color: [255, 0, 0] },
{ position: 1.0, color: [0, 0, 255] },
]);
_loadAnimationState(css.animation);
},
reset() {
document.getElementById('css-editor-gradient-preset').value = '';
gradientInit([
{ position: 0.0, color: [255, 0, 0] },
{ position: 1.0, color: [0, 0, 255] },
]);
_loadAnimationState(null);
},
getPayload(name) {
const gStops = getGradientStops();
if (gStops.length < 2) {
cssEditorModal.showError(t('color_strip.gradient.min_stops'));
return null;
}
return {
name,
stops: gStops.map(s => ({
position: s.position,
color: s.color,
...(s.colorRight ? { color_right: s.colorRight } : {}),
})),
animation: _getAnimationPayload(),
};
},
},
effect: {
load(css) {
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
if (_effectTypeIconSelect) _effectTypeIconSelect.setValue(css.effect_type || 'fire');
onEffectTypeChange();
document.getElementById('css-editor-effect-palette').value = css.palette || 'fire';
if (_effectPaletteIconSelect) _effectPaletteIconSelect.setValue(css.palette || 'fire');
document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]);
document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0;
document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
document.getElementById('css-editor-effect-scale').value = css.scale ?? 1.0;
document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(css.scale ?? 1.0).toFixed(1);
document.getElementById('css-editor-effect-mirror').checked = css.mirror || false;
},
reset() {
document.getElementById('css-editor-effect-type').value = 'fire';
document.getElementById('css-editor-effect-palette').value = 'fire';
document.getElementById('css-editor-effect-color').value = '#ff5000';
document.getElementById('css-editor-effect-intensity').value = 1.0;
document.getElementById('css-editor-effect-intensity-val').textContent = '1.0';
document.getElementById('css-editor-effect-scale').value = 1.0;
document.getElementById('css-editor-effect-scale-val').textContent = '1.0';
document.getElementById('css-editor-effect-mirror').checked = false;
},
getPayload(name) {
const payload = {
name,
effect_type: document.getElementById('css-editor-effect-type').value,
palette: document.getElementById('css-editor-effect-palette').value,
intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value),
scale: parseFloat(document.getElementById('css-editor-effect-scale').value),
mirror: document.getElementById('css-editor-effect-mirror').checked,
};
// Meteor uses a color picker
if (payload.effect_type === 'meteor') {
const hex = document.getElementById('css-editor-effect-color').value;
payload.color = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
}
return payload;
},
},
audio: {
async load(css) {
await _loadAudioSources();
_loadAudioState(css);
},
reset() {
_resetAudioState();
},
getPayload(name) {
return {
name,
visualization_mode: document.getElementById('css-editor-audio-viz').value,
audio_source_id: document.getElementById('css-editor-audio-source').value || null,
sensitivity: parseFloat(document.getElementById('css-editor-audio-sensitivity').value),
smoothing: parseFloat(document.getElementById('css-editor-audio-smoothing').value),
palette: document.getElementById('css-editor-audio-palette').value,
color: hexToRgbArray(document.getElementById('css-editor-audio-color').value),
color_peak: hexToRgbArray(document.getElementById('css-editor-audio-color-peak').value),
mirror: document.getElementById('css-editor-audio-mirror').checked,
};
},
},
composite: {
load(css) {
_loadCompositeState(css);
},
reset() {
_loadCompositeState(null);
},
getPayload(name) {
const layers = _compositeGetLayers();
if (layers.length < 1) {
cssEditorModal.showError(t('color_strip.composite.error.min_layers'));
return null;
}
const hasEmpty = layers.some(l => !l.source_id);
if (hasEmpty) {
cssEditorModal.showError(t('color_strip.composite.error.no_source'));
return null;
}
return { name, layers };
},
},
mapped: {
load(css) {
_loadMappedState(css);
},
reset() {
_resetMappedState();
},
getPayload(name) {
const zones = _mappedGetZones();
const hasEmpty = zones.some(z => !z.source_id);
if (hasEmpty) {
cssEditorModal.showError(t('color_strip.mapped.error.no_source'));
return null;
}
return { name, zones };
},
},
api_input: {
load(css) {
document.getElementById('css-editor-api-input-fallback-color').value =
rgbArrayToHex(css.fallback_color || [0, 0, 0]);
document.getElementById('css-editor-api-input-timeout').value = css.timeout ?? 5.0;
document.getElementById('css-editor-api-input-timeout-val').textContent =
parseFloat(css.timeout ?? 5.0).toFixed(1);
_showApiInputEndpoints(css.id);
},
reset() {
document.getElementById('css-editor-api-input-fallback-color').value = '#000000';
document.getElementById('css-editor-api-input-timeout').value = 5.0;
document.getElementById('css-editor-api-input-timeout-val').textContent = '5.0';
_showApiInputEndpoints(null);
},
getPayload(name) {
const fbHex = document.getElementById('css-editor-api-input-fallback-color').value;
return {
name,
fallback_color: hexToRgbArray(fbHex),
timeout: parseFloat(document.getElementById('css-editor-api-input-timeout').value),
};
},
},
notification: {
load(css) {
_loadNotificationState(css);
},
reset() {
_resetNotificationState();
},
getPayload(name) {
const filterList = document.getElementById('css-editor-notification-filter-list').value
.split('\n').map(s => s.trim()).filter(Boolean);
return {
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(),
};
},
},
daylight: {
load(css) {
document.getElementById('css-editor-daylight-speed').value = css.speed ?? 1.0;
document.getElementById('css-editor-daylight-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
document.getElementById('css-editor-daylight-real-time').checked = css.use_real_time || false;
document.getElementById('css-editor-daylight-latitude').value = css.latitude ?? 50.0;
document.getElementById('css-editor-daylight-latitude-val').textContent = parseFloat(css.latitude ?? 50.0).toFixed(0);
_syncDaylightSpeedVisibility();
},
reset() {
document.getElementById('css-editor-daylight-speed').value = 1.0;
document.getElementById('css-editor-daylight-speed-val').textContent = '1.0';
document.getElementById('css-editor-daylight-real-time').checked = false;
document.getElementById('css-editor-daylight-latitude').value = 50.0;
document.getElementById('css-editor-daylight-latitude-val').textContent = '50';
},
getPayload(name) {
return {
name,
speed: parseFloat(document.getElementById('css-editor-daylight-speed').value),
use_real_time: document.getElementById('css-editor-daylight-real-time').checked,
latitude: parseFloat(document.getElementById('css-editor-daylight-latitude').value),
};
},
},
candlelight: {
load(css) {
document.getElementById('css-editor-candlelight-color').value = rgbArrayToHex(css.color || [255, 147, 41]);
document.getElementById('css-editor-candlelight-intensity').value = css.intensity ?? 1.0;
document.getElementById('css-editor-candlelight-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
document.getElementById('css-editor-candlelight-num-candles').value = css.num_candles ?? 3;
document.getElementById('css-editor-candlelight-speed').value = css.speed ?? 1.0;
document.getElementById('css-editor-candlelight-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
},
reset() {
document.getElementById('css-editor-candlelight-color').value = '#ff9329';
document.getElementById('css-editor-candlelight-intensity').value = 1.0;
document.getElementById('css-editor-candlelight-intensity-val').textContent = '1.0';
document.getElementById('css-editor-candlelight-num-candles').value = 3;
document.getElementById('css-editor-candlelight-speed').value = 1.0;
document.getElementById('css-editor-candlelight-speed-val').textContent = '1.0';
},
getPayload(name) {
return {
name,
color: hexToRgbArray(document.getElementById('css-editor-candlelight-color').value),
intensity: parseFloat(document.getElementById('css-editor-candlelight-intensity').value),
num_candles: parseInt(document.getElementById('css-editor-candlelight-num-candles').value) || 3,
speed: parseFloat(document.getElementById('css-editor-candlelight-speed').value),
};
},
},
processed: {
async load(css) {
await csptCache.fetch();
await colorStripSourcesCache.fetch();
_populateProcessedSelectors();
document.getElementById('css-editor-processed-input').value = css.input_source_id || '';
document.getElementById('css-editor-processed-template').value = css.processing_template_id || '';
},
async reset(presetType) {
if (presetType === 'processed') {
await csptCache.fetch();
await colorStripSourcesCache.fetch();
_populateProcessedSelectors();
}
},
getPayload(name) {
const inputId = document.getElementById('css-editor-processed-input').value;
const templateId = document.getElementById('css-editor-processed-template').value;
if (!inputId) {
cssEditorModal.showError(t('color_strip.processed.error.no_input'));
return null;
}
return {
name,
input_source_id: inputId,
processing_template_id: templateId || null,
};
},
},
picture_advanced: {
load(css) {
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
if (_interpolationIconSelect) _interpolationIconSelect.setValue(css.interpolation_mode || 'average');
const smoothing = css.smoothing ?? 0.3;
document.getElementById('css-editor-smoothing').value = smoothing;
document.getElementById('css-editor-smoothing-value').textContent = parseFloat(smoothing).toFixed(2);
},
reset() {
document.getElementById('css-editor-interpolation').value = 'average';
if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
document.getElementById('css-editor-smoothing').value = 0.3;
document.getElementById('css-editor-smoothing-value').textContent = '0.30';
},
getPayload(name) {
return {
name,
interpolation_mode: document.getElementById('css-editor-interpolation').value,
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
};
},
},
picture: {
load(css, sourceSelect) {
sourceSelect.value = css.picture_source_id || '';
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
if (_interpolationIconSelect) _interpolationIconSelect.setValue(css.interpolation_mode || 'average');
const smoothing = css.smoothing ?? 0.3;
document.getElementById('css-editor-smoothing').value = smoothing;
document.getElementById('css-editor-smoothing-value').textContent = parseFloat(smoothing).toFixed(2);
},
reset() {
document.getElementById('css-editor-interpolation').value = 'average';
if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
document.getElementById('css-editor-smoothing').value = 0.3;
document.getElementById('css-editor-smoothing-value').textContent = '0.30';
},
getPayload(name) {
return {
name,
picture_source_id: document.getElementById('css-editor-picture-source').value,
interpolation_mode: document.getElementById('css-editor-interpolation').value,
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
};
},
},
};
/* ── Editor open/close ────────────────────────────────────────── */
export async function showCSSEditor(cssId = null, cloneData = null, presetType = null) {
@@ -1465,78 +1819,8 @@ export async function showCSSEditor(cssId = null, cloneData = null, presetType =
onCSSClockChange();
}
if (sourceType === 'static') {
document.getElementById('css-editor-color').value = rgbArrayToHex(css.color);
_loadAnimationState(css.animation);
} else if (sourceType === 'color_cycle') {
_loadColorCycleState(css);
} else if (sourceType === 'gradient') {
document.getElementById('css-editor-gradient-preset').value = '';
if (_gradientPresetIconSelect) _gradientPresetIconSelect.setValue('');
gradientInit(css.stops || [
{ position: 0.0, color: [255, 0, 0] },
{ position: 1.0, color: [0, 0, 255] },
]);
_loadAnimationState(css.animation);
} else if (sourceType === 'effect') {
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
if (_effectTypeIconSelect) _effectTypeIconSelect.setValue(css.effect_type || 'fire');
onEffectTypeChange();
document.getElementById('css-editor-effect-palette').value = css.palette || 'fire';
if (_effectPaletteIconSelect) _effectPaletteIconSelect.setValue(css.palette || 'fire');
document.getElementById('css-editor-effect-color').value = rgbArrayToHex(css.color || [255, 80, 0]);
document.getElementById('css-editor-effect-intensity').value = css.intensity ?? 1.0;
document.getElementById('css-editor-effect-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
document.getElementById('css-editor-effect-scale').value = css.scale ?? 1.0;
document.getElementById('css-editor-effect-scale-val').textContent = parseFloat(css.scale ?? 1.0).toFixed(1);
document.getElementById('css-editor-effect-mirror').checked = css.mirror || false;
} else if (sourceType === 'audio') {
await _loadAudioSources();
_loadAudioState(css);
} else if (sourceType === 'composite') {
_loadCompositeState(css);
} else if (sourceType === 'mapped') {
_loadMappedState(css);
} else if (sourceType === 'api_input') {
document.getElementById('css-editor-api-input-fallback-color').value =
rgbArrayToHex(css.fallback_color || [0, 0, 0]);
document.getElementById('css-editor-api-input-timeout').value = css.timeout ?? 5.0;
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 if (sourceType === 'daylight') {
document.getElementById('css-editor-daylight-speed').value = css.speed ?? 1.0;
document.getElementById('css-editor-daylight-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
document.getElementById('css-editor-daylight-real-time').checked = css.use_real_time || false;
document.getElementById('css-editor-daylight-latitude').value = css.latitude ?? 50.0;
document.getElementById('css-editor-daylight-latitude-val').textContent = parseFloat(css.latitude ?? 50.0).toFixed(0);
_syncDaylightSpeedVisibility();
} else if (sourceType === 'candlelight') {
document.getElementById('css-editor-candlelight-color').value = rgbArrayToHex(css.color || [255, 147, 41]);
document.getElementById('css-editor-candlelight-intensity').value = css.intensity ?? 1.0;
document.getElementById('css-editor-candlelight-intensity-val').textContent = parseFloat(css.intensity ?? 1.0).toFixed(1);
document.getElementById('css-editor-candlelight-num-candles').value = css.num_candles ?? 3;
document.getElementById('css-editor-candlelight-speed').value = css.speed ?? 1.0;
document.getElementById('css-editor-candlelight-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
} else if (sourceType === 'processed') {
await csptCache.fetch();
await colorStripSourcesCache.fetch();
_populateProcessedSelectors();
document.getElementById('css-editor-processed-input').value = css.input_source_id || '';
document.getElementById('css-editor-processed-template').value = css.processing_template_id || '';
} else {
if (sourceType === 'picture') sourceSelect.value = css.picture_source_id || '';
document.getElementById('css-editor-interpolation').value = css.interpolation_mode || 'average';
if (_interpolationIconSelect) _interpolationIconSelect.setValue(css.interpolation_mode || 'average');
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 handler = _typeHandlers[sourceType] || _typeHandlers.picture;
await handler.load(css, sourceSelect);
document.getElementById('css-editor-led-count').value = css.led_count ?? 0;
};
@@ -1576,58 +1860,18 @@ export async function showCSSEditor(cssId = null, cloneData = null, presetType =
} else {
document.getElementById('css-editor-id').value = '';
document.getElementById('css-editor-name').value = '';
document.getElementById('css-editor-type').value = presetType || 'picture';
const effectiveType = presetType || 'picture';
document.getElementById('css-editor-type').value = effectiveType;
onCSSTypeChange();
document.getElementById('css-editor-interpolation').value = 'average';
if (_interpolationIconSelect) _interpolationIconSelect.setValue('average');
document.getElementById('css-editor-smoothing').value = 0.3;
document.getElementById('css-editor-smoothing-value').textContent = '0.30';
document.getElementById('css-editor-color').value = '#ffffff';
document.getElementById('css-editor-led-count').value = 0;
_loadAnimationState(null);
_loadColorCycleState(null);
document.getElementById('css-editor-effect-type').value = 'fire';
document.getElementById('css-editor-effect-palette').value = 'fire';
document.getElementById('css-editor-effect-color').value = '#ff5000';
document.getElementById('css-editor-effect-intensity').value = 1.0;
document.getElementById('css-editor-effect-intensity-val').textContent = '1.0';
document.getElementById('css-editor-effect-scale').value = 1.0;
document.getElementById('css-editor-effect-scale-val').textContent = '1.0';
document.getElementById('css-editor-effect-mirror').checked = false;
_loadCompositeState(null);
_resetMappedState();
_resetAudioState();
document.getElementById('css-editor-api-input-fallback-color').value = '#000000';
document.getElementById('css-editor-api-input-timeout').value = 5.0;
document.getElementById('css-editor-api-input-timeout-val').textContent = '5.0';
_showApiInputEndpoints(null);
_resetNotificationState();
// Daylight defaults
document.getElementById('css-editor-daylight-speed').value = 1.0;
document.getElementById('css-editor-daylight-speed-val').textContent = '1.0';
document.getElementById('css-editor-daylight-real-time').checked = false;
document.getElementById('css-editor-daylight-latitude').value = 50.0;
document.getElementById('css-editor-daylight-latitude-val').textContent = '50';
// Candlelight defaults
document.getElementById('css-editor-candlelight-color').value = '#ff9329';
document.getElementById('css-editor-candlelight-intensity').value = 1.0;
document.getElementById('css-editor-candlelight-intensity-val').textContent = '1.0';
document.getElementById('css-editor-candlelight-num-candles').value = 3;
document.getElementById('css-editor-candlelight-speed').value = 1.0;
document.getElementById('css-editor-candlelight-speed-val').textContent = '1.0';
// Processed defaults
if (presetType === 'processed') {
await csptCache.fetch();
await colorStripSourcesCache.fetch();
_populateProcessedSelectors();
// Reset all type handlers to defaults
for (const handler of Object.values(_typeHandlers)) {
await handler.reset(effectiveType);
}
const typeIcon = getColorStripIcon(presetType || 'picture');
document.getElementById('css-editor-title').innerHTML = `${typeIcon} ${t('color_strip.add')}: ${t(`color_strip.type.${presetType || 'picture'}`)}`;
document.getElementById('css-editor-gradient-preset').value = '';
gradientInit([
{ position: 0.0, color: [255, 0, 0] },
{ position: 1.0, color: [0, 0, 255] },
]);
const typeIcon = getColorStripIcon(effectiveType);
document.getElementById('css-editor-title').innerHTML = `${typeIcon} ${t('color_strip.add')}: ${t(`color_strip.type.${effectiveType}`)}`;
_autoGenerateCSSName();
}
@@ -1673,163 +1917,12 @@ export async function saveCSSEditor() {
return;
}
let payload;
if (sourceType === 'static') {
payload = {
name,
color: hexToRgbArray(document.getElementById('css-editor-color').value),
animation: _getAnimationPayload(),
};
if (!cssId) payload.source_type = 'static';
} else if (sourceType === 'color_cycle') {
const cycleColors = _colorCycleGetColors();
if (cycleColors.length < 2) {
cssEditorModal.showError(t('color_strip.color_cycle.min_colors'));
return;
}
payload = {
name,
colors: cycleColors,
};
if (!cssId) payload.source_type = 'color_cycle';
} else if (sourceType === 'gradient') {
const gStops = getGradientStops();
if (gStops.length < 2) {
cssEditorModal.showError(t('color_strip.gradient.min_stops'));
return;
}
payload = {
name,
stops: gStops.map(s => ({
position: s.position,
color: s.color,
...(s.colorRight ? { color_right: s.colorRight } : {}),
})),
animation: _getAnimationPayload(),
};
if (!cssId) payload.source_type = 'gradient';
} else if (sourceType === 'effect') {
payload = {
name,
effect_type: document.getElementById('css-editor-effect-type').value,
palette: document.getElementById('css-editor-effect-palette').value,
intensity: parseFloat(document.getElementById('css-editor-effect-intensity').value),
scale: parseFloat(document.getElementById('css-editor-effect-scale').value),
mirror: document.getElementById('css-editor-effect-mirror').checked,
};
// Meteor uses a color picker
if (payload.effect_type === 'meteor') {
const hex = document.getElementById('css-editor-effect-color').value;
payload.color = [parseInt(hex.slice(1, 3), 16), parseInt(hex.slice(3, 5), 16), parseInt(hex.slice(5, 7), 16)];
}
if (!cssId) payload.source_type = 'effect';
} else if (sourceType === 'audio') {
payload = {
name,
visualization_mode: document.getElementById('css-editor-audio-viz').value,
audio_source_id: document.getElementById('css-editor-audio-source').value || null,
sensitivity: parseFloat(document.getElementById('css-editor-audio-sensitivity').value),
smoothing: parseFloat(document.getElementById('css-editor-audio-smoothing').value),
palette: document.getElementById('css-editor-audio-palette').value,
color: hexToRgbArray(document.getElementById('css-editor-audio-color').value),
color_peak: hexToRgbArray(document.getElementById('css-editor-audio-color-peak').value),
mirror: document.getElementById('css-editor-audio-mirror').checked,
};
if (!cssId) payload.source_type = 'audio';
} else if (sourceType === 'composite') {
const layers = _compositeGetLayers();
if (layers.length < 1) {
cssEditorModal.showError(t('color_strip.composite.error.min_layers'));
return;
}
const hasEmpty = layers.some(l => !l.source_id);
if (hasEmpty) {
cssEditorModal.showError(t('color_strip.composite.error.no_source'));
return;
}
payload = {
name,
layers,
};
if (!cssId) payload.source_type = 'composite';
} else if (sourceType === 'mapped') {
const zones = _mappedGetZones();
const hasEmpty = zones.some(z => !z.source_id);
if (hasEmpty) {
cssEditorModal.showError(t('color_strip.mapped.error.no_source'));
return;
}
payload = { name, zones };
if (!cssId) payload.source_type = 'mapped';
} else if (sourceType === 'api_input') {
const fbHex = document.getElementById('css-editor-api-input-fallback-color').value;
payload = {
name,
fallback_color: hexToRgbArray(fbHex),
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('\n').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(),
};
if (!cssId) payload.source_type = 'notification';
} else if (sourceType === 'daylight') {
payload = {
name,
speed: parseFloat(document.getElementById('css-editor-daylight-speed').value),
use_real_time: document.getElementById('css-editor-daylight-real-time').checked,
latitude: parseFloat(document.getElementById('css-editor-daylight-latitude').value),
};
if (!cssId) payload.source_type = 'daylight';
} else if (sourceType === 'candlelight') {
payload = {
name,
color: hexToRgbArray(document.getElementById('css-editor-candlelight-color').value),
intensity: parseFloat(document.getElementById('css-editor-candlelight-intensity').value),
num_candles: parseInt(document.getElementById('css-editor-candlelight-num-candles').value) || 3,
speed: parseFloat(document.getElementById('css-editor-candlelight-speed').value),
};
if (!cssId) payload.source_type = 'candlelight';
} else if (sourceType === 'processed') {
const inputId = document.getElementById('css-editor-processed-input').value;
const templateId = document.getElementById('css-editor-processed-template').value;
if (!inputId) {
cssEditorModal.showError(t('color_strip.processed.error.no_input'));
return;
}
payload = {
name,
input_source_id: inputId,
processing_template_id: templateId || null,
};
if (!cssId) payload.source_type = 'processed';
} else if (sourceType === 'picture_advanced') {
payload = {
name,
interpolation_mode: document.getElementById('css-editor-interpolation').value,
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
};
if (!cssId) payload.source_type = 'picture_advanced';
} else {
payload = {
name,
picture_source_id: document.getElementById('css-editor-picture-source').value,
interpolation_mode: document.getElementById('css-editor-interpolation').value,
smoothing: parseFloat(document.getElementById('css-editor-smoothing').value),
led_count: parseInt(document.getElementById('css-editor-led-count').value) || 0,
};
if (!cssId) payload.source_type = 'picture';
}
const knownType = sourceType in _typeHandlers;
const handler = knownType ? _typeHandlers[sourceType] : _typeHandlers.picture;
const payload = handler.getPayload(name);
if (payload === null) return; // validation error already shown
if (!cssId) payload.source_type = knownType ? sourceType : 'picture';
// Attach clock_id for animated types
const clockTypes = ['static', 'gradient', 'color_cycle', 'effect', 'daylight', 'candlelight'];

View File

@@ -14,6 +14,7 @@ import {
} from '../core/icons.js';
import { loadScenePresets, renderScenePresetsSection } from './scene-presets.js';
import { cardColorStyle } from '../core/card-colors.js';
import { createFpsSparkline } from '../core/chart-utils.js';
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
const MAX_FPS_SAMPLES = 120;
@@ -88,44 +89,7 @@ function _destroyFpsCharts() {
}
function _createFpsChart(canvasId, actualHistory, currentHistory, fpsTarget) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
return new Chart(canvas, {
type: 'line',
data: {
labels: actualHistory.map(() => ''),
datasets: [
{
data: [...actualHistory],
borderColor: '#2196F3',
backgroundColor: 'rgba(33,150,243,0.12)',
borderWidth: 1.5,
tension: 0.3,
fill: true,
pointRadius: 0,
},
{
data: [...currentHistory],
borderColor: '#4CAF50',
borderWidth: 1.5,
tension: 0.3,
fill: false,
pointRadius: 0,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
animation: false,
plugins: { legend: { display: false }, tooltip: { enabled: false } },
scales: {
x: { display: false },
y: { min: 0, max: fpsTarget * 1.15, display: false },
},
layout: { padding: 0 },
},
});
return createFpsSparkline(canvasId, actualHistory, currentHistory, fpsTarget);
}
async function _initFpsCharts(runningTargetIds) {

View File

@@ -651,6 +651,8 @@ export async function loadDevices() {
await window.loadTargetsTab();
}
document.addEventListener('auth:keyChanged', () => loadDevices());
// ===== OpenRGB zone count enrichment =====
// Cache: baseUrl → { zoneName: ledCount, ... }

View File

@@ -16,6 +16,7 @@ import {
_streamModalPPTemplates, set_streamModalPPTemplates,
_modalFilters, set_modalFilters,
_ppTemplateNameManuallyEdited, set_ppTemplateNameManuallyEdited,
currentTestingTemplate, setCurrentTestingTemplate,
_currentTestStreamId, set_currentTestStreamId,
_currentTestPPTemplateId, set_currentTestPPTemplateId,
_lastValidatedImageSource, set_lastValidatedImageSource,
@@ -59,7 +60,7 @@ import { wrapCard } from '../core/card-colors.js';
import { TagInput, renderTagChips } from '../core/tag-input.js';
import { IconSelect } from '../core/icon-select.js';
import { EntitySelect } from '../core/entity-palette.js';
import * as P from '../core/icon-paths.js';
import { FilterListManager } from '../core/filter-list.js';
// ── TagInput instances for modals ──
let _captureTemplateTagsInput = null;
@@ -315,7 +316,7 @@ export async function showTestTemplateModal(templateId) {
return;
}
window.currentTestingTemplate = template;
setCurrentTestingTemplate(template);
await loadDisplaysForTest();
restoreCaptureDuration();
@@ -328,7 +329,7 @@ export async function showTestTemplateModal(templateId) {
export function closeTestTemplateModal() {
testTemplateModal.forceClose();
window.currentTestingTemplate = null;
setCurrentTestingTemplate(null);
}
async function loadAvailableEngines() {
@@ -470,7 +471,7 @@ function collectEngineConfig() {
async function loadDisplaysForTest() {
try {
// Use engine-specific display list for engines with own devices (camera, scrcpy)
const engineType = window.currentTestingTemplate?.engine_type;
const engineType = currentTestingTemplate?.engine_type;
const engineHasOwnDisplays = availableEngines.find(e => e.type === engineType)?.has_own_displays || false;
const url = engineHasOwnDisplays
? `/config/displays?engine_type=${engineType}`
@@ -508,7 +509,7 @@ async function loadDisplaysForTest() {
}
export function runTemplateTest() {
if (!window.currentTestingTemplate) {
if (!currentTestingTemplate) {
showToast(t('templates.test.error.no_engine'), 'error');
return;
}
@@ -521,7 +522,7 @@ export function runTemplateTest() {
return;
}
const template = window.currentTestingTemplate;
const template = currentTestingTemplate;
localStorage.setItem('lastTestDisplayIndex', displayIndex);
const previewWidth = Math.round(Math.min(window.innerWidth * 0.8, 1920) * Math.min(window.devicePixelRatio || 1, 2));
@@ -559,7 +560,7 @@ function buildTestStatsHtml(result) {
<div class="stat-item"><span>${t('templates.test.results.avg_capture_time')}:</span> <strong>${Number(avgMs).toFixed(1)}ms</strong></div>`;
}
html += `
<div class="stat-item"><span>Resolution:</span> <strong>${res}</strong></div>`;
<div class="stat-item"><span>${t('templates.test.results.resolution')}</span> <strong>${res}</strong></div>`;
return html;
}
@@ -1672,7 +1673,7 @@ export function onStreamDisplaySelected(displayIndex, display) {
export function onTestDisplaySelected(displayIndex, display) {
document.getElementById('test-template-display').value = displayIndex;
const engineType = window.currentTestingTemplate?.engine_type || null;
const engineType = currentTestingTemplate?.engine_type || null;
document.getElementById('test-display-picker-label').textContent = formatDisplayLabel(displayIndex, display, engineType);
}
@@ -2242,169 +2243,22 @@ function _getStripFilterName(filterId) {
return translated;
}
let _filterIconSelect = null;
// ── PP FilterListManager instance ──
const ppFilterManager = new FilterListManager({
getFilters: () => _modalFilters,
getFilterDefs: () => _availableFilters,
getFilterName: _getFilterName,
selectId: 'pp-add-filter-select',
containerId: 'pp-filter-list',
prefix: '',
editingIdInputId: 'pp-template-id',
selfRefFilterId: 'filter_template',
autoNameFn: () => _autoGeneratePPTemplateName(),
initDrag: _initFilterDragForContainer,
initPaletteGrids: _initFilterPaletteGrids,
});
const _FILTER_ICONS = {
brightness: P.sunDim,
saturation: P.palette,
gamma: P.sun,
downscaler: P.monitor,
pixelate: P.layoutDashboard,
auto_crop: P.target,
flip: P.rotateCw,
color_correction: P.palette,
filter_template: P.fileText,
frame_interpolation: P.fastForward,
noise_gate: P.volume2,
palette_quantization: P.sparkles,
css_filter_template: P.fileText,
};
function _populateFilterSelect() {
const select = document.getElementById('pp-add-filter-select');
select.innerHTML = `<option value="">${t('filters.select_type')}</option>`;
const items = [];
for (const f of _availableFilters) {
const name = _getFilterName(f.filter_id);
select.innerHTML += `<option value="${f.filter_id}">${name}</option>`;
const pathData = _FILTER_ICONS[f.filter_id] || P.wrench;
items.push({
value: f.filter_id,
icon: `<svg class="icon" viewBox="0 0 24 24">${pathData}</svg>`,
label: name,
desc: t(`filters.${f.filter_id}.desc`),
});
}
if (_filterIconSelect) {
_filterIconSelect.updateItems(items);
} else if (items.length > 0) {
_filterIconSelect = new IconSelect({
target: select,
items,
columns: 3,
placeholder: t('filters.select_type'),
onChange: () => addFilterFromSelect(),
});
}
}
/**
* Generic filter list renderer — shared by PP template and CSPT modals.
* @param {string} containerId - DOM container ID for filter cards
* @param {Array} filtersArr - mutable array of {filter_id, options, _expanded}
* @param {Array} filterDefs - available filter definitions (with options_schema)
* @param {string} prefix - handler prefix: '' for PP, 'cspt' for CSPT
* @param {string} editingIdInputId - ID of hidden input holding the editing template ID
* @param {string} selfRefFilterId - filter_id that should exclude self ('filter_template' or 'css_filter_template')
*/
function _renderFilterListGeneric(containerId, filtersArr, filterDefs, prefix, editingIdInputId, selfRefFilterId) {
const container = document.getElementById(containerId);
if (filtersArr.length === 0) {
container.innerHTML = `<div class="pp-filter-empty">${t('filters.empty')}</div>`;
return;
}
const toggleFn = prefix ? `${prefix}ToggleFilterExpand` : 'toggleFilterExpand';
const removeFn = prefix ? `${prefix}RemoveFilter` : 'removeFilter';
const updateFn = prefix ? `${prefix}UpdateFilterOption` : 'updateFilterOption';
const inputPrefix = prefix ? `${prefix}-filter` : 'filter';
const nameFn = prefix ? _getStripFilterName : _getFilterName;
let html = '';
filtersArr.forEach((fi, index) => {
const filterDef = filterDefs.find(f => f.filter_id === fi.filter_id);
const filterName = nameFn(fi.filter_id);
const isExpanded = fi._expanded === true;
let summary = '';
if (filterDef && !isExpanded) {
summary = filterDef.options_schema.map(opt => {
const val = fi.options[opt.key] !== undefined ? fi.options[opt.key] : opt.default;
return val;
}).join(', ');
}
html += `<div class="pp-filter-card${isExpanded ? ' expanded' : ''}" data-filter-index="${index}">
<div class="pp-filter-card-header" onclick="${toggleFn}(${index})">
<span class="pp-filter-drag-handle" title="${t('filters.drag_to_reorder')}">&#x2807;</span>
<span class="pp-filter-card-chevron">${isExpanded ? '&#x25BC;' : '&#x25B6;'}</span>
<span class="pp-filter-card-name">${escapeHtml(filterName)}</span>
${summary ? `<span class="pp-filter-card-summary">${escapeHtml(summary)}</span>` : ''}
<div class="pp-filter-card-actions" onclick="event.stopPropagation()">
<button type="button" class="btn-filter-action btn-filter-remove" onclick="${removeFn}(${index})" title="${t('filters.remove')}">&#x2715;</button>
</div>
</div>
<div class="pp-filter-card-options"${isExpanded ? '' : ' style="display:none"'}>`;
if (filterDef) {
for (const opt of filterDef.options_schema) {
const currentVal = fi.options[opt.key] !== undefined ? fi.options[opt.key] : opt.default;
const inputId = `${inputPrefix}-${index}-${opt.key}`;
if (opt.type === 'bool') {
const checked = currentVal === true || currentVal === 'true';
html += `<div class="pp-filter-option pp-filter-option-bool">
<label for="${inputId}">
<span>${escapeHtml(opt.label)}</span>
<input type="checkbox" id="${inputId}" ${checked ? 'checked' : ''}
onchange="${updateFn}(${index}, '${opt.key}', this.checked)">
</label>
</div>`;
} else if (opt.type === 'select' && Array.isArray(opt.choices)) {
const editingId = document.getElementById(editingIdInputId)?.value || '';
const filteredChoices = (fi.filter_id === selfRefFilterId && opt.key === 'template_id' && editingId)
? opt.choices.filter(c => c.value !== editingId)
: opt.choices;
let selectVal = currentVal;
if (filteredChoices.length > 0 && !filteredChoices.some(c => c.value === selectVal)) {
selectVal = filteredChoices[0].value;
fi.options[opt.key] = selectVal;
}
const hasPaletteColors = filteredChoices.some(c => c.colors);
const options = filteredChoices.map(c =>
`<option value="${escapeHtml(c.value)}"${c.value === selectVal ? ' selected' : ''}>${escapeHtml(c.label)}</option>`
).join('');
const gridAttr = hasPaletteColors ? ` data-palette-grid="${escapeHtml(JSON.stringify(filteredChoices))}"` : '';
const isTemplateRef = opt.key === 'template_id';
const entityAttr = isTemplateRef ? ' data-entity-select="template"' : '';
html += `<div class="pp-filter-option">
<label for="${inputId}"><span>${escapeHtml(opt.label)}:</span></label>
<select id="${inputId}"${gridAttr}${entityAttr}
onchange="${updateFn}(${index}, '${opt.key}', this.value)">
${options}
</select>
</div>`;
} else if (opt.type === 'string') {
const maxLen = opt.max_length || 500;
html += `<div class="pp-filter-option">
<label for="${inputId}"><span>${escapeHtml(opt.label)}:</span></label>
<input type="text" id="${inputId}" value="${escapeHtml(String(currentVal))}"
maxlength="${maxLen}" class="pp-filter-text-input"
onchange="${updateFn}(${index}, '${opt.key}', this.value)">
</div>`;
} else {
html += `<div class="pp-filter-option">
<label for="${inputId}">
<span>${escapeHtml(opt.label)}:</span>
<span id="${inputId}-display">${currentVal}</span>
</label>
<input type="range" id="${inputId}"
min="${opt.min_value}" max="${opt.max_value}" step="${opt.step}" value="${currentVal}"
oninput="${updateFn}(${index}, '${opt.key}', this.value); document.getElementById('${inputId}-display').textContent = this.value;">
</div>`;
}
}
}
html += `</div></div>`;
});
container.innerHTML = html;
_initFilterDragForContainer(containerId, filtersArr, () => {
_renderFilterListGeneric(containerId, filtersArr, filterDefs, prefix, editingIdInputId, selfRefFilterId);
});
// Initialize palette icon grids on select elements
_initFilterPaletteGrids(container);
}
// _renderFilterListGeneric has been replaced by FilterListManager.render()
/** Stored IconSelect instances for filter option selects (keyed by select element id). */
const _filterOptionIconSelects = {};
@@ -2449,11 +2303,11 @@ function _initFilterPaletteGrids(container) {
}
export function renderModalFilterList() {
_renderFilterListGeneric('pp-filter-list', _modalFilters, _availableFilters, '', 'pp-template-id', 'filter_template');
ppFilterManager.render();
}
export function renderCSPTModalFilterList() {
_renderFilterListGeneric('cspt-filter-list', _csptModalFilters, _stripFilters, 'cspt', 'cspt-id', 'css_filter_template');
csptFilterManager.render();
}
/* ── Generic filter drag-and-drop reordering ── */
@@ -2611,104 +2465,23 @@ function _filterAutoScroll(clientY, ds) {
ds.scrollRaf = requestAnimationFrame(scroll);
}
/**
* Generic: add a filter from a select element into a filters array.
*/
function _addFilterGeneric(selectId, filtersArr, filterDefs, iconSelect, renderFn, autoNameFn) {
const select = document.getElementById(selectId);
const filterId = select.value;
if (!filterId) return;
// _addFilterGeneric and _updateFilterOptionGeneric have been replaced by FilterListManager methods
const filterDef = filterDefs.find(f => f.filter_id === filterId);
if (!filterDef) return;
// ── PP filter actions (delegate to ppFilterManager) ──
export function addFilterFromSelect() { ppFilterManager.addFromSelect(); }
export function toggleFilterExpand(index) { ppFilterManager.toggleExpand(index); }
export function removeFilter(index) { ppFilterManager.remove(index); }
export function moveFilter(index, direction) { ppFilterManager.move(index, direction); }
export function updateFilterOption(filterIndex, optionKey, value) { ppFilterManager.updateOption(filterIndex, optionKey, value); }
const options = {};
for (const opt of filterDef.options_schema) {
if (opt.type === 'select' && !opt.default && Array.isArray(opt.choices) && opt.choices.length > 0) {
options[opt.key] = opt.choices[0].value;
} else {
options[opt.key] = opt.default;
}
}
filtersArr.push({ filter_id: filterId, options, _expanded: true });
select.value = '';
if (iconSelect) iconSelect.setValue('');
renderFn();
if (autoNameFn) autoNameFn();
}
function _updateFilterOptionGeneric(filterIndex, optionKey, value, filtersArr, filterDefs) {
if (filtersArr[filterIndex]) {
const fi = filtersArr[filterIndex];
const filterDef = filterDefs.find(f => f.filter_id === fi.filter_id);
if (filterDef) {
const optDef = filterDef.options_schema.find(o => o.key === optionKey);
if (optDef && optDef.type === 'bool') {
fi.options[optionKey] = !!value;
} else if (optDef && optDef.type === 'select') {
fi.options[optionKey] = String(value);
} else if (optDef && optDef.type === 'string') {
fi.options[optionKey] = String(value);
} else if (optDef && optDef.type === 'int') {
fi.options[optionKey] = parseInt(value);
} else {
fi.options[optionKey] = parseFloat(value);
}
} else {
fi.options[optionKey] = parseFloat(value);
}
}
}
// ── PP filter actions ──
export function addFilterFromSelect() {
_addFilterGeneric('pp-add-filter-select', _modalFilters, _availableFilters, _filterIconSelect, renderModalFilterList, _autoGeneratePPTemplateName);
}
export function toggleFilterExpand(index) {
if (_modalFilters[index]) { _modalFilters[index]._expanded = !_modalFilters[index]._expanded; renderModalFilterList(); }
}
export function removeFilter(index) {
_modalFilters.splice(index, 1); renderModalFilterList(); _autoGeneratePPTemplateName();
}
export function moveFilter(index, direction) {
const newIndex = index + direction;
if (newIndex < 0 || newIndex >= _modalFilters.length) return;
const tmp = _modalFilters[index];
_modalFilters[index] = _modalFilters[newIndex];
_modalFilters[newIndex] = tmp;
renderModalFilterList(); _autoGeneratePPTemplateName();
}
export function updateFilterOption(filterIndex, optionKey, value) {
_updateFilterOptionGeneric(filterIndex, optionKey, value, _modalFilters, _availableFilters);
}
// ── CSPT filter actions ──
export function csptAddFilterFromSelect() {
_addFilterGeneric('cspt-add-filter-select', _csptModalFilters, _stripFilters, _csptFilterIconSelect, renderCSPTModalFilterList, _autoGenerateCSPTName);
}
export function csptToggleFilterExpand(index) {
if (_csptModalFilters[index]) { _csptModalFilters[index]._expanded = !_csptModalFilters[index]._expanded; renderCSPTModalFilterList(); }
}
export function csptRemoveFilter(index) {
_csptModalFilters.splice(index, 1); renderCSPTModalFilterList(); _autoGenerateCSPTName();
}
export function csptUpdateFilterOption(filterIndex, optionKey, value) {
_updateFilterOptionGeneric(filterIndex, optionKey, value, _csptModalFilters, _stripFilters);
}
// ── CSPT filter actions (delegate to csptFilterManager) ──
export function csptAddFilterFromSelect() { csptFilterManager.addFromSelect(); }
export function csptToggleFilterExpand(index) { csptFilterManager.toggleExpand(index); }
export function csptRemoveFilter(index) { csptFilterManager.remove(index); }
export function csptUpdateFilterOption(filterIndex, optionKey, value) { csptFilterManager.updateOption(filterIndex, optionKey, value); }
function collectFilters() {
return _modalFilters.map(fi => ({
filter_id: fi.filter_id,
options: { ...fi.options },
}));
return ppFilterManager.collect();
}
function _autoGeneratePPTemplateName() {
@@ -2743,7 +2516,7 @@ export async function showAddPPTemplateModal(cloneData = null) {
}
document.getElementById('pp-template-name').oninput = () => { set_ppTemplateNameManuallyEdited(true); };
_populateFilterSelect();
ppFilterManager.populateSelect(() => addFilterFromSelect());
renderModalFilterList();
// Pre-fill from clone data after form is set up
@@ -2780,7 +2553,7 @@ export async function editPPTemplate(templateId) {
options: { ...fi.options },
})));
_populateFilterSelect();
ppFilterManager.populateSelect(() => addFilterFromSelect());
renderModalFilterList();
// Tags
@@ -2894,7 +2667,20 @@ export async function closePPTemplateModal() {
// ===== Color Strip Processing Templates (CSPT) =====
let _csptFilterIconSelect = null;
// ── CSPT FilterListManager instance ──
const csptFilterManager = new FilterListManager({
getFilters: () => _csptModalFilters,
getFilterDefs: () => _stripFilters,
getFilterName: _getStripFilterName,
selectId: 'cspt-add-filter-select',
containerId: 'cspt-filter-list',
prefix: 'cspt',
editingIdInputId: 'cspt-id',
selfRefFilterId: 'css_filter_template',
autoNameFn: () => _autoGenerateCSPTName(),
initDrag: _initFilterDragForContainer,
initPaletteGrids: _initFilterPaletteGrids,
});
async function loadStripFilters() {
await stripFiltersCache.fetch();
@@ -2910,34 +2696,6 @@ async function loadCSPTemplates() {
}
}
function _populateCSPTFilterSelect() {
const select = document.getElementById('cspt-add-filter-select');
select.innerHTML = `<option value="">${t('filters.select_type')}</option>`;
const items = [];
for (const f of _stripFilters) {
const name = _getStripFilterName(f.filter_id);
select.innerHTML += `<option value="${f.filter_id}">${name}</option>`;
const pathData = _FILTER_ICONS[f.filter_id] || P.wrench;
items.push({
value: f.filter_id,
icon: `<svg class="icon" viewBox="0 0 24 24">${pathData}</svg>`,
label: name,
desc: t(`filters.${f.filter_id}.desc`),
});
}
if (_csptFilterIconSelect) {
_csptFilterIconSelect.updateItems(items);
} else if (items.length > 0) {
_csptFilterIconSelect = new IconSelect({
target: select,
items,
columns: 3,
placeholder: t('filters.select_type'),
onChange: () => csptAddFilterFromSelect(),
});
}
}
function _autoGenerateCSPTName() {
if (_csptNameManuallyEdited) return;
if (document.getElementById('cspt-id').value) return;
@@ -2951,10 +2709,7 @@ function _autoGenerateCSPTName() {
}
function collectCSPTFilters() {
return _csptModalFilters.map(fi => ({
filter_id: fi.filter_id,
options: { ...fi.options },
}));
return csptFilterManager.collect();
}
export async function showAddCSPTModal(cloneData = null) {
@@ -2977,7 +2732,7 @@ export async function showAddCSPTModal(cloneData = null) {
}
document.getElementById('cspt-name').oninput = () => { set_csptNameManuallyEdited(true); };
_populateCSPTFilterSelect();
csptFilterManager.populateSelect(() => csptAddFilterFromSelect());
renderCSPTModalFilterList();
if (cloneData) {
@@ -3012,7 +2767,7 @@ export async function editCSPT(templateId) {
options: { ...fi.options },
})));
_populateCSPTFilterSelect();
csptFilterManager.populateSelect(() => csptAddFilterFromSelect());
renderCSPTModalFilterList();
if (_csptTagsInput) { _csptTagsInput.destroy(); _csptTagsInput = null; }

View File

@@ -31,6 +31,7 @@ import { IconSelect } from '../core/icon-select.js';
import * as P from '../core/icon-paths.js';
import { wrapCard } from '../core/card-colors.js';
import { TagInput, renderTagChips } from '../core/tag-input.js';
import { createFpsSparkline } from '../core/chart-utils.js';
import { CardSection } from '../core/card-sections.js';
import { TreeNav } from '../core/tree-nav.js';
import { updateSubTabHash, updateTabBadge } from './tabs.js';
@@ -68,52 +69,7 @@ function _pushTargetFps(targetId, actual, current) {
}
function _createTargetFpsChart(canvasId, actualHistory, currentHistory, fpsTarget, maxHwFps) {
const canvas = document.getElementById(canvasId);
if (!canvas) return null;
const labels = actualHistory.map(() => '');
const datasets = [
{
data: [...actualHistory],
borderColor: '#2196F3',
backgroundColor: 'rgba(33,150,243,0.12)',
borderWidth: 1.5,
tension: 0.3,
fill: true,
pointRadius: 0,
},
{
data: [...currentHistory],
borderColor: '#4CAF50',
borderWidth: 1.5,
tension: 0.3,
fill: false,
pointRadius: 0,
},
];
// Flat line showing hardware max FPS
if (maxHwFps && maxHwFps < fpsTarget * 1.15) {
datasets.push({
data: actualHistory.map(() => maxHwFps),
borderColor: 'rgba(255,152,0,0.5)',
borderWidth: 1,
borderDash: [4, 3],
pointRadius: 0,
fill: false,
});
}
return new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
responsive: true, maintainAspectRatio: false,
animation: false,
plugins: { legend: { display: false }, tooltip: { display: false } },
scales: {
x: { display: false },
y: { display: false, min: 0, max: fpsTarget * 1.15 },
},
},
});
return createFpsSparkline(canvasId, actualHistory, currentHistory, fpsTarget, { maxHwFps });
}
function _updateTargetFpsChart(targetId, fpsTarget) {