Remove per-source speed, fix device dirty check, and add frontend caching
Speed is now exclusively controlled via sync clocks — CSS sources no longer carry their own speed/cycle_speed fields. Streams default to 1.0× when no clock is assigned. Also fixes false-positive dirty check on the device settings modal (array reference comparison) and converts several frontend modules to use DataCache for consistent API response caching. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
calibrationTestState, EDGE_TEST_COLORS,
|
||||
calibrationTestState, EDGE_TEST_COLORS, displaysCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -134,9 +134,9 @@ export async function toggleCalibrationOverlay() {
|
||||
|
||||
export async function showCalibration(deviceId) {
|
||||
try {
|
||||
const [response, displaysResponse] = await Promise.all([
|
||||
const [response, displays] = await Promise.all([
|
||||
fetchWithAuth(`/devices/${deviceId}`),
|
||||
fetchWithAuth('/config/displays'),
|
||||
displaysCache.fetch().catch(() => []),
|
||||
]);
|
||||
|
||||
if (!response.ok) { showToast(t('calibration.error.load_failed'), 'error'); return; }
|
||||
@@ -145,15 +145,10 @@ export async function showCalibration(deviceId) {
|
||||
const calibration = device.calibration;
|
||||
|
||||
const preview = document.querySelector('.calibration-preview');
|
||||
if (displaysResponse.ok) {
|
||||
const displaysData = await displaysResponse.json();
|
||||
const displayIndex = device.settings?.display_index ?? 0;
|
||||
const display = (displaysData.displays || []).find(d => d.index === displayIndex);
|
||||
if (display && display.width && display.height) {
|
||||
preview.style.aspectRatio = `${display.width} / ${display.height}`;
|
||||
} else {
|
||||
preview.style.aspectRatio = '';
|
||||
}
|
||||
const displayIndex = device.settings?.display_index ?? 0;
|
||||
const display = displays.find(d => d.index === displayIndex);
|
||||
if (display && display.width && display.height) {
|
||||
preview.style.aspectRatio = `${display.width} / ${display.height}`;
|
||||
} else {
|
||||
preview.style.aspectRatio = '';
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { _cachedSyncClocks } from '../core/state.js';
|
||||
import { _cachedSyncClocks, audioSourcesCache, streamsCache } from '../core/state.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
@@ -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_FAST_FORWARD, ICON_ACTIVITY, ICON_CLOCK,
|
||||
ICON_LINK, ICON_SPARKLES, ICON_ACTIVITY, ICON_CLOCK,
|
||||
} from '../core/icons.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
|
||||
@@ -37,11 +37,8 @@ class CSSEditorModal extends Modal {
|
||||
led_count: document.getElementById('css-editor-led-count').value,
|
||||
gradient_stops: type === 'gradient' ? JSON.stringify(_gradientStops) : '[]',
|
||||
animation_type: document.getElementById('css-editor-animation-type').value,
|
||||
animation_speed: document.getElementById('css-editor-animation-speed').value,
|
||||
cycle_speed: document.getElementById('css-editor-cycle-speed').value,
|
||||
cycle_colors: JSON.stringify(_colorCycleColors),
|
||||
effect_type: document.getElementById('css-editor-effect-type').value,
|
||||
effect_speed: document.getElementById('css-editor-effect-speed').value,
|
||||
effect_palette: document.getElementById('css-editor-effect-palette').value,
|
||||
effect_color: document.getElementById('css-editor-effect-color').value,
|
||||
effect_intensity: document.getElementById('css-editor-effect-intensity').value,
|
||||
@@ -141,16 +138,7 @@ function _populateClockDropdown(selectedId) {
|
||||
}
|
||||
|
||||
export function onCSSClockChange() {
|
||||
// When a clock is selected, hide speed sliders (speed comes from clock)
|
||||
const clockId = document.getElementById('css-editor-clock').value;
|
||||
const type = document.getElementById('css-editor-type').value;
|
||||
if (type === 'effect') {
|
||||
document.getElementById('css-editor-effect-speed-group').style.display = clockId ? 'none' : '';
|
||||
} else if (type === 'color_cycle') {
|
||||
document.getElementById('css-editor-cycle-speed-group').style.display = clockId ? 'none' : '';
|
||||
} else if (type === 'static' || type === 'gradient') {
|
||||
document.getElementById('css-editor-animation-speed-group').style.display = clockId ? 'none' : '';
|
||||
}
|
||||
// No-op: speed sliders removed; speed is now clock-only
|
||||
}
|
||||
|
||||
function _getAnimationPayload() {
|
||||
@@ -158,15 +146,10 @@ function _getAnimationPayload() {
|
||||
return {
|
||||
enabled: type !== 'none',
|
||||
type: type !== 'none' ? type : 'breathing',
|
||||
speed: parseFloat(document.getElementById('css-editor-animation-speed').value),
|
||||
};
|
||||
}
|
||||
|
||||
function _loadAnimationState(anim) {
|
||||
const speedEl = document.getElementById('css-editor-animation-speed');
|
||||
speedEl.value = (anim && anim.speed != null) ? anim.speed : 1.0;
|
||||
document.getElementById('css-editor-animation-speed-val').textContent =
|
||||
parseFloat(speedEl.value).toFixed(1);
|
||||
// Set type after onCSSTypeChange() has populated the dropdown
|
||||
if (anim && anim.enabled && anim.type) {
|
||||
document.getElementById('css-editor-animation-type').value = anim.type;
|
||||
@@ -182,8 +165,6 @@ export function onAnimationTypeChange() {
|
||||
|
||||
function _syncAnimationSpeedState() {
|
||||
const type = document.getElementById('css-editor-animation-type').value;
|
||||
const isNone = type === 'none';
|
||||
document.getElementById('css-editor-animation-speed').disabled = isNone;
|
||||
const descEl = document.getElementById('css-editor-animation-type-desc');
|
||||
if (descEl) {
|
||||
const desc = t('color_strip.animation.type.' + type + '.desc') || '';
|
||||
@@ -302,13 +283,6 @@ function _loadColorCycleState(css) {
|
||||
? raw.map(c => rgbArrayToHex(c))
|
||||
: [..._DEFAULT_CYCLE_COLORS];
|
||||
_colorCycleRenderList();
|
||||
const speed = (css && css.cycle_speed != null) ? css.cycle_speed : 1.0;
|
||||
const speedEl = document.getElementById('css-editor-cycle-speed');
|
||||
if (speedEl) {
|
||||
speedEl.value = speed;
|
||||
document.getElementById('css-editor-cycle-speed-val').textContent =
|
||||
parseFloat(speed).toFixed(1);
|
||||
}
|
||||
}
|
||||
|
||||
/** Convert an [R, G, B] array to a CSS hex color string like "#rrggbb". */
|
||||
@@ -553,10 +527,7 @@ async function _loadAudioSources() {
|
||||
const select = document.getElementById('css-editor-audio-source');
|
||||
if (!select) return;
|
||||
try {
|
||||
const resp = await fetchWithAuth('/audio-sources');
|
||||
if (!resp.ok) return;
|
||||
const data = await resp.json();
|
||||
const sources = data.sources || [];
|
||||
const sources = await audioSourcesCache.fetch();
|
||||
select.innerHTML = sources.map(s => {
|
||||
const badge = s.source_type === 'multichannel' ? ' [multichannel]' : ' [mono]';
|
||||
return `<option value="${s.id}">${escapeHtml(s.name)}${badge}</option>`;
|
||||
@@ -626,7 +597,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
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>`
|
||||
+ (source.clock_id ? '' : `<span class="stream-card-prop" title="${t('color_strip.animation.speed')}">${ICON_FAST_FORWARD} ${(anim.speed || 1.0).toFixed(1)}×</span>`)
|
||||
: '';
|
||||
|
||||
let propsHtml;
|
||||
@@ -647,7 +617,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
).join('');
|
||||
propsHtml = `
|
||||
<span class="stream-card-prop">${swatches}</span>
|
||||
${source.clock_id ? '' : `<span class="stream-card-prop" title="${t('color_strip.color_cycle.speed')}">${ICON_FAST_FORWARD} ${(source.cycle_speed || 1.0).toFixed(1)}×</span>`}
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
${clockBadge}
|
||||
`;
|
||||
@@ -680,7 +649,6 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
propsHtml = `
|
||||
<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>` : ''}
|
||||
${source.clock_id ? '' : `<span class="stream-card-prop" title="${t('color_strip.effect.speed')}">${ICON_FAST_FORWARD} ${(source.speed || 1.0).toFixed(1)}×</span>`}
|
||||
${source.led_count ? `<span class="stream-card-prop" title="${t('color_strip.leds')}">${ICON_LED} ${source.led_count}</span>` : ''}
|
||||
${clockBadge}
|
||||
`;
|
||||
@@ -771,8 +739,7 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
||||
|
||||
export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
try {
|
||||
const sourcesResp = await fetchWithAuth('/picture-sources');
|
||||
const sources = sourcesResp.ok ? ((await sourcesResp.json()).streams || []) : [];
|
||||
const sources = await streamsCache.fetch();
|
||||
|
||||
// Fetch all color strip sources for composite layer dropdowns
|
||||
const cssListResp = await fetchWithAuth('/color-strip-sources');
|
||||
@@ -821,8 +788,6 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
} else if (sourceType === 'effect') {
|
||||
document.getElementById('css-editor-effect-type').value = css.effect_type || 'fire';
|
||||
onEffectTypeChange();
|
||||
document.getElementById('css-editor-effect-speed').value = css.speed ?? 1.0;
|
||||
document.getElementById('css-editor-effect-speed-val').textContent = parseFloat(css.speed ?? 1.0).toFixed(1);
|
||||
document.getElementById('css-editor-effect-palette').value = 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;
|
||||
@@ -920,8 +885,6 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
||||
_loadAnimationState(null);
|
||||
_loadColorCycleState(null);
|
||||
document.getElementById('css-editor-effect-type').value = 'fire';
|
||||
document.getElementById('css-editor-effect-speed').value = 1.0;
|
||||
document.getElementById('css-editor-effect-speed-val').textContent = '1.0';
|
||||
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;
|
||||
@@ -988,7 +951,6 @@ export async function saveCSSEditor() {
|
||||
payload = {
|
||||
name,
|
||||
colors: cycleColors,
|
||||
cycle_speed: parseFloat(document.getElementById('css-editor-cycle-speed').value),
|
||||
};
|
||||
if (!cssId) payload.source_type = 'color_cycle';
|
||||
} else if (sourceType === 'gradient') {
|
||||
@@ -1010,7 +972,6 @@ export async function saveCSSEditor() {
|
||||
payload = {
|
||||
name,
|
||||
effect_type: document.getElementById('css-editor-effect-type').value,
|
||||
speed: parseFloat(document.getElementById('css-editor-effect-speed').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),
|
||||
|
||||
@@ -28,7 +28,7 @@ class DeviceSettingsModal extends Modal {
|
||||
led_count: this.$('settings-led-count').value,
|
||||
led_type: document.getElementById('settings-led-type')?.value || 'rgb',
|
||||
send_latency: document.getElementById('settings-send-latency')?.value || '0',
|
||||
zones: _getCheckedZones('settings-zone-list'),
|
||||
zones: JSON.stringify(_getCheckedZones('settings-zone-list')),
|
||||
zoneMode: _getZoneMode('settings-zone-mode'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
_kcNameManuallyEdited, set_kcNameManuallyEdited,
|
||||
kcWebSockets,
|
||||
PATTERN_RECT_BORDERS,
|
||||
_cachedValueSources, valueSourcesCache,
|
||||
_cachedValueSources, valueSourcesCache, streamsCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -413,12 +413,11 @@ function _populateKCBrightnessVsDropdown(selectedId = '') {
|
||||
export async function showKCEditor(targetId = null, cloneData = null) {
|
||||
try {
|
||||
// Load sources, pattern templates, and value sources in parallel
|
||||
const [sourcesResp, patResp, valueSources] = await Promise.all([
|
||||
fetchWithAuth('/picture-sources').catch(() => null),
|
||||
const [sources, patResp, valueSources] = await Promise.all([
|
||||
streamsCache.fetch().catch(() => []),
|
||||
fetchWithAuth('/pattern-templates').catch(() => null),
|
||||
valueSourcesCache.fetch(),
|
||||
]);
|
||||
const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : [];
|
||||
const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : [];
|
||||
|
||||
// Populate source select
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
patternEditorHoverHit, setPatternEditorHoverHit,
|
||||
PATTERN_RECT_COLORS,
|
||||
PATTERN_RECT_BORDERS,
|
||||
streamsCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -76,8 +77,7 @@ export function createPatternTemplateCard(pt) {
|
||||
export async function showPatternTemplateEditor(templateId = null, cloneData = null) {
|
||||
try {
|
||||
// Load sources for background capture
|
||||
const sourcesResp = await fetchWithAuth('/picture-sources').catch(() => null);
|
||||
const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : [];
|
||||
const sources = await streamsCache.fetch().catch(() => []);
|
||||
|
||||
const bgSelect = document.getElementById('pattern-bg-source');
|
||||
bgSelect.innerHTML = '';
|
||||
|
||||
@@ -247,10 +247,8 @@ function restoreCaptureDuration() {
|
||||
|
||||
export async function showTestTemplateModal(templateId) {
|
||||
try {
|
||||
const resp = await fetchWithAuth('/capture-templates');
|
||||
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
|
||||
const data = await resp.json();
|
||||
const template = (data.templates || []).find(tp => tp.id === templateId);
|
||||
const templates = await captureTemplatesCache.fetch();
|
||||
const template = templates.find(tp => tp.id === templateId);
|
||||
|
||||
if (!template) {
|
||||
showToast(t('templates.error.load'), 'error');
|
||||
@@ -1597,34 +1595,25 @@ export async function editStream(streamId) {
|
||||
let _streamModalDisplaysEngine = null;
|
||||
|
||||
async function populateStreamModalDropdowns() {
|
||||
const [displaysRes, captureTemplatesRes, streamsRes, ppTemplatesRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/config/displays`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
fetchWithAuth('/picture-sources'),
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
const [captureTemplates, streams, ppTemplates] = await Promise.all([
|
||||
captureTemplatesCache.fetch().catch(() => []),
|
||||
streamsCache.fetch().catch(() => []),
|
||||
ppTemplatesCache.fetch().catch(() => []),
|
||||
displaysCache.fetch().catch(() => []),
|
||||
]);
|
||||
|
||||
// Cache desktop displays (used as default unless engine has own displays)
|
||||
if (displaysRes.ok) {
|
||||
const displaysData = await displaysRes.json();
|
||||
displaysCache.update(displaysData.displays || []);
|
||||
}
|
||||
_streamModalDisplaysEngine = null;
|
||||
|
||||
const templateSelect = document.getElementById('stream-capture-template');
|
||||
templateSelect.innerHTML = '';
|
||||
if (captureTemplatesRes.ok) {
|
||||
const data = await captureTemplatesRes.json();
|
||||
(data.templates || []).forEach(tmpl => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.dataset.name = tmpl.name;
|
||||
opt.dataset.engineType = tmpl.engine_type;
|
||||
opt.dataset.hasOwnDisplays = availableEngines.find(e => e.type === tmpl.engine_type)?.has_own_displays ? '1' : '';
|
||||
opt.textContent = `${tmpl.name} (${tmpl.engine_type})`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
captureTemplates.forEach(tmpl => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.dataset.name = tmpl.name;
|
||||
opt.dataset.engineType = tmpl.engine_type;
|
||||
opt.dataset.hasOwnDisplays = availableEngines.find(e => e.type === tmpl.engine_type)?.has_own_displays ? '1' : '';
|
||||
opt.textContent = `${tmpl.name} (${tmpl.engine_type})`;
|
||||
templateSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
// When template changes, refresh displays if engine type switched
|
||||
templateSelect.addEventListener('change', _onCaptureTemplateChanged);
|
||||
@@ -1640,32 +1629,25 @@ async function populateStreamModalDropdowns() {
|
||||
|
||||
const sourceSelect = document.getElementById('stream-source');
|
||||
sourceSelect.innerHTML = '';
|
||||
if (streamsRes.ok) {
|
||||
const data = await streamsRes.json();
|
||||
const editingId = document.getElementById('stream-id').value;
|
||||
(data.streams || []).forEach(s => {
|
||||
if (s.id === editingId) return;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.dataset.name = s.name;
|
||||
opt.textContent = s.name;
|
||||
sourceSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
const editingId = document.getElementById('stream-id').value;
|
||||
streams.forEach(s => {
|
||||
if (s.id === editingId) return;
|
||||
const opt = document.createElement('option');
|
||||
opt.value = s.id;
|
||||
opt.dataset.name = s.name;
|
||||
opt.textContent = s.name;
|
||||
sourceSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
set_streamModalPPTemplates([]);
|
||||
set_streamModalPPTemplates(ppTemplates);
|
||||
const ppSelect = document.getElementById('stream-pp-template');
|
||||
ppSelect.innerHTML = '';
|
||||
if (ppTemplatesRes.ok) {
|
||||
const data = await ppTemplatesRes.json();
|
||||
set_streamModalPPTemplates(data.templates || []);
|
||||
_streamModalPPTemplates.forEach(tmpl => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.textContent = tmpl.name;
|
||||
ppSelect.appendChild(opt);
|
||||
});
|
||||
}
|
||||
ppTemplates.forEach(tmpl => {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = tmpl.id;
|
||||
opt.textContent = tmpl.name;
|
||||
ppSelect.appendChild(opt);
|
||||
});
|
||||
|
||||
_autoGenerateStreamName();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user