diff --git a/server/app/static/frame_header.js b/server/app/static/frame_header.js index eeab908..e269aea 100644 --- a/server/app/static/frame_header.js +++ b/server/app/static/frame_header.js @@ -60,17 +60,51 @@ // Live "how it's displaying" thumbnail. A real composite render (same // pipeline /frame/image uses), not a cached snapshot, so it's on a slow -// poll and also click-to-refresh rather than something tighter like the -// 10s device-status poll -- no need to hit Immich/calendar/whiteboard -// sources that often just for a header thumbnail. +// poll rather than something tighter like the 10s device-status poll -- +// no need to hit Immich/calendar/whiteboard sources that often just for +// a header thumbnail. Click enlarges it in a dialog (which also fetches +// a fresh render); clicking the enlarged image refreshes it again. (function () { var thumb = document.getElementById('frame-preview-thumb'); + var dialog = document.getElementById('frame-preview-dialog'); + var bigImg = document.getElementById('frame-preview-dialog-img'); + var closeBtn = document.getElementById('frame-preview-dialog-close'); if (!thumb || !window.FRAME_BASE_API) return; - function refresh() { - thumb.src = `${window.FRAME_BASE_API}/preview?t=${Date.now()}`; + function previewUrl() { + return `${window.FRAME_BASE_API}/preview?t=${Date.now()}`; + } + function refreshThumb() { + thumb.src = previewUrl(); + } + // Opening the dialog (or clicking the big image inside it) fetches a + // fresh render and keeps the header thumb in sync, so this single path + // covers both "enlarge" and the old click-to-refresh behavior. + function refreshBig() { + var url = previewUrl(); + bigImg.src = url; + thumb.src = url; + } + + thumb.addEventListener('click', function () { + if (!dialog) { refreshThumb(); return; } + refreshBig(); + dialog.showModal(); + }); + refreshThumb(); + setInterval(refreshThumb, 60000); + + if (dialog && bigImg && closeBtn) { + bigImg.addEventListener('click', refreshBig); + closeBtn.addEventListener('click', function () { dialog.close(); }); + // Same backdrop-click-to-close trick as #widget-dialog: a click that + // lands on the dialog element itself (not its content box) means the + // backdrop was hit. + dialog.addEventListener('click', function (e) { + if (e.target !== dialog) return; + var rect = dialog.getBoundingClientRect(); + var inside = e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom; + if (!inside) dialog.close(); + }); } - thumb.addEventListener('click', refresh); - refresh(); - setInterval(refresh, 60000); })(); diff --git a/server/app/static/theme.css b/server/app/static/theme.css index 0c9d79e..4741103 100644 --- a/server/app/static/theme.css +++ b/server/app/static/theme.css @@ -652,6 +652,36 @@ code { } .frame-preview-thumb:hover { opacity: 0.8; } +.frame-preview-dialog { + position: fixed; + margin: auto; + padding: 0; + border: 1px solid var(--border); + border-radius: 14px; + background: var(--surface); + box-shadow: var(--shadow-hover); + line-height: 0; /* avoid a baseline gap under the image */ +} +.frame-preview-dialog::backdrop { background: var(--overlay); } +.frame-preview-dialog img { + display: block; + max-width: min(90vw, 900px); + max-height: min(85vh, 900px); + width: auto; + height: auto; + border-radius: 14px; + cursor: pointer; +} +#frame-preview-dialog-close { + position: absolute; + top: 14px; + right: 14px; + width: 30px; + height: 30px; + font-size: 18px; + z-index: 1; +} + .control-banner { display: flex; align-items: center; diff --git a/server/app/templates/_frame_name_edit.html b/server/app/templates/_frame_name_edit.html index feaac6d..9d63799 100644 --- a/server/app/templates/_frame_name_edit.html +++ b/server/app/templates/_frame_name_edit.html @@ -7,5 +7,10 @@ -Live preview of what the frame is displaying +Live preview of what the frame is displaying + + + + Live preview of what the frame is displaying +