Merge templates into Streams tab, rename app to WLED Grab
- Merge Capture Templates and Processing Templates main tabs into Picture Streams sub-tabs (Screen Capture shows streams + engine templates, Processed shows streams + filter templates) - Rename "Capture Templates" to "Engine Templates" and "Processing Templates" to "Filter Templates" across all locale strings - Rename "Picture Streams" tab to "Streams" throughout UI and locales - Rename "WLED Screen Controller" to "WLED Grab" across all files - Add subtab section headers and styling for merged template views - Remove add card labels, keeping only plus icon for cleaner UI Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -208,7 +208,7 @@ const supportedLocales = {
|
||||
|
||||
// Minimal inline fallback for critical UI elements
|
||||
const fallbackTranslations = {
|
||||
'app.title': 'WLED Screen Controller',
|
||||
'app.title': 'WLED Grab',
|
||||
'auth.placeholder': 'Enter your API key...',
|
||||
'auth.button.login': 'Login'
|
||||
};
|
||||
@@ -494,18 +494,16 @@ async function loadDisplays() {
|
||||
let _cachedDisplays = null;
|
||||
|
||||
function switchTab(name) {
|
||||
// Migrate legacy tab values from localStorage
|
||||
if (name === 'templates' || name === 'pp-templates') {
|
||||
name = 'streams';
|
||||
}
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name));
|
||||
document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`));
|
||||
localStorage.setItem('activeTab', name);
|
||||
if (name === 'templates') {
|
||||
loadCaptureTemplates();
|
||||
}
|
||||
if (name === 'streams') {
|
||||
loadPictureStreams();
|
||||
}
|
||||
if (name === 'pp-templates') {
|
||||
loadPPTemplates();
|
||||
}
|
||||
}
|
||||
|
||||
function initTabs() {
|
||||
@@ -2423,80 +2421,15 @@ async function loadCaptureTemplates() {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load templates: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
renderTemplatesList(data.templates || []);
|
||||
_cachedCaptureTemplates = data.templates || [];
|
||||
// Re-render the streams tab which now contains template sections
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
} catch (error) {
|
||||
console.error('Error loading capture templates:', error);
|
||||
document.getElementById('templates-list').innerHTML = `
|
||||
<div class="error-message">${t('templates.error.load')}: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Render templates list
|
||||
function renderTemplatesList(templates) {
|
||||
const container = document.getElementById('templates-list');
|
||||
|
||||
if (templates.length === 0) {
|
||||
container.innerHTML = `<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('templates.add')}</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
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}">
|
||||
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
${engineIcon} ${escapeHtml(template.name)}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
${configEntries.length > 0 ? `
|
||||
<details class="template-config-details">
|
||||
<summary>${t('templates.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="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">
|
||||
🧪
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
let html = templates.map(renderCard).join('');
|
||||
|
||||
html += `<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('templates.add')}</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// Get engine icon
|
||||
function getEngineIcon(engineType) {
|
||||
return '🖥️';
|
||||
@@ -3055,28 +2988,31 @@ let _availableFilters = []; // Loaded from GET /filters
|
||||
|
||||
async function loadPictureStreams() {
|
||||
try {
|
||||
// Ensure PP templates and capture templates are cached for stream card display
|
||||
if (_cachedPPTemplates.length === 0 || _cachedCaptureTemplates.length === 0) {
|
||||
try {
|
||||
if (_availableFilters.length === 0) {
|
||||
const fr = await fetchWithAuth('/filters');
|
||||
if (fr.ok) { const fd = await fr.json(); _availableFilters = fd.filters || []; }
|
||||
}
|
||||
if (_cachedPPTemplates.length === 0) {
|
||||
const pr = await fetchWithAuth('/postprocessing-templates');
|
||||
if (pr.ok) { const pd = await pr.json(); _cachedPPTemplates = pd.templates || []; }
|
||||
}
|
||||
if (_cachedCaptureTemplates.length === 0) {
|
||||
const cr = await fetchWithAuth('/capture-templates');
|
||||
if (cr.ok) { const cd = await cr.json(); _cachedCaptureTemplates = cd.templates || []; }
|
||||
}
|
||||
} catch (e) { console.warn('Could not pre-load templates for streams:', e); }
|
||||
// Always fetch templates, filters, and streams in parallel
|
||||
// since templates are now rendered inside stream sub-tabs
|
||||
const [filtersResp, ppResp, captResp, streamsResp] = await Promise.all([
|
||||
_availableFilters.length === 0 ? fetchWithAuth('/filters') : Promise.resolve(null),
|
||||
fetchWithAuth('/postprocessing-templates'),
|
||||
fetchWithAuth('/capture-templates'),
|
||||
fetchWithAuth('/picture-streams')
|
||||
]);
|
||||
|
||||
if (filtersResp && filtersResp.ok) {
|
||||
const fd = await filtersResp.json();
|
||||
_availableFilters = fd.filters || [];
|
||||
}
|
||||
const response = await fetchWithAuth('/picture-streams');
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load streams: ${response.status}`);
|
||||
if (ppResp.ok) {
|
||||
const pd = await ppResp.json();
|
||||
_cachedPPTemplates = pd.templates || [];
|
||||
}
|
||||
const data = await response.json();
|
||||
if (captResp.ok) {
|
||||
const cd = await captResp.json();
|
||||
_cachedCaptureTemplates = cd.templates || [];
|
||||
}
|
||||
if (!streamsResp.ok) {
|
||||
throw new Error(`Failed to load streams: ${streamsResp.status}`);
|
||||
}
|
||||
const data = await streamsResp.json();
|
||||
_cachedStreams = data.streams || [];
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
} catch (error) {
|
||||
@@ -3101,7 +3037,7 @@ function renderPictureStreamsList(streams) {
|
||||
const container = document.getElementById('streams-list');
|
||||
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
||||
|
||||
const renderCard = (stream) => {
|
||||
const renderStreamCard = (stream) => {
|
||||
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
||||
const typeIcon = typeIcons[stream.stream_type] || '📺';
|
||||
|
||||
@@ -3158,34 +3094,145 @@ function renderPictureStreamsList(streams) {
|
||||
`;
|
||||
};
|
||||
|
||||
const renderCaptureTemplateCard = (template) => {
|
||||
const engineIcon = getEngineIcon(template.engine_type);
|
||||
const configEntries = Object.entries(template.engine_config);
|
||||
return `
|
||||
<div class="template-card" data-template-id="${template.id}">
|
||||
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
${engineIcon} ${escapeHtml(template.name)}
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
${configEntries.length > 0 ? `
|
||||
<details class="template-config-details">
|
||||
<summary>${t('templates.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="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">
|
||||
🧪
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const renderPPTemplateCard = (tmpl) => {
|
||||
let filterChainHtml = '';
|
||||
if (tmpl.filters && tmpl.filters.length > 0) {
|
||||
const filterNames = tmpl.filters.map(fi => `<span class="filter-chain-item">${escapeHtml(_getFilterName(fi.filter_id))}</span>`);
|
||||
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
|
||||
}
|
||||
return `
|
||||
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
||||
<button class="card-remove-btn" onclick="deletePPTemplate('${tmpl.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
🎨 ${escapeHtml(tmpl.name)}
|
||||
</div>
|
||||
</div>
|
||||
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
||||
${filterChainHtml}
|
||||
<div class="template-card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestPPTemplateModal('${tmpl.id}')" title="${t('postprocessing.test.title')}">
|
||||
🧪
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
const rawStreams = streams.filter(s => s.stream_type === 'raw');
|
||||
const processedStreams = streams.filter(s => s.stream_type === 'processed');
|
||||
const staticImageStreams = streams.filter(s => s.stream_type === 'static_image');
|
||||
|
||||
const addCard = (type, labelKey) => `
|
||||
const addStreamCard = (type) => `
|
||||
<div class="template-card add-template-card" onclick="showAddStreamModal('${type}')">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t(labelKey)}</div>
|
||||
</div>`;
|
||||
|
||||
const tabs = [
|
||||
{ key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams, addLabelKey: 'streams.add.raw' },
|
||||
{ key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams, addLabelKey: 'streams.add.static_image' },
|
||||
{ key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams, addLabelKey: 'streams.add.processed' },
|
||||
{ key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', streams: rawStreams },
|
||||
{ key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', streams: staticImageStreams },
|
||||
{ key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', streams: processedStreams },
|
||||
];
|
||||
|
||||
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
|
||||
`<button class="stream-tab-btn${tab.key === activeTab ? ' active' : ''}" data-stream-tab="${tab.key}" onclick="switchStreamTab('${tab.key}')">${tab.icon} ${t(tab.titleKey)} <span class="stream-tab-count">${tab.streams.length}</span></button>`
|
||||
).join('')}</div>`;
|
||||
|
||||
const panels = tabs.map(tab =>
|
||||
`<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">
|
||||
<div class="templates-grid">
|
||||
${tab.streams.map(renderCard).join('')}
|
||||
${addCard(tab.key, tab.addLabelKey)}
|
||||
</div>
|
||||
</div>`
|
||||
).join('');
|
||||
const panels = tabs.map(tab => {
|
||||
let panelContent = '';
|
||||
|
||||
if (tab.key === 'raw') {
|
||||
// Screen Capture: streams section + capture templates section
|
||||
panelContent = `
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('streams.section.streams')}</h3>
|
||||
<div class="templates-grid">
|
||||
${tab.streams.map(renderStreamCard).join('')}
|
||||
${addStreamCard(tab.key)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('templates.title')}</h3>
|
||||
<div class="templates-grid">
|
||||
${_cachedCaptureTemplates.map(renderCaptureTemplateCard).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
} else if (tab.key === 'processed') {
|
||||
// Processed: streams section + PP templates section
|
||||
panelContent = `
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('streams.section.streams')}</h3>
|
||||
<div class="templates-grid">
|
||||
${tab.streams.map(renderStreamCard).join('')}
|
||||
${addStreamCard(tab.key)}
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('postprocessing.title')}</h3>
|
||||
<div class="templates-grid">
|
||||
${_cachedPPTemplates.map(renderPPTemplateCard).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddPPTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
// Static Image: just the stream cards, no section headers
|
||||
panelContent = `
|
||||
<div class="templates-grid">
|
||||
${tab.streams.map(renderStreamCard).join('')}
|
||||
${addStreamCard(tab.key)}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
|
||||
}).join('');
|
||||
|
||||
container.innerHTML = tabBar + panels;
|
||||
}
|
||||
@@ -3671,12 +3718,10 @@ async function loadPPTemplates() {
|
||||
}
|
||||
const data = await response.json();
|
||||
_cachedPPTemplates = data.templates || [];
|
||||
renderPPTemplatesList(_cachedPPTemplates);
|
||||
// Re-render the streams tab which now contains template sections
|
||||
renderPictureStreamsList(_cachedStreams);
|
||||
} catch (error) {
|
||||
console.error('Error loading PP templates:', error);
|
||||
document.getElementById('pp-templates-list').innerHTML = `
|
||||
<div class="error-message">${t('postprocessing.error.load')}: ${error.message}</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3691,56 +3736,6 @@ function _getFilterName(filterId) {
|
||||
return translated;
|
||||
}
|
||||
|
||||
function renderPPTemplatesList(templates) {
|
||||
const container = document.getElementById('pp-templates-list');
|
||||
|
||||
if (templates.length === 0) {
|
||||
container.innerHTML = `<div class="template-card add-template-card" onclick="showAddPPTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('postprocessing.add')}</div>
|
||||
</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const renderCard = (tmpl) => {
|
||||
// Build filter chain pills
|
||||
let filterChainHtml = '';
|
||||
if (tmpl.filters && tmpl.filters.length > 0) {
|
||||
const filterNames = tmpl.filters.map(fi => `<span class="filter-chain-item">${escapeHtml(_getFilterName(fi.filter_id))}</span>`);
|
||||
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
||||
<button class="card-remove-btn" onclick="deletePPTemplate('${tmpl.id}')" title="${t('common.delete')}">✕</button>
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">
|
||||
🎨 ${escapeHtml(tmpl.name)}
|
||||
</div>
|
||||
</div>
|
||||
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
||||
${filterChainHtml}
|
||||
<div class="template-card-actions">
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestPPTemplateModal('${tmpl.id}')" title="${t('postprocessing.test.title')}">
|
||||
🧪
|
||||
</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">
|
||||
✏️
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
};
|
||||
|
||||
let html = templates.map(renderCard).join('');
|
||||
html += `<div class="template-card add-template-card" onclick="showAddPPTemplateModal()">
|
||||
<div class="add-template-icon">+</div>
|
||||
<div class="add-template-label">${t('postprocessing.add')}</div>
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// --- Filter list management in PP template modal ---
|
||||
|
||||
let _modalFilters = []; // Current filter list being edited in modal
|
||||
|
||||
Reference in New Issue
Block a user