Add "now displaying" / "up next" preview pair to the frame header
The server now records exactly what was last sent to the device on
every device-facing render (/frame/image, /frame/advance, /frame/back,
and the global hold actions), persisted as Frame.last_displayed_image/
_at and served back via GET /api/frames/{id}/now-displaying. The
header thumbnail is split into that frozen "now displaying" snapshot
and the existing live "up next" re-render, with an arrow between them
-- so editing a layout shows the change immediately on the right while
the left stays exactly what's actually on the panel until the device's
next real wake.
This commit is contained in:
@@ -58,48 +58,31 @@
|
||||
});
|
||||
})();
|
||||
|
||||
// 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 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.
|
||||
// Now-displaying / up-next header preview pair. "Up next" is a real
|
||||
// composite render (same pipeline /frame/image uses), not a cached
|
||||
// snapshot, so it's on a slow 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, and it
|
||||
// shows layout edits live as they're made. "Now displaying" is the
|
||||
// opposite: exactly the bytes last actually sent to the device (see
|
||||
// routers/device.py's _record_last_displayed), frozen until the
|
||||
// device's next real wake even while the layout is being edited live --
|
||||
// that contrast is the point of showing both side by side.
|
||||
(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;
|
||||
var nextThumb = document.getElementById('frame-preview-thumb');
|
||||
var nextDialog = document.getElementById('frame-preview-dialog');
|
||||
var nextBigImg = document.getElementById('frame-preview-dialog-img');
|
||||
var nextCloseBtn = document.getElementById('frame-preview-dialog-close');
|
||||
var nowThumb = document.getElementById('frame-preview-now-thumb');
|
||||
var nowDialog = document.getElementById('frame-preview-now-dialog');
|
||||
var nowBigImg = document.getElementById('frame-preview-now-dialog-img');
|
||||
var nowCloseBtn = document.getElementById('frame-preview-now-dialog-close');
|
||||
if (!nextThumb || !window.FRAME_BASE_API) return;
|
||||
|
||||
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.
|
||||
// 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.
|
||||
function closeOnBackdropClick(dialog) {
|
||||
dialog.addEventListener('click', function (e) {
|
||||
if (e.target !== dialog) return;
|
||||
var rect = dialog.getBoundingClientRect();
|
||||
@@ -107,4 +90,80 @@
|
||||
if (!inside) dialog.close();
|
||||
});
|
||||
}
|
||||
|
||||
function nextUrl() {
|
||||
return `${window.FRAME_BASE_API}/preview?t=${Date.now()}`;
|
||||
}
|
||||
function refreshNext() {
|
||||
nextThumb.src = nextUrl();
|
||||
}
|
||||
// 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 refreshNextBig() {
|
||||
var url = nextUrl();
|
||||
nextBigImg.src = url;
|
||||
nextThumb.src = url;
|
||||
}
|
||||
|
||||
nextThumb.addEventListener('click', function () {
|
||||
if (!nextDialog) { refreshNext(); return; }
|
||||
refreshNextBig();
|
||||
nextDialog.showModal();
|
||||
});
|
||||
refreshNext();
|
||||
setInterval(refreshNext, 60000);
|
||||
|
||||
if (nextDialog && nextBigImg && nextCloseBtn) {
|
||||
nextBigImg.addEventListener('click', refreshNextBig);
|
||||
nextCloseBtn.addEventListener('click', function () { nextDialog.close(); });
|
||||
closeOnBackdropClick(nextDialog);
|
||||
}
|
||||
|
||||
// "Now displaying" fetches rather than sets .src directly: it needs to
|
||||
// tell a 404 (device hasn't fetched yet) apart from a real image to
|
||||
// show its own empty state instead of a broken-image icon, and reads
|
||||
// the capture time off X-Displayed-At for the "N ago" tooltip.
|
||||
if (nowThumb) {
|
||||
var nowObjectUrl = null;
|
||||
function refreshNow() {
|
||||
fetch(`${window.FRAME_BASE_API}/now-displaying?t=${Date.now()}`)
|
||||
.then(function (resp) {
|
||||
if (!resp.ok) {
|
||||
nowThumb.classList.add('frame-preview-thumb-empty');
|
||||
nowThumb.removeAttribute('src');
|
||||
nowThumb.title = "Now displaying -- hasn't shown anything yet";
|
||||
return null;
|
||||
}
|
||||
var displayedAt = resp.headers.get('X-Displayed-At');
|
||||
nowThumb.title = displayedAt
|
||||
? `Now displaying -- ${formatDuration(Math.max(0, Date.now() / 1000 - parseFloat(displayedAt)))} ago -- click to enlarge`
|
||||
: 'Now displaying -- click to enlarge';
|
||||
return resp.blob();
|
||||
})
|
||||
.then(function (blob) {
|
||||
if (!blob) return;
|
||||
nowThumb.classList.remove('frame-preview-thumb-empty');
|
||||
var url = URL.createObjectURL(blob);
|
||||
var old = nowObjectUrl;
|
||||
nowObjectUrl = url;
|
||||
nowThumb.src = url;
|
||||
if (old) URL.revokeObjectURL(old);
|
||||
})
|
||||
.catch(function () { /* transient failure -- leave the last-known thumb showing */ });
|
||||
}
|
||||
|
||||
nowThumb.addEventListener('click', function () {
|
||||
if (nowThumb.classList.contains('frame-preview-thumb-empty') || !nowDialog) return;
|
||||
nowBigImg.src = nowThumb.src;
|
||||
nowDialog.showModal();
|
||||
});
|
||||
refreshNow();
|
||||
setInterval(refreshNow, 60000);
|
||||
|
||||
if (nowDialog && nowCloseBtn) {
|
||||
nowCloseBtn.addEventListener('click', function () { nowDialog.close(); });
|
||||
closeOnBackdropClick(nowDialog);
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
Reference in New Issue
Block a user