Add dashboard crosslinks and card drag-and-drop reordering
Dashboard cards (targets, auto-start, profiles) are now clickable, navigating to the full entity card on the appropriate tab. Card sections support drag-and-drop reordering via grip handles with localStorage persistence. Fix crosslink navigation scoping to avoid matching dashboard cards, and fix highlight race on rapid clicks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -24,6 +24,10 @@
|
||||
import { t } from './i18n.js';
|
||||
|
||||
const STORAGE_KEY = 'sections_collapsed';
|
||||
const ORDER_PREFIX = 'card_order_';
|
||||
const DRAG_THRESHOLD = 5;
|
||||
const SCROLL_EDGE = 60;
|
||||
const SCROLL_SPEED = 12;
|
||||
|
||||
function _getCollapsedMap() {
|
||||
try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; }
|
||||
@@ -48,6 +52,8 @@ export class CardSection {
|
||||
this.keyAttr = keyAttr || '';
|
||||
this._filterValue = '';
|
||||
this._lastItems = null;
|
||||
this._dragState = null;
|
||||
this._dragBound = false;
|
||||
}
|
||||
|
||||
/** True if this section's DOM element exists (i.e. not the first render). */
|
||||
@@ -61,6 +67,7 @@ export class CardSection {
|
||||
*/
|
||||
render(items) {
|
||||
this._lastItems = items;
|
||||
this._dragBound = false; // DOM will be recreated → need to re-init drag
|
||||
const count = items.length;
|
||||
const cardsHtml = items.map(i => i.html).join('');
|
||||
|
||||
@@ -155,6 +162,12 @@ export class CardSection {
|
||||
// Tag card elements with their source HTML for future reconciliation
|
||||
this._tagCards(content);
|
||||
|
||||
// Inject drag handles and initialize drag-and-drop
|
||||
if (this.keyAttr) {
|
||||
this._injectDragHandles(content);
|
||||
this._initDrag(content);
|
||||
}
|
||||
|
||||
// Stagger card entrance animation
|
||||
this._animateEntrance(content);
|
||||
}
|
||||
@@ -232,6 +245,11 @@ export class CardSection {
|
||||
}
|
||||
}
|
||||
|
||||
// Re-inject drag handles on new/replaced cards
|
||||
if (this.keyAttr && (added.size > 0 || replaced.size > 0)) {
|
||||
this._injectDragHandles(content);
|
||||
}
|
||||
|
||||
// Re-apply filter
|
||||
if (this._filterValue) {
|
||||
this._applyFilter(content, this._filterValue);
|
||||
@@ -292,6 +310,34 @@ export class CardSection {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reorder items array according to saved drag order.
|
||||
* Call before render() / reconcile().
|
||||
*/
|
||||
applySortOrder(items) {
|
||||
if (!this.keyAttr) return items;
|
||||
const order = this._getSavedOrder();
|
||||
if (!order.length) return items;
|
||||
const orderMap = new Map(order.map((key, idx) => [key, idx]));
|
||||
const sorted = [...items];
|
||||
sorted.sort((a, b) => {
|
||||
const ia = orderMap.has(a.key) ? orderMap.get(a.key) : Infinity;
|
||||
const ib = orderMap.has(b.key) ? orderMap.get(b.key) : Infinity;
|
||||
if (ia !== ib) return ia - ib;
|
||||
return 0; // preserve original order for unranked items
|
||||
});
|
||||
return sorted;
|
||||
}
|
||||
|
||||
_getSavedOrder() {
|
||||
try { return JSON.parse(localStorage.getItem(ORDER_PREFIX + this.sectionKey)) || []; }
|
||||
catch { return []; }
|
||||
}
|
||||
|
||||
_saveOrder(keys) {
|
||||
localStorage.setItem(ORDER_PREFIX + this.sectionKey, JSON.stringify(keys));
|
||||
}
|
||||
|
||||
// ── private ──
|
||||
|
||||
_animateEntrance(content) {
|
||||
@@ -368,11 +414,13 @@ export class CardSection {
|
||||
const total = cards.length;
|
||||
|
||||
if (!query) {
|
||||
content.classList.remove('cs-filtering');
|
||||
cards.forEach(card => { card.style.display = ''; });
|
||||
if (addCard) addCard.style.display = '';
|
||||
if (countEl) countEl.textContent = total;
|
||||
return;
|
||||
}
|
||||
content.classList.add('cs-filtering');
|
||||
|
||||
const lower = query.toLowerCase();
|
||||
// Comma-separated segments → AND; spaces within a segment → OR
|
||||
@@ -390,4 +438,166 @@ export class CardSection {
|
||||
if (addCard) addCard.style.display = 'none';
|
||||
if (countEl) countEl.textContent = `${visible}/${total}`;
|
||||
}
|
||||
|
||||
// ── drag-and-drop reordering ──
|
||||
|
||||
_injectDragHandles(content) {
|
||||
const cards = content.querySelectorAll(`[${this.keyAttr}]`);
|
||||
cards.forEach(card => {
|
||||
if (card.querySelector('.card-drag-handle')) return;
|
||||
const handle = document.createElement('span');
|
||||
handle.className = 'card-drag-handle';
|
||||
handle.textContent = '\u2807'; // ⠇ braille dots vertical
|
||||
handle.setAttribute('aria-hidden', 'true');
|
||||
card.prepend(handle);
|
||||
});
|
||||
}
|
||||
|
||||
_initDrag(content) {
|
||||
if (this._dragBound) return;
|
||||
this._dragBound = true;
|
||||
|
||||
content.addEventListener('pointerdown', (e) => {
|
||||
if (this._filterValue) return;
|
||||
const handle = e.target.closest('.card-drag-handle');
|
||||
if (!handle) return;
|
||||
const card = handle.closest(`[${this.keyAttr}]`);
|
||||
if (!card) return;
|
||||
|
||||
e.preventDefault();
|
||||
this._dragState = {
|
||||
card,
|
||||
content,
|
||||
startX: e.clientX,
|
||||
startY: e.clientY,
|
||||
started: false,
|
||||
clone: null,
|
||||
placeholder: null,
|
||||
scrollRaf: null,
|
||||
};
|
||||
|
||||
const onMove = (ev) => this._onDragMove(ev);
|
||||
const onUp = (ev) => {
|
||||
document.removeEventListener('pointermove', onMove);
|
||||
document.removeEventListener('pointerup', onUp);
|
||||
this._onDragEnd(ev);
|
||||
};
|
||||
document.addEventListener('pointermove', onMove);
|
||||
document.addEventListener('pointerup', onUp);
|
||||
});
|
||||
}
|
||||
|
||||
_onDragMove(e) {
|
||||
const ds = this._dragState;
|
||||
if (!ds) return;
|
||||
|
||||
if (!ds.started) {
|
||||
const dx = e.clientX - ds.startX;
|
||||
const dy = e.clientY - ds.startY;
|
||||
if (Math.abs(dx) < DRAG_THRESHOLD && Math.abs(dy) < DRAG_THRESHOLD) return;
|
||||
this._startDrag(ds, e);
|
||||
}
|
||||
|
||||
// Position clone at pointer
|
||||
ds.clone.style.left = (e.clientX - ds.offsetX) + 'px';
|
||||
ds.clone.style.top = (e.clientY - ds.offsetY) + 'px';
|
||||
|
||||
// Find drop target
|
||||
const target = this._getDropTarget(e.clientX, e.clientY, ds.content);
|
||||
if (target && target !== ds.placeholder) {
|
||||
const rect = target.getBoundingClientRect();
|
||||
const midX = rect.left + rect.width / 2;
|
||||
const midY = rect.top + rect.height / 2;
|
||||
// For grid: use combined horizontal+vertical check
|
||||
const insertBefore = (e.clientY < midY) || (e.clientY < midY + rect.height * 0.3 && e.clientX < midX);
|
||||
if (insertBefore) {
|
||||
ds.content.insertBefore(ds.placeholder, target);
|
||||
} else {
|
||||
ds.content.insertBefore(ds.placeholder, target.nextSibling);
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-scroll near viewport edges
|
||||
this._autoScroll(e.clientY, ds);
|
||||
}
|
||||
|
||||
_startDrag(ds, e) {
|
||||
ds.started = true;
|
||||
const rect = ds.card.getBoundingClientRect();
|
||||
|
||||
// Clone for visual drag
|
||||
const clone = ds.card.cloneNode(true);
|
||||
clone.className = ds.card.className + ' card-drag-clone';
|
||||
clone.style.width = rect.width + 'px';
|
||||
clone.style.height = rect.height + 'px';
|
||||
clone.style.left = rect.left + 'px';
|
||||
clone.style.top = rect.top + 'px';
|
||||
document.body.appendChild(clone);
|
||||
ds.clone = clone;
|
||||
ds.offsetX = e.clientX - rect.left;
|
||||
ds.offsetY = e.clientY - rect.top;
|
||||
|
||||
// Placeholder
|
||||
const placeholder = document.createElement('div');
|
||||
placeholder.className = 'card-drag-placeholder';
|
||||
placeholder.style.width = rect.width + 'px';
|
||||
placeholder.style.height = rect.height + 'px';
|
||||
ds.card.parentNode.insertBefore(placeholder, ds.card);
|
||||
ds.placeholder = placeholder;
|
||||
|
||||
// Hide original
|
||||
ds.card.style.display = 'none';
|
||||
ds.content.classList.add('cs-dragging');
|
||||
}
|
||||
|
||||
_onDragEnd() {
|
||||
const ds = this._dragState;
|
||||
this._dragState = null;
|
||||
if (!ds || !ds.started) return;
|
||||
|
||||
// Cancel auto-scroll
|
||||
if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);
|
||||
|
||||
// Move original card to placeholder position
|
||||
ds.content.insertBefore(ds.card, ds.placeholder);
|
||||
ds.card.style.display = '';
|
||||
ds.placeholder.remove();
|
||||
ds.clone.remove();
|
||||
ds.content.classList.remove('cs-dragging');
|
||||
|
||||
// Save new order from DOM
|
||||
const keys = this._readDomOrder(ds.content);
|
||||
this._saveOrder(keys);
|
||||
}
|
||||
|
||||
_getDropTarget(x, y, content) {
|
||||
// Temporarily show all cards for hit testing
|
||||
const els = document.elementsFromPoint(x, y);
|
||||
for (const el of els) {
|
||||
if (el === content) break;
|
||||
const card = el.closest(`[${this.keyAttr}]`);
|
||||
if (card && card.style.display !== 'none' && content.contains(card)) return card;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_readDomOrder(content) {
|
||||
return [...content.querySelectorAll(`[${this.keyAttr}]`)]
|
||||
.map(el => el.getAttribute(this.keyAttr));
|
||||
}
|
||||
|
||||
_autoScroll(clientY, ds) {
|
||||
if (ds.scrollRaf) cancelAnimationFrame(ds.scrollRaf);
|
||||
const vp = window.innerHeight;
|
||||
let speed = 0;
|
||||
if (clientY < SCROLL_EDGE) speed = -SCROLL_SPEED * (1 - clientY / SCROLL_EDGE);
|
||||
else if (clientY > vp - SCROLL_EDGE) speed = SCROLL_SPEED * (1 - (vp - clientY) / SCROLL_EDGE);
|
||||
|
||||
if (Math.abs(speed) < 0.5) return;
|
||||
const scroll = () => {
|
||||
window.scrollBy(0, speed);
|
||||
ds.scrollRaf = requestAnimationFrame(scroll);
|
||||
};
|
||||
ds.scrollRaf = requestAnimationFrame(scroll);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +45,13 @@ export function navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue) {
|
||||
}
|
||||
}
|
||||
|
||||
// Scope card search to the destination tab panel (avoid matching
|
||||
// dashboard cards that share the same data-* attributes).
|
||||
const tabPanel = document.getElementById(`tab-${tab}`);
|
||||
const scope = tabPanel || document;
|
||||
|
||||
// Check if card already exists (data previously loaded)
|
||||
const existing = document.querySelector(`[${cardAttr}="${cardValue}"]`);
|
||||
const existing = scope.querySelector(`[${cardAttr}="${cardValue}"]`);
|
||||
if (existing) {
|
||||
_highlightCard(existing);
|
||||
return;
|
||||
@@ -54,17 +59,29 @@ export function navigateToCard(tab, subTab, sectionKey, cardAttr, cardValue) {
|
||||
|
||||
// Card not in DOM — trigger data load and wait for it to appear
|
||||
_triggerTabLoad(tab);
|
||||
_waitForCard(cardAttr, cardValue, 5000).then(card => {
|
||||
_waitForCard(cardAttr, cardValue, 5000, scope).then(card => {
|
||||
if (card) _highlightCard(card);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
let _highlightTimer = 0;
|
||||
let _overlayTimer = 0;
|
||||
let _prevCard = null;
|
||||
|
||||
function _highlightCard(card) {
|
||||
// Clear previous highlight if still active
|
||||
if (_prevCard) _prevCard.classList.remove('card-highlight');
|
||||
clearTimeout(_highlightTimer);
|
||||
|
||||
card.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
card.classList.add('card-highlight');
|
||||
_prevCard = card;
|
||||
_showDimOverlay(2000);
|
||||
setTimeout(() => card.classList.remove('card-highlight'), 2000);
|
||||
_highlightTimer = setTimeout(() => {
|
||||
card.classList.remove('card-highlight');
|
||||
_prevCard = null;
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/** Trigger the tab's data load function (used when card wasn't found in DOM). */
|
||||
@@ -76,6 +93,7 @@ function _triggerTabLoad(tab) {
|
||||
}
|
||||
|
||||
function _showDimOverlay(duration) {
|
||||
clearTimeout(_overlayTimer);
|
||||
let overlay = document.getElementById('nav-dim-overlay');
|
||||
if (!overlay) {
|
||||
overlay = document.createElement('div');
|
||||
@@ -84,19 +102,20 @@ function _showDimOverlay(duration) {
|
||||
document.body.appendChild(overlay);
|
||||
}
|
||||
overlay.classList.add('active');
|
||||
setTimeout(() => overlay.classList.remove('active'), duration);
|
||||
_overlayTimer = setTimeout(() => overlay.classList.remove('active'), duration);
|
||||
}
|
||||
|
||||
function _waitForCard(cardAttr, cardValue, timeout) {
|
||||
function _waitForCard(cardAttr, cardValue, timeout, scope = document) {
|
||||
const root = scope === document ? document.body : scope;
|
||||
return new Promise(resolve => {
|
||||
const card = document.querySelector(`[${cardAttr}="${cardValue}"]`);
|
||||
const card = scope.querySelector(`[${cardAttr}="${cardValue}"]`);
|
||||
if (card) { resolve(card); return; }
|
||||
|
||||
const timer = setTimeout(() => { observer.disconnect(); resolve(null); }, timeout);
|
||||
const observer = new MutationObserver(() => {
|
||||
const el = document.querySelector(`[${cardAttr}="${cardValue}"]`);
|
||||
const el = scope.querySelector(`[${cardAttr}="${cardValue}"]`);
|
||||
if (el) { observer.disconnect(); clearTimeout(timer); resolve(el); }
|
||||
});
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
observer.observe(root, { childList: true, subtree: true });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -447,7 +447,10 @@ export async function loadDashboard(forceFullRender = false) {
|
||||
? `<span class="dashboard-badge-active">${t('profiles.status.active')}</span>`
|
||||
: `<span class="dashboard-badge-stopped">${t('profiles.status.inactive')}</span>`;
|
||||
const subtitle = subtitleParts.length ? `<div class="dashboard-target-subtitle">${escapeHtml(subtitleParts.join(' · '))}</div>` : '';
|
||||
return `<div class="dashboard-target dashboard-autostart" data-target-id="${target.id}">
|
||||
const asNavSub = isLed ? 'led' : 'key_colors';
|
||||
const asNavSec = isLed ? 'led-targets' : 'kc-targets';
|
||||
const asNavAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
|
||||
return `<div class="dashboard-target dashboard-autostart dashboard-card-link" data-target-id="${target.id}" onclick="if(!event.target.closest('button')){navigateToCard('targets','${asNavSub}','${asNavSec}','${asNavAttr}','${target.id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_AUTOSTART}</span>
|
||||
<div>
|
||||
@@ -551,6 +554,10 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
const isLed = target.target_type === 'led' || target.target_type === 'wled';
|
||||
const icon = ICON_TARGET;
|
||||
const typeLabel = isLed ? t('dashboard.type.led') : t('dashboard.type.kc');
|
||||
const navSubTab = isLed ? 'led' : 'key_colors';
|
||||
const navSection = isLed ? 'led-targets' : 'kc-targets';
|
||||
const navAttr = isLed ? 'data-target-id' : 'data-kc-target-id';
|
||||
const navOnclick = `if(!event.target.closest('button')){navigateToCard('targets','${navSubTab}','${navSection}','${navAttr}','${target.id}')}`;
|
||||
|
||||
let subtitleParts = [typeLabel];
|
||||
if (isLed) {
|
||||
@@ -590,7 +597,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
healthDot = `<span class="health-dot ${cls}"></span>`;
|
||||
}
|
||||
|
||||
return `<div class="dashboard-target" data-target-id="${target.id}">
|
||||
return `<div class="dashboard-target dashboard-card-link" data-target-id="${target.id}" onclick="${navOnclick}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${icon}</span>
|
||||
<div>
|
||||
@@ -620,7 +627,7 @@ function renderDashboardTarget(target, isRunning, devicesMap = {}, cssSourceMap
|
||||
</div>
|
||||
</div>`;
|
||||
} else {
|
||||
return `<div class="dashboard-target">
|
||||
return `<div class="dashboard-target dashboard-card-link" onclick="${navOnclick}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${icon}</span>
|
||||
<div>
|
||||
@@ -665,7 +672,7 @@ function renderDashboardProfile(profile) {
|
||||
const activeCount = (profile.active_target_ids || []).length;
|
||||
const targetsInfo = isActive ? `${activeCount}/${targetCount}` : `${targetCount}`;
|
||||
|
||||
return `<div class="dashboard-target dashboard-profile" data-profile-id="${profile.id}">
|
||||
return `<div class="dashboard-target dashboard-profile dashboard-card-link" data-profile-id="${profile.id}" onclick="if(!event.target.closest('button')){navigateToCard('profiles',null,'profiles','data-profile-id','${profile.id}')}">
|
||||
<div class="dashboard-target-info">
|
||||
<span class="dashboard-target-icon">${ICON_PROFILE}</span>
|
||||
<div>
|
||||
|
||||
@@ -26,7 +26,7 @@ class ProfileEditorModal extends Modal {
|
||||
}
|
||||
|
||||
const profileModal = new ProfileEditorModal();
|
||||
const csProfiles = new CardSection('profiles', { titleKey: 'profiles.title', gridClass: 'devices-grid', addCardOnclick: "openProfileEditor()" });
|
||||
const csProfiles = new CardSection('profiles', { titleKey: 'profiles.title', gridClass: 'devices-grid', addCardOnclick: "openProfileEditor()", keyAttr: 'data-profile-id' });
|
||||
|
||||
// Re-render profiles when language changes (only if tab is active)
|
||||
document.addEventListener('languageChanged', () => {
|
||||
@@ -79,7 +79,7 @@ export async function loadProfiles() {
|
||||
function renderProfiles(profiles, runningTargetIds = new Set()) {
|
||||
const container = document.getElementById('profiles-content');
|
||||
|
||||
const items = profiles.map(p => ({ key: p.id, html: createProfileCard(p, runningTargetIds) }));
|
||||
const items = csProfiles.applySortOrder(profiles.map(p => ({ key: p.id, html: createProfileCard(p, runningTargetIds) })));
|
||||
container.innerHTML = csProfiles.render(items);
|
||||
csProfiles.bind();
|
||||
|
||||
|
||||
@@ -43,15 +43,15 @@ import {
|
||||
} from '../core/icons.js';
|
||||
|
||||
// ── Card section instances ──
|
||||
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')" });
|
||||
const csRawTemplates = new CardSection('raw-templates', { titleKey: 'templates.title', gridClass: 'templates-grid', addCardOnclick: "showAddTemplateModal()" });
|
||||
const csProcStreams = new CardSection('proc-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('processed')" });
|
||||
const csProcTemplates = new CardSection('proc-templates', { titleKey: 'postprocessing.title', gridClass: 'templates-grid', addCardOnclick: "showAddPPTemplateModal()" });
|
||||
const csAudioMulti = new CardSection('audio-multi', { titleKey: 'audio_source.group.multichannel', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('multichannel')" });
|
||||
const csAudioMono = new CardSection('audio-mono', { titleKey: 'audio_source.group.mono', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('mono')" });
|
||||
const csStaticStreams = new CardSection('static-streams', { titleKey: 'streams.group.static_image', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('static_image')" });
|
||||
const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_template.title', gridClass: 'templates-grid', addCardOnclick: "showAddAudioTemplateModal()" });
|
||||
const csValueSources = new CardSection('value-sources', { titleKey: 'value_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showValueSourceModal()" });
|
||||
const csRawStreams = new CardSection('raw-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('raw')", keyAttr: 'data-stream-id' });
|
||||
const csRawTemplates = new CardSection('raw-templates', { titleKey: 'templates.title', gridClass: 'templates-grid', addCardOnclick: "showAddTemplateModal()", keyAttr: 'data-template-id' });
|
||||
const csProcStreams = new CardSection('proc-streams', { titleKey: 'streams.section.streams', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('processed')", keyAttr: 'data-stream-id' });
|
||||
const csProcTemplates = new CardSection('proc-templates', { titleKey: 'postprocessing.title', gridClass: 'templates-grid', addCardOnclick: "showAddPPTemplateModal()", keyAttr: 'data-pp-template-id' });
|
||||
const csAudioMulti = new CardSection('audio-multi', { titleKey: 'audio_source.group.multichannel', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('multichannel')", keyAttr: 'data-id' });
|
||||
const csAudioMono = new CardSection('audio-mono', { titleKey: 'audio_source.group.mono', gridClass: 'templates-grid', addCardOnclick: "showAudioSourceModal('mono')", keyAttr: 'data-id' });
|
||||
const csStaticStreams = new CardSection('static-streams', { titleKey: 'streams.group.static_image', gridClass: 'templates-grid', addCardOnclick: "showAddStreamModal('static_image')", keyAttr: 'data-stream-id' });
|
||||
const csAudioTemplates = new CardSection('audio-templates', { titleKey: 'audio_template.title', gridClass: 'templates-grid', addCardOnclick: "showAddAudioTemplateModal()", keyAttr: 'data-audio-template-id' });
|
||||
const csValueSources = new CardSection('value-sources', { titleKey: 'value_source.group.title', gridClass: 'templates-grid', addCardOnclick: "showValueSourceModal()", keyAttr: 'data-id' });
|
||||
|
||||
// Re-render picture sources when language changes
|
||||
document.addEventListener('languageChanged', () => { if (apiKey) loadPictureSources(); });
|
||||
@@ -1385,21 +1385,21 @@ function renderPictureSourcesList(streams) {
|
||||
|
||||
if (tab.key === 'raw') {
|
||||
panelContent =
|
||||
csRawStreams.render(rawStreams.map(s => ({ key: s.id, html: renderStreamCard(s) }))) +
|
||||
csRawTemplates.render(_cachedCaptureTemplates.map(t => ({ key: t.id, html: renderCaptureTemplateCard(t) })));
|
||||
csRawStreams.render(csRawStreams.applySortOrder(rawStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })))) +
|
||||
csRawTemplates.render(csRawTemplates.applySortOrder(_cachedCaptureTemplates.map(t => ({ key: t.id, html: renderCaptureTemplateCard(t) }))));
|
||||
} else if (tab.key === 'processed') {
|
||||
panelContent =
|
||||
csProcStreams.render(processedStreams.map(s => ({ key: s.id, html: renderStreamCard(s) }))) +
|
||||
csProcTemplates.render(_cachedPPTemplates.map(t => ({ key: t.id, html: renderPPTemplateCard(t) })));
|
||||
csProcStreams.render(csProcStreams.applySortOrder(processedStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })))) +
|
||||
csProcTemplates.render(csProcTemplates.applySortOrder(_cachedPPTemplates.map(t => ({ key: t.id, html: renderPPTemplateCard(t) }))));
|
||||
} else if (tab.key === 'audio') {
|
||||
panelContent =
|
||||
csAudioMulti.render(multichannelSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) }))) +
|
||||
csAudioMono.render(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) }))) +
|
||||
csAudioTemplates.render(_cachedAudioTemplates.map(t => ({ key: t.id, html: renderAudioTemplateCard(t) })));
|
||||
csAudioMulti.render(csAudioMulti.applySortOrder(multichannelSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })))) +
|
||||
csAudioMono.render(csAudioMono.applySortOrder(monoSources.map(s => ({ key: s.id, html: renderAudioSourceCard(s) })))) +
|
||||
csAudioTemplates.render(csAudioTemplates.applySortOrder(_cachedAudioTemplates.map(t => ({ key: t.id, html: renderAudioTemplateCard(t) }))));
|
||||
} else if (tab.key === 'value') {
|
||||
panelContent = csValueSources.render(_cachedValueSources.map(s => ({ key: s.id, html: createValueSourceCard(s) })));
|
||||
panelContent = csValueSources.render(csValueSources.applySortOrder(_cachedValueSources.map(s => ({ key: s.id, html: createValueSourceCard(s) }))));
|
||||
} else {
|
||||
panelContent = csStaticStreams.render(staticImageStreams.map(s => ({ key: s.id, html: renderStreamCard(s) })));
|
||||
panelContent = csStaticStreams.render(csStaticStreams.applySortOrder(staticImageStreams.map(s => ({ key: s.id, html: renderStreamCard(s) }))));
|
||||
}
|
||||
|
||||
return `<div class="stream-tab-panel${tab.key === activeTab ? ' active' : ''}" id="stream-tab-${tab.key}">${panelContent}</div>`;
|
||||
|
||||
@@ -581,12 +581,12 @@ export async function loadTargetsTab() {
|
||||
// Use window.createPatternTemplateCard to avoid circular import
|
||||
const createPatternTemplateCard = window.createPatternTemplateCard || (() => '');
|
||||
|
||||
// Build items arrays for each section
|
||||
const deviceItems = ledDevices.map(d => ({ key: d.id, html: createDeviceCard(d) }));
|
||||
const cssItems = Object.values(colorStripSourceMap).map(s => ({ key: s.id, html: createColorStripCard(s, pictureSourceMap, audioSourceMap) }));
|
||||
const ledTargetItems = ledTargets.map(t => ({ key: t.id, html: createTargetCard(t, deviceMap, colorStripSourceMap, valueSourceMap) }));
|
||||
const kcTargetItems = kcTargets.map(t => ({ key: t.id, html: createKCTargetCard(t, pictureSourceMap, patternTemplateMap, valueSourceMap) }));
|
||||
const patternItems = patternTemplates.map(pt => ({ key: pt.id, html: createPatternTemplateCard(pt) }));
|
||||
// Build items arrays for each section (apply saved drag order)
|
||||
const deviceItems = csDevices.applySortOrder(ledDevices.map(d => ({ key: d.id, html: createDeviceCard(d) })));
|
||||
const cssItems = csColorStrips.applySortOrder(Object.values(colorStripSourceMap).map(s => ({ key: s.id, html: createColorStripCard(s, pictureSourceMap, audioSourceMap) })));
|
||||
const ledTargetItems = csLedTargets.applySortOrder(ledTargets.map(t => ({ key: t.id, html: createTargetCard(t, deviceMap, colorStripSourceMap, valueSourceMap) })));
|
||||
const kcTargetItems = csKCTargets.applySortOrder(kcTargets.map(t => ({ key: t.id, html: createKCTargetCard(t, pictureSourceMap, patternTemplateMap, valueSourceMap) })));
|
||||
const patternItems = csPatternTemplates.applySortOrder(patternTemplates.map(pt => ({ key: pt.id, html: createPatternTemplateCard(pt) })));
|
||||
|
||||
// Track which target cards were replaced/added (need chart re-init)
|
||||
let changedTargetIds = null;
|
||||
|
||||
Reference in New Issue
Block a user