|
|
|
|
@@ -20,6 +20,10 @@ import {
|
|
|
|
|
_lastValidatedImageSource, set_lastValidatedImageSource,
|
|
|
|
|
_cachedAudioSources, set_cachedAudioSources,
|
|
|
|
|
_cachedValueSources, set_cachedValueSources,
|
|
|
|
|
_cachedAudioTemplates, set_cachedAudioTemplates,
|
|
|
|
|
availableAudioEngines, setAvailableAudioEngines,
|
|
|
|
|
currentEditingAudioTemplateId, setCurrentEditingAudioTemplateId,
|
|
|
|
|
_audioTemplateNameManuallyEdited, set_audioTemplateNameManuallyEdited,
|
|
|
|
|
_sourcesLoading, set_sourcesLoading,
|
|
|
|
|
apiKey,
|
|
|
|
|
} from '../core/state.js';
|
|
|
|
|
@@ -35,6 +39,7 @@ import {
|
|
|
|
|
getEngineIcon, getPictureSourceIcon, getAudioSourceIcon,
|
|
|
|
|
ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE,
|
|
|
|
|
ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT,
|
|
|
|
|
ICON_AUDIO_TEMPLATE,
|
|
|
|
|
} from '../core/icons.js';
|
|
|
|
|
|
|
|
|
|
// ── Card section instances ──
|
|
|
|
|
@@ -45,6 +50,7 @@ const csProcTemplates = new CardSection('proc-templates', { titleKey: 'postproce
|
|
|
|
|
const csAudioMulti = new CardSection('audio-multi', { titleKey: 'audio_source.group.multichannel', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('multichannel')" });
|
|
|
|
|
const csAudioMono = new CardSection('audio-mono', { titleKey: 'audio_source.group.mono', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('mono')" });
|
|
|
|
|
const csStaticStreams = new CardSection('static-streams', { titleKey: 'streams.group.static_image', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('static_image')" });
|
|
|
|
|
const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_template.title', gridClass: 'templates-grid', addCardOnclick: "showAddAudioTemplateModal()" });
|
|
|
|
|
const csValueSources = new CardSection('value-sources', { titleKey: 'value_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showValueSourceModal()" });
|
|
|
|
|
|
|
|
|
|
// Re-render picture sources when language changes
|
|
|
|
|
@@ -113,12 +119,34 @@ class PPTemplateEditorModal extends Modal {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class AudioTemplateModal extends Modal {
|
|
|
|
|
constructor() { super('audio-template-modal'); }
|
|
|
|
|
|
|
|
|
|
snapshotValues() {
|
|
|
|
|
const vals = {
|
|
|
|
|
name: document.getElementById('audio-template-name').value,
|
|
|
|
|
description: document.getElementById('audio-template-description').value,
|
|
|
|
|
engine: document.getElementById('audio-template-engine').value,
|
|
|
|
|
};
|
|
|
|
|
document.querySelectorAll('#audio-engine-config-fields [data-config-key]').forEach(field => {
|
|
|
|
|
vals['cfg_' + field.dataset.configKey] = field.value;
|
|
|
|
|
});
|
|
|
|
|
return vals;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onForceClose() {
|
|
|
|
|
setCurrentEditingAudioTemplateId(null);
|
|
|
|
|
set_audioTemplateNameManuallyEdited(false);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const templateModal = new CaptureTemplateModal();
|
|
|
|
|
const testTemplateModal = new Modal('test-template-modal');
|
|
|
|
|
const streamModal = new StreamEditorModal();
|
|
|
|
|
const testStreamModal = new Modal('test-stream-modal');
|
|
|
|
|
const ppTemplateModal = new PPTemplateEditorModal();
|
|
|
|
|
const testPPTemplateModal = new Modal('test-pp-template-modal');
|
|
|
|
|
const audioTemplateModal = new AudioTemplateModal();
|
|
|
|
|
|
|
|
|
|
// ===== Capture Templates =====
|
|
|
|
|
|
|
|
|
|
@@ -511,6 +539,261 @@ export async function deleteTemplate(templateId) {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Audio Templates =====
|
|
|
|
|
|
|
|
|
|
async function loadAvailableAudioEngines() {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetchWithAuth('/audio-engines');
|
|
|
|
|
if (!response.ok) throw new Error(`Failed to load audio engines: ${response.status}`);
|
|
|
|
|
const data = await response.json();
|
|
|
|
|
setAvailableAudioEngines(data.engines || []);
|
|
|
|
|
|
|
|
|
|
const select = document.getElementById('audio-template-engine');
|
|
|
|
|
select.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
availableAudioEngines.forEach(engine => {
|
|
|
|
|
const option = document.createElement('option');
|
|
|
|
|
option.value = engine.type;
|
|
|
|
|
option.textContent = `${engine.type.toUpperCase()}`;
|
|
|
|
|
if (!engine.available) {
|
|
|
|
|
option.disabled = true;
|
|
|
|
|
option.textContent += ` (${t('audio_template.engine.unavailable')})`;
|
|
|
|
|
}
|
|
|
|
|
select.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (!select.value) {
|
|
|
|
|
const firstAvailable = availableAudioEngines.find(e => e.available);
|
|
|
|
|
if (firstAvailable) select.value = firstAvailable.type;
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading audio engines:', error);
|
|
|
|
|
showToast(t('audio_template.error.engines') + ': ' + error.message, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function onAudioEngineChange() {
|
|
|
|
|
const engineType = document.getElementById('audio-template-engine').value;
|
|
|
|
|
const configSection = document.getElementById('audio-engine-config-section');
|
|
|
|
|
const configFields = document.getElementById('audio-engine-config-fields');
|
|
|
|
|
|
|
|
|
|
if (!engineType) { configSection.style.display = 'none'; return; }
|
|
|
|
|
|
|
|
|
|
const engine = availableAudioEngines.find(e => e.type === engineType);
|
|
|
|
|
if (!engine) { configSection.style.display = 'none'; return; }
|
|
|
|
|
|
|
|
|
|
if (!_audioTemplateNameManuallyEdited && !document.getElementById('audio-template-id').value) {
|
|
|
|
|
document.getElementById('audio-template-name').value = engine.type.toUpperCase();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const hint = document.getElementById('audio-engine-availability-hint');
|
|
|
|
|
if (!engine.available) {
|
|
|
|
|
hint.textContent = t('audio_template.engine.unavailable.hint');
|
|
|
|
|
hint.style.display = 'block';
|
|
|
|
|
hint.style.color = 'var(--error-color)';
|
|
|
|
|
} else {
|
|
|
|
|
hint.style.display = 'none';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configFields.innerHTML = '';
|
|
|
|
|
const defaultConfig = engine.default_config || {};
|
|
|
|
|
|
|
|
|
|
if (Object.keys(defaultConfig).length === 0) {
|
|
|
|
|
configSection.style.display = 'none';
|
|
|
|
|
return;
|
|
|
|
|
} else {
|
|
|
|
|
let gridHtml = '<div class="config-grid">';
|
|
|
|
|
Object.entries(defaultConfig).forEach(([key, value]) => {
|
|
|
|
|
const fieldType = typeof value === 'number' ? 'number' : 'text';
|
|
|
|
|
const fieldValue = typeof value === 'boolean' ? (value ? 'true' : 'false') : value;
|
|
|
|
|
gridHtml += `
|
|
|
|
|
<label class="config-grid-label" for="audio-config-${key}">${key}</label>
|
|
|
|
|
<div class="config-grid-value">
|
|
|
|
|
${typeof value === 'boolean' ? `
|
|
|
|
|
<select id="audio-config-${key}" data-config-key="${key}">
|
|
|
|
|
<option value="true" ${value ? 'selected' : ''}>true</option>
|
|
|
|
|
<option value="false" ${!value ? 'selected' : ''}>false</option>
|
|
|
|
|
</select>
|
|
|
|
|
` : `
|
|
|
|
|
<input type="${fieldType}" id="audio-config-${key}" data-config-key="${key}" value="${fieldValue}">
|
|
|
|
|
`}
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
});
|
|
|
|
|
gridHtml += '</div>';
|
|
|
|
|
configFields.innerHTML = gridHtml;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
configSection.style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function populateAudioEngineConfig(config) {
|
|
|
|
|
Object.entries(config).forEach(([key, value]) => {
|
|
|
|
|
const field = document.getElementById(`audio-config-${key}`);
|
|
|
|
|
if (field) {
|
|
|
|
|
if (field.tagName === 'SELECT') {
|
|
|
|
|
field.value = value.toString();
|
|
|
|
|
} else {
|
|
|
|
|
field.value = value;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function collectAudioEngineConfig() {
|
|
|
|
|
const config = {};
|
|
|
|
|
document.querySelectorAll('#audio-engine-config-fields [data-config-key]').forEach(field => {
|
|
|
|
|
const key = field.dataset.configKey;
|
|
|
|
|
let value = field.value;
|
|
|
|
|
if (field.type === 'number') {
|
|
|
|
|
value = parseFloat(value);
|
|
|
|
|
} else if (field.tagName === 'SELECT' && (value === 'true' || value === 'false')) {
|
|
|
|
|
value = value === 'true';
|
|
|
|
|
}
|
|
|
|
|
config[key] = value;
|
|
|
|
|
});
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 || []);
|
|
|
|
|
renderPictureSourcesList(_cachedStreams);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.isAuth) return;
|
|
|
|
|
console.error('Error loading audio templates:', error);
|
|
|
|
|
showToast(t('audio_template.error.load'), 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function showAddAudioTemplateModal(cloneData = null) {
|
|
|
|
|
setCurrentEditingAudioTemplateId(null);
|
|
|
|
|
document.getElementById('audio-template-modal-title').textContent = t('audio_template.add');
|
|
|
|
|
document.getElementById('audio-template-form').reset();
|
|
|
|
|
document.getElementById('audio-template-id').value = '';
|
|
|
|
|
document.getElementById('audio-engine-config-section').style.display = 'none';
|
|
|
|
|
document.getElementById('audio-template-error').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
set_audioTemplateNameManuallyEdited(!!cloneData);
|
|
|
|
|
document.getElementById('audio-template-name').oninput = () => { set_audioTemplateNameManuallyEdited(true); };
|
|
|
|
|
|
|
|
|
|
await loadAvailableAudioEngines();
|
|
|
|
|
|
|
|
|
|
if (cloneData) {
|
|
|
|
|
document.getElementById('audio-template-name').value = (cloneData.name || '') + ' (Copy)';
|
|
|
|
|
document.getElementById('audio-template-description').value = cloneData.description || '';
|
|
|
|
|
document.getElementById('audio-template-engine').value = cloneData.engine_type;
|
|
|
|
|
await onAudioEngineChange();
|
|
|
|
|
populateAudioEngineConfig(cloneData.engine_config);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
audioTemplateModal.open();
|
|
|
|
|
audioTemplateModal.snapshot();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function editAudioTemplate(templateId) {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetchWithAuth(`/audio-templates/${templateId}`);
|
|
|
|
|
if (!response.ok) throw new Error(`Failed to load audio template: ${response.status}`);
|
|
|
|
|
const template = await response.json();
|
|
|
|
|
|
|
|
|
|
setCurrentEditingAudioTemplateId(templateId);
|
|
|
|
|
document.getElementById('audio-template-modal-title').textContent = t('audio_template.edit');
|
|
|
|
|
document.getElementById('audio-template-id').value = templateId;
|
|
|
|
|
document.getElementById('audio-template-name').value = template.name;
|
|
|
|
|
document.getElementById('audio-template-description').value = template.description || '';
|
|
|
|
|
|
|
|
|
|
await loadAvailableAudioEngines();
|
|
|
|
|
document.getElementById('audio-template-engine').value = template.engine_type;
|
|
|
|
|
await onAudioEngineChange();
|
|
|
|
|
populateAudioEngineConfig(template.engine_config);
|
|
|
|
|
|
|
|
|
|
document.getElementById('audio-template-error').style.display = 'none';
|
|
|
|
|
|
|
|
|
|
audioTemplateModal.open();
|
|
|
|
|
audioTemplateModal.snapshot();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error loading audio template:', error);
|
|
|
|
|
showToast(t('audio_template.error.load') + ': ' + error.message, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function closeAudioTemplateModal() {
|
|
|
|
|
await audioTemplateModal.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function saveAudioTemplate() {
|
|
|
|
|
const templateId = currentEditingAudioTemplateId;
|
|
|
|
|
const name = document.getElementById('audio-template-name').value.trim();
|
|
|
|
|
const engineType = document.getElementById('audio-template-engine').value;
|
|
|
|
|
|
|
|
|
|
if (!name || !engineType) {
|
|
|
|
|
showToast(t('audio_template.error.required'), 'error');
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const description = document.getElementById('audio-template-description').value.trim();
|
|
|
|
|
const engineConfig = collectAudioEngineConfig();
|
|
|
|
|
|
|
|
|
|
const payload = { name, engine_type: engineType, engine_config: engineConfig, description: description || null };
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
let response;
|
|
|
|
|
if (templateId) {
|
|
|
|
|
response = await fetchWithAuth(`/audio-templates/${templateId}`, { method: 'PUT', body: JSON.stringify(payload) });
|
|
|
|
|
} else {
|
|
|
|
|
response = await fetchWithAuth('/audio-templates', { method: 'POST', body: JSON.stringify(payload) });
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const error = await response.json();
|
|
|
|
|
throw new Error(error.detail || error.message || 'Failed to save audio template');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
showToast(templateId ? t('audio_template.updated') : t('audio_template.created'), 'success');
|
|
|
|
|
audioTemplateModal.forceClose();
|
|
|
|
|
await loadAudioTemplates();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error saving audio template:', error);
|
|
|
|
|
document.getElementById('audio-template-error').textContent = error.message;
|
|
|
|
|
document.getElementById('audio-template-error').style.display = 'block';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function deleteAudioTemplate(templateId) {
|
|
|
|
|
const confirmed = await showConfirm(t('audio_template.delete.confirm'));
|
|
|
|
|
if (!confirmed) return;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetchWithAuth(`/audio-templates/${templateId}`, { method: 'DELETE' });
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
const error = await response.json();
|
|
|
|
|
throw new Error(error.detail || error.message || 'Failed to delete audio template');
|
|
|
|
|
}
|
|
|
|
|
showToast(t('audio_template.deleted'), 'success');
|
|
|
|
|
await loadAudioTemplates();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Error deleting audio template:', error);
|
|
|
|
|
showToast(t('audio_template.error.delete') + ': ' + error.message, 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export async function cloneAudioTemplate(templateId) {
|
|
|
|
|
try {
|
|
|
|
|
const resp = await fetchWithAuth(`/audio-templates/${templateId}`);
|
|
|
|
|
if (!resp.ok) throw new Error('Failed to load audio template');
|
|
|
|
|
const tmpl = await resp.json();
|
|
|
|
|
showAddAudioTemplateModal(tmpl);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error.isAuth) return;
|
|
|
|
|
console.error('Failed to clone audio template:', error);
|
|
|
|
|
showToast('Failed to clone audio template', 'error');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ===== Picture Sources =====
|
|
|
|
|
|
|
|
|
|
export async function loadPictureSources() {
|
|
|
|
|
@@ -518,13 +801,14 @@ export async function loadPictureSources() {
|
|
|
|
|
set_sourcesLoading(true);
|
|
|
|
|
setTabRefreshing('streams-list', true);
|
|
|
|
|
try {
|
|
|
|
|
const [filtersResp, ppResp, captResp, streamsResp, audioResp, valueResp] = await Promise.all([
|
|
|
|
|
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'),
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
if (filtersResp && filtersResp.ok) {
|
|
|
|
|
@@ -547,6 +831,10 @@ export async function loadPictureSources() {
|
|
|
|
|
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 || []);
|
|
|
|
|
@@ -745,7 +1033,9 @@ function renderPictureSourcesList(streams) {
|
|
|
|
|
const devIdx = src.device_index ?? -1;
|
|
|
|
|
const loopback = src.is_loopback !== false;
|
|
|
|
|
const devLabel = loopback ? `${ICON_AUDIO_LOOPBACK} Loopback` : `${ICON_AUDIO_INPUT} Input`;
|
|
|
|
|
propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>`;
|
|
|
|
|
const tpl = src.audio_template_id ? _cachedAudioTemplates.find(t => t.id === src.audio_template_id) : null;
|
|
|
|
|
const tplBadge = tpl ? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.audio_template'))}" onclick="event.stopPropagation(); navigateToCard('streams','audio','audio-templates','data-audio-template-id','${src.audio_template_id}')">${ICON_AUDIO_TEMPLATE} ${escapeHtml(tpl.name)}</span>` : '';
|
|
|
|
|
propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>${tplBadge}`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return `
|
|
|
|
|
@@ -764,6 +1054,40 @@ function renderPictureSourcesList(streams) {
|
|
|
|
|
`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const renderAudioTemplateCard = (template) => {
|
|
|
|
|
const configEntries = Object.entries(template.engine_config || {});
|
|
|
|
|
return `
|
|
|
|
|
<div class="template-card" data-audio-template-id="${template.id}">
|
|
|
|
|
<button class="card-remove-btn" onclick="deleteAudioTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
|
|
|
|
<div class="template-card-header">
|
|
|
|
|
<div class="template-name">${ICON_AUDIO_TEMPLATE} ${escapeHtml(template.name)}</div>
|
|
|
|
|
</div>
|
|
|
|
|
${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
|
|
|
|
|
<div class="stream-card-props">
|
|
|
|
|
<span class="stream-card-prop" title="${t('audio_template.engine')}">${ICON_AUDIO_TEMPLATE} ${template.engine_type.toUpperCase()}</span>
|
|
|
|
|
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('audio_template.config.show')}">🔧 ${configEntries.length}</span>` : ''}
|
|
|
|
|
</div>
|
|
|
|
|
${configEntries.length > 0 ? `
|
|
|
|
|
<details class="template-config-details">
|
|
|
|
|
<summary>${t('audio_template.config.show')}</summary>
|
|
|
|
|
<table class="config-table">
|
|
|
|
|
${configEntries.map(([key, val]) => `
|
|
|
|
|
<tr>
|
|
|
|
|
<td class="config-key">${escapeHtml(key)}</td>
|
|
|
|
|
<td class="config-value">${escapeHtml(String(val))}</td>
|
|
|
|
|
</tr>
|
|
|
|
|
`).join('')}
|
|
|
|
|
</table>
|
|
|
|
|
</details>
|
|
|
|
|
` : ''}
|
|
|
|
|
<div class="template-card-actions">
|
|
|
|
|
<button class="btn btn-icon btn-secondary" onclick="cloneAudioTemplate('${template.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
|
|
|
|
<button class="btn btn-icon btn-secondary" onclick="editAudioTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const panels = tabs.map(tab => {
|
|
|
|
|
let panelContent = '';
|
|
|
|
|
|
|
|
|
|
@@ -778,7 +1102,8 @@ function renderPictureSourcesList(streams) {
|
|
|
|
|
} else if (tab.key === 'audio') {
|
|
|
|
|
panelContent =
|
|
|
|
|
csAudioMulti.render(multichannelSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) }))) +
|
|
|
|
|
csAudioMono.render(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })));
|
|
|
|
|
csAudioMono.render(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) }))) +
|
|
|
|
|
csAudioTemplates.render(_cachedAudioTemplates.map(t => ({ key: t.id, html: renderAudioTemplateCard(t) })));
|
|
|
|
|
} else if (tab.key === 'value') {
|
|
|
|
|
panelContent = csValueSources.render(_cachedValueSources.map(s => ({ key: s.id, html: createValueSourceCard(s) })));
|
|
|
|
|
} else {
|
|
|
|
|
@@ -789,7 +1114,7 @@ function renderPictureSourcesList(streams) {
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
container.innerHTML = tabBar + panels;
|
|
|
|
|
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csAudioMulti, csAudioMono, csStaticStreams, csValueSources]);
|
|
|
|
|
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csAudioMulti, csAudioMono, csAudioTemplates, csStaticStreams, csValueSources]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function onStreamTypeChange() {
|
|
|
|
|
|