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 @@
-
+
+
+