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:
@@ -7,8 +7,10 @@ import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
||||
import { t } from '../core/i18n.js';
|
||||
import { showToast, showConfirm } from '../core/ui.js';
|
||||
import { Modal } from '../core/modal.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
|
||||
const profileModal = new Modal('profile-editor-modal');
|
||||
const csProfiles = new CardSection('profiles', { titleKey: 'profiles.title', gridClass: 'devices-grid', addCardOnclick: "openProfileEditor()" });
|
||||
|
||||
// Re-render profiles when language changes (only if tab is active)
|
||||
document.addEventListener('languageChanged', () => {
|
||||
@@ -53,16 +55,10 @@ export async function loadProfiles() {
|
||||
function renderProfiles(profiles, runningTargetIds = new Set()) {
|
||||
const container = document.getElementById('profiles-content');
|
||||
|
||||
let html = '<div class="devices-grid">';
|
||||
for (const p of profiles) {
|
||||
html += createProfileCard(p, runningTargetIds);
|
||||
}
|
||||
html += `<div class="template-card add-template-card" onclick="openProfileEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>`;
|
||||
html += '</div>';
|
||||
const cardsHtml = profiles.map(p => createProfileCard(p, runningTargetIds)).join('');
|
||||
container.innerHTML = csProfiles.render(cardsHtml, profiles.length);
|
||||
csProfiles.bind();
|
||||
|
||||
container.innerHTML = html;
|
||||
// Localize data-i18n elements within the profiles container only
|
||||
// (calling global updateAllText() would trigger loadProfiles() again → infinite loop)
|
||||
container.querySelectorAll('[data-i18n]').forEach(el => {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -15,10 +15,18 @@ import { Modal } from '../core/modal.js';
|
||||
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.js';
|
||||
import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
|
||||
import { createColorStripCard } from './color-strips.js';
|
||||
import { CardSection } from '../core/card-sections.js';
|
||||
|
||||
// createPatternTemplateCard is imported via window.* to avoid circular deps
|
||||
// (pattern-templates.js calls window.loadTargetsTab)
|
||||
|
||||
// ── Card section instances ──
|
||||
const csDevices = new CardSection('led-devices', { titleKey: 'targets.section.devices', gridClass: 'devices-grid', addCardOnclick: "showAddDevice()" });
|
||||
const csColorStrips = new CardSection('led-css', { titleKey: 'targets.section.color_strips', gridClass: 'devices-grid', addCardOnclick: "showCSSEditor()" });
|
||||
const csLedTargets = new CardSection('led-targets', { titleKey: 'targets.section.targets', gridClass: 'devices-grid', addCardOnclick: "showTargetEditor()" });
|
||||
const csKCTargets = new CardSection('kc-targets', { titleKey: 'targets.section.key_colors', gridClass: 'devices-grid', addCardOnclick: "showKCEditor()" });
|
||||
const csPatternTemplates = new CardSection('kc-patterns', { titleKey: 'targets.section.pattern_templates', gridClass: 'templates-grid', addCardOnclick: "showPatternTemplateEditor()" });
|
||||
|
||||
// Re-render targets tab when language changes (only if tab is active)
|
||||
document.addEventListener('languageChanged', () => {
|
||||
if (apiKey && localStorage.getItem('activeTab') === 'targets') loadTargetsTab();
|
||||
@@ -425,62 +433,27 @@ export async function loadTargetsTab() {
|
||||
// Use window.createPatternTemplateCard to avoid circular import
|
||||
const createPatternTemplateCard = window.createPatternTemplateCard || (() => '');
|
||||
|
||||
// LED panel: devices section + color strip sources section + targets section
|
||||
const devicesHtml = ledDevices.map(device => createDeviceCard(device)).join('');
|
||||
const cssHtml = Object.values(colorStripSourceMap).map(s => createColorStripCard(s, pictureSourceMap)).join('');
|
||||
const ledTargetsHtml = ledTargets.map(target => createTargetCard(target, deviceMap, colorStripSourceMap)).join('');
|
||||
const kcTargetsHtml = kcTargets.map(target => createKCTargetCard(target, pictureSourceMap, patternTemplateMap)).join('');
|
||||
const patternTmplHtml = patternTemplates.map(pt => createPatternTemplateCard(pt)).join('');
|
||||
|
||||
const ledPanel = `
|
||||
<div class="target-sub-tab-panel stream-tab-panel${activeSubTab === 'led' ? ' active' : ''}" id="target-sub-tab-led">
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.devices')}</h3>
|
||||
<div class="devices-grid">
|
||||
${ledDevices.map(device => createDeviceCard(device)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showAddDevice()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.color_strips')}</h3>
|
||||
<div class="devices-grid">
|
||||
${Object.values(colorStripSourceMap).map(s => createColorStripCard(s, pictureSourceMap)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showCSSEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.targets')}</h3>
|
||||
<div class="devices-grid">
|
||||
${ledTargets.map(target => createTargetCard(target, deviceMap, colorStripSourceMap)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showTargetEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${csDevices.render(devicesHtml, ledDevices.length)}
|
||||
${csColorStrips.render(cssHtml, Object.keys(colorStripSourceMap).length)}
|
||||
${csLedTargets.render(ledTargetsHtml, ledTargets.length)}
|
||||
</div>`;
|
||||
|
||||
// Key Colors panel
|
||||
const kcPanel = `
|
||||
<div class="target-sub-tab-panel stream-tab-panel${activeSubTab === 'key_colors' ? ' active' : ''}" id="target-sub-tab-key_colors">
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.key_colors')}</h3>
|
||||
<div class="devices-grid">
|
||||
${kcTargets.map(target => createKCTargetCard(target, pictureSourceMap, patternTemplateMap)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showKCEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="subtab-section">
|
||||
<h3 class="subtab-section-header">${t('targets.section.pattern_templates')}</h3>
|
||||
<div class="templates-grid">
|
||||
${patternTemplates.map(pt => createPatternTemplateCard(pt)).join('')}
|
||||
<div class="template-card add-template-card" onclick="showPatternTemplateEditor()">
|
||||
<div class="add-template-icon">+</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
${csKCTargets.render(kcTargetsHtml, kcTargets.length)}
|
||||
${csPatternTemplates.render(patternTmplHtml, patternTemplates.length)}
|
||||
</div>`;
|
||||
|
||||
container.innerHTML = tabBar + ledPanel + kcPanel;
|
||||
CardSection.bindAll([csDevices, csColorStrips, csLedTargets, csKCTargets, csPatternTemplates]);
|
||||
|
||||
// Attach event listeners and fetch brightness for device cards
|
||||
devicesWithState.forEach(device => {
|
||||
|
||||
Reference in New Issue
Block a user