Add reusable DataCache class, unify frontend cache patterns
- Create DataCache class with fetch deduplication, invalidation, subscribers - Instantiate 10 cache instances in state.js (streams, templates, sources, etc.) - Replace inline fetch+parse+set patterns with cache.fetch() calls across modules - Eliminate dual _scenesCache/_presetsCache sync via shared scenePresetsCache - Remove 9 now-unused setter functions from state.js - Clean up unused setter imports from audio-sources, value-sources, displays Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
* API utilities — base URL, auth headers, fetch wrapper, helpers.
|
||||
*/
|
||||
|
||||
import { apiKey, setApiKey, refreshInterval, setRefreshInterval, set_cachedDisplays } from './state.js';
|
||||
import { apiKey, setApiKey, refreshInterval, setRefreshInterval, displaysCache } from './state.js';
|
||||
|
||||
export const API_BASE = '/api/v1';
|
||||
|
||||
@@ -127,19 +127,20 @@ export async function loadServerInfo() {
|
||||
}
|
||||
|
||||
export async function loadDisplays(engineType = null) {
|
||||
try {
|
||||
const url = engineType
|
||||
? `/config/displays?engine_type=${engineType}`
|
||||
: '/config/displays';
|
||||
const response = await fetchWithAuth(url);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.displays && data.displays.length > 0) {
|
||||
set_cachedDisplays(data.displays);
|
||||
if (engineType) {
|
||||
// Filtered fetch — bypass cache (engine-specific display list)
|
||||
try {
|
||||
const response = await fetchWithAuth(`/config/displays?engine_type=${engineType}`);
|
||||
const data = await response.json();
|
||||
if (data.displays && data.displays.length > 0) {
|
||||
displaysCache.update(data.displays);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.isAuth) return;
|
||||
console.error('Failed to load displays:', error);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.isAuth) return;
|
||||
console.error('Failed to load displays:', error);
|
||||
} else {
|
||||
await displaysCache.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
73
server/src/wled_controller/static/js/core/cache.js
Normal file
73
server/src/wled_controller/static/js/core/cache.js
Normal file
@@ -0,0 +1,73 @@
|
||||
/**
|
||||
* Reusable data cache with fetch deduplication, invalidation, and subscribers.
|
||||
*/
|
||||
|
||||
import { fetchWithAuth } from './api.js';
|
||||
|
||||
export class DataCache {
|
||||
/**
|
||||
* @param {Object} opts
|
||||
* @param {string} opts.endpoint - API path (e.g. '/picture-sources')
|
||||
* @param {function} opts.extractData - Extract array from response JSON
|
||||
* @param {*} [opts.defaultValue=[]] - Initial/fallback value
|
||||
*/
|
||||
constructor({ endpoint, extractData, defaultValue = [] }) {
|
||||
this._endpoint = endpoint;
|
||||
this._extractData = extractData;
|
||||
this._defaultValue = defaultValue;
|
||||
this._data = defaultValue;
|
||||
this._loading = false;
|
||||
this._promise = null;
|
||||
this._subscribers = [];
|
||||
}
|
||||
|
||||
get data() { return this._data; }
|
||||
get loading() { return this._loading; }
|
||||
|
||||
/** Fetch from API. Deduplicates concurrent calls. */
|
||||
async fetch() {
|
||||
if (this._promise) return this._promise;
|
||||
this._loading = true;
|
||||
this._promise = this._doFetch();
|
||||
try {
|
||||
return await this._promise;
|
||||
} finally {
|
||||
this._promise = null;
|
||||
this._loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
async _doFetch() {
|
||||
try {
|
||||
const resp = await fetchWithAuth(this._endpoint);
|
||||
if (!resp.ok) return this._data;
|
||||
const json = await resp.json();
|
||||
this._data = this._extractData(json);
|
||||
this._notify();
|
||||
return this._data;
|
||||
} catch (err) {
|
||||
if (err.isAuth) return this._data;
|
||||
console.error(`Cache fetch ${this._endpoint}:`, err);
|
||||
return this._data;
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear cached data; next fetch() will re-request. */
|
||||
invalidate() {
|
||||
this._data = this._defaultValue;
|
||||
this._notify();
|
||||
}
|
||||
|
||||
/** Manually set cache value (e.g. after a create/update call). */
|
||||
update(value) {
|
||||
this._data = value;
|
||||
this._notify();
|
||||
}
|
||||
|
||||
subscribe(fn) { this._subscribers.push(fn); }
|
||||
unsubscribe(fn) { this._subscribers = this._subscribers.filter(f => f !== fn); }
|
||||
|
||||
_notify() {
|
||||
for (const fn of this._subscribers) fn(this._data);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@
|
||||
* gets a setter function.
|
||||
*/
|
||||
|
||||
import { DataCache } from './cache.js';
|
||||
|
||||
export let apiKey = null;
|
||||
export function setApiKey(v) { apiKey = v; }
|
||||
|
||||
@@ -19,7 +21,6 @@ export let kcTestTargetId = null;
|
||||
export function setKcTestTargetId(v) { kcTestTargetId = v; }
|
||||
|
||||
export let _cachedDisplays = null;
|
||||
export function set_cachedDisplays(v) { _cachedDisplays = v; }
|
||||
|
||||
export let _displayPickerCallback = null;
|
||||
export function set_displayPickerCallback(v) { _displayPickerCallback = v; }
|
||||
@@ -56,16 +57,9 @@ export function set_discoveryCache(v) { _discoveryCache = v; }
|
||||
|
||||
// Streams / templates state
|
||||
export let _cachedStreams = [];
|
||||
export function set_cachedStreams(v) { _cachedStreams = v; }
|
||||
|
||||
export let _cachedPPTemplates = [];
|
||||
export function set_cachedPPTemplates(v) { _cachedPPTemplates = v; }
|
||||
|
||||
export let _cachedCaptureTemplates = [];
|
||||
export function set_cachedCaptureTemplates(v) { _cachedCaptureTemplates = v; }
|
||||
|
||||
export let _availableFilters = [];
|
||||
export function set_availableFilters(v) { _availableFilters = v; }
|
||||
|
||||
export let availableEngines = [];
|
||||
export function setAvailableEngines(v) { availableEngines = v; }
|
||||
@@ -176,11 +170,7 @@ export const PATTERN_RECT_BORDERS = [
|
||||
|
||||
// Audio sources
|
||||
export let _cachedAudioSources = [];
|
||||
export function set_cachedAudioSources(v) { _cachedAudioSources = v; }
|
||||
|
||||
// Audio templates
|
||||
export let _cachedAudioTemplates = [];
|
||||
export function set_cachedAudioTemplates(v) { _cachedAudioTemplates = v; }
|
||||
|
||||
export let availableAudioEngines = [];
|
||||
export function setAvailableAudioEngines(v) { availableAudioEngines = v; }
|
||||
@@ -193,8 +183,70 @@ export function set_audioTemplateNameManuallyEdited(v) { _audioTemplateNameManua
|
||||
|
||||
// Value sources
|
||||
export let _cachedValueSources = [];
|
||||
export function set_cachedValueSources(v) { _cachedValueSources = v; }
|
||||
|
||||
// Automations
|
||||
export let _automationsCache = null;
|
||||
export function set_automationsCache(v) { _automationsCache = v; }
|
||||
|
||||
// ─── DataCache instances ───────────────────────────────────────────
|
||||
// Each cache syncs its data into the existing `export let` variable
|
||||
// via a subscriber, preserving backward compatibility.
|
||||
|
||||
export const displaysCache = new DataCache({
|
||||
endpoint: '/config/displays',
|
||||
extractData: json => json.displays || [],
|
||||
defaultValue: null,
|
||||
});
|
||||
displaysCache.subscribe(v => { _cachedDisplays = v; });
|
||||
|
||||
export const streamsCache = new DataCache({
|
||||
endpoint: '/picture-sources',
|
||||
extractData: json => json.streams || [],
|
||||
});
|
||||
streamsCache.subscribe(v => { _cachedStreams = v; });
|
||||
|
||||
export const ppTemplatesCache = new DataCache({
|
||||
endpoint: '/postprocessing-templates',
|
||||
extractData: json => json.templates || [],
|
||||
});
|
||||
ppTemplatesCache.subscribe(v => { _cachedPPTemplates = v; });
|
||||
|
||||
export const captureTemplatesCache = new DataCache({
|
||||
endpoint: '/capture-templates',
|
||||
extractData: json => json.templates || [],
|
||||
});
|
||||
captureTemplatesCache.subscribe(v => { _cachedCaptureTemplates = v; });
|
||||
|
||||
export const audioSourcesCache = new DataCache({
|
||||
endpoint: '/audio-sources',
|
||||
extractData: json => json.sources || [],
|
||||
});
|
||||
audioSourcesCache.subscribe(v => { _cachedAudioSources = v; });
|
||||
|
||||
export const audioTemplatesCache = new DataCache({
|
||||
endpoint: '/audio-templates',
|
||||
extractData: json => json.templates || [],
|
||||
});
|
||||
audioTemplatesCache.subscribe(v => { _cachedAudioTemplates = v; });
|
||||
|
||||
export const valueSourcesCache = new DataCache({
|
||||
endpoint: '/value-sources',
|
||||
extractData: json => json.sources || [],
|
||||
});
|
||||
valueSourcesCache.subscribe(v => { _cachedValueSources = v; });
|
||||
|
||||
export const filtersCache = new DataCache({
|
||||
endpoint: '/filters',
|
||||
extractData: json => json.filters || [],
|
||||
});
|
||||
filtersCache.subscribe(v => { _availableFilters = v; });
|
||||
|
||||
export const automationsCacheObj = new DataCache({
|
||||
endpoint: '/automations',
|
||||
extractData: json => json.automations || [],
|
||||
});
|
||||
automationsCacheObj.subscribe(v => { _automationsCache = v; });
|
||||
|
||||
export const scenePresetsCache = new DataCache({
|
||||
endpoint: '/scene-presets',
|
||||
extractData: json => json.presets || [],
|
||||
});
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* This module manages the editor modal and API operations.
|
||||
*/
|
||||
|
||||
import { _cachedAudioSources, set_cachedAudioSources, _cachedAudioTemplates, apiKey } from '../core/state.js';
|
||||
import { _cachedAudioSources, _cachedAudioTemplates, apiKey } from '../core/state.js';
|
||||
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm, lockBody, unlockBody } from '../core/ui.js';
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* Automations — automation cards, editor, condition builder, process picker, scene selector.
|
||||
*/
|
||||
|
||||
import { apiKey, _automationsCache, set_automationsCache, _automationsLoading, set_automationsLoading } from '../core/state.js';
|
||||
import { apiKey, _automationsLoading, set_automationsLoading, automationsCacheObj, scenePresetsCache } from '../core/state.js';
|
||||
import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm, setTabRefreshing } from '../core/ui.js';
|
||||
@@ -10,10 +10,7 @@ import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
import { updateTabBadge } from './tabs.js';
|
||||
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
||||
import { csScenes, createSceneCard, updatePresetsCache } from './scene-presets.js';
|
||||
|
||||
// ===== Scene presets cache (shared by both selectors) =====
|
||||
let _scenesCache = [];
|
||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||
|
||||
class AutomationEditorModal extends Modal {
|
||||
constructor() { super('automation-editor-modal'); }
|
||||
@@ -54,23 +51,15 @@ export async function loadAutomations() {
|
||||
setTabRefreshing('automations-content', true);
|
||||
|
||||
try {
|
||||
const [automationsResp, scenesResp] = await Promise.all([
|
||||
fetchWithAuth('/automations'),
|
||||
fetchWithAuth('/scene-presets'),
|
||||
const [automations, scenes] = await Promise.all([
|
||||
automationsCacheObj.fetch(),
|
||||
scenePresetsCache.fetch(),
|
||||
]);
|
||||
if (!automationsResp.ok) throw new Error('Failed to load automations');
|
||||
const data = await automationsResp.json();
|
||||
const scenesData = scenesResp.ok ? await scenesResp.json() : { presets: [] };
|
||||
_scenesCache = scenesData.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
|
||||
// Build scene name map for card rendering
|
||||
const sceneMap = new Map(_scenesCache.map(s => [s.id, s]));
|
||||
|
||||
set_automationsCache(data.automations);
|
||||
const activeCount = data.automations.filter(a => a.is_active).length;
|
||||
const sceneMap = new Map(scenes.map(s => [s.id, s]));
|
||||
const activeCount = automations.filter(a => a.is_active).length;
|
||||
updateTabBadge('automations', activeCount);
|
||||
renderAutomations(data.automations, sceneMap);
|
||||
renderAutomations(automations, sceneMap);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Failed to load automations:', error);
|
||||
@@ -93,7 +82,7 @@ function renderAutomations(automations, sceneMap) {
|
||||
const container = document.getElementById('automations-content');
|
||||
|
||||
const autoItems = csAutomations.applySortOrder(automations.map(a => ({ key: a.id, html: createAutomationCard(a, sceneMap) })));
|
||||
const sceneItems = csScenes.applySortOrder(_scenesCache.map(s => ({ key: s.id, html: createSceneCard(s) })));
|
||||
const sceneItems = csScenes.applySortOrder(scenePresetsCache.data.map(s => ({ key: s.id, html: createSceneCard(s) })));
|
||||
|
||||
const toolbar = `<div class="stream-tab-bar"><span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllAutomationSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllAutomationSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startAutomationsTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
|
||||
container.innerHTML = toolbar + csAutomations.render(autoItems) + csScenes.render(sceneItems);
|
||||
@@ -205,12 +194,7 @@ export async function openAutomationEditor(automationId) {
|
||||
|
||||
// Fetch scenes for selector
|
||||
try {
|
||||
const resp = await fetchWithAuth('/scene-presets');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
_scenesCache = data.presets || [];
|
||||
updatePresetsCache(_scenesCache);
|
||||
}
|
||||
await scenePresetsCache.fetch();
|
||||
} catch { /* use cached */ }
|
||||
|
||||
// Reset deactivation mode
|
||||
@@ -288,7 +272,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
|
||||
// Set initial display text
|
||||
if (selectedId) {
|
||||
const scene = _scenesCache.find(s => s.id === selectedId);
|
||||
const scene = scenePresetsCache.data.find(s => s.id === selectedId);
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
} else {
|
||||
@@ -299,7 +283,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
// Render dropdown items
|
||||
function renderDropdown(filter) {
|
||||
const query = (filter || '').toLowerCase();
|
||||
const filtered = query ? _scenesCache.filter(s => s.name.toLowerCase().includes(query)) : _scenesCache;
|
||||
const filtered = query ? scenePresetsCache.data.filter(s => s.name.toLowerCase().includes(query)) : scenePresetsCache.data;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
dropdown.innerHTML = `<div class="scene-selector-empty">${t('automations.scene.none_available')}</div>`;
|
||||
@@ -314,7 +298,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
dropdown.querySelectorAll('.scene-selector-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const id = item.dataset.sceneId;
|
||||
const scene = _scenesCache.find(s => s.id === id);
|
||||
const scene = scenePresetsCache.data.find(s => s.id === id);
|
||||
hiddenInput.value = id;
|
||||
searchInput.value = scene ? scene.name : '';
|
||||
clearBtn.classList.toggle('visible', true);
|
||||
@@ -333,7 +317,7 @@ function _initSceneSelector(prefix, selectedId) {
|
||||
renderDropdown(searchInput.value);
|
||||
dropdown.classList.add('open');
|
||||
// If text doesn't match any scene, clear the hidden input
|
||||
const exactMatch = _scenesCache.find(s => s.name.toLowerCase() === searchInput.value.toLowerCase());
|
||||
const exactMatch = scenePresetsCache.data.find(s => s.name.toLowerCase() === searchInput.value.toLowerCase());
|
||||
if (!exactMatch) {
|
||||
hiddenInput.value = '';
|
||||
clearBtn.classList.toggle('visible', !!searchInput.value);
|
||||
|
||||
@@ -5,10 +5,9 @@
|
||||
|
||||
import {
|
||||
_cachedDisplays, _displayPickerCallback, _displayPickerSelectedIndex,
|
||||
set_displayPickerCallback, set_displayPickerSelectedIndex, set_cachedDisplays,
|
||||
set_displayPickerCallback, set_displayPickerSelectedIndex, displaysCache,
|
||||
} from '../core/state.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { loadDisplays } from '../core/api.js';
|
||||
import { fetchWithAuth } from '../core/api.js';
|
||||
import { showToast } from '../core/ui.js';
|
||||
|
||||
@@ -32,14 +31,12 @@ export function openDisplayPicker(callback, selectedIndex, engineType = null) {
|
||||
} else {
|
||||
const canvas = document.getElementById('display-picker-canvas');
|
||||
canvas.innerHTML = '<div class="loading-spinner"></div>';
|
||||
loadDisplays().then(() => {
|
||||
import('../core/state.js').then(({ _cachedDisplays: displays }) => {
|
||||
if (displays && displays.length > 0) {
|
||||
renderDisplayPickerLayout(displays);
|
||||
} else {
|
||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
}
|
||||
});
|
||||
displaysCache.fetch().then(displays => {
|
||||
if (displays && displays.length > 0) {
|
||||
renderDisplayPickerLayout(displays);
|
||||
} else {
|
||||
canvas.innerHTML = `<div class="loading">${t('displays.none')}</div>`;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -56,7 +53,7 @@ async function _fetchAndRenderEngineDisplays(engineType) {
|
||||
const displays = data.displays || [];
|
||||
|
||||
// Store in cache so selectDisplay() can look them up
|
||||
set_cachedDisplays(displays);
|
||||
displaysCache.update(displays);
|
||||
|
||||
if (displays.length > 0) {
|
||||
renderDisplayPickerLayout(displays, engineType);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
_kcNameManuallyEdited, set_kcNameManuallyEdited,
|
||||
kcWebSockets,
|
||||
PATTERN_RECT_BORDERS,
|
||||
_cachedValueSources, set_cachedValueSources,
|
||||
_cachedValueSources, valueSourcesCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -413,15 +413,13 @@ function _populateKCBrightnessVsDropdown(selectedId = '') {
|
||||
export async function showKCEditor(targetId = null, cloneData = null) {
|
||||
try {
|
||||
// Load sources, pattern templates, and value sources in parallel
|
||||
const [sourcesResp, patResp, vsResp] = await Promise.all([
|
||||
const [sourcesResp, patResp, valueSources] = await Promise.all([
|
||||
fetchWithAuth('/picture-sources').catch(() => null),
|
||||
fetchWithAuth('/pattern-templates').catch(() => null),
|
||||
fetchWithAuth('/value-sources').catch(() => null),
|
||||
valueSourcesCache.fetch(),
|
||||
]);
|
||||
const sources = (sourcesResp && sourcesResp.ok) ? (await sourcesResp.json()).streams || [] : [];
|
||||
const patTemplates = (patResp && patResp.ok) ? (await patResp.json()).templates || [] : [];
|
||||
const valueSources = (vsResp && vsResp.ok) ? (await vsResp.json()).sources || [] : [];
|
||||
set_cachedValueSources(valueSources);
|
||||
|
||||
// Populate source select
|
||||
const sourceSelect = document.getElementById('kc-editor-source');
|
||||
|
||||
@@ -11,14 +11,11 @@ import { CardSection } from '../core/card-sections.js';
|
||||
import {
|
||||
ICON_SCENE, ICON_CAPTURE, ICON_START, ICON_EDIT, ICON_REFRESH, ICON_TARGET,
|
||||
} from '../core/icons.js';
|
||||
import { scenePresetsCache } from '../core/state.js';
|
||||
|
||||
let _presetsCache = [];
|
||||
let _editingId = null;
|
||||
let _allTargets = []; // fetched on capture open
|
||||
|
||||
/** Update the internal presets cache (called from automations tab after fetching). */
|
||||
export function updatePresetsCache(presets) { _presetsCache = presets; }
|
||||
|
||||
class ScenePresetEditorModal extends Modal {
|
||||
constructor() { super('scene-preset-editor-modal'); }
|
||||
snapshotValues() {
|
||||
@@ -74,15 +71,7 @@ export function createSceneCard(preset) {
|
||||
// ===== Dashboard section (compact cards) =====
|
||||
|
||||
export async function loadScenePresets() {
|
||||
try {
|
||||
const resp = await fetchWithAuth('/scene-presets');
|
||||
if (!resp.ok) return [];
|
||||
const data = await resp.json();
|
||||
_presetsCache = data.presets || [];
|
||||
return _presetsCache;
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
return scenePresetsCache.fetch();
|
||||
}
|
||||
|
||||
export function renderScenePresetsSection(presets) {
|
||||
@@ -153,7 +142,7 @@ export async function openScenePresetCapture() {
|
||||
// ===== Edit metadata =====
|
||||
|
||||
export async function editScenePreset(presetId) {
|
||||
const preset = _presetsCache.find(p => p.id === presetId);
|
||||
const preset = scenePresetsCache.data.find(p => p.id === presetId);
|
||||
if (!preset) return;
|
||||
|
||||
_editingId = presetId;
|
||||
@@ -295,7 +284,7 @@ export async function activateScenePreset(presetId) {
|
||||
// ===== Recapture =====
|
||||
|
||||
export async function recaptureScenePreset(presetId) {
|
||||
const preset = _presetsCache.find(p => p.id === presetId);
|
||||
const preset = scenePresetsCache.data.find(p => p.id === presetId);
|
||||
const name = preset ? preset.name : presetId;
|
||||
const confirmed = await showConfirm(t('scenes.recapture_confirm', { name }));
|
||||
if (!confirmed) return;
|
||||
@@ -319,7 +308,7 @@ export async function recaptureScenePreset(presetId) {
|
||||
// ===== Delete =====
|
||||
|
||||
export async function deleteScenePreset(presetId) {
|
||||
const preset = _presetsCache.find(p => p.id === presetId);
|
||||
const preset = scenePresetsCache.data.find(p => p.id === presetId);
|
||||
const name = preset ? preset.name : presetId;
|
||||
const confirmed = await showConfirm(t('scenes.delete_confirm', { name }));
|
||||
if (!confirmed) return;
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
_cachedDisplays, set_cachedDisplays,
|
||||
_cachedStreams, set_cachedStreams,
|
||||
_cachedPPTemplates, set_cachedPPTemplates,
|
||||
_cachedCaptureTemplates, set_cachedCaptureTemplates,
|
||||
_availableFilters, set_availableFilters,
|
||||
_cachedDisplays,
|
||||
displaysCache,
|
||||
_cachedStreams,
|
||||
_cachedPPTemplates,
|
||||
_cachedCaptureTemplates,
|
||||
_availableFilters,
|
||||
availableEngines, setAvailableEngines,
|
||||
currentEditingTemplateId, setCurrentEditingTemplateId,
|
||||
_templateNameManuallyEdited, set_templateNameManuallyEdited,
|
||||
@@ -18,14 +19,16 @@ import {
|
||||
_currentTestStreamId, set_currentTestStreamId,
|
||||
_currentTestPPTemplateId, set_currentTestPPTemplateId,
|
||||
_lastValidatedImageSource, set_lastValidatedImageSource,
|
||||
_cachedAudioSources, set_cachedAudioSources,
|
||||
_cachedValueSources, set_cachedValueSources,
|
||||
_cachedAudioTemplates, set_cachedAudioTemplates,
|
||||
_cachedAudioSources,
|
||||
_cachedValueSources,
|
||||
_cachedAudioTemplates,
|
||||
availableAudioEngines, setAvailableAudioEngines,
|
||||
currentEditingAudioTemplateId, setCurrentEditingAudioTemplateId,
|
||||
_audioTemplateNameManuallyEdited, set_audioTemplateNameManuallyEdited,
|
||||
_sourcesLoading, set_sourcesLoading,
|
||||
apiKey,
|
||||
streamsCache, ppTemplatesCache, captureTemplatesCache,
|
||||
audioSourcesCache, audioTemplatesCache, valueSourcesCache, filtersCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -153,10 +156,7 @@ const audioTemplateModal = new AudioTemplateModal();
|
||||
|
||||
async function loadCaptureTemplates() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/capture-templates');
|
||||
if (!response.ok) throw new Error(`Failed to load templates: ${response.status}`);
|
||||
const data = await response.json();
|
||||
set_cachedCaptureTemplates(data.templates || []);
|
||||
await captureTemplatesCache.fetch();
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
@@ -397,7 +397,7 @@ async function loadDisplaysForTest() {
|
||||
const response = await fetchWithAuth(url);
|
||||
if (!response.ok) throw new Error(`Failed to load displays: ${response.status}`);
|
||||
const displaysData = await response.json();
|
||||
set_cachedDisplays(displaysData.displays || []);
|
||||
displaysCache.update(displaysData.displays || []);
|
||||
}
|
||||
|
||||
let selectedIndex = null;
|
||||
@@ -745,10 +745,7 @@ function collectAudioEngineConfig() {
|
||||
|
||||
async function loadAudioTemplates() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/audio-templates');
|
||||
if (!response.ok) throw new Error(`Failed to load audio templates: ${response.status}`);
|
||||
const data = await response.json();
|
||||
set_cachedAudioTemplates(data.templates || []);
|
||||
await audioTemplatesCache.fetch();
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
@@ -1096,44 +1093,16 @@ export async function loadPictureSources() {
|
||||
set_sourcesLoading(true);
|
||||
setTabRefreshing('streams-list', true);
|
||||
try {
|
||||
const [filtersResp, ppResp, captResp, streamsResp, audioResp, valueResp, audioTplResp] = await Promise.all([
|
||||
_availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null),
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
fetchWithAuth('/picture-sources'),
|
||||
fetchWithAuth('/audio-sources'),
|
||||
fetchWithAuth('/value-sources'),
|
||||
fetchWithAuth('/audio-templates'),
|
||||
const [streams] = await Promise.all([
|
||||
streamsCache.fetch(),
|
||||
ppTemplatesCache.fetch(),
|
||||
captureTemplatesCache.fetch(),
|
||||
audioSourcesCache.fetch(),
|
||||
valueSourcesCache.fetch(),
|
||||
audioTemplatesCache.fetch(),
|
||||
filtersCache.data.length === 0 ? filtersCache.fetch() : Promise.resolve(filtersCache.data),
|
||||
]);
|
||||
|
||||
if (filtersResp && filtersResp.ok) {
|
||||
const fd = await filtersResp.json();
|
||||
set_availableFilters(fd.filters || []);
|
||||
}
|
||||
if (ppResp.ok) {
|
||||
const pd = await ppResp.json();
|
||||
set_cachedPPTemplates(pd.templates || []);
|
||||
}
|
||||
if (captResp.ok) {
|
||||
const cd = await captResp.json();
|
||||
set_cachedCaptureTemplates(cd.templates || []);
|
||||
}
|
||||
if (audioResp && audioResp.ok) {
|
||||
const ad = await audioResp.json();
|
||||
set_cachedAudioSources(ad.sources || []);
|
||||
}
|
||||
if (valueResp && valueResp.ok) {
|
||||
const vd = await valueResp.json();
|
||||
set_cachedValueSources(vd.sources || []);
|
||||
}
|
||||
if (audioTplResp && audioTplResp.ok) {
|
||||
const atd = await audioTplResp.json();
|
||||
set_cachedAudioTemplates(atd.templates || []);
|
||||
}
|
||||
if (!streamsResp.ok) throw new Error(`Failed to load streams: ${streamsResp.status}`);
|
||||
const data = await streamsResp.json();
|
||||
set_cachedStreams(data.streams || []);
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
renderPictureSourcesList(streams);
|
||||
} catch (error) {
|
||||
if (error.isAuth) return;
|
||||
console.error('Error loading picture sources:', error);
|
||||
@@ -1576,7 +1545,7 @@ async function populateStreamModalDropdowns() {
|
||||
|
||||
if (displaysRes.ok) {
|
||||
const displaysData = await displaysRes.json();
|
||||
set_cachedDisplays(displaysData.displays || []);
|
||||
displaysCache.update(displaysData.displays || []);
|
||||
}
|
||||
_streamModalDisplaysEngine = null; // desktop displays loaded
|
||||
|
||||
@@ -1660,7 +1629,7 @@ async function _refreshStreamDisplaysForEngine(engineType) {
|
||||
const resp = await fetchWithAuth(url);
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
set_cachedDisplays(data.displays || []);
|
||||
displaysCache.update(data.displays || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error refreshing displays for engine:', error);
|
||||
@@ -1841,8 +1810,7 @@ export async function showTestPPTemplateModal(templateId) {
|
||||
select.innerHTML = '';
|
||||
if (_cachedStreams.length === 0) {
|
||||
try {
|
||||
const resp = await fetchWithAuth('/picture-sources');
|
||||
if (resp.ok) { const d = await resp.json(); set_cachedStreams(d.streams || []); }
|
||||
await streamsCache.fetch();
|
||||
} catch (e) { console.warn('Could not load streams for PP test:', e); }
|
||||
}
|
||||
for (const s of _cachedStreams) {
|
||||
@@ -1894,24 +1862,13 @@ export function runPPTemplateTest() {
|
||||
// ===== PP Templates =====
|
||||
|
||||
async function loadAvailableFilters() {
|
||||
try {
|
||||
const response = await fetchWithAuth('/filters');
|
||||
if (!response.ok) throw new Error(`Failed to load filters: ${response.status}`);
|
||||
const data = await response.json();
|
||||
set_availableFilters(data.filters || []);
|
||||
} catch (error) {
|
||||
console.error('Error loading available filters:', error);
|
||||
set_availableFilters([]);
|
||||
}
|
||||
await filtersCache.fetch();
|
||||
}
|
||||
|
||||
async function loadPPTemplates() {
|
||||
try {
|
||||
if (_availableFilters.length === 0) await loadAvailableFilters();
|
||||
const response = await fetchWithAuth('/postprocessing-templates');
|
||||
if (!response.ok) throw new Error(`Failed to load templates: ${response.status}`);
|
||||
const data = await response.json();
|
||||
set_cachedPPTemplates(data.templates || []);
|
||||
if (_availableFilters.length === 0) await filtersCache.fetch();
|
||||
await ppTemplatesCache.fetch();
|
||||
renderPictureSourcesList(_cachedStreams);
|
||||
} catch (error) {
|
||||
console.error('Error loading PP templates:', error);
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
_deviceBrightnessCache,
|
||||
kcWebSockets,
|
||||
ledPreviewWebSockets,
|
||||
_cachedValueSources, set_cachedValueSources,
|
||||
_cachedValueSources, valueSourcesCache,
|
||||
} from '../core/state.js';
|
||||
import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
@@ -241,18 +241,14 @@ function _populateBrightnessVsDropdown(selectedId = '') {
|
||||
export async function showTargetEditor(targetId = null, cloneData = null) {
|
||||
try {
|
||||
// Load devices, CSS sources, and value sources for dropdowns
|
||||
const [devicesResp, cssResp, vsResp] = await Promise.all([
|
||||
const [devicesResp, cssResp] = await Promise.all([
|
||||
fetch(`${API_BASE}/devices`, { headers: getHeaders() }),
|
||||
fetchWithAuth('/color-strip-sources'),
|
||||
fetchWithAuth('/value-sources'),
|
||||
valueSourcesCache.fetch(),
|
||||
]);
|
||||
|
||||
const devices = devicesResp.ok ? (await devicesResp.json()).devices || [] : [];
|
||||
const cssSources = cssResp.ok ? (await cssResp.json()).sources || [] : [];
|
||||
if (vsResp.ok) {
|
||||
const vsData = await vsResp.json();
|
||||
set_cachedValueSources(vsData.sources || []);
|
||||
}
|
||||
set_targetEditorDevices(devices);
|
||||
_editorCssSources = cssSources;
|
||||
|
||||
@@ -478,13 +474,13 @@ export async function loadTargetsTab() {
|
||||
|
||||
try {
|
||||
// Fetch devices, targets, CSS sources, picture sources, pattern templates, and value sources in parallel
|
||||
const [devicesResp, targetsResp, cssResp, psResp, patResp, vsResp, asResp] = await Promise.all([
|
||||
const [devicesResp, targetsResp, cssResp, psResp, patResp, valueSrcArr, asResp] = await Promise.all([
|
||||
fetchWithAuth('/devices'),
|
||||
fetchWithAuth('/picture-targets'),
|
||||
fetchWithAuth('/color-strip-sources').catch(() => null),
|
||||
fetchWithAuth('/picture-sources').catch(() => null),
|
||||
fetchWithAuth('/pattern-templates').catch(() => null),
|
||||
fetchWithAuth('/value-sources').catch(() => null),
|
||||
valueSourcesCache.fetch().catch(() => []),
|
||||
fetchWithAuth('/audio-sources').catch(() => null),
|
||||
]);
|
||||
|
||||
@@ -515,10 +511,7 @@ export async function loadTargetsTab() {
|
||||
}
|
||||
|
||||
let valueSourceMap = {};
|
||||
if (vsResp && vsResp.ok) {
|
||||
const vsData = await vsResp.json();
|
||||
(vsData.sources || []).forEach(s => { valueSourceMap[s.id] = s; });
|
||||
}
|
||||
valueSrcArr.forEach(s => { valueSourceMap[s.id] = s; });
|
||||
|
||||
let audioSourceMap = {};
|
||||
if (asResp && asResp.ok) {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* This module manages the editor modal and API operations.
|
||||
*/
|
||||
|
||||
import { _cachedValueSources, set_cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey } from '../core/state.js';
|
||||
import { _cachedValueSources, _cachedAudioSources, _cachedStreams, apiKey } from '../core/state.js';
|
||||
import { API_BASE, fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
|
||||
Reference in New Issue
Block a user