First commit on the local Gitea server, made after moving the tree off the Windows machine. Build outputs, node_modules and the AI models stay out (see .gitignore); everything the installer build needs is tracked. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
102 lines
4.6 KiB
HTML
102 lines
4.6 KiB
HTML
<!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>
|