Add card color system with wrapCard helper and reset support
Introduce localStorage-backed card color assignment for all card types with a reusable wrapCard() helper that provides consistent card shell structure (top actions, bottom actions with color picker). Move color picker from top-right to bottom-right action bar. Add color reset button to clear card color back to default. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO,
|
||||
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP,
|
||||
} from '../core/icons.js';
|
||||
import { wrapCard } from '../core/card-colors.js';
|
||||
|
||||
// ── Card section instances ──
|
||||
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id' });
|
||||
@@ -1184,29 +1185,35 @@ function renderPictureSourcesList(streams) {
|
||||
</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="template-card" data-stream-id="${stream.id}">
|
||||
<button class="card-remove-btn" onclick="deleteStream('${stream.id}')" title="${t('common.delete')}">✕</button>
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
dataAttr: 'data-stream-id',
|
||||
id: stream.id,
|
||||
removeOnclick: `deleteStream('${stream.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">${typeIcon} ${escapeHtml(stream.name)}</div>
|
||||
</div>
|
||||
${detailsHtml}
|
||||
${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}
|
||||
<div class="template-card-actions">
|
||||
${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestStreamModal('${stream.id}')" title="${t('streams.test.title')}">${ICON_TEST}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneStream('${stream.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editStream('${stream.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<button class="btn btn-icon btn-secondary" onclick="editStream('${stream.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||
});
|
||||
};
|
||||
|
||||
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>
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
dataAttr: 'data-template-id',
|
||||
id: template.id,
|
||||
removeOnclick: `deleteTemplate('${template.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">${ICON_TEMPLATE} ${escapeHtml(template.name)}</div>
|
||||
</div>
|
||||
@@ -1227,14 +1234,12 @@ function renderPictureSourcesList(streams) {
|
||||
`).join('')}
|
||||
</table>
|
||||
</details>
|
||||
` : ''}
|
||||
<div class="template-card-actions">
|
||||
` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestTemplateModal('${template.id}')" title="${t('templates.test.title')}">${ICON_TEST}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneCaptureTemplate('${template.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||
});
|
||||
};
|
||||
|
||||
const renderPPTemplateCard = (tmpl) => {
|
||||
@@ -1243,21 +1248,23 @@ function renderPictureSourcesList(streams) {
|
||||
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>
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
dataAttr: 'data-pp-template-id',
|
||||
id: tmpl.id,
|
||||
removeOnclick: `deletePPTemplate('${tmpl.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">${ICON_TEMPLATE} ${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">
|
||||
${filterChainHtml}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestPPTemplateModal('${tmpl.id}')" title="${t('postprocessing.test.title')}">${ICON_TEST}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="clonePPTemplate('${tmpl.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||
});
|
||||
};
|
||||
|
||||
const rawStreams = streams.filter(s => s.stream_type === 'raw');
|
||||
@@ -1305,28 +1312,34 @@ function renderPictureSourcesList(streams) {
|
||||
propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>${tplBadge}`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="template-card" data-id="${src.id}">
|
||||
<button class="card-remove-btn" onclick="deleteAudioSource('${src.id}')" title="${t('common.delete')}">✕</button>
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
dataAttr: 'data-id',
|
||||
id: src.id,
|
||||
removeOnclick: `deleteAudioSource('${src.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">${icon} ${escapeHtml(src.name)}</div>
|
||||
</div>
|
||||
<div class="stream-card-props">${propsHtml}</div>
|
||||
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}
|
||||
<div class="template-card-actions">
|
||||
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="testAudioSource('${src.id}')" title="${t('audio_source.test')}">${ICON_TEST}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneAudioSource('${src.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editAudioSource('${src.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<button class="btn btn-icon btn-secondary" onclick="editAudioSource('${src.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||
});
|
||||
};
|
||||
|
||||
const renderAudioTemplateCard = (template) => {
|
||||
const configEntries = Object.entries(template.engine_config || {});
|
||||
return `
|
||||
<div class="template-card" data-audio-template-id="${template.id}">
|
||||
<button class="card-remove-btn" onclick="deleteAudioTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
||||
return wrapCard({
|
||||
type: 'template-card',
|
||||
dataAttr: 'data-audio-template-id',
|
||||
id: template.id,
|
||||
removeOnclick: `deleteAudioTemplate('${template.id}')`,
|
||||
removeTitle: t('common.delete'),
|
||||
content: `
|
||||
<div class="template-card-header">
|
||||
<div class="template-name">${ICON_AUDIO_TEMPLATE} ${escapeHtml(template.name)}</div>
|
||||
</div>
|
||||
@@ -1347,14 +1360,12 @@ function renderPictureSourcesList(streams) {
|
||||
`).join('')}
|
||||
</table>
|
||||
</details>
|
||||
` : ''}
|
||||
<div class="template-card-actions">
|
||||
` : ''}`,
|
||||
actions: `
|
||||
<button class="btn btn-icon btn-secondary" onclick="showTestAudioTemplateModal('${template.id}')" title="${t('audio_template.test')}">${ICON_TEST}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="cloneAudioTemplate('${template.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||
<button class="btn btn-icon btn-secondary" onclick="editAudioTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
<button class="btn btn-icon btn-secondary" onclick="editAudioTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||
});
|
||||
};
|
||||
|
||||
const panels = tabs.map(tab => {
|
||||
|
||||
Reference in New Issue
Block a user