Files
bible-app/follower.html
T

102 lines
4.6 KiB
HTML
Raw Normal View History

2026-09-24 13:05:20 -04:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bible App — Follower</title>
<!--
follower.html — the booth screen's outer shell. Open THIS page fullscreen on the machine that feeds
ProPresenter: http://<stage-ip>:8080/follower.html
Why a shell around an iframe instead of the app page itself: the app sizes its reading area and
panels with viewport units (100vh and friends). Loaded directly on a booth display, those resolve
to the BOOTH window's size — so a 1080p landscape screen gave the reading area a fraction of the
stage TV's height, the text stopped two-thirds of the way down, and scroll positions no longer
meant the same thing on both screens. Inside an iframe sized to the stage's own viewport (published
in every snapshot) every viewport unit resolves exactly as it does on the TV, so the layout — and
therefore every pen stroke — is pixel-faithful. This shell then scales that fixed-size frame to fit
the display and slides the toolbar out of view. Solid black everywhere else; nothing else is drawn,
ever, because this picture goes to the program feed.
-->
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; background: #000; overflow: hidden; cursor: none; }
#frame {
position: absolute; left: 0; top: 0; border: 0; background: #000;
transform-origin: 0 0;
/* until the stage tells us its geometry, occupy the whole window so nothing but black shows */
width: 100vw; height: 100vh;
}
</style>
</head>
<body>
<!-- opacity 0 until the first layout(): the frame must never be seen at this window's size, only at the stage's -->
<iframe id="frame" title="Bible App follower" allow="autoplay" src="/?follower=1&framed=1" style="opacity:0"></iframe>
<script>
(function () {
'use strict';
var frame = document.getElementById('frame');
var geom = null;
var splash = false;
/*
* Lay the frame out at the stage's size, then scale so the CONTENT region (everything below the
* toolbar and tab strip) fills this window, letterboxed on black. The toolbar keeps its space inside
* the frame — removing it would reflow the text and misplace every stroke — and is simply pushed
* above the top edge here.
*
* The splash is fitted to this SAME box: the frame confines its splash overlay to the content region
* (follower-view.js), so the program feed never changes size — splash, announcements and every other
* tab share one box. (Fitting the whole frame while the splash was up made it 2.6% smaller.)
*/
function layout() {
if (!geom || !geom.w || !geom.h) return;
var c = geom.content || { x: 0, y: 0, w: geom.w, h: geom.h };
var vw = window.innerWidth, vh = window.innerHeight;
var scale = Math.min(vw / c.w, vh / c.h);
var ox = (vw - c.w * scale) / 2 - c.x * scale;
var oy = (vh - c.h * scale) / 2 - c.y * scale;
frame.style.width = geom.w + 'px';
frame.style.height = geom.h + 'px';
frame.style.transform = 'translate(' + ox + 'px,' + oy + 'px) scale(' + scale + ')';
frame.style.opacity = '1'; // laid out at the stage's size: now it may be seen
}
window.addEventListener('message', function (ev) {
// Same-origin only: the frame is served by this very server.
if (ev.origin !== window.location.origin) return;
var m = ev.data;
if (!m || m.type !== 'follower-geom' || !m.geom) return;
geom = m.geom;
splash = !!m.splash;
layout();
});
window.addEventListener('resize', layout);
/*
* Reload when the stage app is updated. Script URLs are stamped per build, but this page itself
* stays open for weeks on the booth machine and would keep running the version it started with —
* the same "stale page" trap the phone remote hit. remote.html polls the same field and reloads;
* so does this. The frame reloads with the page.
*/
var knownVersion = null;
function pollVersion() {
fetch('/api/remote/status?t=' + Date.now(), { cache: 'no-store' })
.then(function (r) { return r.ok ? r.json() : null; })
.then(function (j) {
var v = j && j.v;
if (!v) return;
if (knownVersion === null) { knownVersion = v; return; }
if (v !== knownVersion) { try { window.location.reload(); } catch (_) {} }
})
.catch(function () {});
}
setInterval(pollVersion, 5000);
pollVersion();
// Expose for the console: what size is the frame running at?
window.__followerShell = { get geom() { return geom; }, get splash() { return splash; }, get version() { return knownVersion; } };
})();
</script>
</body>
</html>