Settings tabs, log overlay, external URL, Sources tree restructure, audio fixes
- Settings modal split into 3 tabs: General, Backup, MQTT - Log viewer moved to full-screen overlay with compact toolbar - External URL setting: API endpoints + UI for configuring server domain used in webhook/WS URLs instead of auto-detected local IP - Sources tab tree restructured: Picture Source (Screen Capture/Static/ Processed sub-groups), Color Strip, Audio, Utility - TreeNav extended to support nested groups (3-level tree) - Audio tab split into Sources and Templates sub-tabs - Fix audio template test: device picker now filters by engine type (was showing WASAPI indices for sounddevice templates) - Audio template test device picker disabled during active test - Rename "Input Source" to "Source" in CSS test preview (en/ru/zh) - Fix i18n: log filter/level items deferred to avoid stale t() calls Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1031,13 +1031,20 @@ const testAudioTemplateModal = new Modal('test-audio-template-modal', { backdrop
|
||||
export async function showTestAudioTemplateModal(templateId) {
|
||||
_currentTestAudioTemplateId = templateId;
|
||||
|
||||
// Load audio devices for picker
|
||||
// Find template's engine type so we show the correct device list
|
||||
const template = _cachedAudioTemplates.find(t => t.id === templateId);
|
||||
const engineType = template ? template.engine_type : null;
|
||||
|
||||
// Load audio devices for picker — filter by engine type
|
||||
const deviceSelect = document.getElementById('test-audio-template-device');
|
||||
try {
|
||||
const resp = await fetchWithAuth('/audio-devices');
|
||||
if (resp.ok) {
|
||||
const data = await resp.json();
|
||||
const devices = data.devices || [];
|
||||
// Use engine-specific device list if available, fall back to flat list
|
||||
const devices = (engineType && data.by_engine && data.by_engine[engineType])
|
||||
? data.by_engine[engineType]
|
||||
: (data.devices || []);
|
||||
deviceSelect.innerHTML = devices.map(d => {
|
||||
const label = d.name;
|
||||
const val = `${d.index}:${d.is_loopback ? '1' : '0'}`;
|
||||
@@ -1082,10 +1089,11 @@ export function startAudioTemplateTest() {
|
||||
const [devIdx, devLoop] = deviceVal.split(':');
|
||||
localStorage.setItem('lastAudioTestDevice', deviceVal);
|
||||
|
||||
// Show canvas + stats, hide run button
|
||||
// Show canvas + stats, hide run button, disable device picker
|
||||
document.getElementById('audio-template-test-canvas').style.display = '';
|
||||
document.getElementById('audio-template-test-stats').style.display = '';
|
||||
document.getElementById('test-audio-template-start-btn').style.display = 'none';
|
||||
document.getElementById('test-audio-template-device').disabled = true;
|
||||
|
||||
const statusEl = document.getElementById('audio-template-test-status');
|
||||
statusEl.textContent = t('audio_source.test.connecting');
|
||||
@@ -1144,6 +1152,9 @@ function _tplCleanupTest() {
|
||||
_tplTestWs = null;
|
||||
}
|
||||
_tplTestLatest = null;
|
||||
// Re-enable device picker
|
||||
const devSel = document.getElementById('test-audio-template-device');
|
||||
if (devSel) devSel.disabled = false;
|
||||
}
|
||||
|
||||
function _tplSizeCanvas(canvas) {
|
||||
@@ -1283,7 +1294,8 @@ const _streamSectionMap = {
|
||||
proc_templates: [csProcTemplates],
|
||||
css_processing: [csCSPTemplates],
|
||||
color_strip: [csColorStrips],
|
||||
audio: [csAudioMulti, csAudioMono, csAudioTemplates],
|
||||
audio: [csAudioMulti, csAudioMono],
|
||||
audio_templates: [csAudioTemplates],
|
||||
value: [csValueSources],
|
||||
sync: [csSyncClocks],
|
||||
};
|
||||
@@ -1490,6 +1502,7 @@ function renderPictureSourcesList(streams) {
|
||||
{ key: 'css_processing', icon: ICON_CSPT, titleKey: 'streams.group.css_processing', count: csptTemplates.length },
|
||||
{ key: 'color_strip', icon: getColorStripIcon('static'), titleKey: 'streams.group.color_strip', count: colorStrips.length },
|
||||
{ key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio', count: _cachedAudioSources.length },
|
||||
{ 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 },
|
||||
];
|
||||
@@ -1497,37 +1510,44 @@ function renderPictureSourcesList(streams) {
|
||||
// Build tree navigation structure
|
||||
const treeGroups = [
|
||||
{
|
||||
key: 'capture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.capture',
|
||||
key: 'picture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.picture',
|
||||
children: [
|
||||
{ key: 'raw', titleKey: 'streams.group.raw', icon: getPictureSourceIcon('raw'), count: rawStreams.length },
|
||||
{ key: 'raw_templates', titleKey: 'streams.group.raw_templates', icon: ICON_CAPTURE_TEMPLATE, count: _cachedCaptureTemplates.length },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image',
|
||||
count: staticImageStreams.length,
|
||||
},
|
||||
{
|
||||
key: 'video', icon: getPictureSourceIcon('video'), titleKey: 'streams.group.video',
|
||||
count: videoStreams.length,
|
||||
},
|
||||
{
|
||||
key: 'processing_group', icon: getPictureSourceIcon('processed'), titleKey: 'tree.group.processing',
|
||||
children: [
|
||||
{ key: 'processed', titleKey: 'streams.group.processed', icon: getPictureSourceIcon('processed'), count: processedStreams.length },
|
||||
{ key: 'proc_templates', titleKey: 'streams.group.proc_templates', icon: ICON_PP_TEMPLATE, count: _cachedPPTemplates.length },
|
||||
{
|
||||
key: 'capture_group', icon: getPictureSourceIcon('raw'), titleKey: 'tree.group.capture',
|
||||
children: [
|
||||
{ key: 'raw', titleKey: 'tree.leaf.sources', icon: getPictureSourceIcon('raw'), count: rawStreams.length },
|
||||
{ key: 'raw_templates', titleKey: 'tree.leaf.engine_templates', icon: ICON_CAPTURE_TEMPLATE, count: _cachedCaptureTemplates.length },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'static_group', icon: getPictureSourceIcon('static_image'), titleKey: 'tree.group.static',
|
||||
children: [
|
||||
{ key: 'static_image', titleKey: 'tree.leaf.images', icon: getPictureSourceIcon('static_image'), count: staticImageStreams.length },
|
||||
{ key: 'video', titleKey: 'tree.leaf.video', icon: getPictureSourceIcon('video'), count: videoStreams.length },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'processing_group', icon: getPictureSourceIcon('processed'), titleKey: 'tree.group.processing',
|
||||
children: [
|
||||
{ key: 'processed', titleKey: 'tree.leaf.sources', icon: getPictureSourceIcon('processed'), count: processedStreams.length },
|
||||
{ key: 'proc_templates', titleKey: 'tree.leaf.filter_templates', icon: ICON_PP_TEMPLATE, count: _cachedPPTemplates.length },
|
||||
]
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'strip_group', icon: getColorStripIcon('static'), titleKey: 'tree.group.strip',
|
||||
children: [
|
||||
{ key: 'color_strip', titleKey: 'streams.group.color_strip', icon: getColorStripIcon('static'), count: colorStrips.length },
|
||||
{ key: 'css_processing', titleKey: 'streams.group.css_processing', icon: ICON_CSPT, count: csptTemplates.length },
|
||||
{ key: 'color_strip', titleKey: 'tree.leaf.sources', icon: getColorStripIcon('static'), count: colorStrips.length },
|
||||
{ key: 'css_processing', titleKey: 'tree.leaf.processing_templates', icon: ICON_CSPT, count: csptTemplates.length },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio',
|
||||
count: _cachedAudioSources.length + _cachedAudioTemplates.length,
|
||||
key: 'audio_group', icon: getAudioSourceIcon('multichannel'), titleKey: 'tree.group.audio',
|
||||
children: [
|
||||
{ key: 'audio', titleKey: 'tree.leaf.sources', icon: getAudioSourceIcon('multichannel'), count: _cachedAudioSources.length },
|
||||
{ key: 'audio_templates', titleKey: 'tree.leaf.templates', icon: ICON_AUDIO_TEMPLATE, count: _cachedAudioTemplates.length },
|
||||
]
|
||||
},
|
||||
{
|
||||
key: 'utility_group', icon: ICON_WRENCH, titleKey: 'tree.group.utility',
|
||||
@@ -1559,7 +1579,7 @@ function renderPictureSourcesList(streams) {
|
||||
const loopback = src.is_loopback !== false;
|
||||
const devLabel = loopback ? `${ICON_AUDIO_LOOPBACK} Loopback` : `${ICON_AUDIO_INPUT} Input`;
|
||||
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>` : '';
|
||||
const tplBadge = tpl ? `<span class="stream-card-prop stream-card-link" title="${escapeHtml(t('audio_source.audio_template'))}" onclick="event.stopPropagation(); navigateToCard('streams','audio_templates','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}`;
|
||||
}
|
||||
|
||||
@@ -1651,7 +1671,8 @@ function renderPictureSourcesList(streams) {
|
||||
proc_templates: _cachedPPTemplates.length,
|
||||
css_processing: csptTemplates.length,
|
||||
color_strip: colorStrips.length,
|
||||
audio: _cachedAudioSources.length + _cachedAudioTemplates.length,
|
||||
audio: _cachedAudioSources.length,
|
||||
audio_templates: _cachedAudioTemplates.length,
|
||||
value: _cachedValueSources.length,
|
||||
sync: _cachedSyncClocks.length,
|
||||
});
|
||||
@@ -1678,7 +1699,8 @@ function renderPictureSourcesList(streams) {
|
||||
else if (tab.key === 'proc_templates') panelContent = csProcTemplates.render(procTemplateItems);
|
||||
else if (tab.key === 'css_processing') panelContent = csCSPTemplates.render(csptItems);
|
||||
else if (tab.key === 'color_strip') panelContent = csColorStrips.render(colorStripItems);
|
||||
else if (tab.key === 'audio') panelContent = csAudioMulti.render(multiItems) + csAudioMono.render(monoItems) + csAudioTemplates.render(audioTemplateItems);
|
||||
else if (tab.key === 'audio') panelContent = csAudioMulti.render(multiItems) + csAudioMono.render(monoItems);
|
||||
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 === 'video') panelContent = csVideoStreams.render(videoItems);
|
||||
@@ -1699,7 +1721,8 @@ function renderPictureSourcesList(streams) {
|
||||
'proc-streams': 'processed', 'proc-templates': 'proc_templates',
|
||||
'css-proc-templates': 'css_processing',
|
||||
'color-strips': 'color_strip',
|
||||
'audio-multi': 'audio', 'audio-mono': 'audio', 'audio-templates': 'audio',
|
||||
'audio-multi': 'audio', 'audio-mono': 'audio',
|
||||
'audio-templates': 'audio_templates',
|
||||
'value-sources': 'value',
|
||||
'sync-clocks': 'sync',
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user