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>
This commit is contained in:
+37
-15
@@ -1345,6 +1345,30 @@
|
||||
momentumRaf = requestAnimationFrame(step);
|
||||
}
|
||||
|
||||
/*
|
||||
* Start a scroll gesture. Two fingers anywhere; on the Browser tab ONE finger too, once it has
|
||||
* travelled — there is nothing to draw on a web page, and one finger is how the page scrolls on the
|
||||
* TV itself. A finger that does not travel is still a tap (mirror_tap), so buttons, the search box
|
||||
* and scripture references stay pressable from the phone.
|
||||
*/
|
||||
var BROWSER_SCROLL_SLOP = 0.012; // fraction of the picture: past this, a one-finger touch is a drag
|
||||
function beginScroll(touches, oneFinger) {
|
||||
scroll.active = true;
|
||||
scroll.oneFinger = !!oneFinger;
|
||||
scroll.lastY = avgY(touches);
|
||||
scroll.total = 0;
|
||||
scroll.seq = 0;
|
||||
scroll.gesture = ++gestureCounter;
|
||||
scrollAnchor = wanted; // the finger takes over from here (null until a stamped frame)
|
||||
scrollSpeed = 0;
|
||||
scroll.samples = [{ t: Date.now(), y: scroll.lastY }];
|
||||
redrawStrokes(); // synchronous: wipe a cancelled live stroke off the mirror right now
|
||||
paintPreview();
|
||||
}
|
||||
function hintText() {
|
||||
return activeTabMode() === 'browser' ? 'tap to press \u00b7 drag to scroll' : 'tap to press \u00b7 1 finger draws \u00b7 2 fingers scroll';
|
||||
}
|
||||
|
||||
pad.addEventListener('touchstart', function (e) {
|
||||
e.preventDefault();
|
||||
var wasCoasting = !!momentumRaf;
|
||||
@@ -1371,16 +1395,7 @@
|
||||
// the app discards it the same way it discards a two-finger-cancelled stroke on the TV.
|
||||
if (stroke.sent) queueDraw('cancel', []);
|
||||
resetStroke();
|
||||
scroll.active = true;
|
||||
scroll.lastY = avgY(e.touches);
|
||||
scroll.total = 0;
|
||||
scroll.seq = 0;
|
||||
scroll.gesture = ++gestureCounter;
|
||||
scrollAnchor = wanted; // the finger takes over from here (null until a stamped frame)
|
||||
scrollSpeed = 0;
|
||||
scroll.samples = [{ t: Date.now(), y: scroll.lastY }];
|
||||
redrawStrokes(); // synchronous: wipe a cancelled live stroke off the mirror right now
|
||||
paintPreview();
|
||||
beginScroll(e.touches, false);
|
||||
return;
|
||||
}
|
||||
if (scroll.active) return;
|
||||
@@ -1390,9 +1405,9 @@
|
||||
var mode = activeTabMode();
|
||||
var tool = tvToolNow();
|
||||
// Match the TV's own refusals up front, so the preview never shows a line the TV will not draw:
|
||||
// no drawing on announcement/graphics/maps tabs, no drawing with a non-drawing tool, and a
|
||||
// no drawing on announcement/graphics/maps/browser tabs, no drawing with a non-drawing tool, and a
|
||||
// stroke must START inside the text area (the TV ignores ones that don't).
|
||||
var blocked = (mode === 'announcements' || mode === 'graphics' || mode === 'maps')
|
||||
var blocked = (mode === 'announcements' || mode === 'graphics' || mode === 'maps' || mode === 'browser')
|
||||
|| !(tool === 'pen' || tool === 'highlight' || tool === 'erase');
|
||||
var r = tvRect();
|
||||
if (!blocked && r && (p.x < r.x || p.x > r.x + r.w || p.y < r.y || p.y > r.y + r.h)) blocked = true;
|
||||
@@ -1408,7 +1423,7 @@
|
||||
if (swallowGesture) return;
|
||||
var now = Date.now();
|
||||
if (scroll.active) {
|
||||
if (e.touches.length < 2) return;
|
||||
if (e.touches.length < (scroll.oneFinger ? 1 : 2)) return;
|
||||
var y = avgY(e.touches);
|
||||
var movedPx = y - scroll.lastY;
|
||||
scroll.lastY = y;
|
||||
@@ -1426,6 +1441,13 @@
|
||||
var p = norm(e.touches[0]);
|
||||
if (!p) return;
|
||||
if (stroke.blocked) {
|
||||
if (activeTabMode() === 'browser') {
|
||||
var f0 = stroke.all[0];
|
||||
if (Math.abs(p.x - f0.x) < BROWSER_SCROLL_SLOP && Math.abs(p.y - f0.y) < BROWSER_SCROLL_SLOP) return; // still a tap
|
||||
resetStroke();
|
||||
beginScroll(e.touches, true);
|
||||
return;
|
||||
}
|
||||
// Tell the operator once WHY nothing is happening instead of silently eating the gesture.
|
||||
if (!stroke.warned) {
|
||||
stroke.warned = true;
|
||||
@@ -1512,7 +1534,7 @@
|
||||
hint.textContent = 'Splash is on — tap the screen to hide it';
|
||||
hint.style.opacity = '1';
|
||||
} else if (!hintIntro) {
|
||||
hint.textContent = 'tap to press · 1 finger draws · 2 fingers scroll';
|
||||
hint.textContent = hintText();
|
||||
hint.style.opacity = '0';
|
||||
}
|
||||
}
|
||||
@@ -1536,7 +1558,7 @@
|
||||
view.classList.remove('hidden');
|
||||
setStatus('connecting…', '');
|
||||
hintIntro = true;
|
||||
hint.textContent = 'tap to press · 1 finger draws · 2 fingers scroll';
|
||||
hint.textContent = hintText();
|
||||
hint.style.opacity = '1';
|
||||
setTimeout(function () { hintIntro = false; updateTvHint(); }, 2600);
|
||||
updateTvHint();
|
||||
|
||||
Reference in New Issue
Block a user