Replace all emoji icons with Lucide SVGs, add accent color picker

- Replace all emoji characters across WebUI with inline Lucide SVG icons
  for cross-platform consistency (icon paths in icon-paths.js)
- Add accent color picker popover with 9 preset colors + custom picker,
  persisted to localStorage, updates all CSS custom properties
- Remove subtab separator line for cleaner look
- Color badge icons with accent color for visual pop
- Remove processing badge from target cards
- Fix hardcoded #4CAF50 in FPS labels and active badges to use CSS vars
- Replace CSS content emoji (▶) with pure CSS triangle

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 16:14:18 +03:00
parent efb6cf7ce6
commit c262ec0775
39 changed files with 634 additions and 311 deletions

View File

@@ -39,7 +39,8 @@ import {
getEngineIcon, getPictureSourceIcon, getAudioSourceIcon,
ICON_TEMPLATE, ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_LINK_SOURCE,
ICON_FPS, ICON_WEB, ICON_VALUE_SOURCE, ICON_AUDIO_LOOPBACK, ICON_AUDIO_INPUT,
ICON_AUDIO_TEMPLATE,
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO,
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP,
} from '../core/icons.js';
// ── Card section instances ──
@@ -166,7 +167,7 @@ async function loadCaptureTemplates() {
export async function showAddTemplateModal(cloneData = null) {
setCurrentEditingTemplateId(null);
document.getElementById('template-modal-title').textContent = t('templates.add');
document.getElementById('template-modal-title').innerHTML = `${ICON_CAPTURE_TEMPLATE} ${t('templates.add')}`;
document.getElementById('template-form').reset();
document.getElementById('template-id').value = '';
document.getElementById('engine-config-section').style.display = 'none';
@@ -197,7 +198,7 @@ export async function editTemplate(templateId) {
const template = await response.json();
setCurrentEditingTemplateId(templateId);
document.getElementById('template-modal-title').textContent = t('templates.edit');
document.getElementById('template-modal-title').innerHTML = `${ICON_CAPTURE_TEMPLATE} ${t('templates.edit')}`;
document.getElementById('template-id').value = templateId;
document.getElementById('template-name').value = template.name;
document.getElementById('template-description').value = template.description || '';
@@ -751,7 +752,7 @@ async function loadAudioTemplates() {
export async function showAddAudioTemplateModal(cloneData = null) {
setCurrentEditingAudioTemplateId(null);
document.getElementById('audio-template-modal-title').textContent = t('audio_template.add');
document.getElementById('audio-template-modal-title').innerHTML = `${ICON_AUDIO_TEMPLATE} ${t('audio_template.add')}`;
document.getElementById('audio-template-form').reset();
document.getElementById('audio-template-id').value = '';
document.getElementById('audio-engine-config-section').style.display = 'none';
@@ -781,7 +782,7 @@ export async function editAudioTemplate(templateId) {
const template = await response.json();
setCurrentEditingAudioTemplateId(templateId);
document.getElementById('audio-template-modal-title').textContent = t('audio_template.edit');
document.getElementById('audio-template-modal-title').innerHTML = `${ICON_AUDIO_TEMPLATE} ${t('audio_template.edit')}`;
document.getElementById('audio-template-id').value = templateId;
document.getElementById('audio-template-name').value = template.name;
document.getElementById('audio-template-description').value = template.description || '';
@@ -900,7 +901,7 @@ export async function showTestAudioTemplateModal(templateId) {
const data = await resp.json();
const devices = data.devices || [];
deviceSelect.innerHTML = devices.map(d => {
const label = d.is_loopback ? `🔊 ${d.name}` : `🎤 ${d.name}`;
const label = d.name;
const val = `${d.index}:${d.is_loopback ? '1' : '0'}`;
return `<option value="${val}">${escapeHtml(label)}</option>`;
}).join('');
@@ -1182,7 +1183,7 @@ function renderPictureSourcesList(streams) {
if (capTmpl) capTmplName = escapeHtml(capTmpl.name);
}
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.display')}">${ICON_MONITOR} ${stream.display_index ?? 0}</span>
<span class="stream-card-prop" title="${t('streams.target_fps')}">${ICON_FPS} ${stream.target_fps ?? 30}</span>
${capTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.capture_template')}" onclick="event.stopPropagation(); navigateToCard('streams','raw','raw-templates','data-id','${stream.capture_template_id}')">${ICON_TEMPLATE} ${capTmplName}</span>` : ''}
</div>`;
@@ -1236,7 +1237,7 @@ function renderPictureSourcesList(streams) {
${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
<div class="stream-card-props">
<span class="stream-card-prop" title="${t('templates.engine')}">${getEngineIcon(template.engine_type)} ${template.engine_type.toUpperCase()}</span>
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('templates.config.show')}">🔧 ${configEntries.length}</span>` : ''}
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('templates.config.show')}">${ICON_WRENCH} ${configEntries.length}</span>` : ''}
</div>
${configEntries.length > 0 ? `
<details class="template-config-details">
@@ -1301,7 +1302,7 @@ function renderPictureSourcesList(streams) {
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.count}</span></button>`
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllStreamSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllStreamSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" title="${t('tour.restart')}">?</button></span></div>`;
).join('')}<span class="cs-expand-collapse-group"><button class="btn-expand-collapse" onclick="expandAllStreamSections()" title="${t('section.expand_all')}">⊞</button><button class="btn-expand-collapse" onclick="collapseAllStreamSections()" title="${t('section.collapse_all')}">⊟</button><button class="tutorial-trigger-btn" onclick="startSourcesTutorial()" title="${t('tour.restart')}">${ICON_HELP}</button></span></div>`;
const renderAudioSourceCard = (src) => {
const isMono = src.source_type === 'mono';
@@ -1317,7 +1318,7 @@ function renderPictureSourcesList(streams) {
: `<span class="stream-card-prop" title="${escapeHtml(t('audio_source.parent'))}">${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}</span>`;
propsHtml = `
${parentBadge}
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.channel'))}">📻 ${chLabel}</span>
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.channel'))}">${ICON_RADIO} ${chLabel}</span>
`;
} else {
const devIdx = src.device_index ?? -1;
@@ -1356,7 +1357,7 @@ function renderPictureSourcesList(streams) {
${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
<div class="stream-card-props">
<span class="stream-card-prop" title="${t('audio_template.engine')}">${ICON_AUDIO_TEMPLATE} ${template.engine_type.toUpperCase()}</span>
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('audio_template.config.show')}">🔧 ${configEntries.length}</span>` : ''}
${configEntries.length > 0 ? `<span class="stream-card-prop" title="${t('audio_template.config.show')}">${ICON_WRENCH} ${configEntries.length}</span>` : ''}
</div>
${configEntries.length > 0 ? `
<details class="template-config-details">
@@ -1456,7 +1457,7 @@ function _autoGenerateStreamName() {
export async function showAddStreamModal(presetType, cloneData = null) {
const streamType = (cloneData && cloneData.stream_type) || presetType || 'raw';
const titleKeys = { raw: 'streams.add.raw', processed: 'streams.add.processed', static_image: 'streams.add.static_image' };
document.getElementById('stream-modal-title').textContent = t(titleKeys[streamType] || 'streams.add');
document.getElementById('stream-modal-title').innerHTML = `${getPictureSourceIcon(streamType)} ${t(titleKeys[streamType] || 'streams.add')}`;
document.getElementById('stream-form').reset();
document.getElementById('stream-id').value = '';
document.getElementById('stream-display-index').value = '';
@@ -1513,7 +1514,7 @@ export async function editStream(streamId) {
const stream = await response.json();
const editTitleKeys = { raw: 'streams.edit.raw', processed: 'streams.edit.processed', static_image: 'streams.edit.static_image' };
document.getElementById('stream-modal-title').textContent = t(editTitleKeys[stream.stream_type] || 'streams.edit');
document.getElementById('stream-modal-title').innerHTML = `${getPictureSourceIcon(stream.stream_type)} ${t(editTitleKeys[stream.stream_type] || 'streams.edit')}`;
document.getElementById('stream-id').value = streamId;
document.getElementById('stream-name').value = stream.name;
document.getElementById('stream-description').value = stream.description || '';
@@ -2108,7 +2109,7 @@ function _autoGeneratePPTemplateName() {
export async function showAddPPTemplateModal(cloneData = null) {
if (_availableFilters.length === 0) await loadAvailableFilters();
document.getElementById('pp-template-modal-title').textContent = t('postprocessing.add');
document.getElementById('pp-template-modal-title').innerHTML = `${ICON_PP_TEMPLATE} ${t('postprocessing.add')}`;
document.getElementById('pp-template-form').reset();
document.getElementById('pp-template-id').value = '';
document.getElementById('pp-template-error').style.display = 'none';
@@ -2146,7 +2147,7 @@ export async function editPPTemplate(templateId) {
if (!response.ok) throw new Error(`Failed to load template: ${response.status}`);
const tmpl = await response.json();
document.getElementById('pp-template-modal-title').textContent = t('postprocessing.edit');
document.getElementById('pp-template-modal-title').innerHTML = `${ICON_PP_TEMPLATE} ${t('postprocessing.edit')}`;
document.getElementById('pp-template-id').value = templateId;
document.getElementById('pp-template-name').value = tmpl.name;
document.getElementById('pp-template-description').value = tmpl.description || '';