Add collapsible card sections with name filtering

Introduces CardSection class that wraps each card grid with a collapsible
header and inline filter input. Collapse state persists in localStorage,
filter value survives auto-refresh re-renders. When filter is active the
add-card button is hidden. Applied to all 13 sections across Targets,
Sources, and Profiles tabs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-24 00:46:14 +03:00
parent 808037775f
commit 166ec351b1
7 changed files with 246 additions and 118 deletions

View File

@@ -26,6 +26,16 @@ import { t } from '../core/i18n.js';
import { Modal } from '../core/modal.js';
import { showToast, showConfirm, openLightbox, openFullImageLightbox, showOverlaySpinner, hideOverlaySpinner } from '../core/ui.js';
import { openDisplayPicker, formatDisplayLabel } from './displays.js';
import { CardSection } from '../core/card-sections.js';
// ── Card section instances ──
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')" });
const csRawTemplates = new CardSection('raw-templates', { titleKey: 'templates.title', gridClass: 'templates-grid', addCardOnclick: "showAddTemplateModal()" });
const csProcStreams = new CardSection('proc-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('processed')" });
const csProcTemplates = new CardSection('proc-templates', { titleKey: 'postprocessing.title', gridClass: 'templates-grid', addCardOnclick: "showAddPPTemplateModal()" });
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')" });
// Re-render picture sources when language changes
document.addEventListener('languageChanged', () => { if (apiKey) loadPictureSources(); });
@@ -605,10 +615,6 @@ function renderPictureSourcesList(streams) {
const multichannelSources = _cachedAudioSources.filter(s => s.source_type === 'multichannel');
const monoSources = _cachedAudioSources.filter(s => s.source_type === 'mono');
const addStreamCard = (type) => `
<div class="template-card add-template-card" onclick="showAddStreamModal('${type}')">
<div class="add-template-icon">+</div>
</div>`;
const tabs = [
{ key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', count: rawStreams.length },
@@ -660,73 +666,26 @@ function renderPictureSourcesList(streams) {
let panelContent = '';
if (tab.key === 'raw') {
panelContent = `
<div class="subtab-section">
<h3 class="subtab-section-header">${t('streams.section.streams')}</h3>
<div class="templates-grid">
${rawStreams.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>`;
panelContent =
csRawStreams.render(rawStreams.map(renderStreamCard).join(''), rawStreams.length) +
csRawTemplates.render(_cachedCaptureTemplates.map(renderCaptureTemplateCard).join(''), _cachedCaptureTemplates.length);
} else if (tab.key === 'processed') {
panelContent = `
<div class="subtab-section">
<h3 class="subtab-section-header">${t('streams.section.streams')}</h3>
<div class="templates-grid">
${processedStreams.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>`;
panelContent =
csProcStreams.render(processedStreams.map(renderStreamCard).join(''), processedStreams.length) +
csProcTemplates.render(_cachedPPTemplates.map(renderPPTemplateCard).join(''), _cachedPPTemplates.length);
} else if (tab.key === 'audio') {
panelContent = `
<div class="subtab-section">
<h3 class="subtab-section-header">${t('audio_source.group.multichannel')}</h3>
<div class="templates-grid">
${multichannelSources.map(renderAudioSourceCard).join('')}
<div class="template-card add-template-card" onclick="showAudioSourceModal('multichannel')">
<div class="add-template-icon">+</div>
</div>
</div>
</div>
<div class="subtab-section">
<h3 class="subtab-section-header">${t('audio_source.group.mono')}</h3>
<div class="templates-grid">
${monoSources.map(renderAudioSourceCard).join('')}
<div class="template-card add-template-card" onclick="showAudioSourceModal('mono')">
<div class="add-template-icon">+</div>
</div>
</div>
</div>`;
panelContent =
csAudioMulti.render(multichannelSources.map(renderAudioSourceCard).join(''), multichannelSources.length) +
csAudioMono.render(monoSources.map(renderAudioSourceCard).join(''), monoSources.length);
} else {
panelContent = `
<div class="templates-grid">
${staticImageStreams.map(renderStreamCard).join('')}
${addStreamCard(tab.key)}
</div>`;
panelContent = csStaticStreams.render(staticImageStreams.map(renderStreamCard).join(''), staticImageStreams.length);
}
return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
}).join('');
container.innerHTML = tabBar + panels;
CardSection.bindAll([csRawStreams, csRawTemplates, csProcStreams, csProcTemplates, csAudioMulti, csAudioMono, csStaticStreams]);
}
export function onStreamTypeChange() {