feat: add weather source entity and weather-reactive CSS source type
Lint & Test / test (push) Failing after 34s
Lint & Test / test (push) Failing after 34s
New standalone WeatherSource entity with pluggable provider architecture (Open-Meteo v1, free, no API key). Full CRUD, test endpoint, browser geolocation, IconSelect provider picker, CardSection with test/clone/edit. WeatherColorStripStream maps WMO weather codes to ambient color palettes with temperature hue shifting and thunderstorm flash effects. Ref-counted WeatherManager polls API and caches data per source. CSS editor integration: weather type with EntitySelect source picker, speed and temperature influence sliders. Backup/restore support. i18n for en/ru/zh.
This commit is contained in:
@@ -23,6 +23,7 @@ import {
|
||||
_cachedAudioSources,
|
||||
_cachedValueSources,
|
||||
_cachedSyncClocks,
|
||||
_cachedWeatherSources,
|
||||
_cachedAudioTemplates,
|
||||
_cachedCSPTemplates,
|
||||
_csptModalFilters, set_csptModalFilters,
|
||||
@@ -34,7 +35,7 @@ import {
|
||||
_sourcesLoading, set_sourcesLoading,
|
||||
apiKey,
|
||||
streamsCache, ppTemplatesCache, captureTemplatesCache,
|
||||
audioSourcesCache, audioTemplatesCache, valueSourcesCache, syncClocksCache, filtersCache,
|
||||
audioSourcesCache, audioTemplatesCache, valueSourcesCache, syncClocksCache, weatherSourcesCache, filtersCache,
|
||||
colorStripSourcesCache,
|
||||
csptCache, stripFiltersCache,
|
||||
gradientsCache, GradientEntity,
|
||||
@@ -49,6 +50,7 @@ import { TreeNav } from '../core/tree-nav.ts';
|
||||
import { updateSubTabHash } from './tabs.ts';
|
||||
import { createValueSourceCard } from './value-sources.ts';
|
||||
import { createSyncClockCard, initSyncClockDelegation } from './sync-clocks.ts';
|
||||
import { createWeatherSourceCard, initWeatherSourceDelegation } from './weather-sources.ts';
|
||||
import { createColorStripCard } from './color-strips.ts';
|
||||
import { initAudioSourceDelegation } from './audio-sources.ts';
|
||||
import {
|
||||
@@ -94,6 +96,7 @@ const _audioTemplateDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', ic
|
||||
const _colorStripDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('color-strip-sources', colorStripSourcesCache, 'color_strip.deleted') }];
|
||||
const _valueSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('value-sources', valueSourcesCache, 'value_source.deleted') }];
|
||||
const _syncClockDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('sync-clocks', syncClocksCache, 'sync_clock.deleted') }];
|
||||
const _weatherSourceDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('weather-sources', weatherSourcesCache, 'weather_source.deleted') }];
|
||||
const _csptDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('color-strip-processing-templates', csptCache, 'templates.deleted') }];
|
||||
|
||||
// ── Card section instances ──
|
||||
@@ -109,6 +112,7 @@ const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_t
|
||||
const csColorStrips = new CardSection('color-strips', { titleKey: 'targets.section.color_strips', gridClass: 'templates-grid', addCardOnclick: "showCSSEditor()", keyAttr: 'data-css-id', emptyKey: 'section.empty.color_strips', bulkActions: _colorStripDeleteAction });
|
||||
const csValueSources = new CardSection('value-sources', { titleKey: 'value_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showValueSourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.value_sources', bulkActions: _valueSourceDeleteAction });
|
||||
const csSyncClocks = new CardSection('sync-clocks', { titleKey: 'sync_clock.group.title', gridClass: 'templates-grid', addCardOnclick: "showSyncClockModal()", keyAttr: 'data-id', emptyKey: 'section.empty.sync_clocks', bulkActions: _syncClockDeleteAction });
|
||||
const csWeatherSources = new CardSection('weather-sources', { titleKey: 'weather_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showWeatherSourceModal()", keyAttr: 'data-id', emptyKey: 'section.empty.weather_sources', bulkActions: _weatherSourceDeleteAction });
|
||||
const csCSPTemplates = new CardSection('css-proc-templates', { titleKey: 'css_processing.title', gridClass: 'templates-grid', addCardOnclick: "showAddCSPTModal()", keyAttr: 'data-cspt-id', emptyKey: 'section.empty.cspt', bulkActions: _csptDeleteAction });
|
||||
const _gradientDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('gradients', gradientsCache, 'gradient.deleted') }];
|
||||
const csGradients = new CardSection('gradients', { titleKey: 'gradient.group.title', gridClass: 'templates-grid', addCardOnclick: "showGradientModal()", keyAttr: 'data-id', emptyKey: 'section.empty.gradients', bulkActions: _gradientDeleteAction });
|
||||
@@ -220,6 +224,7 @@ export async function loadPictureSources() {
|
||||
audioSourcesCache.fetch(),
|
||||
valueSourcesCache.fetch(),
|
||||
syncClocksCache.fetch(),
|
||||
weatherSourcesCache.fetch(),
|
||||
audioTemplatesCache.fetch(),
|
||||
colorStripSourcesCache.fetch(),
|
||||
csptCache.fetch(),
|
||||
@@ -274,6 +279,7 @@ const _streamSectionMap = {
|
||||
audio_templates: [csAudioTemplates],
|
||||
value: [csValueSources],
|
||||
sync: [csSyncClocks],
|
||||
weather: [csWeatherSources],
|
||||
};
|
||||
|
||||
type StreamCardRenderer = (stream: any) => string;
|
||||
@@ -483,6 +489,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
{ key: 'audio_templates', icon: ICON_AUDIO_TEMPLATE, titleKey: 'streams.group.audio_templates', count: _cachedAudioTemplates.length },
|
||||
{ key: 'value', icon: ICON_VALUE_SOURCE, titleKey: 'streams.group.value', count: _cachedValueSources.length },
|
||||
{ key: 'sync', icon: ICON_CLOCK, titleKey: 'streams.group.sync', count: _cachedSyncClocks.length },
|
||||
{ key: 'weather', icon: `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`, titleKey: 'streams.group.weather', count: _cachedWeatherSources.length },
|
||||
];
|
||||
|
||||
// Build tree navigation structure
|
||||
@@ -533,6 +540,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
children: [
|
||||
{ key: 'value', titleKey: 'streams.group.value', icon: ICON_VALUE_SOURCE, count: _cachedValueSources.length },
|
||||
{ key: 'sync', titleKey: 'streams.group.sync', icon: ICON_CLOCK, count: _cachedSyncClocks.length },
|
||||
{ key: 'weather', titleKey: 'streams.group.weather', icon: `<svg class="icon" viewBox="0 0 24 24">${P.cloudSun}</svg>`, count: _cachedWeatherSources.length },
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -663,6 +671,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
const gradientItems = csGradients.applySortOrder(gradients.map(g => ({ key: g.id, html: renderGradientCard(g) })));
|
||||
const valueItems = csValueSources.applySortOrder(_cachedValueSources.map(s => ({ key: s.id, html: createValueSourceCard(s) })));
|
||||
const syncClockItems = csSyncClocks.applySortOrder(_cachedSyncClocks.map(s => ({ key: s.id, html: createSyncClockCard(s) })));
|
||||
const weatherSourceItems = csWeatherSources.applySortOrder(_cachedWeatherSources.map(s => ({ key: s.id, html: createWeatherSourceCard(s) })));
|
||||
const csptItems = csCSPTemplates.applySortOrder(csptTemplates.map(t => ({ key: t.id, html: renderCSPTCard(t) })));
|
||||
|
||||
if (csRawStreams.isMounted()) {
|
||||
@@ -681,6 +690,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
audio_templates: _cachedAudioTemplates.length,
|
||||
value: _cachedValueSources.length,
|
||||
sync: _cachedSyncClocks.length,
|
||||
weather: _cachedWeatherSources.length,
|
||||
});
|
||||
csRawStreams.reconcile(rawStreamItems);
|
||||
csRawTemplates.reconcile(rawTemplateItems);
|
||||
@@ -696,6 +706,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
csVideoStreams.reconcile(videoItems);
|
||||
csValueSources.reconcile(valueItems);
|
||||
csSyncClocks.reconcile(syncClockItems);
|
||||
csWeatherSources.reconcile(weatherSourceItems);
|
||||
} else {
|
||||
// First render: build full HTML
|
||||
const panels = tabs.map(tab => {
|
||||
@@ -711,16 +722,18 @@ function renderPictureSourcesList(streams: any) {
|
||||
else if (tab.key === 'audio_templates') panelContent = csAudioTemplates.render(audioTemplateItems);
|
||||
else if (tab.key === 'value') panelContent = csValueSources.render(valueItems);
|
||||
else if (tab.key === 'sync') panelContent = csSyncClocks.render(syncClockItems);
|
||||
else if (tab.key === 'weather') panelContent = csWeatherSources.render(weatherSourceItems);
|
||||
else if (tab.key === 'video') panelContent = csVideoStreams.render(videoItems);
|
||||
else panelContent = csStaticStreams.render(staticItems);
|
||||
return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = panels;
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks]);
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources]);
|
||||
|
||||
// Event delegation for card actions (replaces inline onclick handlers)
|
||||
initSyncClockDelegation(container);
|
||||
initWeatherSourceDelegation(container);
|
||||
initAudioSourceDelegation(container);
|
||||
|
||||
// Render tree sidebar with expand/collapse buttons
|
||||
@@ -738,6 +751,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
'audio-templates': 'audio_templates',
|
||||
'value-sources': 'value',
|
||||
'sync-clocks': 'sync',
|
||||
'weather-sources': 'weather',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user