From 64871fd555517b64b34b0c3b1bed33060e6406b0 Mon Sep 17 00:00:00 2001 From: ChuckBuilds Date: Thu, 16 Jul 2026 14:46:59 -0400 Subject: [PATCH] fix(web): floating preview shows a frame immediately on open + is resizable The floating preview opened empty and stayed empty until the display next CHANGED - the SSE stream only pushes frames on change, and the panel only consumed frames while already open, so the connection's initial frame (sent while the panel was closed) was dropped. Reported from mobile testing as "the button doesn't work". - updateDisplayPreview now caches the latest frame globally regardless of panel state; opening the panel populates the image from that cache instantly, then live frames take over. - Resizable: a size button cycles 192/256/384/512px presets (persisted per browser; works on touch), and desktop additionally gets a native drag handle (CSS resize: both). The image is fluid within the panel; on phones the panel is capped to the viewport width. The size icon (fa-up-right-and-down-left-from-center) is verified present in the vendored FA 6.0.0. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ --- web_interface/static/v3/app.css | 10 +++++-- web_interface/static/v3/app.js | 36 ++++++++++++++++++++++++- web_interface/static/v3/js/app-shell.js | 7 +++++ web_interface/templates/v3/base.html | 17 ++++++++---- 4 files changed, 62 insertions(+), 8 deletions(-) diff --git a/web_interface/static/v3/app.css b/web_interface/static/v3/app.css index 26b7ac4a..27416ede 100644 --- a/web_interface/static/v3/app.css +++ b/web_interface/static/v3/app.css @@ -1368,6 +1368,11 @@ button.bg-white { border-radius: 0.5rem; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4); overflow: hidden; + /* Desktop: draggable resize handle (bottom-left visually, since the + panel is anchored to the right). Touch devices use the size button. */ + resize: both; + min-width: 160px; + max-width: 90vw; } .floating-preview img { background-color: #000; @@ -1387,7 +1392,8 @@ button.bg-white { justify-content: center; } @media (max-width: 640px) { - .floating-preview img { - width: 192px !important; + /* Whatever size is chosen, never wider than the phone viewport */ + .floating-preview { + max-width: calc(100vw - 2rem); } } diff --git a/web_interface/static/v3/app.js b/web_interface/static/v3/app.js index c84cb4b0..82b2a9e5 100644 --- a/web_interface/static/v3/app.js +++ b/web_interface/static/v3/app.js @@ -389,6 +389,29 @@ window.toggleFloatingPreview = function(open) { window.updateFloatingPreviewVisibility(); }; +// Preset widths the size button cycles through (px). Desktop users can also +// drag the panel's native resize handle (CSS resize: both). +const FLOATING_PREVIEW_SIZES = [192, 256, 384, 512]; + +window.applyFloatingPreviewSize = function() { + const panel = document.getElementById('floating-preview'); + if (!panel) return; + let size = 256; + try { size = parseInt(localStorage.getItem('ledmatrix-floating-preview-size'), 10) || 256; } catch { /* no-op */ } + panel.style.width = size + 'px'; + // Clear any manual drag-resize height so the image's aspect ratio rules + panel.style.height = ''; +}; + +window.cycleFloatingPreviewSize = function() { + let size = 256; + try { size = parseInt(localStorage.getItem('ledmatrix-floating-preview-size'), 10) || 256; } catch { /* no-op */ } + const idx = FLOATING_PREVIEW_SIZES.indexOf(size); + const next = FLOATING_PREVIEW_SIZES[(idx + 1) % FLOATING_PREVIEW_SIZES.length]; + try { localStorage.setItem('ledmatrix-floating-preview-size', String(next)); } catch { /* no-op */ } + window.applyFloatingPreviewSize(); +}; + window.updateFloatingPreviewVisibility = function(tab) { const panel = document.getElementById('floating-preview'); const toggle = document.getElementById('floating-preview-toggle'); @@ -402,8 +425,19 @@ window.updateFloatingPreviewVisibility = function(tab) { const onOverview = active === 'overview'; let open = false; try { open = localStorage.getItem('ledmatrix-floating-preview') === '1'; } catch { /* no-op */ } - panel.style.display = (!onOverview && open) ? 'block' : 'none'; + const showPanel = !onOverview && open; + panel.style.display = showPanel ? 'block' : 'none'; toggle.style.display = (!onOverview && !open) ? 'flex' : 'none'; + if (showPanel) { + window.applyFloatingPreviewSize(); + // Show the last cached frame immediately — SSE only pushes on + // display changes, so a freshly opened panel would otherwise stay + // empty until the next change. + const img = document.getElementById('floating-preview-img'); + if (img && !img.src && window._lastPreviewFrame) { + img.src = 'data:image/png;base64,' + window._lastPreviewFrame; + } + } }; document.addEventListener('DOMContentLoaded', function() { diff --git a/web_interface/static/v3/js/app-shell.js b/web_interface/static/v3/js/app-shell.js index 0abfada8..bfe6a25f 100644 --- a/web_interface/static/v3/js/app-shell.js +++ b/web_interface/static/v3/js/app-shell.js @@ -1878,6 +1878,13 @@ const ledCanvas = document.getElementById('ledCanvas'); const placeholder = document.getElementById('displayPlaceholder'); + // Always cache the latest frame so the floating preview can show + // something the moment it's opened — SSE only sends frames when + // the display CHANGES, so a late subscriber would otherwise stare + // at an empty panel until the next change. + if (data.image) { + window._lastPreviewFrame = data.image; + } // Feed the floating mini preview (lives in base.html, present on // every tab) before the overview-only guard below. const floatImg = document.getElementById('floating-preview-img'); diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index d54d8cc1..2223106c 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -563,13 +563,20 @@