feat: asset-based image/video sources, notification sounds, UI improvements
Lint & Test / test (push) Has been cancelled
Lint & Test / test (push) Has been cancelled
- Replace URL-based image_source/url fields with image_asset_id/video_asset_id on StaticImagePictureSource and VideoCaptureSource (clean break, no migration) - Resolve asset IDs to file paths at runtime via AssetStore.get_file_path() - Add EntitySelect asset pickers for image/video in stream editor modal - Add notification sound configuration (global sound + per-app overrides) - Unify per-app color and sound overrides into single "Per-App Overrides" section - Persist notification history between server restarts - Add asset management system (upload, edit, delete, soft-delete) - Replace emoji buttons with SVG icons throughout UI - Various backend improvements: SQLite stores, auth, backup, MQTT, webhooks
This commit is contained in:
@@ -19,7 +19,6 @@ import {
|
||||
currentTestingTemplate, setCurrentTestingTemplate,
|
||||
_currentTestStreamId, set_currentTestStreamId,
|
||||
_currentTestPPTemplateId, set_currentTestPPTemplateId,
|
||||
_lastValidatedImageSource, set_lastValidatedImageSource,
|
||||
_cachedAudioSources,
|
||||
_cachedValueSources,
|
||||
_cachedSyncClocks,
|
||||
@@ -35,7 +34,7 @@ import {
|
||||
_sourcesLoading, set_sourcesLoading,
|
||||
apiKey,
|
||||
streamsCache, ppTemplatesCache, captureTemplatesCache,
|
||||
audioSourcesCache, audioTemplatesCache, valueSourcesCache, syncClocksCache, weatherSourcesCache, filtersCache,
|
||||
audioSourcesCache, audioTemplatesCache, valueSourcesCache, syncClocksCache, weatherSourcesCache, assetsCache, _cachedAssets, filtersCache,
|
||||
colorStripSourcesCache,
|
||||
csptCache, stripFiltersCache,
|
||||
gradientsCache, GradientEntity,
|
||||
@@ -51,6 +50,7 @@ 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 { createAssetCard, initAssetDelegation } from './assets.ts';
|
||||
import { createColorStripCard } from './color-strips.ts';
|
||||
import { initAudioSourceDelegation } from './audio-sources.ts';
|
||||
import {
|
||||
@@ -58,7 +58,8 @@ import {
|
||||
ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE,
|
||||
ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_CLOCK, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT,
|
||||
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO, ICON_ACTIVITY,
|
||||
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_CSPT, ICON_HELP, ICON_TRASH, ICON_PALETTE,
|
||||
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_CSPT, ICON_HELP, ICON_TRASH, ICON_PALETTE, ICON_ASSET,
|
||||
getAssetTypeIcon,
|
||||
} from '../core/icons.ts';
|
||||
import * as P from '../core/icon-paths.ts';
|
||||
|
||||
@@ -97,8 +98,61 @@ const _colorStripDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon:
|
||||
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 _assetDeleteAction = [{ key: 'delete', labelKey: 'bulk.delete', icon: ICON_TRASH, style: 'danger', confirm: 'bulk.confirm_delete', handler: _bulkDeleteFactory('assets', assetsCache, 'asset.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') }];
|
||||
|
||||
/** Resolve an asset ID to its display name. */
|
||||
function _getAssetName(assetId?: string | null): string {
|
||||
if (!assetId) return '—';
|
||||
const asset = _cachedAssets.find((a: any) => a.id === assetId);
|
||||
return asset ? asset.name : assetId;
|
||||
}
|
||||
|
||||
/** Get EntitySelect items for a given asset type (image/video). */
|
||||
function _getAssetItems(assetType: string) {
|
||||
return _cachedAssets
|
||||
.filter((a: any) => a.asset_type === assetType)
|
||||
.map((a: any) => ({ value: a.id, label: a.name, icon: getAssetTypeIcon(assetType), desc: a.filename }));
|
||||
}
|
||||
|
||||
let _imageAssetEntitySelect: EntitySelect | null = null;
|
||||
let _videoAssetEntitySelect: EntitySelect | null = null;
|
||||
|
||||
function _ensureImageAssetEntitySelect() {
|
||||
const sel = document.getElementById('stream-image-asset') as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
if (_imageAssetEntitySelect) _imageAssetEntitySelect.destroy();
|
||||
_imageAssetEntitySelect = null;
|
||||
const items = _getAssetItems('image');
|
||||
sel.innerHTML = `<option value="">${t('streams.image_asset.select')}</option>` +
|
||||
items.map(a => `<option value="${a.value}">${escapeHtml(a.label)}</option>`).join('');
|
||||
_imageAssetEntitySelect = new EntitySelect({
|
||||
target: sel,
|
||||
getItems: () => _getAssetItems('image'),
|
||||
placeholder: t('streams.image_asset.search') || 'Search image assets…',
|
||||
});
|
||||
}
|
||||
|
||||
function _ensureVideoAssetEntitySelect() {
|
||||
const sel = document.getElementById('stream-video-asset') as HTMLSelectElement | null;
|
||||
if (!sel) return;
|
||||
if (_videoAssetEntitySelect) _videoAssetEntitySelect.destroy();
|
||||
_videoAssetEntitySelect = null;
|
||||
const items = _getAssetItems('video');
|
||||
sel.innerHTML = `<option value="">${t('streams.video_asset.select')}</option>` +
|
||||
items.map(a => `<option value="${a.value}">${escapeHtml(a.label)}</option>`).join('');
|
||||
_videoAssetEntitySelect = new EntitySelect({
|
||||
target: sel,
|
||||
getItems: () => _getAssetItems('video'),
|
||||
placeholder: t('streams.video_asset.search') || 'Search video assets…',
|
||||
});
|
||||
}
|
||||
|
||||
function _destroyAssetEntitySelects() {
|
||||
if (_imageAssetEntitySelect) { _imageAssetEntitySelect.destroy(); _imageAssetEntitySelect = null; }
|
||||
if (_videoAssetEntitySelect) { _videoAssetEntitySelect.destroy(); _videoAssetEntitySelect = null; }
|
||||
}
|
||||
|
||||
// ── Card section instances ──
|
||||
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id', emptyKey: 'section.empty.picture_sources', bulkActions: _streamDeleteAction });
|
||||
const csRawTemplates = new CardSection('raw-templates', { titleKey: 'templates.title', gridClass: 'templates-grid', addCardOnclick: "showAddTemplateModal()", keyAttr: 'data-template-id', emptyKey: 'section.empty.capture_templates', bulkActions: _captureTemplateDeleteAction });
|
||||
@@ -114,6 +168,7 @@ const csColorStrips = new CardSection('color-strips', { titleKey: 'targets.secti
|
||||
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 csAssets = new CardSection('assets', { titleKey: 'asset.group.title', gridClass: 'templates-grid', addCardOnclick: "showAssetUploadModal()", keyAttr: 'data-id', emptyKey: 'section.empty.assets', bulkActions: _assetDeleteAction });
|
||||
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 });
|
||||
@@ -137,13 +192,14 @@ class StreamEditorModal extends Modal {
|
||||
targetFps: (document.getElementById('stream-target-fps') as HTMLInputElement).value,
|
||||
source: (document.getElementById('stream-source') as HTMLSelectElement).value,
|
||||
ppTemplate: (document.getElementById('stream-pp-template') as HTMLSelectElement).value,
|
||||
imageSource: (document.getElementById('stream-image-source') as HTMLInputElement).value,
|
||||
imageAsset: (document.getElementById('stream-image-asset') as HTMLSelectElement).value,
|
||||
tags: JSON.stringify(_streamTagsInput ? _streamTagsInput.getValue() : []),
|
||||
};
|
||||
}
|
||||
|
||||
onForceClose() {
|
||||
if (_streamTagsInput) { _streamTagsInput.destroy(); _streamTagsInput = null; }
|
||||
_destroyAssetEntitySelects();
|
||||
(document.getElementById('stream-type') as HTMLSelectElement).disabled = false;
|
||||
set_streamNameManuallyEdited(false);
|
||||
}
|
||||
@@ -226,6 +282,7 @@ export async function loadPictureSources() {
|
||||
valueSourcesCache.fetch(),
|
||||
syncClocksCache.fetch(),
|
||||
weatherSourcesCache.fetch(),
|
||||
assetsCache.fetch(),
|
||||
audioTemplatesCache.fetch(),
|
||||
colorStripSourcesCache.fetch(),
|
||||
csptCache.fetch(),
|
||||
@@ -316,16 +373,15 @@ const PICTURE_SOURCE_CARD_RENDERERS: Record<string, StreamCardRenderer> = {
|
||||
</div>`;
|
||||
},
|
||||
static_image: (stream) => {
|
||||
const src = stream.image_source || '';
|
||||
const assetName = _getAssetName(stream.image_asset_id);
|
||||
return `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(src)}">${ICON_WEB} ${escapeHtml(src)}</span>
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(assetName)}">${ICON_ASSET} ${escapeHtml(assetName)}</span>
|
||||
</div>`;
|
||||
},
|
||||
video: (stream) => {
|
||||
const url = stream.url || '';
|
||||
const shortUrl = url.length > 40 ? url.slice(0, 37) + '...' : url;
|
||||
const assetName = _getAssetName(stream.video_asset_id);
|
||||
return `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(url)}">${ICON_WEB} ${escapeHtml(shortUrl)}</span>
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(assetName)}">${ICON_ASSET} ${escapeHtml(assetName)}</span>
|
||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
|
||||
${stream.loop !== false ? `<span class="stream-card-prop">↻</span>` : ''}
|
||||
${stream.playback_speed && stream.playback_speed !== 1.0 ? `<span class="stream-card-prop">${stream.playback_speed}×</span>` : ''}
|
||||
@@ -510,6 +566,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
{ 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 },
|
||||
{ key: 'assets', icon: ICON_ASSET, titleKey: 'streams.group.assets', count: _cachedAssets.length },
|
||||
];
|
||||
|
||||
// Build tree navigation structure
|
||||
@@ -563,6 +620,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
{ 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 },
|
||||
{ key: 'assets', titleKey: 'streams.group.assets', icon: ICON_ASSET, count: _cachedAssets.length },
|
||||
]
|
||||
}
|
||||
];
|
||||
@@ -723,6 +781,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
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 assetItems = csAssets.applySortOrder(_cachedAssets.map(a => ({ key: a.id, html: createAssetCard(a) })));
|
||||
const csptItems = csCSPTemplates.applySortOrder(csptTemplates.map(t => ({ key: t.id, html: renderCSPTCard(t) })));
|
||||
|
||||
if (csRawStreams.isMounted()) {
|
||||
@@ -742,6 +801,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
value: _cachedValueSources.length,
|
||||
sync: _cachedSyncClocks.length,
|
||||
weather: _cachedWeatherSources.length,
|
||||
assets: _cachedAssets.length,
|
||||
});
|
||||
csRawStreams.reconcile(rawStreamItems);
|
||||
csRawTemplates.reconcile(rawTemplateItems);
|
||||
@@ -759,6 +819,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
csValueSources.reconcile(valueItems);
|
||||
csSyncClocks.reconcile(syncClockItems);
|
||||
csWeatherSources.reconcile(weatherSourceItems);
|
||||
csAssets.reconcile(assetItems);
|
||||
} else {
|
||||
// First render: build full HTML
|
||||
const panels = tabs.map(tab => {
|
||||
@@ -777,18 +838,20 @@ function renderPictureSourcesList(streams: any) {
|
||||
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 === 'assets') panelContent = csAssets.render(assetItems);
|
||||
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, csAudioBandExtract, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources]);
|
||||
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csCSPTemplates, csColorStrips, csGradients, csAudioMulti, csAudioMono, csAudioBandExtract, csAudioTemplates, csStaticStreams, csVideoStreams, csValueSources, csSyncClocks, csWeatherSources, csAssets]);
|
||||
|
||||
// Event delegation for card actions (replaces inline onclick handlers)
|
||||
initSyncClockDelegation(container);
|
||||
initWeatherSourceDelegation(container);
|
||||
initAudioSourceDelegation(container);
|
||||
initAssetDelegation(container);
|
||||
|
||||
// Render tree sidebar with expand/collapse buttons
|
||||
_streamsTree.setExtraHtml(`<button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" data-i18n-title="tour.restart" title="${t('tour.restart')}">${ICON_HELP}</button>`);
|
||||
@@ -806,6 +869,7 @@ function renderPictureSourcesList(streams: any) {
|
||||
'value-sources': 'value',
|
||||
'sync-clocks': 'sync',
|
||||
'weather-sources': 'weather',
|
||||
'assets': 'assets',
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -867,14 +931,8 @@ export async function showAddStreamModal(presetType: any, cloneData: any = null)
|
||||
document.getElementById('stream-display-picker-label')!.textContent = t('displays.picker.select');
|
||||
document.getElementById('stream-error')!.style.display = 'none';
|
||||
(document.getElementById('stream-type') as HTMLSelectElement).value = streamType;
|
||||
set_lastValidatedImageSource('');
|
||||
const imgSrcInput = document.getElementById('stream-image-source') as HTMLInputElement;
|
||||
imgSrcInput.value = '';
|
||||
document.getElementById('stream-image-preview-container')!.style.display = 'none';
|
||||
document.getElementById('stream-image-validation-status')!.style.display = 'none';
|
||||
imgSrcInput.onblur = () => validateStaticImage();
|
||||
imgSrcInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); validateStaticImage(); } };
|
||||
imgSrcInput.onpaste = () => setTimeout(() => validateStaticImage(), 0);
|
||||
_ensureImageAssetEntitySelect();
|
||||
_ensureVideoAssetEntitySelect();
|
||||
onStreamTypeChange();
|
||||
|
||||
set_streamNameManuallyEdited(!!cloneData);
|
||||
@@ -906,10 +964,15 @@ export async function showAddStreamModal(presetType: any, cloneData: any = null)
|
||||
(document.getElementById('stream-source') as HTMLSelectElement).value = cloneData.source_stream_id || '';
|
||||
(document.getElementById('stream-pp-template') as HTMLSelectElement).value = cloneData.postprocessing_template_id || '';
|
||||
} else if (streamType === 'static_image') {
|
||||
(document.getElementById('stream-image-source') as HTMLInputElement).value = cloneData.image_source || '';
|
||||
if (cloneData.image_source) validateStaticImage();
|
||||
if (cloneData.image_asset_id) {
|
||||
(document.getElementById('stream-image-asset') as HTMLSelectElement).value = cloneData.image_asset_id;
|
||||
if (_imageAssetEntitySelect) _imageAssetEntitySelect.setValue(cloneData.image_asset_id);
|
||||
}
|
||||
} else if (streamType === 'video') {
|
||||
(document.getElementById('stream-video-url') as HTMLInputElement).value = cloneData.url || '';
|
||||
if (cloneData.video_asset_id) {
|
||||
(document.getElementById('stream-video-asset') as HTMLSelectElement).value = cloneData.video_asset_id;
|
||||
if (_videoAssetEntitySelect) _videoAssetEntitySelect.setValue(cloneData.video_asset_id);
|
||||
}
|
||||
(document.getElementById('stream-video-loop') as HTMLInputElement).checked = cloneData.loop !== false;
|
||||
(document.getElementById('stream-video-speed') as HTMLInputElement).value = cloneData.playback_speed || 1.0;
|
||||
const cloneSpeedLabel = document.getElementById('stream-video-speed-value');
|
||||
@@ -951,13 +1014,8 @@ export async function editStream(streamId: any) {
|
||||
(document.getElementById('stream-description') as HTMLInputElement).value = stream.description || '';
|
||||
|
||||
(document.getElementById('stream-type') as HTMLSelectElement).value = stream.stream_type;
|
||||
set_lastValidatedImageSource('');
|
||||
const imgSrcInput = document.getElementById('stream-image-source') as HTMLInputElement;
|
||||
document.getElementById('stream-image-preview-container')!.style.display = 'none';
|
||||
document.getElementById('stream-image-validation-status')!.style.display = 'none';
|
||||
imgSrcInput.onblur = () => validateStaticImage();
|
||||
imgSrcInput.onkeydown = (e) => { if (e.key === 'Enter') { e.preventDefault(); validateStaticImage(); } };
|
||||
imgSrcInput.onpaste = () => setTimeout(() => validateStaticImage(), 0);
|
||||
_ensureImageAssetEntitySelect();
|
||||
_ensureVideoAssetEntitySelect();
|
||||
onStreamTypeChange();
|
||||
|
||||
await populateStreamModalDropdowns();
|
||||
@@ -976,10 +1034,15 @@ export async function editStream(streamId: any) {
|
||||
(document.getElementById('stream-source') as HTMLSelectElement).value = stream.source_stream_id || '';
|
||||
(document.getElementById('stream-pp-template') as HTMLSelectElement).value = stream.postprocessing_template_id || '';
|
||||
} else if (stream.stream_type === 'static_image') {
|
||||
(document.getElementById('stream-image-source') as HTMLInputElement).value = stream.image_source || '';
|
||||
if (stream.image_source) validateStaticImage();
|
||||
if (stream.image_asset_id) {
|
||||
(document.getElementById('stream-image-asset') as HTMLSelectElement).value = stream.image_asset_id;
|
||||
if (_imageAssetEntitySelect) _imageAssetEntitySelect.setValue(stream.image_asset_id);
|
||||
}
|
||||
} else if (stream.stream_type === 'video') {
|
||||
(document.getElementById('stream-video-url') as HTMLInputElement).value = stream.url || '';
|
||||
if (stream.video_asset_id) {
|
||||
(document.getElementById('stream-video-asset') as HTMLSelectElement).value = stream.video_asset_id;
|
||||
if (_videoAssetEntitySelect) _videoAssetEntitySelect.setValue(stream.video_asset_id);
|
||||
}
|
||||
(document.getElementById('stream-video-loop') as HTMLInputElement).checked = stream.loop !== false;
|
||||
(document.getElementById('stream-video-speed') as HTMLInputElement).value = stream.playback_speed || 1.0;
|
||||
const speedLabel = document.getElementById('stream-video-speed-value');
|
||||
@@ -1164,13 +1227,13 @@ export async function saveStream() {
|
||||
payload.source_stream_id = (document.getElementById('stream-source') as HTMLSelectElement).value;
|
||||
payload.postprocessing_template_id = (document.getElementById('stream-pp-template') as HTMLSelectElement).value;
|
||||
} else if (streamType === 'static_image') {
|
||||
const imageSource = (document.getElementById('stream-image-source') as HTMLInputElement).value.trim();
|
||||
if (!imageSource) { showToast(t('streams.error.required'), 'error'); return; }
|
||||
payload.image_source = imageSource;
|
||||
const imageAssetId = (document.getElementById('stream-image-asset') as HTMLSelectElement).value;
|
||||
if (!imageAssetId) { showToast(t('streams.error.required'), 'error'); return; }
|
||||
payload.image_asset_id = imageAssetId;
|
||||
} else if (streamType === 'video') {
|
||||
const url = (document.getElementById('stream-video-url') as HTMLInputElement).value.trim();
|
||||
if (!url) { showToast(t('streams.error.required'), 'error'); return; }
|
||||
payload.url = url;
|
||||
const videoAssetId = (document.getElementById('stream-video-asset') as HTMLSelectElement).value;
|
||||
if (!videoAssetId) { showToast(t('streams.error.required'), 'error'); return; }
|
||||
payload.video_asset_id = videoAssetId;
|
||||
payload.loop = (document.getElementById('stream-video-loop') as HTMLInputElement).checked;
|
||||
payload.playback_speed = parseFloat((document.getElementById('stream-video-speed') as HTMLInputElement).value) || 1.0;
|
||||
payload.target_fps = parseInt((document.getElementById('stream-video-fps') as HTMLInputElement).value) || 30;
|
||||
@@ -1239,55 +1302,6 @@ export async function closeStreamModal() {
|
||||
await streamModal.close();
|
||||
}
|
||||
|
||||
async function validateStaticImage() {
|
||||
const source = (document.getElementById('stream-image-source') as HTMLInputElement).value.trim();
|
||||
const previewContainer = document.getElementById('stream-image-preview-container')!;
|
||||
const previewImg = document.getElementById('stream-image-preview') as HTMLImageElement;
|
||||
const infoEl = document.getElementById('stream-image-info')!;
|
||||
const statusEl = document.getElementById('stream-image-validation-status')!;
|
||||
|
||||
if (!source) {
|
||||
set_lastValidatedImageSource('');
|
||||
previewContainer.style.display = 'none';
|
||||
statusEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
if (source === _lastValidatedImageSource) return;
|
||||
|
||||
statusEl.textContent = t('streams.validate_image.validating');
|
||||
statusEl.className = 'validation-status loading';
|
||||
statusEl.style.display = 'block';
|
||||
previewContainer.style.display = 'none';
|
||||
|
||||
try {
|
||||
const response = await fetchWithAuth('/picture-sources/validate-image', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ image_source: source }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
set_lastValidatedImageSource(source);
|
||||
if (data.valid) {
|
||||
previewImg.src = data.preview;
|
||||
previewImg.style.cursor = 'pointer';
|
||||
previewImg.onclick = () => openFullImageLightbox(source);
|
||||
infoEl.textContent = `${data.width} × ${data.height} px`;
|
||||
previewContainer.style.display = '';
|
||||
statusEl.textContent = t('streams.validate_image.valid');
|
||||
statusEl.className = 'validation-status success';
|
||||
} else {
|
||||
previewContainer.style.display = 'none';
|
||||
statusEl.textContent = `${t('streams.validate_image.invalid')}: ${data.error}`;
|
||||
statusEl.className = 'validation-status error';
|
||||
}
|
||||
} catch (err) {
|
||||
previewContainer.style.display = 'none';
|
||||
statusEl.textContent = `${t('streams.validate_image.invalid')}: ${err.message}`;
|
||||
statusEl.className = 'validation-status error';
|
||||
}
|
||||
}
|
||||
|
||||
// ===== Picture Source Test =====
|
||||
|
||||
export async function showTestStreamModal(streamId: any) {
|
||||
|
||||
Reference in New Issue
Block a user