Refactor stream and template cards: collapsible groups, icon pills, compact layout
- Make picture stream groups expandable/collapsible with localStorage persistence - Replace text labels with icon pill badges on stream and capture template cards - Remove section description text from all tabs - Add auto-validation on edit for static image streams - Show full URL on static image cards instead of truncating at 50 chars Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -2448,6 +2448,7 @@ function renderTemplatesList(templates) {
|
||||
|
||||
const renderCard = (template) => {
|
||||
const engineIcon = getEngineIcon(template.engine_type);
|
||||
const configEntries = Object.entries(template.engine_config);
|
||||
|
||||
return `
|
||||
<div class="template-card" data-template-id="${template.id}">
|
||||
@@ -2457,14 +2458,15 @@ function renderTemplatesList(templates) {
|
||||
${engineIcon} ${escapeHtml(template.name)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="template-config">
|
||||
<strong>${t('templates.engine')}</strong> ${template.engine_type.toUpperCase()}
|
||||
<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('templates.engine')}">⚙️ ${template.engine_type.toUpperCase()}</span>
|
||||
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('templates.config.show')}">🔧 ${configEntries.length}</span>` : ''}
|
||||
</div>
|
||||
${Object.keys(template.engine_config).length > 0 ? `
|
||||
${configEntries.length > 0 ? `
|
||||
<details class="template-config-details">
|
||||
<summary>${t('templates.config.show')}</summary>
|
||||
<table class="config-table">
|
||||
${Object.entries(template.engine_config).map(([key, val]) => `
|
||||
${configEntries.map(([key, val]) => `
|
||||
<tr>
|
||||
<td class="config-key">${escapeHtml(key)}</td>
|
||||
<td class="config-value">${escapeHtml(String(val))}</td>
|
||||
@@ -2472,9 +2474,7 @@ function renderTemplatesList(templates) {
|
||||
`).join('')}
|
||||
</table>
|
||||
</details>
|
||||
` : `
|
||||
<div class="template-no-config">${t('templates.config.none')}</div>
|
||||
`}
|
||||
` : ''}
|
||||
<div class="template-card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">
|
||||
🧪
|
||||
@@ -3087,50 +3087,46 @@ async function loadPictureStreams() {
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStreamGroup(groupKey) {
|
||||
const stored = JSON.parse(localStorage.getItem('streamGroupState') || '{}');
|
||||
stored[groupKey] = !stored[groupKey];
|
||||
localStorage.setItem('streamGroupState', JSON.stringify(stored));
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
}
|
||||
|
||||
function isStreamGroupCollapsed(groupKey) {
|
||||
const stored = JSON.parse(localStorage.getItem('streamGroupState') || '{}');
|
||||
return !!stored[groupKey];
|
||||
}
|
||||
|
||||
function renderStreamGroup(groupKey, icon, titleKey, count, addType, addLabelKey, cardsHtml) {
|
||||
const collapsed = isStreamGroupCollapsed(groupKey);
|
||||
const chevron = collapsed ? '▶' : '▼';
|
||||
return `<div class="stream-group${collapsed ? ' collapsed' : ''}">
|
||||
<div class="stream-group-header" onclick="toggleStreamGroup('${groupKey}')">
|
||||
<span class="stream-group-chevron">${chevron}</span>
|
||||
<span class="stream-group-icon">${icon}</span>
|
||||
<span class="stream-group-title">${t(titleKey)}</span>
|
||||
<span class="stream-group-count">${count}</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
${cardsHtml}
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('${addType}')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t(addLabelKey)}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
function renderPictureStreamsList(streams) {
|
||||
const container = document.getElementById('streams-list');
|
||||
|
||||
if (streams.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="stream-group">
|
||||
<div class="stream-group-header">
|
||||
<span class="stream-group-icon">🖥️</span>
|
||||
<span class="stream-group-title">${t('streams.group.raw')}</span>
|
||||
<span class="stream-group-count">0</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('raw')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('streams.add.raw')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stream-group">
|
||||
<div class="stream-group-header">
|
||||
<span class="stream-group-icon">🖼️</span>
|
||||
<span class="stream-group-title">${t('streams.group.static_image')}</span>
|
||||
<span class="stream-group-count">0</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('static_image')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('streams.add.static_image')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="stream-group">
|
||||
<div class="stream-group-header">
|
||||
<span class="stream-group-icon">🎨</span>
|
||||
<span class="stream-group-title">${t('streams.group.processed')}</span>
|
||||
<span class="stream-group-count">0</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('processed')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('streams.add.processed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
container.innerHTML =
|
||||
renderStreamGroup('raw', '🖥️', 'streams.group.raw', 0, 'raw', 'streams.add.raw', '') +
|
||||
renderStreamGroup('static_image', '🖼️', 'streams.group.static_image', 0, 'static_image', 'streams.add.static_image', '') +
|
||||
renderStreamGroup('processed', '🎨', 'streams.group.processed', 0, 'processed', 'streams.add.processed', '');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -3146,49 +3142,33 @@ function renderPictureStreamsList(streams) {
|
||||
|
||||
let detailsHtml = '';
|
||||
if (stream.stream_type === 'raw') {
|
||||
let captureTemplateHtml = '';
|
||||
let capTmplName = '';
|
||||
if (stream.capture_template_id) {
|
||||
const capTmpl = _cachedCaptureTemplates.find(t => t.id === stream.capture_template_id);
|
||||
if (capTmpl) {
|
||||
captureTemplateHtml = `<div class="template-config"><strong>${t('streams.capture_template')}</strong> ${escapeHtml(capTmpl.name)}</div>`;
|
||||
}
|
||||
if (capTmpl) capTmplName = escapeHtml(capTmpl.name);
|
||||
}
|
||||
detailsHtml = `
|
||||
<div class="template-config">
|
||||
<strong>${t('streams.display')}</strong> ${stream.display_index ?? 0}
|
||||
</div>
|
||||
<div class="template-config">
|
||||
<strong>${t('streams.target_fps')}</strong> ${stream.target_fps ?? 30}
|
||||
</div>
|
||||
${captureTemplateHtml}
|
||||
`;
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('streams.display')}">🖥️ ${stream.display_index ?? 0}</span>
|
||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">⚡ ${stream.target_fps ?? 30}</span>
|
||||
${capTmplName ? `<span class="stream-card-prop" title="${t('streams.capture_template')}">${capTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
} else if (stream.stream_type === 'processed') {
|
||||
// Find source stream name and PP template name
|
||||
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
||||
const sourceName = sourceStream ? escapeHtml(sourceStream.name) : (stream.source_stream_id || '-');
|
||||
// Find PP template name
|
||||
let ppTemplateHtml = '';
|
||||
let ppTmplName = '';
|
||||
if (stream.postprocessing_template_id) {
|
||||
const ppTmpl = _cachedPPTemplates.find(p => p.id === stream.postprocessing_template_id);
|
||||
if (ppTmpl) {
|
||||
ppTemplateHtml = `<div class="template-config"><strong>${t('streams.pp_template')}</strong> ${escapeHtml(ppTmpl.name)}</div>`;
|
||||
}
|
||||
if (ppTmpl) ppTmplName = escapeHtml(ppTmpl.name);
|
||||
}
|
||||
detailsHtml = `
|
||||
<div class="template-config">
|
||||
<strong>${t('streams.source')}</strong> ${sourceName}
|
||||
</div>
|
||||
${ppTemplateHtml}
|
||||
`;
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop" title="${t('streams.source')}">📺 ${sourceName}</span>
|
||||
${ppTmplName ? `<span class="stream-card-prop" title="${t('streams.pp_template')}">🎨 ${ppTmplName}</span>` : ''}
|
||||
</div>`;
|
||||
} else if (stream.stream_type === 'static_image') {
|
||||
const src = stream.image_source || '';
|
||||
const truncated = src.length > 50 ? src.substring(0, 47) + '...' : src;
|
||||
detailsHtml = `
|
||||
<div class="template-config">
|
||||
<strong>${t('streams.image_source')}</strong>
|
||||
</div>
|
||||
<div class="stream-card-image-source" title="${escapeHtml(src)}">${escapeHtml(truncated)}</div>
|
||||
`;
|
||||
detailsHtml = `<div class="stream-card-props">
|
||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(src)}">🌐 ${escapeHtml(src)}</span>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
@@ -3218,57 +3198,10 @@ function renderPictureStreamsList(streams) {
|
||||
const processedStreams = streams.filter(s => s.stream_type === 'processed');
|
||||
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
||||
|
||||
let html = '';
|
||||
|
||||
// Screen Capture streams section
|
||||
html += `<div class="stream-group">
|
||||
<div class="stream-group-header">
|
||||
<span class="stream-group-icon">🖥️</span>
|
||||
<span class="stream-group-title">${t('streams.group.raw')}</span>
|
||||
<span class="stream-group-count">${rawStreams.length}</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
${rawStreams.map(renderCard).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('raw')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('streams.add.raw')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Static Image streams section
|
||||
html += `<div class="stream-group">
|
||||
<div class="stream-group-header">
|
||||
<span class="stream-group-icon">🖼️</span>
|
||||
<span class="stream-group-title">${t('streams.group.static_image')}</span>
|
||||
<span class="stream-group-count">${staticImageStreams.length}</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
${staticImageStreams.map(renderCard).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('static_image')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('streams.add.static_image')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
// Processed streams section
|
||||
html += `<div class="stream-group">
|
||||
<div class="stream-group-header">
|
||||
<span class="stream-group-icon">🎨</span>
|
||||
<span class="stream-group-title">${t('streams.group.processed')}</span>
|
||||
<span class="stream-group-count">${processedStreams.length}</span>
|
||||
</div>
|
||||
<div class="templates-grid">
|
||||
${processedStreams.map(renderCard).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('processed')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('streams.add.processed')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
container.innerHTML =
|
||||
renderStreamGroup('raw', '🖥️', 'streams.group.raw', rawStreams.length, 'raw', 'streams.add.raw', rawStreams.map(renderCard).join('')) +
|
||||
renderStreamGroup('static_image', '🖼️', 'streams.group.static_image', staticImageStreams.length, 'static_image', 'streams.add.static_image', staticImageStreams.map(renderCard).join('')) +
|
||||
renderStreamGroup('processed', '🎨', 'streams.group.processed', processedStreams.length, 'processed', 'streams.add.processed', processedStreams.map(renderCard).join(''));
|
||||
}
|
||||
|
||||
function onStreamTypeChange() {
|
||||
@@ -3323,9 +3256,14 @@ async function editStream(streamId) {
|
||||
|
||||
// Set type (hidden input)
|
||||
document.getElementById('stream-type').value = stream.stream_type;
|
||||
// Clear static image preview
|
||||
// Clear static image preview and wire up auto-validation
|
||||
_lastValidatedImageSource = '';
|
||||
const imgSrcInput = document.getElementById('stream-image-source');
|
||||
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);
|
||||
onStreamTypeChange();
|
||||
|
||||
// Populate dropdowns before setting values
|
||||
|
||||
Reference in New Issue
Block a user