Files
bible-app/index.html
T
christianandClaude Opus 5.5 e7e527197f Add the Browser tab: a live web page with no browser chrome
A new tab (globe, after Maps) shows one web page full-bleed in the content area —
no address bar, bookmarks or scrollbars — built for It Is Written but usable with
any page. It loads at startup and keeps running in the background.

Stage (browser-tab.js, browser-guest-preload.cjs):
- An Electron <webview> per Browser tab, in its own sandboxed process and persistent
  session. The pages live in one persistent layer inside .tab-content: every tab
  switch rebuilds .tab-content and moving a webview reloads it, so app.js now clears
  around that layer (clearTabContentKeepingBrowser).
- Tapping a text field brings up the Prayer tab's keyboard (createPrayerKeyboard with
  a sink) and types into the page with trusted input; Enter submits; the keyboard puts
  itself away when the page moves focus on (e.g. after a search). The OS touch
  keyboard is suppressed with inputmode="none" while a field is focused.
- Tapping a scripture reference opens it in a new Bible tab, in the translation
  printed beside it when that one is switched on. References are found with the app's
  own scanner: scripture-scan.js gains scanAll() (every match with its offsets),
  translationAfter() and isTranslation(); scanText/parseSlide are unchanged.
- Input inside a webview never reaches the app, so the page reports activity (idle
  splash) and five-finger touches (blank the screen) itself.
- main.cjs: webviewTag on; every webview is forced sandboxed, context-isolated, no Node,
  http(s) only, with the guest preload. No new windows (opened in place), no permission
  prompts, no downloads, no pinch-zoom, no alert/confirm/prompt boxes, and "Leave
  site?" can never block Reload or Start page. The session reports plain Chrome.
- server.js CSP: frame-src allows http(s) (a webview is a frame to CSP); style-src and
  font-src allow https (the follower's srcdoc copy inherits this policy).

Everywhere a tab kind is listed:
- app.js: create at startup and on Reset, icon, visibility defaults, + menu, fixed
  mode, no ink layer, tool handling, OSC kind + information line (the page's search
  term), phone remote scroll / tap / page buttons, OSC show / reload / home actions,
  live setting changes, translation for tapped references.
- quick-dock, tool-wheel, presentation-tools: no drawing tools on the tab.
- Phone remote: tab label, one finger scrolls the page (taps still tap), hint text;
  mirror-stream.js reports the page's scroll for the phone's local preview.
- Follower screen: the stage publishes a script-free copy of the page's DOM (only while
  a follower is linked), its scroll and form fields; the follower shows it in
  sandboxed, double-buffered frames; the relay keeps the page for late joiners.
- OSC: /bibleapp/browser, /bibleapp/browser/reload, /bibleapp/browser/home;
  tab_kind/browser feedback. Admin and phone allow-lists updated.
- Settings: Tab Visibility, a Browser Tab section (start page, page size, scripture
  taps, return to start page), Service Profiles carry its settings, OSC references.
- Admin console: show the tab, reload / start page, start page and page size.
- BROWSER_TAB.md documents it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 20:25:15 -04:00

217 lines
15 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Bible App</title>
<link rel="stylesheet" href="styles.css?v=__BUILD__">
<!-- Removed remote Google Fonts to ensure consistent offline rendering -->
<!-- Font Awesome 6.0.0 (free) vendored locally so toolbar icons render with zero internet dependency -->
<link rel="stylesheet" href="vendor/fontawesome/css/all.min.css?v=__BUILD__">
<script>
// Apply the saved Bible reading font size before first paint (set in Settings > Bible Reading).
// Defaults to the built-in 60px when unset, so existing behavior is unchanged.
try {
var __bfs = parseInt(localStorage.getItem('bibleFontSize'), 10);
if (__bfs && !isNaN(__bfs)) {
__bfs = Math.max(40, Math.min(120, __bfs));
document.documentElement.style.setProperty('--bible-font-size', __bfs + 'px');
}
} catch (e) {}
</script>
<script>
/*
* Follower marker — this window is the BOOTH screen (?follower=1), not the stage TV.
*
* Set before every other script for the same reason as the admin marker below: remote-state.js
* and follower-publish.js decide on their very first tick whether they are the stage renderer,
* and a follower that published its own state would fight the real TV for the feed.
*
* The follower renders the identical page so live pen strokes land on the identical words. It
* is driven entirely by follower-view.js from the state the stage TV publishes.
*/
try {
if (/(^|[?&])follower=1(&|$)/.test(window.location.search)) {
window.__followerMode = true;
document.documentElement.style.background = '#000';
}
} catch (e) {}
</script>
<script>
/*
* Offstage (admin) copy marker.
*
* `?admin=1` means this window is the admin console's PRIVATE copy of the app, running in
* the admin's own browser — not the renderer on the TV. Set before any other script so
* remote-state.js sees it on its first tick and never publishes this window's state as the
* stage state.
*
* The badge is not decoration: this copy looks pixel-identical to the live stage, and an
* admin who forgets which one they are driving will make changes believing the congregation
* can see them (or worse, believing they cannot).
*/
try {
if (/(^|[?&])admin=1(&|$)/.test(window.location.search)) {
window.__adminOffstage = true;
document.addEventListener('DOMContentLoaded', function () {
try {
var b = document.createElement('div');
b.id = 'offstageBadge';
b.textContent = 'ADMIN COPY — NOT ON TV';
b.setAttribute('role', 'status');
b.style.cssText = [
'position:fixed', 'top:0', 'left:0', 'right:0', 'z-index:2147483647',
'background:#b26a00', 'color:#fff', 'font:600 12px/1.6 system-ui,sans-serif',
'letter-spacing:.08em', 'text-align:center', 'padding:3px 8px',
'pointer-events:none', 'text-transform:uppercase'
].join(';');
document.body.appendChild(b);
} catch (_) {}
});
}
} catch (e) {}
</script>
<link rel="icon" type="image/x-icon" href="data:image/x-icon;base64,AAABAAEAEBAAAAEAIABoBAAAFgAAACgAAAAQAAAAIAAAAAEAIAAAAAAAAAQAABILAAASCwAAAAAAAAAAAAD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAP//AAD//wAA//8AAA==">
</head>
<body>
<div id="app">
<!-- Upper Toolbar -->
<div class="toolbar">
<div class="toolbar-left">
<div class="browser-tabs">
<button class="reset-app-btn" id="resetAppBtn" title="Refresh app (Ctrl+R)"><i class="fas fa-rotate-right"></i></button>
<div class="tab-bar">
<!-- Tabs will be created dynamically by JavaScript -->
</div>
<button class="new-tab-btn" id="newTabBtn">+</button>
</div>
</div>
<div class="toolbar-right">
<div class="tool-indicator" id="toolIndicator" aria-live="polite" title="Current tool"><i class="fas"></i></div>
<div class="tool-rail-edge" id="toolRailEdge" aria-hidden="true"></div>
<div class="tool-rail" id="toolRail">
<div class="tool-slider" id="toolSlider">
<div class="qd-rail-grip" id="railGrip" title="Drag to float the toolbar"><i class="fas fa-grip-vertical"></i></div>
<div class="tool-wrapper">
<button class="tool-icon selected" data-tool="pen" id="penToolBtn"><i class="fas fa-pen"></i></button>
<div class="pen-color-dropdown" id="penColorDropdown">
<button class="pen-color-option" style="--pen-color: #fefefe;" data-color="#fefefe" title="White">&nbsp;</button>
<button class="pen-color-option" style="--pen-color: #ff3333;" data-color="#ff3333" title="Red">&nbsp;</button>
<button class="pen-color-option" style="--pen-color: #0090ff;" data-color="#0090ff" title="Blue">&nbsp;</button>
<button class="pen-color-option" style="--pen-color: #00ff00;" data-color="#00ff00" title="Green">&nbsp;</button>
<button class="pen-color-option" style="--pen-color: #ffff00;" data-color="#ffff00" title="Yellow">&nbsp;</button>
</div>
<div class="size-slider-popup" id="penSizeSliderPopup">
<div class="size-slider-wrap">
<div class="size-slider-ticks" id="penSizeTicks" aria-hidden="true"></div>
<input type="range" min="0" max="6" step="1" value="1" class="size-slider" id="penSizeSlider" aria-label="Pen size">
</div>
</div>
</div>
<div class="tool-wrapper">
<button class="tool-icon" data-tool="highlight" id="highlightToolBtn"><i class="fas fa-highlighter"></i></button>
<div class="highlighter-color-dropdown" id="highlighterColorDropdown">
<button class="highlighter-color-option" style="--highlighter-color: #fefefe;" data-color="#fefefe" title="White">&nbsp;</button>
<button class="highlighter-color-option" style="--highlighter-color: #ff3333;" data-color="#ff3333" title="Red">&nbsp;</button>
<button class="highlighter-color-option" style="--highlighter-color: #0090ff;" data-color="#0090ff" title="Blue">&nbsp;</button>
<button class="highlighter-color-option" style="--highlighter-color: #00ff00;" data-color="#00ff00" title="Green">&nbsp;</button>
<button class="highlighter-color-option" style="--highlighter-color: #ffff00;" data-color="#ffff00" title="Yellow">&nbsp;</button>
</div>
</div>
<button class="tool-icon" data-tool="erase"><i class="fas fa-eraser"></i></button>
<div class="tool-wrapper">
<button class="tool-icon" data-tool="laser" id="laserToolBtn" title="Laser pointer" style="--laser-color: #ffff00;"><i class="fas fa-circle-dot"></i></button>
<div class="laser-color-dropdown" id="laserColorDropdown">
<button class="laser-color-option" style="background: #fefefe;" data-color="#fefefe" title="White">&nbsp;</button>
<button class="laser-color-option" style="background: #ff3333;" data-color="#ff3333" title="Red">&nbsp;</button>
<button class="laser-color-option" style="background: #0090ff;" data-color="#0090ff" title="Blue">&nbsp;</button>
<button class="laser-color-option" style="background: #00ff00;" data-color="#00ff00" title="Green">&nbsp;</button>
<button class="laser-color-option selected" style="background: #ffff00;" data-color="#ffff00" title="Yellow">&nbsp;</button>
</div>
</div>
<button class="tool-icon" data-tool="spotlight" id="spotlightToolBtn" title="Spotlight"><i class="fas fa-bullseye"></i></button>
<button class="tool-icon" id="touchMenuBtn" data-tool="menu" title="Menu (Settings & Content Manager)"><i class="fas fa-bars"></i></button>
</div>
<button class="toolbar-btn" id="undoBtn" tabindex="-1">
<i class="fas fa-undo"></i>
</button>
<button class="toolbar-btn" id="redoBtn" tabindex="-1">
<i class="fas fa-redo"></i>
</button>
<button class="toolbar-btn" id="resetBtn" title="Clear current tab (tap) or all annotations (hold 1.5s)" tabindex="-1">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
</div>
<!-- Tab Content Panels -->
<div class="tab-content">
<!-- Tab panels will be created dynamically by JavaScript -->
</div>
</div>
<!-- Touch Menu Overlay -->
<div id="touchMenuOverlay" class="touch-menu-overlay hidden">
<div class="touch-menu-content">
<button class="touch-menu-close" aria-label="Close menu">
<i class="fas fa-times"></i>
</button>
<div class="touch-menu-items">
<button class="touch-menu-item" id="touchMenuSettings">
<i class="fas fa-cog"></i>
<span>Settings</span>
</button>
<button class="touch-menu-item" id="touchMenuContentManager">
<i class="fas fa-folder-open"></i>
<span>Content Manager</span>
</button>
<button class="touch-menu-item" id="touchMenuAbout">
<i class="fas fa-info-circle"></i>
<span>About</span>
</button>
<button class="touch-menu-item" id="touchMenuExit">
<i class="fas fa-sign-out-alt"></i>
<span>Exit</span>
</button>
</div>
</div>
</div>
<!-- Strong's dictionaries are served via /api/dict endpoints; avoid loading Node-formatted JS directly in browser -->
<!-- Removed direct script tags to prevent "module is not defined" errors in browser -->
<script type="module" src="init.js?v=__BUILD__"></script>
<script type="module" src="wordnet-init.js?v=__BUILD__"></script>
<!-- Presentation overlays (laser / spotlight) — isolated, additive -->
<script src="presentation-tools.js?v=__BUILD__"></script>
<!-- Optional draggable quick-tool dock (toggled in Settings) — isolated, additive -->
<script src="quick-dock.js?v=__BUILD__"></script>
<!-- Two-finger tap → radial tool selector under the fingers (Settings › Presentation) — isolated, additive -->
<script src="tool-wheel.js?v=__BUILD__"></script>
<!-- Red-letter (words of Christ) — isolated, additive -->
<script src="red-letter.js?v=__BUILD__"></script>
<!-- Watchdog: re-render a Bible tab that came up blank — isolated, additive -->
<script src="text-watchdog.js?v=__BUILD__"></script>
<!-- Draw on the word-definition popup cards — isolated, additive -->
<script src="popup-draw.js?v=__BUILD__"></script>
<!-- Live UI-state feed for the phone remote — isolated, additive -->
<script src="remote-state.js?v=__BUILD__"></script>
<!-- Fast frame source for the phone remote's mirror (renderer self-capture) — isolated, additive -->
<script src="mirror-stream.js?v=__BUILD__"></script>
<!-- An X may only close: stops a closing overlay's tap falling through to what was behind it -->
<script src="overlay-close-guard.js?v=__BUILD__"></script>
<!-- Book/abbreviation reference resolver, shared with the phone remote -->
<script src="book-aliases.js?v=__BUILD__"></script>
<!-- Follower link: the stage TV publishes its state so the booth screen can re-render this exact
picture for ProPresenter / the ATEM without a video cable — isolated, additive -->
<script src="follower-publish.js?v=__BUILD__"></script>
<script src="follower-view.js?v=__BUILD__"></script>
<!-- ProPresenter Sync tab: slide-reference scanner + the live link client — isolated, additive -->
<script src="scripture-scan.js?v=__BUILD__"></script>
<script src="propresenter-sync.js?v=__BUILD__"></script>
<!-- Browser tab: a live web page with no browser chrome (an Electron <webview> on the stage, the stage's
copy of it on the booth follower) — isolated, additive -->
<script src="browser-tab.js?v=__BUILD__"></script>
</body>
</html>