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:
@@ -204,6 +204,26 @@ body.cs-drag-active .card-drag-handle {
|
|||||||
position: static;
|
position: static;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.card-actions .color-picker-wrapper,
|
||||||
|
.template-card-actions .color-picker-wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card-actions .color-picker-popover,
|
||||||
|
.template-card-actions .color-picker-popover {
|
||||||
|
top: auto;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
left: auto;
|
||||||
|
right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card.cp-elevated,
|
||||||
|
.template-card.cp-elevated {
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
.card-autostart-btn {
|
.card-autostart-btn {
|
||||||
background: none;
|
background: none;
|
||||||
border: none;
|
border: none;
|
||||||
|
|||||||
@@ -315,6 +315,20 @@ h2 {
|
|||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
.color-picker-reset {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 6px;
|
||||||
|
padding-top: 6px;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
font-size: 0.78rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.color-picker-reset:hover {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
.color-picker-custom input[type="color"] {
|
.color-picker-custom input[type="color"] {
|
||||||
width: 28px;
|
width: 28px;
|
||||||
height: 28px;
|
height: 28px;
|
||||||
|
|||||||
113
server/src/wled_controller/static/js/core/card-colors.js
Normal file
113
server/src/wled_controller/static/js/core/card-colors.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
/**
|
||||||
|
* Card color assignment — localStorage-backed color labels for any card.
|
||||||
|
*
|
||||||
|
* Usage in card creation functions:
|
||||||
|
* import { wrapCard } from '../core/card-colors.js';
|
||||||
|
*
|
||||||
|
* return wrapCard({
|
||||||
|
* dataAttr: 'data-device-id',
|
||||||
|
* id: device.id,
|
||||||
|
* removeOnclick: `removeDevice('${device.id}')`,
|
||||||
|
* removeTitle: t('common.delete'),
|
||||||
|
* content: `<div class="card-header">...</div>`,
|
||||||
|
* actions: `<button class="btn btn-icon btn-secondary" ...>Edit</button>`,
|
||||||
|
* });
|
||||||
|
*
|
||||||
|
* The helper wraps content in a standard card shell:
|
||||||
|
* - Outer div (.card / .template-card) with border-left color
|
||||||
|
* - .card-top-actions with remove button + optional top buttons
|
||||||
|
* - Bottom actions (.card-actions / .template-card-actions) with color picker
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createColorPicker, registerColorPicker } from './color-picker.js';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'cardColors';
|
||||||
|
const DEFAULT_SWATCH = '#808080';
|
||||||
|
|
||||||
|
function _getAll() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; }
|
||||||
|
catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCardColor(id) {
|
||||||
|
return _getAll()[id] || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setCardColor(id, hex) {
|
||||||
|
const m = _getAll();
|
||||||
|
if (hex) m[id] = hex; else delete m[id];
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(m));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns inline style string for card border-left.
|
||||||
|
* Empty string when no color is set.
|
||||||
|
*/
|
||||||
|
export function cardColorStyle(entityId) {
|
||||||
|
const c = getCardColor(entityId);
|
||||||
|
return c ? `border-left: 3px solid ${c}` : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns color picker HTML + registers the pick callback.
|
||||||
|
* @param {string} entityId Unique entity ID
|
||||||
|
* @param {string} cardAttr Data attribute selector, e.g. 'data-device-id'
|
||||||
|
*/
|
||||||
|
export function cardColorButton(entityId, cardAttr) {
|
||||||
|
const color = getCardColor(entityId) || DEFAULT_SWATCH;
|
||||||
|
const pickerId = `cc-${entityId}`;
|
||||||
|
|
||||||
|
registerColorPicker(pickerId, (hex) => {
|
||||||
|
setCardColor(entityId, hex);
|
||||||
|
const card = document.querySelector(`[${cardAttr}="${entityId}"]`);
|
||||||
|
if (card) card.style.borderLeft = hex ? `3px solid ${hex}` : '';
|
||||||
|
});
|
||||||
|
|
||||||
|
return createColorPicker({ id: pickerId, currentColor: color, anchor: 'left', showReset: true, resetColor: DEFAULT_SWATCH });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a standard card shell with color support.
|
||||||
|
*
|
||||||
|
* Provides consistent structure across all card types:
|
||||||
|
* - .card-top-actions: remove button + optional extra top buttons
|
||||||
|
* - Bottom actions: action buttons + color picker (always last)
|
||||||
|
* - Automatic border-left color from localStorage
|
||||||
|
*
|
||||||
|
* @param {object} opts
|
||||||
|
* @param {'card'|'template-card'} [opts.type='card'] Card CSS class
|
||||||
|
* @param {string} opts.dataAttr Data attribute name, e.g. 'data-device-id'
|
||||||
|
* @param {string} opts.id Entity ID value
|
||||||
|
* @param {string} [opts.classes] Extra CSS classes on root element
|
||||||
|
* @param {string} [opts.topButtons] HTML for extra top-right buttons (power, autostart)
|
||||||
|
* @param {string} opts.removeOnclick onclick handler string for remove button
|
||||||
|
* @param {string} opts.removeTitle title attribute for remove button
|
||||||
|
* @param {string} opts.content Inner HTML (header, props, metrics, etc.)
|
||||||
|
* @param {string} opts.actions Action button HTML (without wrapper div)
|
||||||
|
*/
|
||||||
|
export function wrapCard({
|
||||||
|
type = 'card',
|
||||||
|
dataAttr,
|
||||||
|
id,
|
||||||
|
classes = '',
|
||||||
|
topButtons = '',
|
||||||
|
removeOnclick,
|
||||||
|
removeTitle,
|
||||||
|
content,
|
||||||
|
actions,
|
||||||
|
}) {
|
||||||
|
const actionsClass = type === 'template-card' ? 'template-card-actions' : 'card-actions';
|
||||||
|
const colorStyle = cardColorStyle(id);
|
||||||
|
return `
|
||||||
|
<div class="${type}${classes ? ' ' + classes : ''}" ${dataAttr}="${id}"${colorStyle ? ` style="${colorStyle}"` : ''}>
|
||||||
|
<div class="card-top-actions">
|
||||||
|
${topButtons}
|
||||||
|
<button class="card-remove-btn" onclick="${removeOnclick}" title="${removeTitle}">✕</button>
|
||||||
|
</div>
|
||||||
|
${content}
|
||||||
|
<div class="${actionsClass}">
|
||||||
|
${actions}
|
||||||
|
${cardColorButton(id, dataAttr)}
|
||||||
|
</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
@@ -28,12 +28,16 @@ const PRESETS = [
|
|||||||
/**
|
/**
|
||||||
* Build the HTML string for a color-picker widget.
|
* Build the HTML string for a color-picker widget.
|
||||||
*/
|
*/
|
||||||
export function createColorPicker({ id, currentColor, onPick, anchor = 'right' }) {
|
export function createColorPicker({ id, currentColor, onPick, anchor = 'right', showReset = false, resetColor = '#808080' }) {
|
||||||
const dots = PRESETS.map(c => {
|
const dots = PRESETS.map(c => {
|
||||||
const active = c.toLowerCase() === currentColor.toLowerCase() ? ' active' : '';
|
const active = c.toLowerCase() === currentColor.toLowerCase() ? ' active' : '';
|
||||||
return `<button class="color-picker-dot${active}" style="background:${c}" onclick="event.stopPropagation(); window._cpPick('${id}','${c}')"></button>`;
|
return `<button class="color-picker-dot${active}" style="background:${c}" onclick="event.stopPropagation(); window._cpPick('${id}','${c}')"></button>`;
|
||||||
}).join('');
|
}).join('');
|
||||||
|
|
||||||
|
const resetBtn = showReset
|
||||||
|
? `<div class="color-picker-reset" onclick="event.stopPropagation(); window._cpReset('${id}','${resetColor}')"><span>${t('accent.reset')}</span></div>`
|
||||||
|
: '';
|
||||||
|
|
||||||
return `<span class="color-picker-wrapper" id="cp-wrap-${id}">` +
|
return `<span class="color-picker-wrapper" id="cp-wrap-${id}">` +
|
||||||
`<span class="color-picker-swatch" id="cp-swatch-${id}" style="background:${currentColor}" onclick="event.stopPropagation(); window._cpToggle('${id}')"></span>` +
|
`<span class="color-picker-swatch" id="cp-swatch-${id}" style="background:${currentColor}" onclick="event.stopPropagation(); window._cpToggle('${id}')"></span>` +
|
||||||
`<div class="color-picker-popover anchor-${anchor}" id="cp-pop-${id}" style="display:none" onclick="event.stopPropagation()">` +
|
`<div class="color-picker-popover anchor-${anchor}" id="cp-pop-${id}" style="display:none" onclick="event.stopPropagation()">` +
|
||||||
@@ -44,6 +48,7 @@ export function createColorPicker({ id, currentColor, onPick, anchor = 'right' }
|
|||||||
`onchange="event.stopPropagation(); window._cpPick('${id}',this.value)">` +
|
`onchange="event.stopPropagation(); window._cpPick('${id}',this.value)">` +
|
||||||
`<span>${t('accent.custom')}</span>` +
|
`<span>${t('accent.custom')}</span>` +
|
||||||
`</div>` +
|
`</div>` +
|
||||||
|
resetBtn +
|
||||||
`</div>` +
|
`</div>` +
|
||||||
`</span>`;
|
`</span>`;
|
||||||
}
|
}
|
||||||
@@ -65,14 +70,21 @@ function _rgbToHex(rgb) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
window._cpToggle = function (id) {
|
window._cpToggle = function (id) {
|
||||||
// Close all other pickers first
|
// Close all other pickers first (and drop their card elevation)
|
||||||
document.querySelectorAll('.color-picker-popover').forEach(p => {
|
document.querySelectorAll('.color-picker-popover').forEach(p => {
|
||||||
if (p.id !== `cp-pop-${id}`) p.style.display = 'none';
|
if (p.id !== `cp-pop-${id}`) {
|
||||||
|
p.style.display = 'none';
|
||||||
|
const card = p.closest('.card, .template-card');
|
||||||
|
if (card) card.classList.remove('cp-elevated');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
const pop = document.getElementById(`cp-pop-${id}`);
|
const pop = document.getElementById(`cp-pop-${id}`);
|
||||||
if (!pop) return;
|
if (!pop) return;
|
||||||
const show = pop.style.display === 'none';
|
const show = pop.style.display === 'none';
|
||||||
pop.style.display = show ? '' : 'none';
|
pop.style.display = show ? '' : 'none';
|
||||||
|
// Elevate the card so the popover isn't clipped by sibling cards
|
||||||
|
const card = pop.closest('.card, .template-card');
|
||||||
|
if (card) card.classList.toggle('cp-elevated', show);
|
||||||
if (show) {
|
if (show) {
|
||||||
// Mark active dot
|
// Mark active dot
|
||||||
const swatch = document.getElementById(`cp-swatch-${id}`);
|
const swatch = document.getElementById(`cp-swatch-${id}`);
|
||||||
@@ -99,13 +111,35 @@ window._cpPick = function (id, hex) {
|
|||||||
d.classList.toggle('active', dHex.toLowerCase() === hex.toLowerCase());
|
d.classList.toggle('active', dHex.toLowerCase() === hex.toLowerCase());
|
||||||
});
|
});
|
||||||
pop.style.display = 'none';
|
pop.style.display = 'none';
|
||||||
|
const card = pop.closest('.card, .template-card');
|
||||||
|
if (card) card.classList.remove('cp-elevated');
|
||||||
}
|
}
|
||||||
// Fire callback
|
// Fire callback
|
||||||
if (_callbacks[id]) _callbacks[id](hex);
|
if (_callbacks[id]) _callbacks[id](hex);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
window._cpReset = function (id, resetColor) {
|
||||||
|
// Reset swatch to neutral color
|
||||||
|
const swatch = document.getElementById(`cp-swatch-${id}`);
|
||||||
|
if (swatch) swatch.style.background = resetColor;
|
||||||
|
// Clear active dots and close popover
|
||||||
|
const pop = document.getElementById(`cp-pop-${id}`);
|
||||||
|
if (pop) {
|
||||||
|
pop.querySelectorAll('.color-picker-dot').forEach(d => d.classList.remove('active'));
|
||||||
|
pop.style.display = 'none';
|
||||||
|
const card = pop.closest('.card, .template-card');
|
||||||
|
if (card) card.classList.remove('cp-elevated');
|
||||||
|
}
|
||||||
|
// Fire callback with empty string to signal removal
|
||||||
|
if (_callbacks[id]) _callbacks[id]('');
|
||||||
|
};
|
||||||
|
|
||||||
export function closeAllColorPickers() {
|
export function closeAllColorPickers() {
|
||||||
document.querySelectorAll('.color-picker-popover').forEach(p => p.style.display = 'none');
|
document.querySelectorAll('.color-picker-popover').forEach(p => {
|
||||||
|
p.style.display = 'none';
|
||||||
|
const card = p.closest('.card, .template-card');
|
||||||
|
if (card) card.classList.remove('cp-elevated');
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close on outside click
|
// Close on outside click
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
import { ICON_SETTINGS, ICON_START, ICON_PAUSE, ICON_CLOCK, ICON_AUTOMATION, ICON_HELP, ICON_OK, ICON_TIMER, ICON_MONITOR, ICON_RADIO, ICON_SCENE } from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { csScenes, createSceneCard } from './scene-presets.js';
|
import { csScenes, createSceneCard } from './scene-presets.js';
|
||||||
|
|
||||||
class AutomationEditorModal extends Modal {
|
class AutomationEditorModal extends Modal {
|
||||||
@@ -152,11 +153,13 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
|||||||
lastActivityMeta = `<span class="card-meta" title="${t('automations.last_activated')}">${ICON_CLOCK} ${ts.toLocaleString()}</span>`;
|
lastActivityMeta = `<span class="card-meta" title="${t('automations.last_activated')}">${ICON_CLOCK} ${ts.toLocaleString()}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="card${!automation.enabled ? ' automation-status-disabled' : ''}" data-automation-id="${automation.id}">
|
dataAttr: 'data-automation-id',
|
||||||
<div class="card-top-actions">
|
id: automation.id,
|
||||||
<button class="card-remove-btn" onclick="deleteAutomation('${automation.id}', '${escapeHtml(automation.name)}')" title="${t('common.delete')}">✕</button>
|
classes: !automation.enabled ? 'automation-status-disabled' : '',
|
||||||
</div>
|
removeOnclick: `deleteAutomation('${automation.id}', '${escapeHtml(automation.name)}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
${escapeHtml(automation.name)}
|
${escapeHtml(automation.name)}
|
||||||
@@ -169,14 +172,13 @@ function createAutomationCard(automation, sceneMap = new Map()) {
|
|||||||
${deactivationLabel ? `<span class="card-meta">${deactivationLabel}</span>` : ''}
|
${deactivationLabel ? `<span class="card-meta">${deactivationLabel}</span>` : ''}
|
||||||
${lastActivityMeta}
|
${lastActivityMeta}
|
||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">${condPills}</div>
|
<div class="stream-card-props">${condPills}</div>`,
|
||||||
<div class="card-actions">
|
actions: `
|
||||||
<button class="btn btn-icon btn-secondary" onclick="openAutomationEditor('${automation.id}')" title="${t('automations.edit')}">${ICON_SETTINGS}</button>
|
<button class="btn btn-icon btn-secondary" onclick="openAutomationEditor('${automation.id}')" title="${t('automations.edit')}">${ICON_SETTINGS}</button>
|
||||||
<button class="btn btn-icon ${automation.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleAutomationEnabled('${automation.id}', ${!automation.enabled})" title="${automation.enabled ? t('automations.action.disable') : t('automations.status.active')}">
|
<button class="btn btn-icon ${automation.enabled ? 'btn-warning' : 'btn-success'}" onclick="toggleAutomationEnabled('${automation.id}', ${!automation.enabled})" title="${automation.enabled ? t('automations.action.disable') : t('automations.status.active')}">
|
||||||
${automation.enabled ? ICON_PAUSE : ICON_START}
|
${automation.enabled ? ICON_PAUSE : ICON_START}
|
||||||
</button>
|
</button>`,
|
||||||
</div>
|
});
|
||||||
</div>`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function openAutomationEditor(automationId) {
|
export async function openAutomationEditor(automationId) {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
ICON_AUDIO_LOOPBACK, ICON_TIMER, ICON_LINK_SOURCE, ICON_FILM,
|
||||||
ICON_LINK, ICON_SPARKLES, ICON_FAST_FORWARD, ICON_ACTIVITY,
|
ICON_LINK, ICON_SPARKLES, ICON_FAST_FORWARD, ICON_ACTIVITY,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
|
||||||
class CSSEditorModal extends Modal {
|
class CSSEditorModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -707,9 +708,12 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|||||||
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
? `<button class="btn btn-icon btn-secondary" onclick="showCSSCalibration('${source.id}')" title="${t('calibration.title')}">${ICON_CALIBRATION}</button>`
|
||||||
: '';
|
: '';
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="card" data-css-id="${source.id}">
|
dataAttr: 'data-css-id',
|
||||||
<button class="card-remove-btn" onclick="deleteColorStrip('${source.id}')" title="${t('common.delete')}">✕</button>
|
id: source.id,
|
||||||
|
removeOnclick: `deleteColorStrip('${source.id}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
${icon} ${escapeHtml(source.name)}
|
${icon} ${escapeHtml(source.name)}
|
||||||
@@ -717,14 +721,12 @@ export function createColorStripCard(source, pictureSourceMap, audioSourceMap) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="stream-card-props">
|
<div class="stream-card-props">
|
||||||
${propsHtml}
|
${propsHtml}
|
||||||
</div>
|
</div>`,
|
||||||
<div class="card-actions">
|
actions: `
|
||||||
<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="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')}">${ICON_EDIT}</button>
|
<button class="btn btn-icon btn-secondary" onclick="showCSSEditor('${source.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>
|
||||||
${calibrationBtn}
|
${calibrationBtn}`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Editor open/close ────────────────────────────────────────── */
|
/* ── Editor open/close ────────────────────────────────────────── */
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ 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, ICON_PLUG } from '../core/icons.js';
|
import { ICON_SETTINGS, ICON_STOP_PLAIN, ICON_LED, ICON_WEB, ICON_PLUG } from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
|
||||||
class DeviceSettingsModal extends Modal {
|
class DeviceSettingsModal extends Modal {
|
||||||
constructor() { super('device-settings-modal'); }
|
constructor() { super('device-settings-modal'); }
|
||||||
@@ -77,12 +78,13 @@ export function createDeviceCard(device) {
|
|||||||
|
|
||||||
const ledCount = state.device_led_count || device.led_count;
|
const ledCount = state.device_led_count || device.led_count;
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="card" data-device-id="${device.id}">
|
dataAttr: 'data-device-id',
|
||||||
<div class="card-top-actions">
|
id: device.id,
|
||||||
${(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>` : ''}
|
topButtons: (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>
|
removeOnclick: `removeDevice('${device.id}')`,
|
||||||
</div>
|
removeTitle: t('device.button.remove'),
|
||||||
|
content: `
|
||||||
<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>
|
||||||
@@ -105,15 +107,12 @@ export function createDeviceCard(device) {
|
|||||||
onchange="saveCardBrightness('${device.id}', this.value)"
|
onchange="saveCardBrightness('${device.id}', this.value)"
|
||||||
title="${_deviceBrightnessCache[device.id] != null ? Math.round(_deviceBrightnessCache[device.id] / 255 * 100) + '%' : '...'}"
|
title="${_deviceBrightnessCache[device.id] != null ? Math.round(_deviceBrightnessCache[device.id] / 255 * 100) + '%' : '...'}"
|
||||||
${_deviceBrightnessCache[device.id] == null ? 'disabled' : ''}>
|
${_deviceBrightnessCache[device.id] == null ? 'disabled' : ''}>
|
||||||
</div>` : ''}
|
</div>` : ''}`,
|
||||||
<div class="card-actions">
|
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}
|
${ICON_SETTINGS}
|
||||||
</button>
|
</button>`,
|
||||||
|
});
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function turnOffDevice(deviceId) {
|
export async function turnOffDevice(deviceId) {
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, ICON_PAUSE,
|
ICON_CLONE, ICON_EDIT, ICON_TEST, ICON_START, ICON_STOP, ICON_PAUSE,
|
||||||
ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE,
|
ICON_LINK_SOURCE, ICON_PATTERN_TEMPLATE, ICON_FPS, ICON_PALETTE,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
|
||||||
class KCEditorModal extends Modal {
|
class KCEditorModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -118,12 +119,13 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
|||||||
swatchesHtml = `<span class="kc-no-colors">${t('kc.colors.none')}</span>`;
|
swatchesHtml = `<span class="kc-no-colors">${t('kc.colors.none')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="card" data-kc-target-id="${target.id}">
|
dataAttr: 'data-kc-target-id',
|
||||||
<div class="card-top-actions">
|
id: target.id,
|
||||||
<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>
|
topButtons: `<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>`,
|
||||||
<button class="card-remove-btn" onclick="deleteKCTarget('${target.id}')" title="${t('common.delete')}">✕</button>
|
removeOnclick: `deleteKCTarget('${target.id}')`,
|
||||||
</div>
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="card-title">
|
<div class="card-title">
|
||||||
${escapeHtml(target.name)}
|
${escapeHtml(target.name)}
|
||||||
@@ -182,8 +184,8 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
|||||||
<div class="timing-breakdown" data-tm="timing"></div>
|
<div class="timing-breakdown" data-tm="timing"></div>
|
||||||
` : ''}
|
` : ''}
|
||||||
</div>
|
</div>
|
||||||
` : ''}
|
` : ''}`,
|
||||||
<div class="card-actions">
|
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}
|
${ICON_STOP}
|
||||||
@@ -201,10 +203,8 @@ export function createKCTargetCard(target, sourceMap, patternTemplateMap, valueS
|
|||||||
</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}
|
${ICON_EDIT}
|
||||||
</button>
|
</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== KEY COLORS TEST =====
|
// ===== KEY COLORS TEST =====
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ 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';
|
import { getPictureSourceIcon, ICON_PATTERN_TEMPLATE, ICON_CLONE, ICON_EDIT } from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
|
||||||
class PatternTemplateModal extends Modal {
|
class PatternTemplateModal extends Modal {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -52,22 +53,24 @@ const patternModal = new PatternTemplateModal();
|
|||||||
export function createPatternTemplateCard(pt) {
|
export function createPatternTemplateCard(pt) {
|
||||||
const rectCount = (pt.rectangles || []).length;
|
const rectCount = (pt.rectangles || []).length;
|
||||||
const desc = pt.description ? `<div class="template-description">${escapeHtml(pt.description)}</div>` : '';
|
const desc = pt.description ? `<div class="template-description">${escapeHtml(pt.description)}</div>` : '';
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-pattern-template-id="${pt.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deletePatternTemplate('${pt.id}')" title="${t('common.delete')}">✕</button>
|
dataAttr: 'data-pattern-template-id',
|
||||||
|
id: pt.id,
|
||||||
|
removeOnclick: `deletePatternTemplate('${pt.id}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<span class="template-name">${ICON_PATTERN_TEMPLATE} ${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">
|
actions: `
|
||||||
<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="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')}">${ICON_EDIT}</button>
|
<button class="btn btn-icon btn-secondary" onclick="showPatternTemplateEditor('${pt.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showPatternTemplateEditor(templateId = null, cloneData = null) {
|
export async function showPatternTemplateEditor(templateId = null, cloneData = null) {
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ import {
|
|||||||
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO,
|
ICON_AUDIO_TEMPLATE, ICON_MONITOR, ICON_WRENCH, ICON_RADIO,
|
||||||
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP,
|
ICON_CAPTURE_TEMPLATE, ICON_PP_TEMPLATE, ICON_HELP,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
|
|
||||||
// ── Card section instances ──
|
// ── Card section instances ──
|
||||||
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id' });
|
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>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-stream-id="${stream.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deleteStream('${stream.id}')" title="${t('common.delete')}">✕</button>
|
dataAttr: 'data-stream-id',
|
||||||
|
id: stream.id,
|
||||||
|
removeOnclick: `deleteStream('${stream.id}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">${typeIcon} ${escapeHtml(stream.name)}</div>
|
<div class="template-name">${typeIcon} ${escapeHtml(stream.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
${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">
|
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="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="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>
|
<button class="btn btn-icon btn-secondary" onclick="editStream('${stream.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderCaptureTemplateCard = (template) => {
|
const renderCaptureTemplateCard = (template) => {
|
||||||
const engineIcon = getEngineIcon(template.engine_type);
|
const engineIcon = getEngineIcon(template.engine_type);
|
||||||
const configEntries = Object.entries(template.engine_config);
|
const configEntries = Object.entries(template.engine_config);
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-template-id="${template.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deleteTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
dataAttr: 'data-template-id',
|
||||||
|
id: template.id,
|
||||||
|
removeOnclick: `deleteTemplate('${template.id}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">${ICON_TEMPLATE} ${escapeHtml(template.name)}</div>
|
<div class="template-name">${ICON_TEMPLATE} ${escapeHtml(template.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1227,14 +1234,12 @@ function renderPictureSourcesList(streams) {
|
|||||||
`).join('')}
|
`).join('')}
|
||||||
</table>
|
</table>
|
||||||
</details>
|
</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="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="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>
|
<button class="btn btn-icon btn-secondary" onclick="editTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPPTemplateCard = (tmpl) => {
|
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>`);
|
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>`;
|
filterChainHtml = `<div class="filter-chain">${filterNames.join('<span class="filter-chain-arrow">→</span>')}</div>`;
|
||||||
}
|
}
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-pp-template-id="${tmpl.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deletePPTemplate('${tmpl.id}')" title="${t('common.delete')}">✕</button>
|
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-card-header">
|
||||||
<div class="template-name">${ICON_TEMPLATE} ${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">
|
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="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="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>
|
<button class="btn btn-icon btn-secondary" onclick="editPPTemplate('${tmpl.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const rawStreams = streams.filter(s => s.stream_type === 'raw');
|
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}`;
|
propsHtml = `<span class="stream-card-prop">${devLabel} #${devIdx}</span>${tplBadge}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-id="${src.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deleteAudioSource('${src.id}')" title="${t('common.delete')}">✕</button>
|
dataAttr: 'data-id',
|
||||||
|
id: src.id,
|
||||||
|
removeOnclick: `deleteAudioSource('${src.id}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">${icon} ${escapeHtml(src.name)}</div>
|
<div class="template-name">${icon} ${escapeHtml(src.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
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="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="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>
|
<button class="btn btn-icon btn-secondary" onclick="editAudioSource('${src.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAudioTemplateCard = (template) => {
|
const renderAudioTemplateCard = (template) => {
|
||||||
const configEntries = Object.entries(template.engine_config || {});
|
const configEntries = Object.entries(template.engine_config || {});
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-audio-template-id="${template.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deleteAudioTemplate('${template.id}')" title="${t('common.delete')}">✕</button>
|
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-card-header">
|
||||||
<div class="template-name">${ICON_AUDIO_TEMPLATE} ${escapeHtml(template.name)}</div>
|
<div class="template-name">${ICON_AUDIO_TEMPLATE} ${escapeHtml(template.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1347,14 +1360,12 @@ function renderPictureSourcesList(streams) {
|
|||||||
`).join('')}
|
`).join('')}
|
||||||
</table>
|
</table>
|
||||||
</details>
|
</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="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="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>
|
<button class="btn btn-icon btn-secondary" onclick="editAudioTemplate('${template.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const panels = tabs.map(tab => {
|
const panels = tabs.map(tab => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
|
ICON_LED, ICON_FPS, ICON_OVERLAY, ICON_LED_PREVIEW,
|
||||||
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
|
ICON_GLOBE, ICON_RADIO, ICON_PLUG, ICON_FILM, ICON_SUN_DIM, ICON_TARGET_ICON, ICON_HELP,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.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';
|
||||||
|
|
||||||
@@ -850,12 +851,13 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
healthTitle = devOnline ? t('device.health.online') : t('device.health.offline');
|
healthTitle = devOnline ? t('device.health.online') : t('device.health.offline');
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="card" data-target-id="${target.id}">
|
dataAttr: 'data-target-id',
|
||||||
<div class="card-top-actions">
|
id: target.id,
|
||||||
<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>
|
topButtons: `<button class="card-autostart-btn${target.auto_start ? ' active' : ''}" onclick="toggleTargetAutoStart('${target.id}', ${!target.auto_start})" title="${target.auto_start ? t('autostart.toggle.enabled') : t('autostart.toggle.disabled')}">★</button>`,
|
||||||
<button class="card-remove-btn" onclick="deleteTarget('${target.id}')" title="${t('common.delete')}">✕</button>
|
removeOnclick: `deleteTarget('${target.id}')`,
|
||||||
</div>
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<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>
|
||||||
@@ -908,8 +910,8 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<div id="led-preview-panel-${target.id}" class="led-preview-panel" style="display:${ledPreviewWebSockets[target.id] ? '' : 'none'}">
|
<div id="led-preview-panel-${target.id}" class="led-preview-panel" style="display:${ledPreviewWebSockets[target.id] ? '' : 'none'}">
|
||||||
<canvas id="led-preview-canvas-${target.id}" class="led-preview-canvas"></canvas>
|
<canvas id="led-preview-canvas-${target.id}" class="led-preview-canvas"></canvas>
|
||||||
<span id="led-preview-brightness-${target.id}" class="led-preview-brightness" style="display:none"${bvsId ? ' data-has-bvs="1"' : ''}></span>
|
<span id="led-preview-brightness-${target.id}" class="led-preview-brightness" style="display:none"${bvsId ? ' data-has-bvs="1"' : ''}></span>
|
||||||
</div>
|
</div>`,
|
||||||
<div class="card-actions">
|
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}
|
${ICON_STOP}
|
||||||
@@ -938,10 +940,8 @@ export function createTargetCard(target, deviceMap, colorStripSourceMap, valueSo
|
|||||||
<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}
|
${ICON_OVERLAY}
|
||||||
</button>
|
</button>
|
||||||
`) : ''}
|
`) : ''}`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function _targetAction(action) {
|
async function _targetAction(action) {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import {
|
|||||||
ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL,
|
ICON_LED_PREVIEW, ICON_ACTIVITY, ICON_TIMER, ICON_MOVE_VERTICAL,
|
||||||
ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH,
|
ICON_MUSIC, ICON_TRENDING_UP, ICON_MAP_PIN, ICON_MONITOR, ICON_REFRESH,
|
||||||
} from '../core/icons.js';
|
} from '../core/icons.js';
|
||||||
|
import { wrapCard } from '../core/card-colors.js';
|
||||||
import { loadPictureSources } from './streams.js';
|
import { loadPictureSources } from './streams.js';
|
||||||
|
|
||||||
export { getValueSourceIcon };
|
export { getValueSourceIcon };
|
||||||
@@ -522,21 +523,23 @@ export function createValueSourceCard(src) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return `
|
return wrapCard({
|
||||||
<div class="template-card" data-id="${src.id}">
|
type: 'template-card',
|
||||||
<button class="card-remove-btn" onclick="deleteValueSource('${src.id}')" title="${t('common.delete')}">✕</button>
|
dataAttr: 'data-id',
|
||||||
|
id: src.id,
|
||||||
|
removeOnclick: `deleteValueSource('${src.id}')`,
|
||||||
|
removeTitle: t('common.delete'),
|
||||||
|
content: `
|
||||||
<div class="template-card-header">
|
<div class="template-card-header">
|
||||||
<div class="template-name">${icon} ${escapeHtml(src.name)}</div>
|
<div class="template-name">${icon} ${escapeHtml(src.name)}</div>
|
||||||
</div>
|
</div>
|
||||||
<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">
|
actions: `
|
||||||
<button class="btn btn-icon btn-secondary" onclick="testValueSource('${src.id}')" title="${t('value_source.test')}">${ICON_TEST}</button>
|
<button class="btn btn-icon btn-secondary" onclick="testValueSource('${src.id}')" title="${t('value_source.test')}">${ICON_TEST}</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="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')}">${ICON_EDIT}</button>
|
<button class="btn btn-icon btn-secondary" onclick="editValueSource('${src.id}')" title="${t('common.edit')}">${ICON_EDIT}</button>`,
|
||||||
</div>
|
});
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Helpers ───────────────────────────────────────────────────
|
// ── Helpers ───────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"theme.toggle": "Toggle theme",
|
"theme.toggle": "Toggle theme",
|
||||||
"accent.title": "Accent color",
|
"accent.title": "Accent color",
|
||||||
"accent.custom": "Custom",
|
"accent.custom": "Custom",
|
||||||
|
"accent.reset": "Reset",
|
||||||
"locale.change": "Change language",
|
"locale.change": "Change language",
|
||||||
"auth.login": "Login",
|
"auth.login": "Login",
|
||||||
"auth.logout": "Logout",
|
"auth.logout": "Logout",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"theme.toggle": "Переключить тему",
|
"theme.toggle": "Переключить тему",
|
||||||
"accent.title": "Цвет акцента",
|
"accent.title": "Цвет акцента",
|
||||||
"accent.custom": "Свой",
|
"accent.custom": "Свой",
|
||||||
|
"accent.reset": "Сброс",
|
||||||
"locale.change": "Изменить язык",
|
"locale.change": "Изменить язык",
|
||||||
"auth.login": "Войти",
|
"auth.login": "Войти",
|
||||||
"auth.logout": "Выйти",
|
"auth.logout": "Выйти",
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
"theme.toggle": "切换主题",
|
"theme.toggle": "切换主题",
|
||||||
"accent.title": "主题色",
|
"accent.title": "主题色",
|
||||||
"accent.custom": "自定义",
|
"accent.custom": "自定义",
|
||||||
|
"accent.reset": "重置",
|
||||||
"locale.change": "切换语言",
|
"locale.change": "切换语言",
|
||||||
"auth.login": "登录",
|
"auth.login": "登录",
|
||||||
"auth.logout": "退出",
|
"auth.logout": "退出",
|
||||||
|
|||||||
Reference in New Issue
Block a user