Centralize icon resolution into core/icons.js, fix auto-start row alignment
- Create core/icons.js with type-resolution getters and icon constants - Replace inline emoji literals across 11 feature files with imports - Remove duplicate icon maps (getEngineIcon, _vsTypeIcons, typeIcons, etc.) - Fix dashboard auto-start row missing metrics placeholder div Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,11 @@
|
|||||||
import { fetchWithAuth } from './api.js';
|
import { fetchWithAuth } from './api.js';
|
||||||
import { t } from './i18n.js';
|
import { t } from './i18n.js';
|
||||||
import { navigateToCard } from './navigation.js';
|
import { navigateToCard } from './navigation.js';
|
||||||
|
import {
|
||||||
|
getTargetTypeIcon, getPictureSourceIcon, getColorStripIcon, getAudioSourceIcon,
|
||||||
|
ICON_DEVICE, ICON_TARGET, ICON_PROFILE, ICON_VALUE_SOURCE,
|
||||||
|
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_PATTERN_TEMPLATE,
|
||||||
|
} from './icons.js';
|
||||||
|
|
||||||
let _isOpen = false;
|
let _isOpen = false;
|
||||||
let _items = [];
|
let _items = [];
|
||||||
@@ -30,67 +35,66 @@ function _buildItems(results) {
|
|||||||
const items = [];
|
const items = [];
|
||||||
|
|
||||||
_mapEntities(devices, d => items.push({
|
_mapEntities(devices, d => items.push({
|
||||||
name: d.name, detail: d.device_type, group: 'devices', icon: '🖥️',
|
name: d.name, detail: d.device_type, group: 'devices', icon: ICON_DEVICE,
|
||||||
nav: ['targets', 'led', 'led-devices', 'data-device-id', d.id],
|
nav: ['targets', 'led', 'led-devices', 'data-device-id', d.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(targets, tgt => {
|
_mapEntities(targets, tgt => {
|
||||||
if (tgt.target_type === 'key_colors') {
|
if (tgt.target_type === 'key_colors') {
|
||||||
items.push({
|
items.push({
|
||||||
name: tgt.name, detail: 'key_colors', group: 'kc_targets', icon: '🎨',
|
name: tgt.name, detail: 'key_colors', group: 'kc_targets', icon: getTargetTypeIcon('key_colors'),
|
||||||
nav: ['targets', 'key_colors', 'kc-targets', 'data-kc-target-id', tgt.id],
|
nav: ['targets', 'key_colors', 'kc-targets', 'data-kc-target-id', tgt.id],
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
items.push({
|
items.push({
|
||||||
name: tgt.name, detail: tgt.target_type, group: 'targets', icon: '⚡',
|
name: tgt.name, detail: tgt.target_type, group: 'targets', icon: ICON_TARGET,
|
||||||
nav: ['targets', 'led', 'led-targets', 'data-target-id', tgt.id],
|
nav: ['targets', 'led', 'led-targets', 'data-target-id', tgt.id],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
_mapEntities(css, c => items.push({
|
_mapEntities(css, c => items.push({
|
||||||
name: c.name, detail: c.css_type || c.source_type, group: 'css', icon: '🎨',
|
name: c.name, detail: c.css_type || c.source_type, group: 'css', icon: getColorStripIcon(c.css_type || c.source_type),
|
||||||
nav: ['targets', 'led', 'led-css', 'data-css-id', c.id],
|
nav: ['targets', 'led', 'led-css', 'data-css-id', c.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(profiles, p => items.push({
|
_mapEntities(profiles, p => items.push({
|
||||||
name: p.name, detail: p.enabled ? 'enabled' : '', group: 'profiles', icon: '📋',
|
name: p.name, detail: p.enabled ? 'enabled' : '', group: 'profiles', icon: ICON_PROFILE,
|
||||||
nav: ['profiles', null, 'profiles', 'data-profile-id', p.id],
|
nav: ['profiles', null, 'profiles', 'data-profile-id', p.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(capTempl, ct => items.push({
|
_mapEntities(capTempl, ct => items.push({
|
||||||
name: ct.name, detail: ct.engine_type, group: 'capture_templates', icon: '📷',
|
name: ct.name, detail: ct.engine_type, group: 'capture_templates', icon: ICON_CAPTURE_TEMPLATE,
|
||||||
nav: ['streams', 'raw', 'raw-templates', 'data-template-id', ct.id],
|
nav: ['streams', 'raw', 'raw-templates', 'data-template-id', ct.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(ppTempl, pp => items.push({
|
_mapEntities(ppTempl, pp => items.push({
|
||||||
name: pp.name, detail: '', group: 'pp_templates', icon: '🔧',
|
name: pp.name, detail: '', group: 'pp_templates', icon: ICON_PP_TEMPLATE,
|
||||||
nav: ['streams', 'processed', 'proc-templates', 'data-pp-template-id', pp.id],
|
nav: ['streams', 'processed', 'proc-templates', 'data-pp-template-id', pp.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(patTempl, pt => items.push({
|
_mapEntities(patTempl, pt => items.push({
|
||||||
name: pt.name, detail: '', group: 'pattern_templates', icon: '🧩',
|
name: pt.name, detail: '', group: 'pattern_templates', icon: ICON_PATTERN_TEMPLATE,
|
||||||
nav: ['targets', 'key_colors', 'kc-patterns', 'data-pattern-template-id', pt.id],
|
nav: ['targets', 'key_colors', 'kc-patterns', 'data-pattern-template-id', pt.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(audioSrc, a => {
|
_mapEntities(audioSrc, a => {
|
||||||
const section = a.source_type === 'mono' ? 'audio-mono' : 'audio-multi';
|
const section = a.source_type === 'mono' ? 'audio-mono' : 'audio-multi';
|
||||||
items.push({
|
items.push({
|
||||||
name: a.name, detail: a.source_type, group: 'audio', icon: '🎵',
|
name: a.name, detail: a.source_type, group: 'audio', icon: getAudioSourceIcon(a.source_type),
|
||||||
nav: ['streams', 'audio', section, 'data-id', a.id],
|
nav: ['streams', 'audio', section, 'data-id', a.id],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
_mapEntities(valSrc, v => items.push({
|
_mapEntities(valSrc, v => items.push({
|
||||||
name: v.name, detail: v.source_type, group: 'value', icon: '🔢',
|
name: v.name, detail: v.source_type, group: 'value', icon: ICON_VALUE_SOURCE,
|
||||||
nav: ['streams', 'value', 'value-sources', 'data-id', v.id],
|
nav: ['streams', 'value', 'value-sources', 'data-id', v.id],
|
||||||
}));
|
}));
|
||||||
|
|
||||||
_mapEntities(streams, s => {
|
_mapEntities(streams, s => {
|
||||||
const mapping = _streamSubTab[s.stream_type] || _streamSubTab.raw;
|
const mapping = _streamSubTab[s.stream_type] || _streamSubTab.raw;
|
||||||
const icon = s.stream_type === 'processed' ? '🎞️' : s.stream_type === 'static_image' ? '🖼️' : '🖥️';
|
|
||||||
items.push({
|
items.push({
|
||||||
name: s.name, detail: s.stream_type, group: 'streams', icon,
|
name: s.name, detail: s.stream_type, group: 'streams', icon: getPictureSourceIcon(s.stream_type),
|
||||||
nav: ['streams', mapping.sub, mapping.section, 'data-stream-id', s.id],
|
nav: ['streams', mapping.sub, mapping.section, 'data-stream-id', s.id],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
96
server/src/wled_controller/static/js/core/icons.js
Normal file
96
server/src/wled_controller/static/js/core/icons.js
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
/**
|
||||||
|
* Centralized emoji icon maps and getter functions.
|
||||||
|
*
|
||||||
|
* Import icons from this module instead of using inline emoji literals.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// ── Type-resolution maps (private) ──────────────────────────
|
||||||
|
|
||||||
|
const _targetTypeIcons = { led: '\uD83D\uDCA1', wled: '\uD83D\uDCA1', key_colors: '\uD83C\uDFA8' };
|
||||||
|
const _pictureSourceTypeIcons = { raw: '\uD83D\uDDA5\uFE0F', processed: '\uD83C\uDFA8', static_image: '\uD83D\uDDBC\uFE0F' };
|
||||||
|
const _colorStripTypeIcons = {
|
||||||
|
static: '\uD83C\uDFA8', color_cycle: '\uD83D\uDD04', gradient: '\uD83C\uDF08',
|
||||||
|
effect: '\u26A1', composite: '\uD83D\uDD17',
|
||||||
|
mapped: '\uD83D\uDCCD', mapped_zones: '\uD83D\uDCCD',
|
||||||
|
audio: '\uD83C\uDFB5', audio_visualization: '\uD83C\uDFB5',
|
||||||
|
api_input: '\uD83D\uDCE1',
|
||||||
|
};
|
||||||
|
const _valueSourceTypeIcons = {
|
||||||
|
static: '\uD83D\uDCCA', animated: '\uD83D\uDD04', audio: '\uD83C\uDFB5',
|
||||||
|
adaptive_time: '\uD83D\uDD50', adaptive_scene: '\uD83C\uDF24\uFE0F',
|
||||||
|
};
|
||||||
|
const _audioSourceTypeIcons = { mono: '\uD83C\uDFA4', multichannel: '\uD83D\uDD0A' };
|
||||||
|
const _engineTypeIcons = { scrcpy: '\uD83D\uDCF1' };
|
||||||
|
|
||||||
|
// ── Type-resolution getters ─────────────────────────────────
|
||||||
|
|
||||||
|
/** Target type → emoji (fallback: ⚡) */
|
||||||
|
export function getTargetTypeIcon(targetType) {
|
||||||
|
return _targetTypeIcons[targetType] || '\u26A1';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Picture source / stream type → emoji (fallback: 📺) */
|
||||||
|
export function getPictureSourceIcon(streamType) {
|
||||||
|
return _pictureSourceTypeIcons[streamType] || '\uD83D\uDCFA';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Color strip source type → emoji (fallback: 🎞️) */
|
||||||
|
export function getColorStripIcon(sourceType) {
|
||||||
|
return _colorStripTypeIcons[sourceType] || '\uD83C\uDF9E\uFE0F';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Value source type → emoji (fallback: 🎚️) */
|
||||||
|
export function getValueSourceIcon(sourceType) {
|
||||||
|
return _valueSourceTypeIcons[sourceType] || '\uD83C\uDF9A\uFE0F';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Audio source type → emoji (fallback: 🎵) */
|
||||||
|
export function getAudioSourceIcon(sourceType) {
|
||||||
|
return _audioSourceTypeIcons[sourceType] || '\uD83C\uDFB5';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Capture engine type → emoji (fallback: 🚀) */
|
||||||
|
export function getEngineIcon(engineType) {
|
||||||
|
return _engineTypeIcons[engineType] || '\uD83D\uDE80';
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Entity-kind constants ───────────────────────────────────
|
||||||
|
|
||||||
|
export const ICON_PROFILE = '\uD83D\uDCCB'; // 📋
|
||||||
|
export const ICON_DEVICE = '\uD83D\uDDA5\uFE0F'; // 🖥️
|
||||||
|
export const ICON_TARGET = '\u26A1'; // ⚡
|
||||||
|
export const ICON_VALUE_SOURCE = '\uD83D\uDD22'; // 🔢
|
||||||
|
|
||||||
|
// ── Template-kind constants ─────────────────────────────────
|
||||||
|
|
||||||
|
export const ICON_TEMPLATE = '\uD83D\uDCCB'; // 📋 (generic card header)
|
||||||
|
export const ICON_CAPTURE_TEMPLATE = '\uD83D\uDCF7'; // 📷
|
||||||
|
export const ICON_PP_TEMPLATE = '\uD83D\uDD27'; // 🔧
|
||||||
|
export const ICON_PATTERN_TEMPLATE = '\uD83D\uDCC4'; // 📄
|
||||||
|
|
||||||
|
// ── Action constants ────────────────────────────────────────
|
||||||
|
|
||||||
|
export const ICON_CLONE = '\uD83D\uDCCB'; // 📋
|
||||||
|
export const ICON_EDIT = '\u270F\uFE0F'; // ✏️
|
||||||
|
export const ICON_TEST = '\uD83E\uDDEA'; // 🧪
|
||||||
|
export const ICON_START = '\u25B6\uFE0F'; // ▶️
|
||||||
|
export const ICON_STOP = '\u23F9\uFE0F'; // ⏹️
|
||||||
|
export const ICON_STOP_PLAIN = '\u23F9'; // ⏹
|
||||||
|
export const ICON_PAUSE = '\u23F8'; // ⏸
|
||||||
|
export const ICON_SETTINGS = '\u2699\uFE0F'; // ⚙️
|
||||||
|
export const ICON_CALIBRATION = '\uD83D\uDCD0'; // 📐
|
||||||
|
|
||||||
|
// ── Misc badge constants ────────────────────────────────────
|
||||||
|
|
||||||
|
export const ICON_AUDIO_LOOPBACK = '\uD83D\uDD0A'; // 🔊
|
||||||
|
export const ICON_AUDIO_INPUT = '\uD83C\uDFA4'; // 🎤
|
||||||
|
export const ICON_CLOCK = '\uD83D\uDD50'; // 🕐
|
||||||
|
export const ICON_WARNING = '\u26A0\uFE0F'; // ⚠️
|
||||||
|
export const ICON_OK = '\u2705'; // ✅
|
||||||
|
export const ICON_LINK_SOURCE = '\uD83D\uDCFA'; // 📺
|
||||||
|
export const ICON_LED = '\uD83D\uDCA1'; // 💡
|
||||||
|
export const ICON_FPS = '\u26A1'; // ⚡
|
||||||
|
export const ICON_WEB = '\uD83C\uDF10'; // 🌐
|
||||||
|
export const ICON_OVERLAY = '\uD83D\uDC41\uFE0F'; // 👁️
|
||||||
|
export const ICON_LED_PREVIEW = '\uD83D\uDCCA'; // 📊
|
||||||
|
export const ICON_AUTOSTART = '\u2B50'; // ⭐
|
||||||
@@ -6,6 +6,10 @@ import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
|||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
|
import {
|
||||||
|
getColorStripIcon, getPictureSourceIcon,
|
||||||
|
ICON_CLONE, ICON_EDIT, ICON_CALIBRATION,
|
||||||
|
} from '../core/icons.js';
|
||||||
|
|
||||||
class CSSEditorModal extends Modal {
|
class CSSEditorModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -686,9 +690,9 @@ export function createColorStripCard(source, pictureSourceMap) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const icon = isStatic ? '🎨' : isColorCycle ? '🔄' : isGradient ? '🌈' : isEffect ? '⚡' : isComposite ? '🔗' : isMapped ? '📍' : isAudio ? '🎵' : isApiInput ? '📡' : '🎞️';
|
const icon = getColorStripIcon(source.source_type);
|
||||||
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput)
|
const calibrationBtn = (!isStatic && !isGradient && !isColorCycle && !isEffect && !isComposite && !isMapped && !isAudio && !isApiInput)
|
||||||
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">📐</button>`
|
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
@@ -703,8 +707,8 @@ export function createColorStripCard(source, pictureSourceMap) {
|
|||||||
${propsHtml}
|
${propsHtml}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="cloneColorStrip('${source.id}')" title="${t('common.clone')}">📋</button>
|
<button class="btn btn-icon btn-secondary" onclick="cloneColorStrip('${source.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showCSSEditor('${source.id}')" title="${t('common.edit')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="showCSSEditor('${source.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
${calibrationBtn}
|
${calibrationBtn}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -734,8 +738,7 @@ export async function showCSSEditor(cssId = null, cloneData = null) {
|
|||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = s.id;
|
opt.value = s.id;
|
||||||
opt.dataset.name = s.name;
|
opt.dataset.name = s.name;
|
||||||
const typeIcon = s.stream_type === 'raw' ? '🖥️' : s.stream_type === 'static_image' ? '🖼️' : '🎨';
|
opt.textContent = `${getPictureSourceIcon(s.stream_type)} ${s.name}`;
|
||||||
opt.textContent = `${typeIcon} ${s.name}`;
|
|
||||||
sourceSelect.appendChild(opt);
|
sourceSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,11 @@ import { t } from '../core/i18n.js';
|
|||||||
import { showToast, formatUptime } from '../core/ui.js';
|
import { showToast, formatUptime } from '../core/ui.js';
|
||||||
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
|
import { renderPerfSection, initPerfCharts, startPerfPolling, stopPerfPolling } from './perf-charts.js';
|
||||||
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
import { startAutoRefresh, updateTabBadge } from './tabs.js';
|
||||||
|
import {
|
||||||
|
getTargetTypeIcon,
|
||||||
|
ICON_TARGET, ICON_PROFILE, ICON_CLOCK, ICON_WARNING, ICON_OK,
|
||||||
|
ICON_STOP, ICON_STOP_PLAIN, ICON_AUTOSTART,
|
||||||
|
} from '../core/icons.js';
|
||||||
|
|
||||||
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
const DASHBOARD_COLLAPSED_KEY = 'dashboard_collapsed';
|
||||||
const MAX_FPS_SAMPLES = 120;
|
const MAX_FPS_SAMPLES = 120;
|
||||||
@@ -53,7 +58,7 @@ function _startUptimeTimer() {
|
|||||||
if (!el) continue;
|
if (!el) continue;
|
||||||
const seconds = _getInterpolatedUptime(id);
|
const seconds = _getInterpolatedUptime(id);
|
||||||
if (seconds != null) {
|
if (seconds != null) {
|
||||||
el.textContent = `🕐 ${formatUptime(seconds)}`;
|
el.textContent = `${ICON_CLOCK} ${formatUptime(seconds)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
@@ -184,7 +189,7 @@ function _updateRunningMetrics(enrichedRunning) {
|
|||||||
if (fpsEl) fpsEl.innerHTML = `${fpsActual}<span class="dashboard-fps-target">/${fpsTarget}</span>`;
|
if (fpsEl) fpsEl.innerHTML = `${fpsActual}<span class="dashboard-fps-target">/${fpsTarget}</span>`;
|
||||||
|
|
||||||
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
const errorsEl = cached?.errors || document.querySelector(`[data-errors-text="${target.id}"]`);
|
||||||
if (errorsEl) errorsEl.textContent = `${errors > 0 ? '⚠️' : '✅'} ${errors}`;
|
if (errorsEl) errorsEl.textContent = `${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}`;
|
||||||
|
|
||||||
// Update health dot
|
// Update health dot
|
||||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||||
@@ -228,7 +233,7 @@ function _updateProfilesInPlace(profiles) {
|
|||||||
if (btn) {
|
if (btn) {
|
||||||
btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`;
|
btn.className = `btn btn-icon ${p.enabled ? 'btn-warning' : 'btn-success'}`;
|
||||||
btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`);
|
btn.setAttribute('onclick', `dashboardToggleProfile('${p.id}', ${!p.enabled})`);
|
||||||
btn.textContent = p.enabled ? '⏹' : '▶';
|
btn.textContent = p.enabled ? ICON_STOP_PLAIN : '▶';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,21 +374,22 @@ export async function loadDashboard(forceFullRender = false) {
|
|||||||
const isRunning = !!(target.state && target.state.processing);
|
const isRunning = !!(target.state && target.state.processing);
|
||||||
const device = devicesMap[target.device_id];
|
const device = devicesMap[target.device_id];
|
||||||
const deviceName = device ? device.name : '';
|
const deviceName = device ? device.name : '';
|
||||||
const typeIcon = target.target_type === 'key_colors' ? '🎨' : '💡';
|
const typeIcon = getTargetTypeIcon(target.target_type);
|
||||||
const statusBadge = isRunning
|
const statusBadge = isRunning
|
||||||
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
|
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
|
||||||
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
|
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
|
||||||
return `<div class="dashboard-target dashboard-autostart" data-target-id="${target.id}">
|
return `<div class="dashboard-target dashboard-autostart" data-target-id="${target.id}">
|
||||||
<div class="dashboard-target-info">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">⭐</span>
|
<span class="dashboard-target-icon">${ICON_AUTOSTART}</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="dashboard-target-name">${escapeHtml(target.name)} ${statusBadge}</div>
|
<div class="dashboard-target-name">${escapeHtml(target.name)} ${statusBadge}</div>
|
||||||
${deviceName ? `<div class="dashboard-target-subtitle">${typeIcon} ${escapeHtml(deviceName)}</div>` : `<div class="dashboard-target-subtitle">${typeIcon}</div>`}
|
${deviceName ? `<div class="dashboard-target-subtitle">${typeIcon} ${escapeHtml(deviceName)}</div>` : `<div class="dashboard-target-subtitle">${typeIcon}</div>`}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="dashboard-target-metrics"></div>
|
||||||
<div class="dashboard-target-actions">
|
<div class="dashboard-target-actions">
|
||||||
<button class="btn btn-icon ${isRunning ? 'btn-warning' : 'btn-success'}" onclick="${isRunning ? `dashboardStopTarget('${target.id}')` : `dashboardStartTarget('${target.id}')`}" title="${isRunning ? t('device.stop') : t('device.start')}">
|
<button class="btn btn-icon ${isRunning ? 'btn-warning' : 'btn-success'}" onclick="${isRunning ? `dashboardStopTarget('${target.id}')` : `dashboardStartTarget('${target.id}')`}" title="${isRunning ? t('device.stop') : t('device.start')}">
|
||||||
${isRunning ? '⏹' : '▶'}
|
${isRunning ? ICON_STOP_PLAIN : '▶'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -412,7 +418,7 @@ export async function loadDashboard(forceFullRender = false) {
|
|||||||
|
|
||||||
if (running.length > 0) {
|
if (running.length > 0) {
|
||||||
runningIds = running.map(t => t.id);
|
runningIds = running.map(t => t.id);
|
||||||
const stopAllBtn = `<button class="btn btn-sm btn-danger dashboard-stop-all" onclick="event.stopPropagation(); dashboardStopAll()" title="${t('dashboard.stop_all')}">⏹️ ${t('dashboard.stop_all')}</button>`;
|
const stopAllBtn = `<button class="btn btn-sm btn-danger dashboard-stop-all" onclick="event.stopPropagation(); dashboardStopAll()" title="${t('dashboard.stop_all')}">${ICON_STOP} ${t('dashboard.stop_all')}</button>`;
|
||||||
const runningItems = running.map(target => renderDashboardTarget(target, true, devicesMap, cssSourceMap)).join('');
|
const runningItems = running.map(target => renderDashboardTarget(target, true, devicesMap, cssSourceMap)).join('');
|
||||||
|
|
||||||
targetsInner += `<div class="dashboard-subsection">
|
targetsInner += `<div class="dashboard-subsection">
|
||||||
@@ -472,7 +478,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
const state = target.state || {};
|
const state = target.state || {};
|
||||||
const metrics = target.metrics || {};
|
const metrics = target.metrics || {};
|
||||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||||
const icon = '⚡';
|
const icon = ICON_TARGET;
|
||||||
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
|
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
|
||||||
|
|
||||||
let subtitleParts = [typeLabel];
|
let subtitleParts = [typeLabel];
|
||||||
@@ -530,14 +536,14 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-metric" title="${t('dashboard.uptime')}">
|
<div class="dashboard-metric" title="${t('dashboard.uptime')}">
|
||||||
<div class="dashboard-metric-value" data-uptime-text="${target.id}">🕐 ${uptime}</div>
|
<div class="dashboard-metric-value" data-uptime-text="${target.id}">${ICON_CLOCK} ${uptime}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-metric" title="${t('dashboard.errors')}">
|
<div class="dashboard-metric" title="${t('dashboard.errors')}">
|
||||||
<div class="dashboard-metric-value" data-errors-text="${target.id}">${errors > 0 ? '⚠️' : '✅'} ${errors}</div>
|
<div class="dashboard-metric-value" data-errors-text="${target.id}">${errors > 0 ? ICON_WARNING : ICON_OK} ${errors}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-actions">
|
<div class="dashboard-target-actions">
|
||||||
<button class="btn btn-icon btn-warning" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">⏹</button>
|
<button class="btn btn-icon btn-warning" onclick="dashboardStopTarget('${target.id}')" title="${t('device.button.stop')}">${ICON_STOP_PLAIN}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
} else {
|
} else {
|
||||||
@@ -587,7 +593,7 @@ function renderDashboardProfile(profile) {
|
|||||||
|
|
||||||
return `<div class="dashboard-target dashboard-profile" data-profile-id="${profile.id}">
|
return `<div class="dashboard-target dashboard-profile" data-profile-id="${profile.id}">
|
||||||
<div class="dashboard-target-info">
|
<div class="dashboard-target-info">
|
||||||
<span class="dashboard-target-icon">📋</span>
|
<span class="dashboard-target-icon">${ICON_PROFILE}</span>
|
||||||
<div>
|
<div>
|
||||||
<div class="dashboard-target-name">${escapeHtml(profile.name)}</div>
|
<div class="dashboard-target-name">${escapeHtml(profile.name)}</div>
|
||||||
${condSummary ? `<div class="dashboard-target-subtitle">${escapeHtml(condSummary)}</div>` : ''}
|
${condSummary ? `<div class="dashboard-target-subtitle">${escapeHtml(condSummary)}</div>` : ''}
|
||||||
@@ -602,7 +608,7 @@ function renderDashboardProfile(profile) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="dashboard-target-actions">
|
<div class="dashboard-target-actions">
|
||||||
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="dashboardToggleProfile('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="dashboardToggleProfile('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
||||||
${profile.enabled ? '⏹' : '▶'}
|
${profile.enabled ? ICON_STOP_PLAIN : '▶'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { API_BASE, getHeaders, fetchWithAuth, escapeHtml, isSerialDevice, isMock
|
|||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
|
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB } from '../core/icons.js';
|
||||||
|
|
||||||
class DeviceSettingsModal extends Modal {
|
class DeviceSettingsModal extends Modal {
|
||||||
constructor() { super('device-settings-modal'); }
|
constructor() { super('device-settings-modal'); }
|
||||||
@@ -75,20 +76,20 @@ export function createDeviceCard(device) {
|
|||||||
return `
|
return `
|
||||||
<div class="card" data-device-id="${device.id}">
|
<div class="card" data-device-id="${device.id}">
|
||||||
<div class="card-top-actions">
|
<div class="card-top-actions">
|
||||||
${(device.capabilities || []).includes('power_control') ? `<button class="card-top-btn card-power-btn" onclick="turnOffDevice('${device.id}')" title="${t('device.button.power_off')}">⏹</button>` : ''}
|
${(device.capabilities || []).includes('power_control') ? `<button class="card-top-btn card-power-btn" onclick="turnOffDevice('${device.id}')" title="${t('device.button.power_off')}">${ICON_STOP_PLAIN}</button>` : ''}
|
||||||
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">✕</button>
|
<button class="card-remove-btn" onclick="removeDevice('${device.id}')" title="${t('device.button.remove')}">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
<span class="health-dot ${healthClass}" title="${healthTitle}"></span>
|
||||||
${device.name || device.id}
|
${device.name || device.id}
|
||||||
${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">🌐</span></a>` : (device.url && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
|
${device.url && device.url.startsWith('http') ? `<a class="device-url-badge" href="${device.url}" target="_blank" rel="noopener" title="${t('device.button.webui')}"><span class="device-url-text">${escapeHtml(device.url.replace(/^https?:\/\//, ''))}</span><span class="device-url-icon">${ICON_WEB}</span></a>` : (device.url && !device.url.startsWith('http') ? `<span class="device-url-badge"><span class="device-url-text">${escapeHtml(device.url)}</span></span>` : '')}
|
||||||
${healthLabel}
|
${healthLabel}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-subtitle">
|
<div class="card-subtitle">
|
||||||
<span class="card-meta device-type-badge">${(device.device_type || 'wled').toUpperCase()}</span>
|
<span class="card-meta device-type-badge">${(device.device_type || 'wled').toUpperCase()}</span>
|
||||||
${ledCount ? `<span class="card-meta" title="${t('device.led_count')}">💡 ${ledCount}</span>` : ''}
|
${ledCount ? `<span class="card-meta" title="${t('device.led_count')}">${ICON_LED} ${ledCount}</span>` : ''}
|
||||||
${state.device_led_type ? `<span class="card-meta">🔌 ${state.device_led_type.replace(/ RGBW$/, '')}</span>` : ''}
|
${state.device_led_type ? `<span class="card-meta">🔌 ${state.device_led_type.replace(/ RGBW$/, '')}</span>` : ''}
|
||||||
<span class="card-meta" title="${state.device_rgbw ? 'RGBW' : 'RGB'}"><span class="channel-indicator"><span class="ch" style="background:#e53935"></span><span class="ch" style="background:#43a047"></span><span class="ch" style="background:#1e88e5"></span>${state.device_rgbw ? '<span class="ch" style="background:#eee"></span>' : ''}</span></span>
|
<span class="card-meta" title="${state.device_rgbw ? 'RGBW' : 'RGB'}"><span class="channel-indicator"><span class="ch" style="background:#e53935"></span><span class="ch" style="background:#43a047"></span><span class="ch" style="background:#1e88e5"></span>${state.device_rgbw ? '<span class="ch" style="background:#eee"></span>' : ''}</span></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +104,7 @@ export function createDeviceCard(device) {
|
|||||||
</div>` : ''}
|
</div>` : ''}
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
<button class="btn btn-icon btn-secondary" onclick="showSettings('${device.id}')" title="${t('device.button.settings')}">
|
||||||
⚙️
|
${ICON_SETTINGS}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -14,7 +14,10 @@ import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'
|
|||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { lockBody, showToast, showConfirm, formatUptime } from '../core/ui.js';
|
import { lockBody, showToast, showConfirm, formatUptime } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { getValueSourceIcon } from './value-sources.js';
|
import {
|
||||||
|
getValueSourceIcon, getPictureSourceIcon,
|
||||||
|
ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP,
|
||||||
|
} from '../core/icons.js';
|
||||||
|
|
||||||
class KCEditorModal extends Modal {
|
class KCEditorModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -152,21 +155,21 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
|||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('targets.button.stop')}">
|
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('targets.button.stop')}">
|
||||||
⏹️
|
${ICON_STOP}
|
||||||
</button>
|
</button>
|
||||||
` : `
|
` : `
|
||||||
<button class="btn btn-icon btn-primary" onclick="startTargetProcessing('${target.id}')" title="${t('targets.button.start')}">
|
<button class="btn btn-icon btn-primary" onclick="startTargetProcessing('${target.id}')" title="${t('targets.button.start')}">
|
||||||
▶️
|
${ICON_START}
|
||||||
</button>
|
</button>
|
||||||
`}
|
`}
|
||||||
<button class="btn btn-icon btn-secondary" onclick="testKCTarget('${target.id}')" title="${t('kc.test')}">
|
<button class="btn btn-icon btn-secondary" onclick="testKCTarget('${target.id}')" title="${t('kc.test')}">
|
||||||
🧪
|
${ICON_TEST}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="cloneKCTarget('${target.id}')" title="${t('common.clone')}">
|
<button class="btn btn-icon btn-secondary" onclick="cloneKCTarget('${target.id}')" title="${t('common.clone')}">
|
||||||
📋
|
${ICON_CLONE}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showKCEditor('${target.id}')" title="${t('common.edit')}">
|
<button class="btn btn-icon btn-secondary" onclick="showKCEditor('${target.id}')" title="${t('common.edit')}">
|
||||||
✏️
|
${ICON_EDIT}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -396,8 +399,7 @@ export async function showKCEditor(targetId = null, cloneData = null) {
|
|||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = s.id;
|
opt.value = s.id;
|
||||||
opt.dataset.name = s.name;
|
opt.dataset.name = s.name;
|
||||||
const typeIcon = s.stream_type === 'raw' ? '\uD83D\uDDA5\uFE0F' : s.stream_type === 'static_image' ? '\uD83D\uDDBC\uFE0F' : '\uD83C\uDFA8';
|
opt.textContent = `${getPictureSourceIcon(s.stream_type)} ${s.name}`;
|
||||||
opt.textContent = `${typeIcon} ${s.name}`;
|
|
||||||
sourceSelect.appendChild(opt);
|
sourceSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import { API_BASE, getHeaders, fetchWithAuth, escapeHtml } from '../core/api.js'
|
|||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
|
import { getPictureSourceIcon, ICON_PATTERN_TEMPLATE, ICON_CLONE, ICON_EDIT } from '../core/icons.js';
|
||||||
|
|
||||||
class PatternTemplateModal extends Modal {
|
class PatternTemplateModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -55,15 +56,15 @@ export function createPatternTemplateCard(pt) {
|
|||||||
<div class="template-card" data-pattern-template-id="${pt.id}">
|
<div class="template-card" data-pattern-template-id="${pt.id}">
|
||||||
<button class="card-remove-btn" onclick="deletePatternTemplate('${pt.id}')" title="${t('common.delete')}">✕</button>
|
<button class="card-remove-btn" onclick="deletePatternTemplate('${pt.id}')" title="${t('common.delete')}">✕</button>
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<span class="template-name">📄 ${escapeHtml(pt.name)}</span>
|
<span class="template-name">${ICON_PATTERN_TEMPLATE} ${escapeHtml(pt.name)}</span>
|
||||||
</div>
|
</div>
|
||||||
${desc}
|
${desc}
|
||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
<span class="stream-card-prop">▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''}</span>
|
<span class="stream-card-prop">▭ ${rectCount} rect${rectCount !== 1 ? 's' : ''}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="template-card-actions">
|
<div class="template-card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="clonePatternTemplate('${pt.id}')" title="${t('common.clone')}">📋</button>
|
<button class="btn btn-icon btn-secondary" onclick="clonePatternTemplate('${pt.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showPatternTemplateEditor('${pt.id}')" title="${t('common.edit')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="showPatternTemplateEditor('${pt.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -80,8 +81,7 @@ export async function showPatternTemplateEditor(templateId = null, cloneData = n
|
|||||||
sources.forEach(s => {
|
sources.forEach(s => {
|
||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = s.id;
|
opt.value = s.id;
|
||||||
const typeIcon = s.stream_type === 'raw' ? '\uD83D\uDDA5\uFE0F' : s.stream_type === 'static_image' ? '\uD83D\uDDBC\uFE0F' : '\uD83C\uDFA8';
|
opt.textContent = `${getPictureSourceIcon(s.stream_type)} ${s.name}`;
|
||||||
opt.textContent = `${typeIcon} ${s.name}`;
|
|
||||||
bgSelect.appendChild(opt);
|
bgSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { showToast, showConfirm } from '../core/ui.js';
|
|||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
import { updateTabBadge } from './tabs.js';
|
import { updateTabBadge } from './tabs.js';
|
||||||
|
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_START, ICON_PAUSE } from '../core/icons.js';
|
||||||
|
|
||||||
class ProfileEditorModal extends Modal {
|
class ProfileEditorModal extends Modal {
|
||||||
constructor() { super('profile-editor-modal'); }
|
constructor() { super('profile-editor-modal'); }
|
||||||
@@ -132,17 +133,17 @@ function createProfileCard(profile, runningTargetIds = new Set()) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">${condPills}</div>
|
<div class="stream-card-props">${condPills}</div>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="openProfileEditor('${profile.id}')" title="${t('profiles.edit')}">⚙️</button>
|
<button class="btn btn-icon btn-secondary" onclick="openProfileEditor('${profile.id}')" title="${t('profiles.edit')}">${ICON_SETTINGS}</button>
|
||||||
${profile.target_ids.length > 0 ? (() => {
|
${profile.target_ids.length > 0 ? (() => {
|
||||||
const anyRunning = profile.target_ids.some(id => runningTargetIds.has(id));
|
const anyRunning = profile.target_ids.some(id => runningTargetIds.has(id));
|
||||||
return `<button class="btn btn-icon ${anyRunning ? 'btn-warning' : 'btn-success'}"
|
return `<button class="btn btn-icon ${anyRunning ? 'btn-warning' : 'btn-success'}"
|
||||||
onclick="toggleProfileTargets('${profile.id}')"
|
onclick="toggleProfileTargets('${profile.id}')"
|
||||||
title="${anyRunning ? t('profiles.toggle_all.stop') : t('profiles.toggle_all.start')}">
|
title="${anyRunning ? t('profiles.toggle_all.stop') : t('profiles.toggle_all.start')}">
|
||||||
${anyRunning ? '⏹' : '▶️'}
|
${anyRunning ? ICON_STOP_PLAIN : ICON_START}
|
||||||
</button>`;
|
</button>`;
|
||||||
})() : ''}
|
})() : ''}
|
||||||
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleProfileEnabled('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
<button class="btn btn-icon ${profile.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleProfileEnabled('${profile.id}', ${!profile.enabled})" title="${profile.enabled ? t('profiles.action.disable') : t('profiles.status.active')}">
|
||||||
${profile.enabled ? '⏸' : '▶'}
|
${profile.enabled ? ICON_PAUSE : '▶'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ import { openDisplayPicker, formatDisplayLabel } from './displays.js';
|
|||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
import { updateSubTabHash } from './tabs.js';
|
import { updateSubTabHash } from './tabs.js';
|
||||||
import { createValueSourceCard } from './value-sources.js';
|
import { createValueSourceCard } from './value-sources.js';
|
||||||
|
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,
|
||||||
|
} from '../core/icons.js';
|
||||||
|
|
||||||
// ── Card section instances ──
|
// ── Card section instances ──
|
||||||
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')" });
|
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')" });
|
||||||
@@ -128,11 +133,6 @@ async function loadCaptureTemplates() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getEngineIcon(engineType) {
|
|
||||||
if (engineType === 'scrcpy') return '📱';
|
|
||||||
return '🚀';
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function showAddTemplateModal(cloneData = null) {
|
export async function showAddTemplateModal(cloneData = null) {
|
||||||
setCurrentEditingTemplateId(null);
|
setCurrentEditingTemplateId(null);
|
||||||
document.getElementById('template-modal-title').textContent = t('templates.add');
|
document.getElementById('template-modal-title').textContent = t('templates.add');
|
||||||
@@ -587,8 +587,7 @@ function renderPictureSourcesList(streams) {
|
|||||||
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
const activeTab = localStorage.getItem('activeStreamTab') || 'raw';
|
||||||
|
|
||||||
const renderStreamCard = (stream) => {
|
const renderStreamCard = (stream) => {
|
||||||
const typeIcons = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
const typeIcon = getPictureSourceIcon(stream.stream_type);
|
||||||
const typeIcon = typeIcons[stream.stream_type] || '📺';
|
|
||||||
|
|
||||||
let detailsHtml = '';
|
let detailsHtml = '';
|
||||||
if (stream.stream_type === 'raw') {
|
if (stream.stream_type === 'raw') {
|
||||||
@@ -599,8 +598,8 @@ function renderPictureSourcesList(streams) {
|
|||||||
}
|
}
|
||||||
detailsHtml = `<div class="stream-card-props">
|
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')}">🖥️ ${stream.display_index ?? 0}</span>
|
||||||
<span class="stream-card-prop" title="${t('streams.target_fps')}">⚡ ${stream.target_fps ?? 30}</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}')">📋 ${capTmplName}</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>`;
|
</div>`;
|
||||||
} else if (stream.stream_type === 'processed') {
|
} else if (stream.stream_type === 'processed') {
|
||||||
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
const sourceStream = _cachedStreams.find(s => s.id === stream.source_stream_id);
|
||||||
@@ -613,13 +612,13 @@ function renderPictureSourcesList(streams) {
|
|||||||
if (ppTmpl) ppTmplName = escapeHtml(ppTmpl.name);
|
if (ppTmpl) ppTmplName = escapeHtml(ppTmpl.name);
|
||||||
}
|
}
|
||||||
detailsHtml = `<div class="stream-card-props">
|
detailsHtml = `<div class="stream-card-props">
|
||||||
<span class="stream-card-prop stream-card-link" title="${t('streams.source')}" onclick="event.stopPropagation(); navigateToCard('streams','${sourceSubTab}','${sourceSection}','data-stream-id','${stream.source_stream_id}')">📺 ${sourceName}</span>
|
<span class="stream-card-prop stream-card-link" title="${t('streams.source')}" onclick="event.stopPropagation(); navigateToCard('streams','${sourceSubTab}','${sourceSection}','data-stream-id','${stream.source_stream_id}')">${ICON_LINK_SOURCE} ${sourceName}</span>
|
||||||
${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','processed','proc-templates','data-id','${stream.postprocessing_template_id}')">📋 ${ppTmplName}</span>` : ''}
|
${ppTmplName ? `<span class="stream-card-prop stream-card-link" title="${t('streams.pp_template')}" onclick="event.stopPropagation(); navigateToCard('streams','processed','proc-templates','data-id','${stream.postprocessing_template_id}')">${ICON_TEMPLATE} ${ppTmplName}</span>` : ''}
|
||||||
</div>`;
|
</div>`;
|
||||||
} else if (stream.stream_type === 'static_image') {
|
} else if (stream.stream_type === 'static_image') {
|
||||||
const src = stream.image_source || '';
|
const src = stream.image_source || '';
|
||||||
detailsHtml = `<div class="stream-card-props">
|
detailsHtml = `<div class="stream-card-props">
|
||||||
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(src)}">🌐 ${escapeHtml(src)}</span>
|
<span class="stream-card-prop stream-card-prop-full" title="${escapeHtml(src)}">${ICON_WEB} ${escapeHtml(src)}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,9 +631,9 @@ function renderPictureSourcesList(streams) {
|
|||||||
${detailsHtml}
|
${detailsHtml}
|
||||||
${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}
|
${stream.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(stream.description)}</div>` : ''}
|
||||||
<div class="template-card-actions">
|
<div class="template-card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showTestStreamModal('${stream.id}')" title="${t('streams.test.title')}">🧪</button>
|
<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')}">📋</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')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="editStream('${stream.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -647,11 +646,11 @@ function renderPictureSourcesList(streams) {
|
|||||||
<div class="template-card" data-template-id="${template.id}">
|
<div class="template-card" data-template-id="${template.id}">
|
||||||
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">📋 ${escapeHtml(template.name)}</div>
|
<div class="template-name">${ICON_TEMPLATE} ${escapeHtml(template.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
|
${template.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(template.description)}</div>` : ''}
|
||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
<span class="stream-card-prop" title="${t('templates.engine')}">🚀 ${template.engine_type.toUpperCase()}</span>
|
<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')}">🔧 ${configEntries.length}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
${configEntries.length > 0 ? `
|
${configEntries.length > 0 ? `
|
||||||
@@ -668,9 +667,9 @@ function renderPictureSourcesList(streams) {
|
|||||||
</details>
|
</details>
|
||||||
` : ''}
|
` : ''}
|
||||||
<div class="template-card-actions">
|
<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="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')}">📋</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')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -686,14 +685,14 @@ function renderPictureSourcesList(streams) {
|
|||||||
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
||||||
<button class="card-remove-btn" onclick="deletePPTemplate('${tmpl.id}')" title="${t('common.delete')}">✕</button>
|
<button class="card-remove-btn" onclick="deletePPTemplate('${tmpl.id}')" title="${t('common.delete')}">✕</button>
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">📋 ${escapeHtml(tmpl.name)}</div>
|
<div class="template-name">${ICON_TEMPLATE} ${escapeHtml(tmpl.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
${tmpl.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(tmpl.description)}</div>` : ''}
|
||||||
${filterChainHtml}
|
${filterChainHtml}
|
||||||
<div class="template-card-actions">
|
<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="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')}">📋</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')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -708,11 +707,11 @@ function renderPictureSourcesList(streams) {
|
|||||||
|
|
||||||
|
|
||||||
const tabs = [
|
const tabs = [
|
||||||
{ key: 'raw', icon: '🖥️', titleKey: 'streams.group.raw', count: rawStreams.length },
|
{ key: 'raw', icon: getPictureSourceIcon('raw'), titleKey: 'streams.group.raw', count: rawStreams.length },
|
||||||
{ key: 'static_image', icon: '🖼️', titleKey: 'streams.group.static_image', count: staticImageStreams.length },
|
{ key: 'static_image', icon: getPictureSourceIcon('static_image'), titleKey: 'streams.group.static_image', count: staticImageStreams.length },
|
||||||
{ key: 'processed', icon: '🎨', titleKey: 'streams.group.processed', count: processedStreams.length },
|
{ key: 'processed', icon: getPictureSourceIcon('processed'), titleKey: 'streams.group.processed', count: processedStreams.length },
|
||||||
{ key: 'audio', icon: '🔊', titleKey: 'streams.group.audio', count: _cachedAudioSources.length },
|
{ key: 'audio', icon: getAudioSourceIcon('multichannel'), titleKey: 'streams.group.audio', count: _cachedAudioSources.length },
|
||||||
{ key: 'value', icon: '🔢', titleKey: 'streams.group.value', count: _cachedValueSources.length },
|
{ key: 'value', icon: ICON_VALUE_SOURCE, titleKey: 'streams.group.value', count: _cachedValueSources.length },
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
|
const tabBar = `<div class="stream-tab-bar">${tabs.map(tab =>
|
||||||
@@ -721,7 +720,7 @@ function renderPictureSourcesList(streams) {
|
|||||||
|
|
||||||
const renderAudioSourceCard = (src) => {
|
const renderAudioSourceCard = (src) => {
|
||||||
const isMono = src.source_type === 'mono';
|
const isMono = src.source_type === 'mono';
|
||||||
const icon = isMono ? '🎤' : '🔊';
|
const icon = getAudioSourceIcon(src.source_type);
|
||||||
|
|
||||||
let propsHtml = '';
|
let propsHtml = '';
|
||||||
if (isMono) {
|
if (isMono) {
|
||||||
@@ -729,13 +728,13 @@ function renderPictureSourcesList(streams) {
|
|||||||
const parentName = parent ? parent.name : src.audio_source_id;
|
const parentName = parent ? parent.name : src.audio_source_id;
|
||||||
const chLabel = src.channel === 'left' ? 'L' : src.channel === 'right' ? 'R' : 'M';
|
const chLabel = src.channel === 'left' ? 'L' : src.channel === 'right' ? 'R' : 'M';
|
||||||
propsHtml = `
|
propsHtml = `
|
||||||
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.parent'))}">🔊 ${escapeHtml(parentName)}</span>
|
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.parent'))}">${ICON_AUDIO_LOOPBACK} ${escapeHtml(parentName)}</span>
|
||||||
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.channel'))}">📻 ${chLabel}</span>
|
<span class="stream-card-prop" title="${escapeHtml(t('audio_source.channel'))}">📻 ${chLabel}</span>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
const devIdx = src.device_index ?? -1;
|
const devIdx = src.device_index ?? -1;
|
||||||
const loopback = src.is_loopback !== false;
|
const loopback = src.is_loopback !== false;
|
||||||
const devLabel = loopback ? '🔊 Loopback' : '🎤 Input';
|
const devLabel = loopback ? `${ICON_AUDIO_LOOPBACK} Loopback` : `${ICON_AUDIO_INPUT} Input`;
|
||||||
propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>`;
|
propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -748,8 +747,8 @@ function renderPictureSourcesList(streams) {
|
|||||||
<div class="stream-card-props">${propsHtml}</div>
|
<div class="stream-card-props">${propsHtml}</div>
|
||||||
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}
|
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}
|
||||||
<div class="template-card-actions">
|
<div class="template-card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="cloneAudioSource('${src.id}')" title="${t('common.clone')}">📋</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')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="editAudioSource('${src.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -978,9 +977,7 @@ async function populateStreamModalDropdowns() {
|
|||||||
const opt = document.createElement('option');
|
const opt = document.createElement('option');
|
||||||
opt.value = s.id;
|
opt.value = s.id;
|
||||||
opt.dataset.name = s.name;
|
opt.dataset.name = s.name;
|
||||||
const typeLabels = { raw: '🖥️', processed: '🎨', static_image: '🖼️' };
|
opt.textContent = `${getPictureSourceIcon(s.stream_type)} ${s.name}`;
|
||||||
const typeLabel = typeLabels[s.stream_type] || '📺';
|
|
||||||
opt.textContent = `${typeLabel} ${s.name}`;
|
|
||||||
sourceSelect.appendChild(opt);
|
sourceSelect.appendChild(opt);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { Modal } from '../core/modal.js';
|
|||||||
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.js';
|
import { createDeviceCard, attachDeviceListeners, fetchDeviceBrightness, _computeMaxFps } from './devices.js';
|
||||||
import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
|
import { createKCTargetCard, connectKCWebSocket, disconnectKCWebSocket } from './kc-targets.js';
|
||||||
import { createColorStripCard } from './color-strips.js';
|
import { createColorStripCard } from './color-strips.js';
|
||||||
import { getValueSourceIcon } from './value-sources.js';
|
import {
|
||||||
|
getValueSourceIcon, getTargetTypeIcon,
|
||||||
|
ICON_CLONE, ICON_EDIT, ICON_START, ICON_STOP,
|
||||||
|
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
|
||||||
|
} from '../core/icons.js';
|
||||||
import { CardSection } from '../core/card-sections.js';
|
import { CardSection } from '../core/card-sections.js';
|
||||||
import { updateSubTabHash, updateTabBadge } from './tabs.js';
|
import { updateSubTabHash, updateTabBadge } from './tabs.js';
|
||||||
|
|
||||||
@@ -493,8 +497,8 @@ export async function loadTargetsTab() {
|
|||||||
if (activeSubTab === 'wled') activeSubTab = 'led';
|
if (activeSubTab === 'wled') activeSubTab = 'led';
|
||||||
|
|
||||||
const subTabs = [
|
const subTabs = [
|
||||||
{ key: 'led', icon: '\uD83D\uDCA1', titleKey: 'targets.subtab.led', count: ledDevices.length + Object.keys(colorStripSourceMap).length + ledTargets.length },
|
{ key: 'led', icon: getTargetTypeIcon('led'), titleKey: 'targets.subtab.led', count: ledDevices.length + Object.keys(colorStripSourceMap).length + ledTargets.length },
|
||||||
{ key: 'key_colors', icon: '\uD83C\uDFA8', titleKey: 'targets.subtab.key_colors', count: kcTargets.length + patternTemplates.length },
|
{ key: 'key_colors', icon: getTargetTypeIcon('key_colors'), titleKey: 'targets.subtab.key_colors', count: kcTargets.length + patternTemplates.length },
|
||||||
];
|
];
|
||||||
|
|
||||||
const tabBar = `<div class="stream-tab-bar">${subTabs.map(tab =>
|
const tabBar = `<div class="stream-tab-bar">${subTabs.map(tab =>
|
||||||
@@ -701,8 +705,8 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">💡 ${escapeHtml(deviceName)}</span>
|
<span class="stream-card-prop stream-card-link" title="${t('targets.device')}" onclick="event.stopPropagation(); navigateToCard('targets','led','led-devices','data-device-id','${target.device_id}')">${ICON_LED} ${escapeHtml(deviceName)}</span>
|
||||||
<span class="stream-card-prop" title="${t('targets.fps')}">⚡ ${target.fps || 30}</span>
|
<span class="stream-card-prop" title="${t('targets.fps')}">${ICON_FPS} ${target.fps || 30}</span>
|
||||||
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>🎞️ ${cssSummary}</span>
|
<span class="stream-card-prop stream-card-prop-full${cssId ? ' stream-card-link' : ''}" title="${t('targets.color_strip_source')}"${cssId ? ` onclick="event.stopPropagation(); navigateToCard('targets','led','led-css','data-css-id','${cssId}')"` : ''}>🎞️ ${cssSummary}</span>
|
||||||
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
|
${bvs ? `<span class="stream-card-prop stream-card-prop-full stream-card-link" title="${t('targets.brightness_vs')}" onclick="event.stopPropagation(); navigateToCard('streams','value','value-sources','data-id','${bvsId}')">${getValueSourceIcon(bvs.source_type)} ${escapeHtml(bvs.name)}</span>` : ''}
|
||||||
</div>
|
</div>
|
||||||
@@ -776,31 +780,31 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('device.button.stop')}">
|
<button class="btn btn-icon btn-danger" onclick="stopTargetProcessing('${target.id}')" title="${t('device.button.stop')}">
|
||||||
⏹️
|
${ICON_STOP}
|
||||||
</button>
|
</button>
|
||||||
` : `
|
` : `
|
||||||
<button class="btn btn-icon btn-primary" onclick="startTargetProcessing('${target.id}')" title="${t('device.button.start')}">
|
<button class="btn btn-icon btn-primary" onclick="startTargetProcessing('${target.id}')" title="${t('device.button.start')}">
|
||||||
▶️
|
${ICON_START}
|
||||||
</button>
|
</button>
|
||||||
`}
|
`}
|
||||||
${isProcessing ? `
|
${isProcessing ? `
|
||||||
<button class="btn btn-icon ${ledPreviewWebSockets[target.id] ? 'btn-warning' : 'btn-secondary'}" onclick="toggleLedPreview('${target.id}')" title="LED Preview">
|
<button class="btn btn-icon ${ledPreviewWebSockets[target.id] ? 'btn-warning' : 'btn-secondary'}" onclick="toggleLedPreview('${target.id}')" title="LED Preview">
|
||||||
📊
|
${ICON_LED_PREVIEW}
|
||||||
</button>
|
</button>
|
||||||
` : ''}
|
` : ''}
|
||||||
<button class="btn btn-icon btn-secondary" onclick="cloneTarget('${target.id}')" title="${t('common.clone')}">
|
<button class="btn btn-icon btn-secondary" onclick="cloneTarget('${target.id}')" title="${t('common.clone')}">
|
||||||
📋
|
${ICON_CLONE}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="showTargetEditor('${target.id}')" title="${t('common.edit')}">
|
<button class="btn btn-icon btn-secondary" onclick="showTargetEditor('${target.id}')" title="${t('common.edit')}">
|
||||||
✏️
|
${ICON_EDIT}
|
||||||
</button>
|
</button>
|
||||||
${overlayAvailable ? (state.overlay_active ? `
|
${overlayAvailable ? (state.overlay_active ? `
|
||||||
<button class="btn btn-icon btn-warning" onclick="stopTargetOverlay('${target.id}')" title="${t('overlay.button.hide')}">
|
<button class="btn btn-icon btn-warning" onclick="stopTargetOverlay('${target.id}')" title="${t('overlay.button.hide')}">
|
||||||
👁️
|
${ICON_OVERLAY}
|
||||||
</button>
|
</button>
|
||||||
` : `
|
` : `
|
||||||
<button class="btn btn-icon btn-secondary" onclick="startTargetOverlay('${target.id}')" title="${t('overlay.button.show')}">
|
<button class="btn btn-icon btn-secondary" onclick="startTargetOverlay('${target.id}')" title="${t('overlay.button.show')}">
|
||||||
👁️
|
${ICON_OVERLAY}
|
||||||
</button>
|
</button>
|
||||||
`) : ''}
|
`) : ''}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -15,8 +15,11 @@ import { fetchWithAuth, escapeHtml } from '../core/api.js';
|
|||||||
import { t } from '../core/i18n.js';
|
import { t } from '../core/i18n.js';
|
||||||
import { showToast, showConfirm } from '../core/ui.js';
|
import { showToast, showConfirm } from '../core/ui.js';
|
||||||
import { Modal } from '../core/modal.js';
|
import { Modal } from '../core/modal.js';
|
||||||
|
import { getValueSourceIcon, ICON_CLONE, ICON_EDIT } from '../core/icons.js';
|
||||||
import { loadPictureSources } from './streams.js';
|
import { loadPictureSources } from './streams.js';
|
||||||
|
|
||||||
|
export { getValueSourceIcon };
|
||||||
|
|
||||||
class ValueSourceModal extends Modal {
|
class ValueSourceModal extends Modal {
|
||||||
constructor() { super('value-source-modal'); }
|
constructor() { super('value-source-modal'); }
|
||||||
|
|
||||||
@@ -271,13 +274,6 @@ export async function deleteValueSource(sourceId) {
|
|||||||
|
|
||||||
// ── Card rendering (used by streams.js) ───────────────────────
|
// ── Card rendering (used by streams.js) ───────────────────────
|
||||||
|
|
||||||
const _vsTypeIcons = { static: '📊', animated: '🔄', audio: '🎵', adaptive_time: '🕐', adaptive_scene: '🌤️' };
|
|
||||||
|
|
||||||
/** Return the emoji icon for a given value source type. */
|
|
||||||
export function getValueSourceIcon(sourceType) {
|
|
||||||
return _vsTypeIcons[sourceType] || '🎚️';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createValueSourceCard(src) {
|
export function createValueSourceCard(src) {
|
||||||
const icon = getValueSourceIcon(src.source_type);
|
const icon = getValueSourceIcon(src.source_type);
|
||||||
|
|
||||||
@@ -324,8 +320,8 @@ export function createValueSourceCard(src) {
|
|||||||
<div class="stream-card-props">${propsHtml}</div>
|
<div class="stream-card-props">${propsHtml}</div>
|
||||||
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}
|
${src.description ? `<div class="template-config" style="opacity:0.7;">${escapeHtml(src.description)}</div>` : ''}
|
||||||
<div class="template-card-actions">
|
<div class="template-card-actions">
|
||||||
<button class="btn btn-icon btn-secondary" onclick="cloneValueSource('${src.id}')" title="${t('common.clone')}">📋</button>
|
<button class="btn btn-icon btn-secondary" onclick="cloneValueSource('${src.id}')" title="${t('common.clone')}">${ICON_CLONE}</button>
|
||||||
<button class="btn btn-icon btn-secondary" onclick="editValueSource('${src.id}')" title="${t('common.edit')}">✏️</button>
|
<button class="btn btn-icon btn-secondary" onclick="editValueSource('${src.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
|||||||
Reference in New Issue
Block a user