diff --git a/server/src/wled_controller/static/app.js b/server/src/wled_controller/static/app.js index fb1b424..b9e0631 100644 --- a/server/src/wled_controller/static/app.js +++ b/server/src/wled_controller/static/app.js @@ -125,29 +125,101 @@ async function loadDisplays() { if (!data.displays || data.displays.length === 0) { container.innerHTML = '
No displays available
'; + document.getElementById('display-layout-canvas').innerHTML = '
No displays available
'; return; } + // Render display cards with enhanced information container.innerHTML = data.displays.map(display => `
-
${display.name}
+
+
${display.name}
+ ${display.is_primary ? 'Primary' : 'Secondary'} +
Resolution: ${display.width} × ${display.height}
Position: - ${display.x}, ${display.y} + (${display.x}, ${display.y}) +
+
+ Display Index: + ${display.index}
`).join(''); + + // Render visual layout + renderDisplayLayout(data.displays); } catch (error) { console.error('Failed to load displays:', error); document.getElementById('displays-list').innerHTML = '
Failed to load displays
'; + document.getElementById('display-layout-canvas').innerHTML = + '
Failed to load layout
'; } } +function renderDisplayLayout(displays) { + const canvas = document.getElementById('display-layout-canvas'); + + if (!displays || displays.length === 0) { + canvas.innerHTML = '
No displays to visualize
'; + 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 (600px wide max, maintain aspect ratio) + const maxCanvasWidth = 600; + const maxCanvasHeight = 350; + const scaleX = maxCanvasWidth / totalWidth; + const scaleY = maxCanvasHeight / totalHeight; + const scale = Math.min(scaleX, scaleY, 0.3); // Max 0.3 scale to keep monitors reasonably sized + + 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.name} + ${display.width}×${display.height} +
+ ${display.is_primary ? '
' : ''} +
+ `; + }).join(''); + + canvas.innerHTML = ` +
+ ${displayElements} +
+ `; +} + // Load devices async function loadDevices() { try { diff --git a/server/src/wled_controller/static/index.html b/server/src/wled_controller/static/index.html index efa0767..8205541 100644 --- a/server/src/wled_controller/static/index.html +++ b/server/src/wled_controller/static/index.html @@ -4,6 +4,7 @@ WLED Screen Controller + @@ -30,6 +31,21 @@

Available Displays

+ + +
+

Display Layout

+
+
Loading layout...
+
+
+ Primary Display + Secondary Display +
+
+ + +

Display Information

Loading displays...
diff --git a/server/src/wled_controller/static/style.css b/server/src/wled_controller/static/style.css index f90375c..ba562e1 100644 --- a/server/src/wled_controller/static/style.css +++ b/server/src/wled_controller/static/style.css @@ -223,14 +223,151 @@ section { border: 1px solid var(--border-color); border-radius: 8px; padding: 15px; - text-align: center; +} + +.display-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; } .display-index { - font-size: 2rem; + font-size: 1.3rem; font-weight: 700; color: var(--info-color); - margin-bottom: 10px; +} + +.badge-primary { + background: var(--primary-color); + color: white; +} + +.badge-secondary { + background: var(--border-color); + color: var(--text-secondary); +} + +/* Display Layout Visualization */ +.display-layout-preview { + background: var(--card-bg); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 20px; + margin-bottom: 20px; +} + +.display-layout-preview h3 { + margin: 0 0 15px 0; + font-size: 1.1rem; + color: var(--text-color); +} + +.display-layout-canvas { + background: var(--bg-color); + border: 2px dashed var(--border-color); + border-radius: 8px; + padding: 30px; + min-height: 300px; + display: flex; + align-items: center; + justify-content: center; +} + +.layout-container { + position: relative; + background: transparent; +} + +.layout-display { + position: absolute; + border: 3px solid; + border-radius: 8px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.2); + transition: transform 0.2s, box-shadow 0.2s; + cursor: help; +} + +.layout-display:hover { + transform: scale(1.05); + box-shadow: 0 6px 12px rgba(0, 0, 0, 0.3); + z-index: 10; +} + +.layout-display.primary { + border-color: var(--primary-color); + background: linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(76, 175, 80, 0.05)); +} + +.layout-display.secondary { + border-color: var(--border-color); + background: linear-gradient(135deg, rgba(128, 128, 128, 0.1), rgba(128, 128, 128, 0.05)); +} + +.layout-display-label { + text-align: center; + padding: 5px; + display: flex; + flex-direction: column; + gap: 2px; +} + +.layout-display-label strong { + font-size: 0.9rem; + color: var(--text-color); + font-weight: 600; +} + +.layout-display-label small { + font-size: 0.75rem; + color: var(--text-secondary); +} + +.primary-indicator { + position: absolute; + top: 5px; + right: 5px; + color: var(--primary-color); + font-size: 1.2rem; + text-shadow: 0 0 3px rgba(0, 0, 0, 0.3); +} + +.layout-legend { + display: flex; + gap: 20px; + justify-content: center; + margin-top: 15px; + padding-top: 15px; + border-top: 1px solid var(--border-color); +} + +.legend-item { + display: flex; + align-items: center; + gap: 8px; + font-size: 0.9rem; + color: var(--text-secondary); +} + +.legend-dot { + width: 16px; + height: 16px; + border-radius: 3px; + border: 2px solid; +} + +.legend-dot.primary { + border-color: var(--primary-color); + background: rgba(76, 175, 80, 0.2); +} + +.legend-dot.secondary { + border-color: var(--border-color); + background: rgba(128, 128, 128, 0.2); } .add-device-section {