diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index 47253da..94922ed 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -56,11 +56,125 @@ function closeLightbox(event) { } document.addEventListener('keydown', (e) => { - if (e.key === 'Escape' && document.getElementById('image-lightbox').classList.contains('active')) { - closeLightbox(); + if (e.key === 'Escape') { + if (document.getElementById('display-picker-lightbox').classList.contains('active')) { + closeDisplayPicker(); + } else if (document.getElementById('image-lightbox').classList.contains('active')) { + closeLightbox(); + } } }); +// Display picker lightbox +let _displayPickerCallback = null; + +function openDisplayPicker(callback) { + _displayPickerCallback = callback; + const lightbox = document.getElementById('display-picker-lightbox'); + const canvas = document.getElementById('display-picker-canvas'); + + lightbox.classList.add('active'); + + // Defer render to next frame so the lightbox has been laid out and canvas has dimensions + requestAnimationFrame(() => { + if (_cachedDisplays && _cachedDisplays.length > 0) { + renderDisplayPickerLayout(_cachedDisplays); + } else { + canvas.innerHTML = '
'; + loadDisplays().then(() => { + if (_cachedDisplays && _cachedDisplays.length > 0) { + renderDisplayPickerLayout(_cachedDisplays); + } else { + canvas.innerHTML = `
${t('displays.none')}
`; + } + }); + } + }); +} + +function closeDisplayPicker(event) { + if (event && event.target && event.target.closest('.display-picker-content')) return; + const lightbox = document.getElementById('display-picker-lightbox'); + lightbox.classList.remove('active'); + _displayPickerCallback = null; +} + +function selectDisplay(displayIndex) { + if (_displayPickerCallback) { + const display = _cachedDisplays ? _cachedDisplays.find(d => d.index === displayIndex) : null; + _displayPickerCallback(displayIndex, display); + } + closeDisplayPicker(); +} + +function renderDisplayPickerLayout(displays) { + const canvas = document.getElementById('display-picker-canvas'); + + if (!displays || displays.length === 0) { + canvas.innerHTML = `
${t('displays.none')}
`; + return; + } + + let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; + displays.forEach(display => { + minX = Math.min(minX, display.x); + minY = Math.min(minY, display.y); + maxX = Math.max(maxX, display.x + display.width); + maxY = Math.max(maxY, display.y + display.height); + }); + + const totalWidth = maxX - minX; + const totalHeight = maxY - minY; + const aspect = totalHeight / totalWidth; + + // Use percentage-based positioning so layout always fits its container + const displayElements = displays.map(display => { + const leftPct = ((display.x - minX) / totalWidth) * 100; + const topPct = ((display.y - minY) / totalHeight) * 100; + const widthPct = (display.width / totalWidth) * 100; + const heightPct = (display.height / totalHeight) * 100; + + return ` +
+
(${display.x}, ${display.y})
+
#${display.index}
+
+ ${display.name} + ${display.width}×${display.height} + ${display.refresh_rate}Hz +
+ ${display.is_primary ? '
' : ''} +
+ `; + }).join(''); + + canvas.innerHTML = ` +
+ ${displayElements} +
+ `; +} + +function formatDisplayLabel(displayIndex, display) { + if (display) { + return `#${display.index}: ${display.width}×${display.height}${display.is_primary ? ' (' + t('displays.badge.primary') + ')' : ''}`; + } + return `Display ${displayIndex}`; +} + +function onStreamDisplaySelected(displayIndex, display) { + document.getElementById('stream-display-index').value = displayIndex; + document.getElementById('stream-display-picker-label').textContent = formatDisplayLabel(displayIndex, display); +} + +function onTestDisplaySelected(displayIndex, display) { + document.getElementById('test-template-display').value = displayIndex; + document.getElementById('test-display-picker-label').textContent = formatDisplayLabel(displayIndex, display); +} + // Locale management let currentLocale = 'en'; let translations = {}; @@ -346,23 +460,11 @@ async function loadDisplays() { const data = await response.json(); - const container = document.getElementById('displays-list'); - - if (!data.displays || data.displays.length === 0) { - container.innerHTML = `
${t('displays.none')}
`; - document.getElementById('display-layout-canvas').innerHTML = `
${t('displays.none')}
`; - return; + if (data.displays && data.displays.length > 0) { + _cachedDisplays = data.displays; } - - // Cache and render visual layout - _cachedDisplays = data.displays; - renderDisplayLayout(data.displays); } catch (error) { console.error('Failed to load displays:', error); - document.getElementById('displays-list').innerHTML = - `
${t('displays.failed')}
`; - document.getElementById('display-layout-canvas').innerHTML = - `
${t('displays.failed')}
`; } } @@ -372,9 +474,6 @@ function switchTab(name) { document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.toggle('active', btn.dataset.tab === name)); document.querySelectorAll('.tab-panel').forEach(panel => panel.classList.toggle('active', panel.id === `tab-${name}`)); localStorage.setItem('activeTab', name); - if (name === 'displays' && _cachedDisplays) { - requestAnimationFrame(() => renderDisplayLayout(_cachedDisplays)); - } if (name === 'templates') { loadCaptureTemplates(); } @@ -393,66 +492,6 @@ function initTabs() { } } -function renderDisplayLayout(displays) { - const canvas = document.getElementById('display-layout-canvas'); - - if (!displays || displays.length === 0) { - canvas.innerHTML = `
${t('displays.none')}
`; - return; - } - - // Calculate bounding box for all displays - let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; - - displays.forEach(display => { - minX = Math.min(minX, display.x); - minY = Math.min(minY, display.y); - maxX = Math.max(maxX, display.x + display.width); - maxY = Math.max(maxY, display.y + display.height); - }); - - const totalWidth = maxX - minX; - const totalHeight = maxY - minY; - - // Scale factor to fit in canvas (respect available width, maintain aspect ratio) - const availableWidth = canvas.clientWidth - 60; // account for padding - const maxCanvasHeight = 350; - const scaleX = availableWidth / totalWidth; - const scaleY = maxCanvasHeight / totalHeight; - const scale = Math.min(scaleX, scaleY); - - const canvasWidth = totalWidth * scale; - const canvasHeight = totalHeight * scale; - - // Create display elements - const displayElements = displays.map(display => { - const left = (display.x - minX) * scale; - const top = (display.y - minY) * scale; - const width = display.width * scale; - const height = display.height * scale; - - return ` -
-
(${display.x}, ${display.y})
-
#${display.index}
-
- ${display.name} - ${display.width}×${display.height} - ${display.refresh_rate}Hz -
- ${display.is_primary ? '
' : ''} -
- `; - }).join(''); - - canvas.innerHTML = ` -
- ${displayElements} -
- `; -} // Load devices async function loadDevices() { @@ -2815,33 +2854,31 @@ function collectEngineConfig() { // Load displays for test selector async function loadDisplaysForTest() { try { - const response = await fetchWithAuth('/config/displays'); - if (!response.ok) { - throw new Error(`Failed to load displays: ${response.status}`); + if (!_cachedDisplays) { + const response = await fetchWithAuth('/config/displays'); + if (!response.ok) throw new Error(`Failed to load displays: ${response.status}`); + const displaysData = await response.json(); + _cachedDisplays = displaysData.displays || []; } - const displaysData = await response.json(); - const select = document.getElementById('test-template-display'); - select.innerHTML = ''; - - let primaryIndex = null; - (displaysData.displays || []).forEach(display => { - const option = document.createElement('option'); - option.value = display.index; - option.textContent = `Display ${display.index} (${display.width}x${display.height})`; - if (display.is_primary) { - option.textContent += ' ★'; - primaryIndex = display.index; - } - select.appendChild(option); - }); - // Auto-select: last used display, or primary as fallback + let selectedIndex = null; const lastDisplay = localStorage.getItem('lastTestDisplayIndex'); - if (lastDisplay !== null && select.querySelector(`option[value="${lastDisplay}"]`)) { - select.value = lastDisplay; - } else if (primaryIndex !== null) { - select.value = String(primaryIndex); + + if (lastDisplay !== null) { + const found = _cachedDisplays.find(d => d.index === parseInt(lastDisplay)); + if (found) selectedIndex = found.index; + } + + if (selectedIndex === null) { + const primary = _cachedDisplays.find(d => d.is_primary); + if (primary) selectedIndex = primary.index; + else if (_cachedDisplays.length > 0) selectedIndex = _cachedDisplays[0].index; + } + + if (selectedIndex !== null) { + const display = _cachedDisplays.find(d => d.index === selectedIndex); + onTestDisplaySelected(selectedIndex, display); } } catch (error) { console.error('Error loading displays:', error); @@ -3147,6 +3184,8 @@ async function showAddStreamModal(presetType) { document.getElementById('stream-modal-title').textContent = t(titleKey); document.getElementById('stream-form').reset(); document.getElementById('stream-id').value = ''; + document.getElementById('stream-display-index').value = ''; + document.getElementById('stream-display-picker-label').textContent = t('displays.picker.select'); document.getElementById('stream-error').style.display = 'none'; document.getElementById('stream-type').value = streamType; onStreamTypeChange(); @@ -3181,7 +3220,9 @@ async function editStream(streamId) { await populateStreamModalDropdowns(); if (stream.stream_type === 'raw') { - document.getElementById('stream-display-index').value = String(stream.display_index ?? 0); + const displayIdx = stream.display_index ?? 0; + const display = _cachedDisplays ? _cachedDisplays.find(d => d.index === displayIdx) : null; + onStreamDisplaySelected(displayIdx, display); document.getElementById('stream-capture-template').value = stream.capture_template_id || ''; const fps = stream.target_fps ?? 30; document.getElementById('stream-target-fps').value = fps; @@ -3210,23 +3251,16 @@ async function populateStreamModalDropdowns() { fetchWithAuth('/postprocessing-templates'), ]); - // Displays - const displaySelect = document.getElementById('stream-display-index'); - displaySelect.innerHTML = ''; + // Displays - warm cache for display picker if (displaysRes.ok) { const displaysData = await displaysRes.json(); - (displaysData.displays || []).forEach(d => { - const opt = document.createElement('option'); - opt.value = d.index; - opt.textContent = `${d.index}: ${d.width}x${d.height}${d.is_primary ? ` (${t('displays.badge.primary')})` : ''}`; - displaySelect.appendChild(opt); - }); + _cachedDisplays = displaysData.displays || []; } - if (displaySelect.options.length === 0) { - const opt = document.createElement('option'); - opt.value = '0'; - opt.textContent = '0'; - displaySelect.appendChild(opt); + + // Auto-select primary display if none selected yet + if (!document.getElementById('stream-display-index').value && _cachedDisplays && _cachedDisplays.length > 0) { + const primary = _cachedDisplays.find(d => d.is_primary) || _cachedDisplays[0]; + onStreamDisplaySelected(primary.index, primary); } // Capture templates diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index 3c3d490..227e4ae 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -35,7 +35,7 @@
- + @@ -55,14 +55,6 @@
-
-
-
-
-
-
- -

@@ -451,10 +443,11 @@